Skip to content
File

Blob: src/client/pages/landing.tsx

typescript88 lines
1import type { ReactNode } from "react";
2 
3import { Launcher, LauncherSkeleton } from "@/client/pages/launcher";
4import { useSessionWithInitialPending } from "@/client/lib/auth-client";
5import { useClientConfig } from "@/client/lib/config";
6 
7const metadatumLinkClass =
8 "block truncate font-mono text-sm text-zinc-300 underline decoration-zinc-700 underline-offset-[6px] transition-colors hover:text-accent-400 hover:decoration-accent-500/60";
9 
10const Metadatum = ({ label, children }: { label: string; children: ReactNode }) => (
11 <>
12 <dt className="text-[11px] font-medium uppercase tracking-[0.18em] text-zinc-500">{label}</dt>
13 <dd className="min-w-0">{children}</dd>
14 </>
15);
16 
17export const LandingPage = () => {
18 const session = useSessionWithInitialPending();
19 const config = useClientConfig();
20 const issuerUrl = config?.status === "ok" ? config.issuer : null;
21 
22 // Signed-in users land on the app launcher (Notion/Linear pattern):
23 // the marketing copy below is for first-time visitors and the not-yet-
24 // signed-in. Avoiding navigate() here keeps the URL at `/` stable when
25 // moving between launcher and settings — the user's "home" is always
26 // the slash.
27 if (session.data) {
28 return <Launcher greetingName={session.data.user.name || session.data.user.email} />;
29 }
30 if (session.isInitialPending) {
31 return <LauncherSkeleton />;
32 }
33 
34 // Render the issuer host derived from /api/config so preview / staging
35 // deploys reflect their own deployed origin instead of a hard-coded
36 // production label. Falls back to the relative `/` (and a placeholder
37 // label) until the config call resolves.
38 const issuerLabel = issuerUrl ? new URL(issuerUrl).host : "—";
39 const issuerHref = issuerUrl ?? "/";
40 
41 return (
42 <div className="mx-auto max-w-2xl py-12 sm:py-20">
43 <h1 className="mb-10 font-display text-[clamp(2.25rem,6.5vw,4rem)] font-semibold leading-[1.04] tracking-tight text-zinc-100">
44 Single sign-on for limic.
45 </h1>
46
47 <p className="mb-16 max-w-[60ch] text-base leading-[1.7] text-zinc-300">
48 A hand-cut OAuth 2.1 and OpenID Connect provider. tessera issues one stable subject claim per human, recognized
49 by <span className="text-zinc-100">anvil</span>, <span className="text-zinc-100">bland</span>,{" "}
50 <span className="text-zinc-100">flamemail</span>,{" "}
51 <span className="text-zinc-100">git&#8209;on&#8209;cloudflare</span>, and Cloudflare Access. Sign in with email,
52 GitHub, or Google. Registration is invite-only.
53 </p>
54
55 <div aria-hidden="true" className="mb-10 h-px w-12 bg-zinc-800/80" />
56
57 <dl className="grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-8 gap-y-4">
58 <Metadatum label="Issuer">
59 {issuerUrl ? (
60 <a href={issuerHref} target="_blank" rel="noopener noreferrer" className={metadatumLinkClass}>
61 {issuerLabel}
62 </a>
63 ) : (
64 <a href={issuerHref} className={metadatumLinkClass}>
65 {issuerLabel}
66 </a>
67 )}
68 </Metadatum>
69 <Metadatum label="OIDC discovery">
70 <a href="/.well-known/openid-configuration" className={metadatumLinkClass}>
71 /.well-known/openid-configuration
72 </a>
73 </Metadatum>
74 <Metadatum label="OAuth metadata">
75 <a href="/.well-known/oauth-authorization-server" className={metadatumLinkClass}>
76 /.well-known/oauth-authorization-server
77 </a>
78 </Metadatum>
79 <Metadatum label="JWKS">
80 <a href="/api/auth/jwks" className={metadatumLinkClass}>
81 /api/auth/jwks
82 </a>
83 </Metadatum>
84 </dl>
85 </div>
86 );
87};