File
Blob: src/client/pages/landing.tsx
| 1 | import type { ReactNode } from "react"; |
| 2 | |
| 3 | import { Launcher, LauncherSkeleton } from "@/client/pages/launcher"; |
| 4 | import { useSessionWithInitialPending } from "@/client/lib/auth-client"; |
| 5 | import { useClientConfig } from "@/client/lib/config"; |
| 6 | |
| 7 | const 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 | |
| 10 | const 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 | |
| 17 | export 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‑on‑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 | }; |