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:
parent
47cab788fc
commit
da13caf9cb
4 changed files with 102 additions and 18 deletions
30
app/mobile/profile/page.tsx
Normal file
30
app/mobile/profile/page.tsx
Normal 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 & Preferences</h1>
|
||||
<div className="space-y-4">
|
||||
<ProfileTimezoneSection />
|
||||
<ProfileThemeSection />
|
||||
<ProfileNotificationMatrix />
|
||||
<ProfileChannelsSection />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 & preferences</span>
|
||||
</Link>
|
||||
</SheetClose>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignOut}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
21
components/mobile/profile/ProfileSectionSkeleton.tsx
Normal file
21
components/mobile/profile/ProfileSectionSkeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue