Skip to content
File

Blob: src/client/pages/AccountPage.tsx

typescript117 lines
1import { ShieldCheck } from "lucide-react";
2 
3import { EmptyState } from "@/client/components/ui";
4import { IslandHost } from "@/client/server/IslandHost";
5import { RepositoriesIsland } from "@/client/islands/repositories";
6import { TokensIsland, type TokensIslandSummary } from "@/client/islands/tokens";
7 
8export type AccountNamespace = {
9 id: string;
10 slug: string;
11};
12 
13export type AccountRepository = {
14 id: string;
15 slug: string;
16 namespaceSlug: string;
17 visibility: "public" | "private";
18 updatedAt: number;
19};
20 
21export type AccountPageProps = {
22 userId: string;
23 primaryNamespaceSlug?: string;
24 namespaces: AccountNamespace[];
25 repositories: AccountRepository[];
26 tokens: TokensIslandSummary[];
27};
28 
29function truncateUserId(userId: string): string {
30 if (userId.length <= 12) return userId;
31 return `${userId.slice(0, 8)}…${userId.slice(-4)}`;
32}
33 
34export function AccountPage({
35 userId,
36 primaryNamespaceSlug,
37 namespaces,
38 repositories,
39 tokens,
40}: AccountPageProps) {
41 const handle = primaryNamespaceSlug ? `@${primaryNamespaceSlug}` : "Identity not yet claimed";
42 return (
43 <div className="mx-auto max-w-3xl py-8">
44 <header className="mb-12">
45 <p className="m-0 mb-2 text-xs font-medium uppercase tracking-widest text-accent-500 dark:text-accent-400">
46 Identity
47 </p>
48 <div className="flex flex-col items-baseline justify-between gap-2 sm:flex-row sm:gap-6">
49 <h1 className="m-0 font-display text-4xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
50 {handle}
51 </h1>
52 <span
53 className="font-mono text-[11px] uppercase tracking-widest text-zinc-500"
54 title={userId}
55 >
56 id {truncateUserId(userId)}
57 </span>
58 </div>
59 </header>
60
61 <section className="mb-10">
62 <h2 className="m-0 mb-3 text-xs font-semibold uppercase tracking-wider text-zinc-500">
63 Namespaces
64 </h2>
65 {namespaces.length === 0 ? (
66 <EmptyState
67 icon={<ShieldCheck className="h-5 w-5 text-zinc-400" aria-hidden="true" />}
68 title="No namespace yet"
69 detail="Sign in once with a unique tessera username to claim a namespace."
70 />
71 ) : (
72 <div className="flex flex-wrap gap-2">
73 {namespaces.map((ns) => (
74 <a
75 key={ns.id}
76 href={`/${ns.slug}`}
77 className="inline-flex items-center rounded-lg border border-zinc-300 bg-white/60 px-3 py-1.5 font-mono text-sm text-zinc-700 no-underline transition-colors hover:border-accent-500/40 hover:text-accent-600 dark:border-zinc-800/60 dark:bg-zinc-900/30 dark:text-zinc-300 dark:hover:text-accent-400"
78 >
79 @{ns.slug}
80 </a>
81 ))}
82 </div>
83 )}
84 </section>
85
86 <section className="mb-10">
87 <h2 className="m-0 mb-3 text-xs font-semibold uppercase tracking-wider text-zinc-500">
88 Repositories
89 </h2>
90 <IslandHost name="repositories" props={{ primaryNamespaceSlug, repositories }}>
91 <RepositoriesIsland
92 primaryNamespaceSlug={primaryNamespaceSlug}
93 repositories={repositories}
94 />
95 </IslandHost>
96 </section>
97
98 <section id="tokens" className="scroll-mt-20">
99 <h2 className="m-0 mb-1.5 text-xs font-semibold uppercase tracking-wider text-zinc-500">
100 Tokens
101 </h2>
102 <p className="m-0 mb-4 text-sm text-zinc-500">
103 Personal access tokens scope to a namespace or a single repo. The plaintext is shown only
104 once at creation.
105 </p>
106 <IslandHost name="tokens" props={{ primaryNamespaceSlug, repositories, tokens }}>
107 <TokensIsland
108 primaryNamespaceSlug={primaryNamespaceSlug}
109 repositories={repositories}
110 tokens={tokens}
111 />
112 </IslandHost>
113 </section>
114 </div>
115 );
116}