File
Blob: src/client/pages/account/index.tsx
| 1 | import { NavCard } from "@/client/components/ui/nav-card"; |
| 2 | import { PageHeader } from "@/client/components/ui/page-header"; |
| 3 | import { Skeleton } from "@/client/components/ui/skeleton"; |
| 4 | import { useRequireSession } from "@/client/hooks/use-auth-guard"; |
| 5 | import { type SessionUser } from "@/client/lib/auth-client"; |
| 6 | import { fallbackHandle } from "@/client/lib/handle"; |
| 7 | |
| 8 | const HUB_CARD_COUNT = 6; |
| 9 | |
| 10 | const AccountHubSkeleton = () => ( |
| 11 | <div className="mx-auto max-w-3xl" aria-busy="true"> |
| 12 | <div className="mb-8"> |
| 13 | <Skeleton className="mb-3 h-8 w-44" /> |
| 14 | <Skeleton className="h-4 w-64" /> |
| 15 | </div> |
| 16 | <div className="flex flex-col gap-3"> |
| 17 | {Array.from({ length: HUB_CARD_COUNT }, (_, i) => ( |
| 18 | <div |
| 19 | key={i} |
| 20 | className="flex h-[5.5rem] items-center justify-between gap-4 rounded-2xl border border-zinc-800/60 bg-zinc-900/40 p-5 sm:p-6" |
| 21 | > |
| 22 | <div className="min-w-0 flex-1 space-y-2"> |
| 23 | <Skeleton className="h-5 w-32" /> |
| 24 | <Skeleton className="h-3 w-56" /> |
| 25 | </div> |
| 26 | <Skeleton className="h-5 w-5 shrink-0 rounded-full" /> |
| 27 | </div> |
| 28 | ))} |
| 29 | </div> |
| 30 | </div> |
| 31 | ); |
| 32 | |
| 33 | // `/account` is the settings hub. Each card is a focused subpage that |
| 34 | // owns one concern; the hub never edits anything inline. Adding new |
| 35 | // surfaces (2FA, email change, delete account) means appending another |
| 36 | // NavCard, not rearranging this page. |
| 37 | export const AccountPage = () => { |
| 38 | const session = useRequireSession("/account"); |
| 39 | |
| 40 | if (session.isPending || !session.data) { |
| 41 | return <AccountHubSkeleton />; |
| 42 | } |
| 43 | |
| 44 | const sessionUser = session.data.user as SessionUser; |
| 45 | const handle = sessionUser.preferredUsername ?? fallbackHandle(sessionUser); |
| 46 | |
| 47 | return ( |
| 48 | <div className="mx-auto max-w-3xl"> |
| 49 | <PageHeader title="Your account" description={`Signed in as ${session.data.user.email}.`} /> |
| 50 | <div className="flex flex-col gap-3"> |
| 51 | <NavCard |
| 52 | to="/account/profile" |
| 53 | title="Profile" |
| 54 | description="Your name and username." |
| 55 | meta={<code className="font-mono">{handle}</code>} |
| 56 | /> |
| 57 | <NavCard |
| 58 | to="/account/password" |
| 59 | title="Password" |
| 60 | description="Change the password on your email + password credential." |
| 61 | /> |
| 62 | <NavCard |
| 63 | to="/account/identities" |
| 64 | title="Identities" |
| 65 | description="Link or unlink GitHub and Google for sign-in." |
| 66 | /> |
| 67 | <NavCard to="/account/sessions" title="Sessions" description="Devices currently signed in to tessera." /> |
| 68 | <NavCard |
| 69 | to="/account/apps" |
| 70 | title="Connected apps" |
| 71 | description="Third-party apps you've authorized to sign you in." |
| 72 | /> |
| 73 | <NavCard |
| 74 | to="/account/userinfo" |
| 75 | title="User info" |
| 76 | description="The OIDC claims downstream apps see when you sign in." |
| 77 | /> |
| 78 | </div> |
| 79 | </div> |
| 80 | ); |
| 81 | }; |