From e862a228d43a3feb900329a51ea45972ca3cbb48 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 17 Jul 2026 11:00:17 +0000 Subject: [PATCH] feat: add support to freight portal --- .../portal/src/components/AppLayout.tsx | 4 + .../src/features/support/SupportPanel.tsx | 219 ++++++++++++++++++ .../src/features/support/SupportWidget.tsx | 91 ++++++++ .../portal/src/features/support/supportApi.ts | 41 ++++ .../portal/src/features/support/useSupport.ts | 62 +++++ .../src/features/support/useSupportSocket.ts | 75 ++++++ 6 files changed, 492 insertions(+) create mode 100644 apps/edr-freight-web/portal/src/features/support/SupportPanel.tsx create mode 100644 apps/edr-freight-web/portal/src/features/support/SupportWidget.tsx create mode 100644 apps/edr-freight-web/portal/src/features/support/supportApi.ts create mode 100644 apps/edr-freight-web/portal/src/features/support/useSupport.ts create mode 100644 apps/edr-freight-web/portal/src/features/support/useSupportSocket.ts diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx index 901c492c8..35641f9fc 100644 --- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx @@ -35,6 +35,7 @@ import { import { type CSSProperties, Fragment, type ReactNode, useState } from "react"; import { PROFILE_TYPE_LABELS } from "@/constants/profileMode"; import NotificationBellContainer from "@/features/notifications/NotificationBellContainer"; +import SupportWidget from "@/features/support/SupportWidget"; export interface SidebarItem { label: string; @@ -781,6 +782,9 @@ export function AppLayout({ {children} + {/* Floating customer-support chat launcher. */} + + {/* Create-profile modal β€” opens when switching to a mode the company doesn't have a profile for yet. */} void; +} + +/** + * The whole support surface: one ongoing thread with the EDR team. There is + * nothing to pick and nothing to open β€” the company has a single conversation, + * created server-side the moment the first message is sent. + */ +export function SupportPanel({ onClose }: SupportPanelProps) { + const { data: conversation } = useConversation(); + const { data: messages, isLoading } = useMessages(); + const send = useSendMessage(); + const markRead = useMarkConversationRead(); + const [draft, setDraft] = useState(""); + const viewport = useRef(null); + + // Mark read on open + whenever new messages arrive. No-op before the thread + // exists, so opening the panel never creates one. + useEffect(() => { + if (conversation) markRead.mutate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conversation?.id, messages?.length]); + + // Auto-scroll to newest. + useEffect(() => { + viewport.current?.scrollTo({ top: viewport.current.scrollHeight }); + }, [messages?.length]); + + const submit = async () => { + const body = draft.trim(); + if (!body) return; + setDraft(""); + await send.mutateAsync(body); + }; + + const isEmpty = !isLoading && (messages ?? []).length === 0; + + return ( + + + + + + + + + Support + + + We usually reply within a few minutes + + + + + + + + + + {isLoading ? ( + + + + ) : isEmpty ? ( + + + + + + Send us a message and our team will help you out. + + + ) : ( + + {(messages ?? []).map((m) => ( + + ))} + + )} + + + + +