feat(09-04): page shell, drawer wiring, skeleton helper, channels placeholder

- app/mobile/profile/page.tsx: server-component shell gated by requireAuth() + redirect('/auth/sign-in')
- ProfileSectionSkeleton.tsx: generic 3-row pulsing skeleton Card
- ProfileChannelsSectionPlaceholder.tsx: stub Channels card (Plan 05 swaps real component)
- MoreDrawer.tsx Account section: identity row wrapped in Link, new Profile & preferences row above Sign-out
This commit is contained in:
lorentz 2026-05-10 07:40:40 -04:00
parent 47cab788fc
commit da13caf9cb
4 changed files with 102 additions and 18 deletions

View file

@ -0,0 +1,30 @@
// /mobile/profile (PROF-01) — gated by requireAuth(), server-rendered shell
// hosts the four client sections.
import { redirect } from 'next/navigation';
import { requireAuth } from '@/lib/auth-utils';
import { ProfileTimezoneSection } from '@/components/mobile/profile/ProfileTimezoneSection';
import { ProfileThemeSection } from '@/components/mobile/profile/ProfileThemeSection';
import { ProfileNotificationMatrix } from '@/components/mobile/profile/ProfileNotificationMatrix';
import { ProfileChannelsSection } from '@/components/mobile/profile/ProfileChannelsSectionPlaceholder';
export default async function MobileProfilePage() {
const { session, error } = await requireAuth();
// CRITICAL: requireAuth() returns NextResponse on miss — that shape is for
// API routes only. A Page route MUST call redirect() instead. Returning
// `error` from here would render a JSON 401 body as the page, which is wrong.
if (error || !session) {
redirect('/auth/sign-in');
}
return (
<main className="px-4 pb-safe">
<h1 className="text-xl font-semibold pt-4 pb-2">Profile &amp; Preferences</h1>
<div className="space-y-4">
<ProfileTimezoneSection />
<ProfileThemeSection />
<ProfileNotificationMatrix />
<ProfileChannelsSection />
</div>
</main>
);
}

View file

@ -125,31 +125,39 @@ export function MoreDrawer({ open, onOpenChange }: MoreDrawerProps) {
</div>
</div>
{/* Section 3: Account (DRAWER-05) */}
{/* Section 3: Account (DRAWER-05, PROF-04) */}
<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>
<SheetClose asChild>
<Link href="/mobile/profile" className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors 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>
</Link>
</SheetClose>
)}
<SheetClose asChild>
<Link href="/mobile/profile" className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors border-b">
<Settings className="w-4 h-4 text-muted-foreground shrink-0" />
<span className="text-sm flex-1">Profile &amp; preferences</span>
</Link>
</SheetClose>
<button
type="button"
onClick={handleSignOut}

View file

@ -0,0 +1,25 @@
'use client';
/**
* Placeholder for ProfileChannelsSection wired in Plan 04 so the page
* compiles. Plan 05 replaces this file with the real component (Teams +
* ntfy + QR code). The named export `ProfileChannelsSection` matches the
* import in app/mobile/profile/page.tsx so the swap is a one-file change.
*/
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
export function ProfileChannelsSection() {
return (
<Card>
<CardHeader className="px-4 pt-4 pb-0">
<CardTitle>Personal Channels</CardTitle>
</CardHeader>
<CardContent className="px-4 py-4">
<p className="text-sm text-muted-foreground">
Coming soon channel configuration ships in Plan 05.
</p>
</CardContent>
</Card>
);
}

View file

@ -0,0 +1,21 @@
'use client';
import { Card, CardHeader, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export default function ProfileSectionSkeleton() {
return (
<Card>
<CardHeader className="px-4 pt-4 pb-0">
<Skeleton className="h-5 w-32" />
</CardHeader>
<CardContent className="px-4 py-4">
<div className="space-y-3">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
</CardContent>
</Card>
);
}