mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
feat(mobile): inbox single-pane flow on phone, unchanged on desktop
The inbox was a 3-pane flex row (list + thread + contact sidebar). On a 375px phone both list and thread tried to share ~187px each and the whole UI was unusable. Now: - On <lg the inbox shows ONE pane at a time: list when nothing's selected, thread when a conversation is open. - Thread gains a back-arrow in its header (mobile only) that pops back to the list and clears the ?c= URL param so refreshing lands on the list, not on the thread the user just backed out of. - Conversation list grows to w-full on mobile (was fixed w-80) so each row uses the whole viewport width. - Thread header tightens: name/phone truncate with min-w-0, session timer badge is hidden under sm to save room for back arrow + name. - Dashboard shell's p-6 is already p-4 on mobile (PR #48); matched the -m-6 offset in the inbox to -m-4 sm:-m-6. Desktop layout (lg+) is unchanged: list + thread + contact sidebar side-by-side exactly as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f223d5b86f
commit
7612e6c781
@ -10,6 +10,7 @@ import { MessageThread } from "@/components/inbox/message-thread";
|
||||
import { ContactSidebar } from "@/components/inbox/contact-sidebar";
|
||||
import { toast } from "sonner";
|
||||
import { WifiOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function InboxPage() {
|
||||
const router = useRouter();
|
||||
@ -191,6 +192,19 @@ export default function InboxPage() {
|
||||
[activeConversation?.id, router]
|
||||
);
|
||||
|
||||
// Mobile "back" — deselect the conversation so the list pane comes
|
||||
// back. Also clears the ?c= param so a refresh lands on the list
|
||||
// instead of re-opening the thread the user just backed out of.
|
||||
const handleCloseConversation = useCallback(() => {
|
||||
setActiveConversation(null);
|
||||
setActiveContact(null);
|
||||
setMessages([]);
|
||||
// Clearing the ref lets the deep-link auto-selector fire again if
|
||||
// the user later visits /inbox?c=<same-id> — desirable UX.
|
||||
autoSelectedForDeepLinkRef.current = null;
|
||||
router.replace("/inbox", { scroll: false });
|
||||
}, [router]);
|
||||
|
||||
|
||||
const handleMessagesLoaded = useCallback((loaded: Message[]) => {
|
||||
setMessages(loaded);
|
||||
@ -224,8 +238,15 @@ export default function InboxPage() {
|
||||
[activeConversation]
|
||||
);
|
||||
|
||||
// On mobile (<lg) we show a SINGLE pane — either the list or the
|
||||
// thread — rather than cramming both side-by-side. Selecting a
|
||||
// conversation slides the thread in; the thread's back button pops
|
||||
// it back to the list. On lg+ both panes render side-by-side as
|
||||
// before, unchanged.
|
||||
const hasActiveConv = !!activeConversation;
|
||||
|
||||
return (
|
||||
<div className="-m-6 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden">
|
||||
<div className="-m-4 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden sm:-m-6">
|
||||
{/* WhatsApp connection banner — in the flex column, not absolute,
|
||||
so it pushes the panels down instead of overlapping them. */}
|
||||
{whatsappConnected === false && (
|
||||
@ -238,15 +259,33 @@ export default function InboxPage() {
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Left panel: Conversation list */}
|
||||
{/* Left panel: Conversation list.
|
||||
Hidden on mobile when a conversation is selected so the
|
||||
thread can occupy the full width. Always visible on lg+. */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-1 lg:flex-none",
|
||||
hasActiveConv ? "hidden lg:flex" : "flex",
|
||||
)}
|
||||
>
|
||||
<ConversationList
|
||||
activeConversationId={activeConversation?.id ?? null}
|
||||
onSelect={handleSelectConversation}
|
||||
conversations={conversations}
|
||||
onConversationsLoaded={handleConversationsLoaded}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Center panel: Message thread */}
|
||||
{/* Center panel: Message thread.
|
||||
Hidden on mobile when no conversation is selected so the
|
||||
list can occupy the full width. Always visible on lg+
|
||||
(shows its own empty-state if no thread is picked yet). */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-1 lg:flex",
|
||||
hasActiveConv ? "flex" : "hidden lg:flex",
|
||||
)}
|
||||
>
|
||||
<MessageThread
|
||||
conversation={activeConversation}
|
||||
contact={activeContact}
|
||||
@ -255,9 +294,11 @@ export default function InboxPage() {
|
||||
onNewMessage={handleNewMessage}
|
||||
onUpdateMessage={handleUpdateMessage}
|
||||
onStatusChange={handleStatusChange}
|
||||
onBack={handleCloseConversation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right panel: Contact sidebar (hidden on small screens) */}
|
||||
{/* Right panel: Contact sidebar — desktop only. */}
|
||||
<div className="hidden lg:block">
|
||||
<ContactSidebar contact={activeContact} />
|
||||
</div>
|
||||
|
||||
@ -110,7 +110,10 @@ export function ConversationList({
|
||||
const activeFilter = FILTER_OPTIONS.find((o) => o.value === filter);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-80 flex-col border-r border-slate-800 bg-slate-900">
|
||||
// w-full on mobile so the list occupies the whole viewport when it's
|
||||
// the single pane showing; fixed 320px on desktop where it shares the
|
||||
// row with the thread + contact sidebar.
|
||||
<div className="flex h-full w-full flex-col border-r border-slate-800 bg-slate-900 lg:w-80">
|
||||
{/* Search + Filter */}
|
||||
<div className="space-y-2 border-b border-slate-800 p-3">
|
||||
<div className="relative">
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
ChevronDown,
|
||||
UserPlus,
|
||||
Clock,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { format, isToday, isYesterday, differenceInHours } from "date-fns";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@ -32,6 +33,13 @@ interface MessageThreadProps {
|
||||
onNewMessage: (message: Message) => void;
|
||||
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
||||
onStatusChange: (conversationId: string, status: ConversationStatus) => void;
|
||||
/**
|
||||
* On mobile, the thread is shown full-screen with the conversation list
|
||||
* hidden. This callback lets the page deselect the active conversation
|
||||
* and reveal the list again. Rendered as a back-arrow in the header on
|
||||
* mobile only.
|
||||
*/
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
function formatDateSeparator(dateStr: string): string {
|
||||
@ -72,6 +80,7 @@ export function MessageThread({
|
||||
onNewMessage,
|
||||
onUpdateMessage,
|
||||
onStatusChange,
|
||||
onBack,
|
||||
}: MessageThreadProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@ -278,20 +287,33 @@ export function MessageThread({
|
||||
return (
|
||||
<div className="flex flex-1 flex-col bg-slate-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-800 bg-slate-900 px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-slate-800 bg-slate-900 px-3 py-3 sm:px-4">
|
||||
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
|
||||
{/* Back-to-list button — mobile only. Hidden on lg+ where the
|
||||
conversation list is always visible next to the thread. */}
|
||||
{onBack && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
aria-label="Back to conversations"
|
||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-300 hover:bg-slate-800 hover:text-white lg:hidden"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-white">{displayName}</h2>
|
||||
<p className="text-xs text-slate-400">{contact.phone}</p>
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate text-sm font-semibold text-white">{displayName}</h2>
|
||||
<p className="truncate text-xs text-slate-400">{contact.phone}</p>
|
||||
</div>
|
||||
{/* Session timer badge */}
|
||||
{/* Session timer badge — hidden on the narrowest phones so
|
||||
the name + back arrow keep their room. */}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"ml-2 gap-1 border-slate-700 text-[10px]",
|
||||
"ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2",
|
||||
sessionInfo.expired ? "text-red-400" : "text-emerald-400"
|
||||
)}
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user