feat(02-01): create MoreDrawer component with Sheet, 3 sections, sign out

- shadcn Sheet side=right with controlled open/onOpenChange props
- Section 1: Mobile sections (Engagement in-shell route)
- Section 2: Full site links with ExternalLink hints (Quotes, Config Items, Backup Status, Ticket Digest, Admin/Sync)
- Section 3: Account — user initials display + sign out via Better Auth
- Implements DRAWER-01..05 requirements
This commit is contained in:
lorentz 2026-05-03 16:07:47 -04:00
parent 08467494bf
commit 2ef843766d

View file

@ -0,0 +1,166 @@
'use client';
/* MoreDrawer phase 02 (DRAWER-01..05).
*
* shadcn Sheet (side="right") with three top-to-bottom sections:
* 1. Mobile sections Engagement (in-shell route, no ExternalLink hint)
* 2. Full site desktop-only routes, each with ExternalLink hint
* 3. Account current user (read-only) + Sign out
*
* Open state is controlled by the parent so the header avatar AND the
* bottom-nav More cell can both trigger this single drawer. */
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ExternalLink,
FileText,
Server,
HardDrive,
BarChart3,
Settings,
Users,
LogOut,
} from 'lucide-react';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
SheetClose,
} from '@/components/ui/sheet';
import { useSession, signOut } from '@/lib/auth-client';
import { toast } from 'sonner';
const MOBILE_SECTIONS = [
{ href: '/mobile/engagement', label: 'Engagement', icon: Users },
];
const DESKTOP_LINKS = [
{ href: '/quotes', label: 'Quotes', icon: FileText },
{ href: '/configuration-items', label: 'Configuration Items', icon: Server },
{ href: '/backup-status', label: 'Backup Status', icon: HardDrive },
{ href: '/admin/ticket-digest', label: 'Ticket Digest', icon: BarChart3 },
{ href: '/admin/sync', label: 'Admin / Sync', icon: Settings },
];
interface MoreDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function MoreDrawer({ open, onOpenChange }: MoreDrawerProps) {
const router = useRouter();
const { data: session } = useSession();
const user = session?.user as
| { name?: string; email?: string; image?: string | null }
| undefined;
const initials = (user?.name ?? user?.email ?? '?')
.split(/[\s@]/)
.filter(Boolean)
.slice(0, 2)
.map((p) => p[0]?.toUpperCase())
.join('');
async function handleSignOut() {
try {
await signOut();
router.push('/auth/sign-in');
} catch (e) {
toast.error('Sign out failed');
console.error('Sign out failed:', e);
}
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-80 sm:max-w-sm flex flex-col">
<SheetHeader>
<SheetTitle>Menu</SheetTitle>
<SheetDescription className="sr-only">
Navigation, full-site links, and account actions.
</SheetDescription>
</SheetHeader>
{/* Section 1: Mobile sections (DRAWER-03) */}
<div className="px-4">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Mobile sections
</p>
<div className="rounded-2xl border divide-y overflow-hidden">
{MOBILE_SECTIONS.map(({ href, label, icon: Icon }) => (
<SheetClose asChild key={href}>
<Link
href={href}
className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors"
>
<Icon className="w-4 h-4 text-muted-foreground shrink-0" />
<span className="text-sm flex-1">{label}</span>
</Link>
</SheetClose>
))}
</div>
</div>
{/* Section 2: Full site (DRAWER-04) */}
<div className="px-4">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Full site
</p>
<div className="rounded-2xl border divide-y overflow-hidden">
{DESKTOP_LINKS.map(({ href, label, icon: Icon }) => (
<SheetClose asChild key={href}>
<Link
href={href}
className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors"
>
<Icon className="w-4 h-4 text-muted-foreground shrink-0" />
<span className="text-sm flex-1">{label}</span>
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
</Link>
</SheetClose>
))}
</div>
</div>
{/* Section 3: Account (DRAWER-05) */}
<div className="px-4 mt-auto pb-safe">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Account
</p>
<div className="rounded-2xl border overflow-hidden">
{user && (
<div className="flex items-center gap-3 px-4 py-3 border-b">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary/15 text-primary text-xs font-semibold shrink-0">
{initials}
</span>
<div className="flex-1 min-w-0">
{user.name && (
<p className="text-sm font-medium leading-tight truncate">
{user.name}
</p>
)}
{user.email && (
<p className="text-xs text-muted-foreground truncate" title={user.email}>
{user.email}
</p>
)}
</div>
</div>
)}
<button
type="button"
onClick={handleSignOut}
className="w-full flex items-center gap-3 px-4 py-3 text-destructive hover:bg-destructive/10 transition-colors"
>
<LogOut className="w-4 h-4 shrink-0" />
<span className="text-sm">Sign out</span>
</button>
</div>
</div>
</SheetContent>
</Sheet>
);
}