diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 643583c2..509198a7 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -70,8 +70,12 @@ export default defineConfig({ const path = new URL(page).pathname.replace(/\/$/, "") || "/"; const isTeamIndex = path === "/team" || /\/[a-z]{2}-[A-Z]{2}\/team$/.test(path); + // Squeeze page: indexable and shareable, but kept out of the sitemap. + const isLaunch = + path === "/launch" || /\/[a-z]{2}-[A-Z]{2}\/launch$/.test(path); return ( !isTeamIndex && + !isLaunch && !page.includes("/tags/") && !page.includes("/account/") && !page.includes("/oauth/") && diff --git a/website/src/api/client.ts b/website/src/api/client.ts index ae6d1c21..d383e8b8 100644 --- a/website/src/api/client.ts +++ b/website/src/api/client.ts @@ -35,6 +35,10 @@ interface ContactData { interface SubscribeData { email: string; + firstName: string; + lastName: string; + /** Which list this signup belongs to. Omitted by the site newsletter. */ + source?: string; } export interface Entrant { @@ -106,11 +110,15 @@ const createApiClient = () => { /** * Subscribe to waitlist + * + * `source` identifies which list the signup belongs to. Omitting it + * produces the original payload, so existing callers are unaffected. */ subscribe: ( email: string, firstName: string, lastName: string, + source?: string, ): ApiResponse => { return fetch(`${env.API_URL}/waitlist`, { headers: { @@ -121,6 +129,7 @@ const createApiClient = () => { email, firstName, lastName, + ...(source ? { source } : {}), } as SubscribeData), }); }, diff --git a/website/src/components/features/launch/AsciiFrame.astro b/website/src/components/features/launch/AsciiFrame.astro new file mode 100644 index 00000000..0e288de4 --- /dev/null +++ b/website/src/components/features/launch/AsciiFrame.astro @@ -0,0 +1,95 @@ +--- +/** + * ASCII backdrop for /launch. + * + * This is Home's hero field, not a copy of it: the element carries + * id="ascii-hero" and the script drives it through the very same + * `calcHeroDims` / `heroLoop` exports in scripts/home/hero.ts, which render + * via renderASCIIRect over chladniRect with the shared RAMP. + * + * hero.ts resolves #ascii-hero at module scope and gates its loop behind + * `heroActive`, which on Home is flipped by the terminal loader. There is no + * loader here, so we flip it ourselves. `updateMousePosition` is deliberately + * not wired — it is the one export that writes to Home's #fval readout and + * would throw on this page. + * + * Framing is done with a radial mask rather than by slicing the field, so the + * pattern stays whole and simply fades out behind the content. + */ +--- + + + + + + diff --git a/website/src/components/features/launch/FactsSection.astro b/website/src/components/features/launch/FactsSection.astro new file mode 100644 index 00000000..8a688f37 --- /dev/null +++ b/website/src/components/features/launch/FactsSection.astro @@ -0,0 +1,93 @@ +--- +import Button from "@/components/ui/Button.astro"; +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); +--- + +
+
+ {t("launch.facts.supply.label")} + {t("launch.facts.supply.value")} +
+ +
+ {t("launch.facts.consensus.label")} + + +
+
+ + diff --git a/website/src/components/features/launch/HeroSection.astro b/website/src/components/features/launch/HeroSection.astro new file mode 100644 index 00000000..7d1e6ab1 --- /dev/null +++ b/website/src/components/features/launch/HeroSection.astro @@ -0,0 +1,49 @@ +--- +import LogoLong from "@/assets/brand/logo-long.svg"; +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); +--- + +
+
+ + diff --git a/website/src/components/features/launch/SignupForm.astro b/website/src/components/features/launch/SignupForm.astro new file mode 100644 index 00000000..07e1a0ee --- /dev/null +++ b/website/src/components/features/launch/SignupForm.astro @@ -0,0 +1,309 @@ +--- +import Button from "@/components/ui/Button.astro"; +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; +import { WAITLIST_SOURCE, type WaitlistSource } from "@/constants/waitlist"; + +interface Props { + /** Which waitlist these signups belong to. */ + source?: WaitlistSource; +} + +const { source = WAITLIST_SOURCE.LAUNCH } = Astro.props; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); +--- + +
+
+

{t("launch.signup.headline")}

+ +
+
+ + {/* Decorative resting cursor; the real caret takes over on focus. */} + +
+ +
+ + + {t("launch.signup.error_message")} + +
+ +
+ + {t("launch.signup.success.headline")} + + + {t("launch.signup.success.sub")} + +
+
+ + + + diff --git a/website/src/components/layout/Layout.astro b/website/src/components/layout/Layout.astro index c761ad42..f3a581ee 100644 --- a/website/src/components/layout/Layout.astro +++ b/website/src/components/layout/Layout.astro @@ -39,9 +39,11 @@ import env from "@/config"; interface Props extends SEOProps { jsonLd?: WithContext | Graph; + /** Render the navbar and footer. Off for standalone pages like /launch. */ + showChrome?: boolean; } -const { jsonLd, ...metadata } = Astro.props; +const { jsonLd, showChrome = true, ...metadata } = Astro.props; const currentLocale = getLocaleFromUrl(Astro.url.pathname); const t = await createTranslator(currentLocale); @@ -115,11 +117,11 @@ const breadcrumbs = generateBreadcrumbs({ ) } - + {showChrome && }
-