diff --git a/website/src/components/features/launch/AsciiFrame.astro b/website/src/components/features/launch/AsciiFrame.astro deleted file mode 100644 index 0e288de4..00000000 --- a/website/src/components/features/launch/AsciiFrame.astro +++ /dev/null @@ -1,95 +0,0 @@ ---- -/** - * 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/CardsSection.astro b/website/src/components/features/launch/CardsSection.astro new file mode 100644 index 00000000..b8e68fa9 --- /dev/null +++ b/website/src/components/features/launch/CardsSection.astro @@ -0,0 +1,72 @@ +--- +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); + +const CARDS = ["quantum_secure", "encrypted", "sound_money"] as const; +--- + +
+ { + CARDS.map((card) => ( +
+

{t(`launch.cards.${card}.title`)}

+

{t(`launch.cards.${card}.body`)}

+
+ )) + } +
+ + diff --git a/website/src/components/features/launch/FactsSection.astro b/website/src/components/features/launch/FactsSection.astro deleted file mode 100644 index be8e07bb..00000000 --- a/website/src/components/features/launch/FactsSection.astro +++ /dev/null @@ -1,95 +0,0 @@ ---- -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 index 7d1e6ab1..7048a637 100644 --- a/website/src/components/features/launch/HeroSection.astro +++ b/website/src/components/features/launch/HeroSection.astro @@ -1,49 +1,48 @@ --- -import LogoLong from "@/assets/brand/logo-long.svg"; +import SignupForm from "./SignupForm.astro"; import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; const locale = getLocaleFromUrl(Astro.url.pathname); const t = await createTranslator(locale); --- -
-
+
+ {/* The key predates the redesign, when this line was the eyebrow. */} +

{t("launch.hero.eyebrow")}

+

{t("launch.hero.sub")}

+ +
diff --git a/website/src/components/features/launch/LaunchField.astro b/website/src/components/features/launch/LaunchField.astro new file mode 100644 index 00000000..fb7d696c --- /dev/null +++ b/website/src/components/features/launch/LaunchField.astro @@ -0,0 +1,49 @@ +--- +/** + * Fixed Chladni field behind the /launch frame. Driven by + * scripts/launch/field.ts, which is independent of Home's hero field. + */ +--- + + + + + + diff --git a/website/src/components/features/launch/LaunchPage.astro b/website/src/components/features/launch/LaunchPage.astro new file mode 100644 index 00000000..737ccfcf --- /dev/null +++ b/website/src/components/features/launch/LaunchPage.astro @@ -0,0 +1,172 @@ +--- +/** + * Everything /launch renders. Shared by pages/launch.astro and + * pages/[lang]/launch.astro, which differ only in getStaticPaths. + * + * Section wrappers live here rather than in each section so the dividers + * between them can be styled in one place. + */ +import Layout from "@/components/layout/Layout.astro"; +import LogoLong from "@/assets/brand/logo-long.svg"; +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; +import { createMetadata } from "@/utils/create-metadata"; +import { JsonLdGraph } from "@/utils/build-json-ld"; +import { organizationJsonLd, websiteJsonLd } from "@/constants/default-jsonld"; +import LaunchField from "./LaunchField.astro"; +import HeroSection from "./HeroSection.astro"; +import CardsSection from "./CardsSection.astro"; +import MiningSection from "./MiningSection.astro"; +import WaysSection from "./WaysSection.astro"; +import SignupForm from "./SignupForm.astro"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); + +const metadata = createMetadata({ + title: t("launch.meta.title"), + description: t("launch.meta.description"), + pathname: Astro.url.pathname, +}); + +const jsonLd = JsonLdGraph({ + "@context": "https://schema.org", + "@graph": [websiteJsonLd, organizationJsonLd], +}); +--- + + +
+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+

{t("launch.bridge")}

+
+ +
+ +
+ +
+ +
+ +
+
+

{t("launch.final.title")}

+ +
+
+
+
+
+ + diff --git a/website/src/components/features/launch/MiningSection.astro b/website/src/components/features/launch/MiningSection.astro new file mode 100644 index 00000000..6f2aa83f --- /dev/null +++ b/website/src/components/features/launch/MiningSection.astro @@ -0,0 +1,149 @@ +--- +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); + +const MINING_DOCS_URL = "https://docs.quantus.com/guides/mining/"; +const ARROW_PATH = + "M13.376 11.552l-.264-10.44-10.44-.24.024 2.28 6.96-.048L.2 12.56l1.488 1.488 9.432-9.432-.048 6.912 2.304.024z"; +--- + +
+

{t("launch.mining.title")}

+

{t("launch.mining.body")}

+ + + {t("launch.mining.cta")} + {/* On hover the arrow exits top-right and its copy enters bottom-left. */} + + +
+ + diff --git a/website/src/components/features/launch/SignupForm.astro b/website/src/components/features/launch/SignupForm.astro index 07e1a0ee..147e8370 100644 --- a/website/src/components/features/launch/SignupForm.astro +++ b/website/src/components/features/launch/SignupForm.astro @@ -1,232 +1,285 @@ --- -import Button from "@/components/ui/Button.astro"; import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; import { WAITLIST_SOURCE, type WaitlistSource } from "@/constants/waitlist"; interface Props { + /** Unique per instance; prefixes the input and error ids. */ + id: string; /** Which waitlist these signups belong to. */ source?: WaitlistSource; + /** Optional line under the field, hidden once signed up. */ + note?: string; } -const { source = WAITLIST_SOURCE.LAUNCH } = Astro.props; +const { id, source = WAITLIST_SOURCE.LAUNCH, note } = Astro.props; const locale = getLocaleFromUrl(Astro.url.pathname); const t = await createTranslator(locale); --- -
-
-

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

- + -
- + {note &&

{note}

} + + + {/* Receipt: the field becomes a confirmed row carrying the address. */} + + + {/* Filled only in the instance that was submitted, so it announces once. */} +

@@ -235,46 +288,93 @@ const t = await createTranslator(locale); import apiClient from "@/api/client"; import { trackEvent } from "@/scripts/shared/analytics"; - const root = document.querySelector(".launch-signup"); - const submitBtn = document.getElementById( - "launch-submit", - ) as HTMLButtonElement | null; - const emailInput = document.getElementById( - "launch-email", - ) as HTMLInputElement | null; - const errorMsg = document.getElementById("launch-error"); - const formWrap = document.getElementById("launch-form-wrap"); - const successWrap = document.getElementById("launch-success"); - - if (root && submitBtn && emailInput && errorMsg && formWrap && successWrap) { - const source = root.dataset.source; + interface Signup { + root: HTMLElement; + form: HTMLFormElement; + input: HTMLInputElement; + submit: HTMLButtonElement; + error: HTMLElement; + success: HTMLElement; + mail: HTMLElement; + line: HTMLElement; + status: HTMLElement; + } - const isValidEmail = (val: string) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val); + const FORM_FADE_MS = 400; + + const isValidEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val); + + const signups = Array.from( + document.querySelectorAll("[data-launch-signup]"), + ).flatMap((root): Signup[] => { + const q = (selector: string) => + root.querySelector(selector); + const parts = { + root, + form: q("[data-signup-form]"), + input: q("[data-signup-email]"), + submit: q("[data-signup-submit]"), + error: q("[data-signup-error]"), + success: q("[data-signup-success]"), + mail: q("[data-signup-mail]"), + line: q("[data-signup-line]"), + status: q("[data-signup-status]"), + }; + return Object.values(parts).every(Boolean) ? [parts as Signup] : []; + }); + + const setLoading = (submit: HTMLButtonElement, loading: boolean) => { + submit.disabled = loading; + if (loading) { + submit.dataset.originalText = submit.innerText; + submit.innerText = "..."; + } else { + submit.innerText = submit.dataset.originalText ?? ""; + } + }; + + const setInvalid = (signup: Signup, invalid: boolean) => { + signup.input.setAttribute("aria-invalid", String(invalid)); + signup.error.classList.toggle("is-shown", invalid); + }; + + /** Every form on the page flips to the receipt, not just the one used. */ + const showReceipt = (email: string, submitted: Signup) => { + for (const signup of signups) { + signup.mail.textContent = email; + signup.form.classList.add("is-leaving"); + } - const setLoading = (loading: boolean) => { - submitBtn.disabled = loading; - if (loading) { - submitBtn.dataset.originalText = submitBtn.innerText; - submitBtn.innerText = "..."; - } else { - submitBtn.innerText = submitBtn.dataset.originalText ?? ""; + setTimeout(() => { + for (const signup of signups) { + signup.form.hidden = true; + signup.success.hidden = false; + requestAnimationFrame(() => signup.success.classList.add("is-shown")); } - }; + submitted.status.textContent = (submitted.line.textContent ?? "") + .replace(/\s+/g, " ") + .trim(); + }, FORM_FADE_MS); + }; + + for (const signup of signups) { + const { root, form, input, submit } = signup; + const source = root.dataset.source; + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + if (submit.disabled) return; - submitBtn.addEventListener("click", async () => { - emailInput.classList.remove("error"); - errorMsg.classList.remove("visible"); + setInvalid(signup, false); - const email = emailInput.value.trim(); + const email = input.value.trim(); if (!email || !isValidEmail(email)) { - emailInput.classList.add("error"); - errorMsg.classList.add("visible"); + setInvalid(signup, true); return; } - setLoading(true); + setLoading(submit, true); try { // The squeeze page collects email only; the API still expects the @@ -284,7 +384,7 @@ const t = await createTranslator(locale); if (resp.status !== 201) { if (data.error) window.toast.error(data.error); - setLoading(false); + setLoading(submit, false); return; } @@ -293,16 +393,10 @@ const t = await createTranslator(locale); form_id: "launch_squeeze", }); - formWrap.style.transition = "opacity 0.4s ease"; - formWrap.style.opacity = "0"; - setTimeout(() => { - formWrap.style.display = "none"; - successWrap.style.display = "flex"; - requestAnimationFrame(() => successWrap.classList.add("visible")); - }, 400); + showReceipt(email, signup); } catch (error) { window.toast.error((error as Error).message); - setLoading(false); + setLoading(submit, false); } }); } diff --git a/website/src/components/features/launch/WaysSection.astro b/website/src/components/features/launch/WaysSection.astro new file mode 100644 index 00000000..2fbe14b6 --- /dev/null +++ b/website/src/components/features/launch/WaysSection.astro @@ -0,0 +1,112 @@ +--- +import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; +import { APP_LINKS } from "@/constants/app-links"; +import { SITE_SOCIALS } from "@/constants/site-socials"; + +const locale = getLocaleFromUrl(Astro.url.pathname); +const t = await createTranslator(locale); +--- + +
+
+

{t("launch.wallet.title")}

+

{t("launch.wallet.body")}

+ + {t("launch.wallet.ios")} + + + {t("launch.wallet.android")} + +
+ +
+

{t("launch.community.title")}

+

{t("launch.community.body")}

+ + {t("launch.community.telegram")} + +
+
+ + diff --git a/website/src/i18n/de-DE.json b/website/src/i18n/de-DE.json index d63a18ad..c16989c1 100644 --- a/website/src/i18n/de-DE.json +++ b/website/src/i18n/de-DE.json @@ -858,29 +858,54 @@ "description": "Quantensicheres verschlüsseltes Geld. 21 Millionen Angebotsgrenze, Proof of Work. Erhalten Sie die Launch-Details." }, "hero": { - "eyebrow": "Quantensicheres verschlüsseltes Geld" + "eyebrow": "Quantensicheres verschlüsseltes Geld", + "sub": "Das Mainnet ist live. Als Nächstes kommen Swaps. Erfahren Sie es zuerst." }, "signup": { - "headline": "Launch-Details erhalten.", - "email_placeholder": "your@email.com", + "email_placeholder": "E-Mail-Adresse eingeben", "email_aria": "E-Mail-Adresse", - "submit_label": "BENACHRICHTIGEN", + "submit_label": "Benachrichtigen", "error_message": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", + "micro": "Eine E-Mail, wenn Swaps live gehen. Sonst nichts.", "success": { "headline": "Sie stehen auf der Liste.", - "sub": "Die Launch-Details kommen in Ihr Postfach." + "sub": "Sie erfahren es vor allen anderen." } }, - "facts": { - "supply": { - "label": "ANGEBOTSGRENZE", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "Quantensicher", + "body": "Geld, das Quantencomputer nicht knacken können. Post-Quanten-sicher ab dem Genesis-Block." + }, + "encrypted": { + "title": "Verschlüsselt", + "body": "Halten Sie Ihre Guthaben transparent oder verborgen." }, - "consensus": { - "label": "PROOF OF WORK" + "sound_money": { + "title": "Solides Geld", + "body": "21.000.000 $QTC Angebotsgrenze. Proof-of-Work. Vollendet, was Bitcoin begonnen hat." } }, - "mining_cta": "MINING-GUIDE LESEN" + "bridge": "Verschlüsseltes Peer-to-Peer-Bargeld.", + "mining": { + "title": "Mining starten", + "body": "Machen Sie Ihre Rechenleistung zu $QTC.", + "cta": "Geben Sie Ihrem LLM den Mining-Guide" + }, + "wallet": { + "title": "Swappen, sparen und bezahlen mit $QTC.", + "body": "Eine Wallet für die Post-Quanten-Zukunft. iOS und Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "Diskutieren Sie mit.", + "body": "Die Menschen, die Quantus bauen und minen, tauschen sich zuerst hier aus.", + "telegram": "Telegram" + }, + "final": { + "title": "Erfahren Sie es zuerst, wenn Swaps live gehen." + } }, "blog": { "title": "Die neuesten Nachrichten von Quantus.", diff --git a/website/src/i18n/en-US.json b/website/src/i18n/en-US.json index 03486661..91a63a0f 100644 --- a/website/src/i18n/en-US.json +++ b/website/src/i18n/en-US.json @@ -854,29 +854,54 @@ "description": "Quantum secure encrypted money. 21 million supply cap, Proof of Work. Get launch details." }, "hero": { - "eyebrow": "Quantum Secure Encrypted Money" + "eyebrow": "Quantum Secure Encrypted Money", + "sub": "Mainnet is live. Swaps are next. Get notified first." }, "signup": { - "headline": "Get launch details.", - "email_placeholder": "your@email.com", + "email_placeholder": "Enter your email", "email_aria": "Email address", - "submit_label": "NOTIFY ME", + "submit_label": "Notify me", "error_message": "Please enter a valid email address.", + "micro": "One email when swaps go live. Nothing else.", "success": { "headline": "You're on the list.", - "sub": "Launch details will land in your inbox." + "sub": "You'll hear before anyone else does." } }, - "facts": { - "supply": { - "label": "SUPPLY CAP", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "Quantum secure", + "body": "Money quantum computers can't break. Post-quantum from genesis." + }, + "encrypted": { + "title": "Encrypted", + "body": "Keep your funds transparent or hidden." }, - "consensus": { - "label": "PROOF OF WORK" + "sound_money": { + "title": "Sound money", + "body": "21,000,000 $QTC supply cap. Proof-of-work. Finishing what Bitcoin started." } }, - "mining_cta": "READ THE MINING GUIDE" + "bridge": "Peer-to-peer encrypted cash.", + "mining": { + "title": "Start mining", + "body": "Turn your compute into $QTC.", + "cta": "Point your LLM at the mining guide" + }, + "wallet": { + "title": "Swap, stack, and pay with $QTC.", + "body": "A wallet built for the post-quantum future. iOS and Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "Join the conversation.", + "body": "The people building and mining Quantus talk here first.", + "telegram": "Telegram" + }, + "final": { + "title": "Be first when swaps go live." + } }, "blog": { "title": "The latest news from the Quantus.", diff --git a/website/src/i18n/es-ES.json b/website/src/i18n/es-ES.json index cad62b2a..27a15e38 100644 --- a/website/src/i18n/es-ES.json +++ b/website/src/i18n/es-ES.json @@ -858,29 +858,54 @@ "description": "Dinero encriptado seguro cuánticamente. Suministro máximo de 21 millones, Proof of Work. Reciba los detalles del lanzamiento." }, "hero": { - "eyebrow": "Dinero Encriptado Seguro Cuánticamente" + "eyebrow": "Dinero Encriptado Seguro Cuánticamente", + "sub": "La mainnet ya está activa. Los swaps son lo siguiente. Reciba el aviso antes que nadie." }, "signup": { - "headline": "Reciba los detalles del lanzamiento.", - "email_placeholder": "your@email.com", + "email_placeholder": "Introduzca su correo electrónico", "email_aria": "Dirección de correo electrónico", - "submit_label": "AVÍSAME", + "submit_label": "Avísame", "error_message": "Por favor, introduzca una dirección de correo electrónico válida.", + "micro": "Un correo cuando los swaps estén activos. Nada más.", "success": { "headline": "Está en la lista.", - "sub": "Los detalles del lanzamiento llegarán a su bandeja de entrada." + "sub": "Se enterará antes que nadie." } }, - "facts": { - "supply": { - "label": "SUMINISTRO MÁXIMO", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "Seguro cuánticamente", + "body": "Dinero que los ordenadores cuánticos no pueden romper. Poscuántico desde el génesis." + }, + "encrypted": { + "title": "Encriptado", + "body": "Mantenga sus fondos transparentes u ocultos." }, - "consensus": { - "label": "PROOF OF WORK" + "sound_money": { + "title": "Dinero sólido", + "body": "Suministro máximo de 21.000.000 $QTC. Proof-of-work. Terminando lo que Bitcoin empezó." } }, - "mining_cta": "LEER LA GUÍA DE MINERÍA" + "bridge": "Efectivo encriptado entre pares.", + "mining": { + "title": "Empiece a minar", + "body": "Convierta su capacidad de cómputo en $QTC.", + "cta": "Pase la guía de minería a su LLM" + }, + "wallet": { + "title": "Intercambie, acumule y pague con $QTC.", + "body": "Una billetera creada para el futuro poscuántico. iOS y Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "Únase a la conversación.", + "body": "Quienes construyen y minan Quantus hablan aquí primero.", + "telegram": "Telegram" + }, + "final": { + "title": "Sea el primero en saber cuándo llegan los swaps." + } }, "blog": { "title": "Las últimas noticias de Quantus.", diff --git a/website/src/i18n/hi-IN.json b/website/src/i18n/hi-IN.json index cd6e1cad..74134305 100644 --- a/website/src/i18n/hi-IN.json +++ b/website/src/i18n/hi-IN.json @@ -854,29 +854,54 @@ "description": "क्वांटम-सुरक्षित एन्क्रिप्टेड मनी। 21 मिलियन सप्लाई कैप, Proof of Work। लॉन्च की जानकारी पाएं।" }, "hero": { - "eyebrow": "क्वांटम-सुरक्षित एन्क्रिप्टेड मनी" + "eyebrow": "क्वांटम-सुरक्षित एन्क्रिप्टेड मनी", + "sub": "मेननेट लाइव है। अगला कदम स्वैप है। सबसे पहले सूचना पाएं।" }, "signup": { - "headline": "लॉन्च की जानकारी पाएं।", - "email_placeholder": "your@email.com", + "email_placeholder": "अपना ईमेल दर्ज करें", "email_aria": "ईमेल पता", "submit_label": "मुझे सूचित करें", "error_message": "कृपया एक मान्य ईमेल पता दर्ज करें।", + "micro": "स्वैप लाइव होने पर सिर्फ एक ईमेल। और कुछ नहीं।", "success": { "headline": "आप सूची में हैं।", - "sub": "लॉन्च की जानकारी आपके इनबॉक्स में पहुंचेगी।" + "sub": "आपको सबसे पहले पता चलेगा।" } }, - "facts": { - "supply": { - "label": "सप्लाई कैप", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "क्वांटम-सुरक्षित", + "body": "ऐसा पैसा जिसे क्वांटम कंप्यूटर नहीं तोड़ सकते। जेनेसिस से ही पोस्ट-क्वांटम।" }, - "consensus": { - "label": "PROOF OF WORK" + "encrypted": { + "title": "एन्क्रिप्टेड", + "body": "अपने फंड को पारदर्शी रखें या छिपाकर।" + }, + "sound_money": { + "title": "साउंड मनी", + "body": "21 मिलियन $QTC सप्लाई कैप। Proof-of-work। Bitcoin ने जो शुरू किया, उसे पूरा करना।" } }, - "mining_cta": "माइनिंग गाइड पढ़ें" + "bridge": "पीयर-टू-पीयर एन्क्रिप्टेड कैश।", + "mining": { + "title": "माइनिंग शुरू करें", + "body": "अपनी कंप्यूटिंग पावर को $QTC में बदलें।", + "cta": "अपने LLM को माइनिंग गाइड दें" + }, + "wallet": { + "title": "$QTC से स्वैप करें, जमा करें और भुगतान करें।", + "body": "पोस्ट-क्वांटम भविष्य के लिए बना वॉलेट। iOS और Android।", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "बातचीत में शामिल हों।", + "body": "Quantus बनाने और माइन करने वाले लोग सबसे पहले यहीं बात करते हैं।", + "telegram": "Telegram" + }, + "final": { + "title": "स्वैप लाइव होते ही सबसे पहले जानें।" + } }, "blog": { "title": "Quantus की नवीनतम खबरें।", diff --git a/website/src/i18n/id-ID.json b/website/src/i18n/id-ID.json index 0f71fe1a..6d39fa55 100644 --- a/website/src/i18n/id-ID.json +++ b/website/src/i18n/id-ID.json @@ -854,29 +854,54 @@ "description": "Uang terenkripsi aman secara kuantum. Batas pasokan 21 juta, Proof of Work. Dapatkan detail peluncuran." }, "hero": { - "eyebrow": "Uang Terenkripsi Aman Secara Kuantum" + "eyebrow": "Uang Terenkripsi Aman Secara Kuantum", + "sub": "Mainnet sudah aktif. Berikutnya swap. Dapatkan notifikasi lebih dulu." }, "signup": { - "headline": "Dapatkan detail peluncuran.", - "email_placeholder": "your@email.com", + "email_placeholder": "Masukkan email Anda", "email_aria": "Alamat email", - "submit_label": "BERI TAHU SAYA", + "submit_label": "Beri tahu saya", "error_message": "Harap masukkan alamat email yang valid.", + "micro": "Satu email saat swap aktif. Tidak lebih.", "success": { "headline": "Anda sudah terdaftar.", - "sub": "Detail peluncuran akan dikirim ke kotak masuk Anda." + "sub": "Anda akan tahu sebelum orang lain." } }, - "facts": { - "supply": { - "label": "BATAS PASOKAN", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "Aman secara kuantum", + "body": "Uang yang tidak dapat dibobol komputer kuantum. Pascakuantum sejak genesis." }, - "consensus": { - "label": "PROOF OF WORK" + "encrypted": { + "title": "Terenkripsi", + "body": "Simpan dana Anda secara transparan atau tersembunyi." + }, + "sound_money": { + "title": "Uang yang sehat", + "body": "Batas pasokan 21.000.000 $QTC. Proof-of-work. Menuntaskan apa yang dimulai Bitcoin." } }, - "mining_cta": "BACA PANDUAN MINING" + "bridge": "Uang tunai terenkripsi peer-to-peer.", + "mining": { + "title": "Mulai menambang", + "body": "Ubah daya komputasi Anda menjadi $QTC.", + "cta": "Arahkan LLM Anda ke panduan mining" + }, + "wallet": { + "title": "Swap, simpan, dan bayar dengan $QTC.", + "body": "Dompet yang dibuat untuk masa depan pascakuantum. iOS dan Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "Bergabung dalam percakapan.", + "body": "Orang-orang yang membangun dan menambang Quantus berdiskusi di sini lebih dulu.", + "telegram": "Telegram" + }, + "final": { + "title": "Jadilah yang pertama tahu saat swap aktif." + } }, "blog": { "title": "Berita terbaru dari Quantus.", diff --git a/website/src/i18n/ja-JP.json b/website/src/i18n/ja-JP.json index 91f1cd84..8aa3210b 100644 --- a/website/src/i18n/ja-JP.json +++ b/website/src/i18n/ja-JP.json @@ -858,29 +858,54 @@ "description": "量子セキュアな暗号化通貨。供給上限2,100万、Proof of Work。ローンチ情報を受け取る。" }, "hero": { - "eyebrow": "量子セキュアな暗号化通貨" + "eyebrow": "量子セキュアな暗号化通貨", + "sub": "メインネットは稼働中。次はスワップです。いち早く通知を受け取りましょう。" }, "signup": { - "headline": "ローンチ情報を受け取る。", - "email_placeholder": "your@email.com", + "email_placeholder": "メールアドレスを入力", "email_aria": "メールアドレス", "submit_label": "通知を受け取る", "error_message": "有効なメールアドレスを入力してください。", + "micro": "スワップ開始時にメールを1通だけお送りします。それ以外は送りません。", "success": { "headline": "登録が完了しました。", - "sub": "ローンチ情報をメールでお届けします。" + "sub": "誰よりも早くお知らせします。" } }, - "facts": { - "supply": { - "label": "供給上限", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "量子セキュア", + "body": "量子コンピュータにも破られないお金。ジェネシスからポスト量子。" }, - "consensus": { - "label": "PROOF OF WORK" + "encrypted": { + "title": "暗号化", + "body": "資金は公開にも非公開にもできます。" + }, + "sound_money": { + "title": "健全なお金", + "body": "供給上限2,100万 $QTC。プルーフ・オブ・ワーク。ビットコインが始めたことを完成させる。" } }, - "mining_cta": "マイニングガイドを読む" + "bridge": "ピアツーピアの暗号化キャッシュ。", + "mining": { + "title": "マイニングを始める", + "body": "計算資源を $QTC に変えましょう。", + "cta": "LLMにマイニングガイドを読ませる" + }, + "wallet": { + "title": "$QTC でスワップ、保有、支払い。", + "body": "ポスト量子時代のためのウォレット。iOS と Android に対応。", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "会話に参加しましょう。", + "body": "Quantus を開発・マイニングしている人たちは、まずここで話しています。", + "telegram": "Telegram" + }, + "final": { + "title": "スワップ開始をいち早く知る。" + } }, "blog": { "title": "Quantusからの最新ニュース。", diff --git a/website/src/i18n/ko-KR.json b/website/src/i18n/ko-KR.json index dceba0ab..c170043a 100644 --- a/website/src/i18n/ko-KR.json +++ b/website/src/i18n/ko-KR.json @@ -854,29 +854,54 @@ "description": "양자 보안 암호화 화폐. 총 발행 한도 2,100만, Proof of Work. 출시 소식을 받아보세요." }, "hero": { - "eyebrow": "양자 보안 암호화 화폐" + "eyebrow": "양자 보안 암호화 화폐", + "sub": "메인넷이 출시되었습니다. 다음은 스왑입니다. 가장 먼저 알림을 받아보세요." }, "signup": { - "headline": "출시 소식을 받아보세요.", - "email_placeholder": "your@email.com", + "email_placeholder": "이메일을 입력하세요", "email_aria": "이메일 주소", "submit_label": "알림 받기", "error_message": "유효한 이메일 주소를 입력하세요.", + "micro": "스왑이 시작되면 이메일 한 통만 보내드립니다. 그 외에는 없습니다.", "success": { "headline": "등록되었습니다.", - "sub": "출시 소식을 이메일로 보내드립니다." + "sub": "누구보다 먼저 소식을 전해드립니다." } }, - "facts": { - "supply": { - "label": "총 발행 한도", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "양자 보안", + "body": "양자 컴퓨터도 깰 수 없는 화폐. 제네시스부터 포스트 양자." + }, + "encrypted": { + "title": "암호화", + "body": "자금을 공개하거나 숨길 수 있습니다." }, - "consensus": { - "label": "PROOF OF WORK" + "sound_money": { + "title": "건전한 화폐", + "body": "2,100만 $QTC 발행 한도. 작업 증명. 비트코인이 시작한 일을 완성합니다." } }, - "mining_cta": "채굴 가이드 읽기" + "bridge": "P2P 암호화 현금.", + "mining": { + "title": "채굴 시작하기", + "body": "컴퓨팅 파워를 $QTC로 바꾸세요.", + "cta": "LLM에게 채굴 가이드를 맡기세요" + }, + "wallet": { + "title": "$QTC로 스왑하고, 모으고, 결제하세요.", + "body": "포스트 양자 시대를 위한 지갑. iOS 및 Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "대화에 참여하세요.", + "body": "Quantus를 만들고 채굴하는 사람들이 가장 먼저 이야기하는 곳입니다.", + "telegram": "Telegram" + }, + "final": { + "title": "스왑 출시 소식을 가장 먼저 받아보세요." + } }, "blog": { "title": "Quantus의 최신 소식.", diff --git a/website/src/i18n/ru-RU.json b/website/src/i18n/ru-RU.json index b8b43f66..70c59979 100644 --- a/website/src/i18n/ru-RU.json +++ b/website/src/i18n/ru-RU.json @@ -854,29 +854,54 @@ "description": "Квантово-безопасные зашифрованные деньги. Максимальная эмиссия 21 млн, Proof of Work. Получите детали запуска." }, "hero": { - "eyebrow": "Квантово-безопасные зашифрованные деньги" + "eyebrow": "Квантово-безопасные зашифрованные деньги", + "sub": "Mainnet запущен. Следующий шаг — свопы. Узнайте первыми." }, "signup": { - "headline": "Получите детали запуска.", - "email_placeholder": "your@email.com", + "email_placeholder": "Введите ваш email", "email_aria": "Адрес электронной почты", - "submit_label": "СООБЩИТЬ МНЕ", + "submit_label": "Сообщить мне", "error_message": "Пожалуйста, введите корректный адрес электронной почты.", + "micro": "Одно письмо, когда свопы заработают. Больше ничего.", "success": { "headline": "Вы в списке.", - "sub": "Детали запуска придут на вашу почту." + "sub": "Вы узнаете раньше всех." } }, - "facts": { - "supply": { - "label": "МАКСИМАЛЬНАЯ ЭМИССИЯ", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "Квантовая безопасность", + "body": "Деньги, которые не взломает квантовый компьютер. Постквантовая защита с генезиса." }, - "consensus": { - "label": "PROOF OF WORK" + "encrypted": { + "title": "Шифрование", + "body": "Держите средства открытыми или скрытыми." + }, + "sound_money": { + "title": "Твёрдые деньги", + "body": "Максимальная эмиссия 21 000 000 $QTC. Proof-of-work. Завершаем то, что начал Bitcoin." } }, - "mining_cta": "ЧИТАТЬ РУКОВОДСТВО ПО МАЙНИНГУ" + "bridge": "Одноранговые зашифрованные наличные.", + "mining": { + "title": "Начните майнить", + "body": "Превратите вычислительную мощность в $QTC.", + "cta": "Передайте руководство по майнингу своей LLM" + }, + "wallet": { + "title": "Обменивайте, копите и платите в $QTC.", + "body": "Кошелёк для постквантового будущего. iOS и Android.", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "Присоединяйтесь к общению.", + "body": "Те, кто создаёт и майнит Quantus, первыми обсуждают всё здесь.", + "telegram": "Telegram" + }, + "final": { + "title": "Узнайте первыми, когда запустятся свопы." + } }, "blog": { "title": "Последние новости от Quantus.", diff --git a/website/src/i18n/zh-CN.json b/website/src/i18n/zh-CN.json index 608337c2..6f075259 100644 --- a/website/src/i18n/zh-CN.json +++ b/website/src/i18n/zh-CN.json @@ -847,29 +847,54 @@ "description": "量子安全加密货币。供应上限 2,100 万,工作量证明(Proof of Work)。获取上线详情。" }, "hero": { - "eyebrow": "量子安全加密货币" + "eyebrow": "量子安全加密货币", + "sub": "主网已上线。接下来是兑换功能。抢先获取通知。" }, "signup": { - "headline": "获取上线详情。", - "email_placeholder": "your@email.com", + "email_placeholder": "输入您的电子邮件", "email_aria": "电子邮件地址", "submit_label": "通知我", "error_message": "请输入有效的电子邮件地址。", + "micro": "兑换上线时只发一封邮件,别无其他。", "success": { "headline": "您已加入名单。", - "sub": "上线详情将发送至您的邮箱。" + "sub": "您将比任何人都更早知道。" } }, - "facts": { - "supply": { - "label": "供应上限", - "value": "21,000,000" + "cards": { + "quantum_secure": { + "title": "量子安全", + "body": "量子计算机无法破解的货币。从创世起即抗量子。" }, - "consensus": { - "label": "工作量证明" + "encrypted": { + "title": "加密", + "body": "资金可公开,也可隐藏。" + }, + "sound_money": { + "title": "稳健货币", + "body": "供应上限 2,100 万 $QTC。工作量证明。完成比特币未竟之事。" } }, - "mining_cta": "阅读挖矿指南" + "bridge": "点对点加密现金。", + "mining": { + "title": "开始挖矿", + "body": "将您的算力转化为 $QTC。", + "cta": "让您的 LLM 读取挖矿指南" + }, + "wallet": { + "title": "使用 $QTC 兑换、持有和支付。", + "body": "为后量子时代打造的钱包。支持 iOS 和 Android。", + "ios": "iOS", + "android": "Android" + }, + "community": { + "title": "加入对话。", + "body": "构建和挖掘 Quantus 的人们最先在这里交流。", + "telegram": "Telegram" + }, + "final": { + "title": "兑换上线时,第一时间知道。" + } }, "blog": { "title": "来自 Quantus 的最新消息。", diff --git a/website/src/pages/[lang]/launch.astro b/website/src/pages/[lang]/launch.astro index 3ef1c3ec..d7a08e58 100644 --- a/website/src/pages/[lang]/launch.astro +++ b/website/src/pages/[lang]/launch.astro @@ -1,87 +1,10 @@ --- -import Layout from "@/components/layout/Layout.astro"; -import { - createTranslator, - getLocaleFromUrl, - PREFIXED_LOCALES, -} from "@/utils/i18n"; -import { createMetadata } from "@/utils/create-metadata"; -import { JsonLdGraph } from "@/utils/build-json-ld"; -import { organizationJsonLd, websiteJsonLd } from "@/constants/default-jsonld"; -import HeroSection from "@/components/features/launch/HeroSection.astro"; -import SignupForm from "@/components/features/launch/SignupForm.astro"; -import FactsSection from "@/components/features/launch/FactsSection.astro"; -import AsciiFrame from "@/components/features/launch/AsciiFrame.astro"; +import LaunchPage from "@/components/features/launch/LaunchPage.astro"; +import { PREFIXED_LOCALES } from "@/utils/i18n"; export async function getStaticPaths() { return PREFIXED_LOCALES.map((lang) => ({ params: { lang } })); } - -const locale = getLocaleFromUrl(Astro.url.pathname); -const t = await createTranslator(locale); - -const metadata = createMetadata({ - title: t("launch.meta.title"), - description: t("launch.meta.description"), - pathname: Astro.url.pathname, -}); - -const jsonLd = JsonLdGraph({ - "@context": "https://schema.org", - "@graph": [websiteJsonLd, organizationJsonLd], -}); --- - -
- -
- - - -
-
-
- - + diff --git a/website/src/pages/launch.astro b/website/src/pages/launch.astro index 90c391b2..798e456b 100644 --- a/website/src/pages/launch.astro +++ b/website/src/pages/launch.astro @@ -1,79 +1,5 @@ --- -import Layout from "@/components/layout/Layout.astro"; -import { createTranslator, getLocaleFromUrl } from "@/utils/i18n"; -import { createMetadata } from "@/utils/create-metadata"; -import { JsonLdGraph } from "@/utils/build-json-ld"; -import { organizationJsonLd, websiteJsonLd } from "@/constants/default-jsonld"; -import HeroSection from "@/components/features/launch/HeroSection.astro"; -import SignupForm from "@/components/features/launch/SignupForm.astro"; -import FactsSection from "@/components/features/launch/FactsSection.astro"; -import AsciiFrame from "@/components/features/launch/AsciiFrame.astro"; - -const locale = getLocaleFromUrl(Astro.url.pathname); -const t = await createTranslator(locale); - -const metadata = createMetadata({ - title: t("launch.meta.title"), - description: t("launch.meta.description"), - pathname: Astro.url.pathname, -}); - -const jsonLd = JsonLdGraph({ - "@context": "https://schema.org", - "@graph": [websiteJsonLd, organizationJsonLd], -}); +import LaunchPage from "@/components/features/launch/LaunchPage.astro"; --- - -
- -
- - - -
-
-
- - + diff --git a/website/src/scripts/launch/field.ts b/website/src/scripts/launch/field.ts new file mode 100644 index 00000000..a56798ea --- /dev/null +++ b/website/src/scripts/launch/field.ts @@ -0,0 +1,127 @@ +/** + * Chladni field behind the /launch frame. + * + * Deliberately independent of scripts/home/*: Home owns hero.ts and its + * shared chunk, and this page's field behaves differently — one pattern for + * the whole page whose phase follows scroll position, drifting upward as the + * page scrolls and morphing slowly on its own. + * + * Under prefers-reduced-motion it paints once and stays put: no morph, no + * drift, no repaint on scroll. + */ + +const CHARS = " .:;+xX|"; + +/** How much of the field reads as nodal line versus empty plate. */ +const DENSITY = 11; +/** Ambient morph speed. 0 would leave the field driven by scroll alone. */ +const MORPH = 2; + +/** Glyph cell for Geist Mono at 11px with the field's letter-spacing. */ +const CHAR_WIDTH = 7.4; +const LINE_HEIGHT = 11.6; + +/** Extra rows above and below the viewport so the drift never shows an edge. */ +const HEADROOM_RATIO = 0.5; +/** Minimum ms between repaints while morphing (~22fps is plenty here). */ +const PAINT_INTERVAL = 45; +/** Scroll progress change below which a scroll-only repaint is skipped. */ +const SCROLL_EPSILON = 0.015; + +export const initLaunchField = (pre: HTMLElement) => { + const reduceMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + const morph = reduceMotion ? 0 : MORPH; + + let cols = 0; + let rows = 0; + let headroom = 0; + let phase = 0; + let lastFrame = 0; + let lastPaint = 0; + let drawnAt = -1; + let scrollQueued = false; + + const progress = () => { + if (reduceMotion) return 0; + const max = document.documentElement.scrollHeight - window.innerHeight; + return max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0; + }; + + const measure = () => { + const width = window.innerWidth; + const height = window.innerHeight; + headroom = reduceMotion ? 0 : height * HEADROOM_RATIO; + cols = Math.ceil(width / CHAR_WIDTH) + 2; + rows = Math.ceil((height + headroom * 2) / LINE_HEIGHT); + drawnAt = -1; + }; + + const paint = (p: number, force: boolean) => { + if (!force && morph === 0 && Math.abs(p - drawnAt) < SCROLL_EPSILON) { + return; + } + drawnAt = p; + + const a = 2.4 + p * 0.55 + Math.sin(phase * 0.16) * 0.85; + const b = 3.3 - p * 0.4 + Math.cos(phase * 0.11) * 0.65; + const last = CHARS.length - 1; + let out = ""; + + for (let y = 0; y < rows; y++) { + const v = y / rows; + for (let x = 0; x < cols; x++) { + const u = x / cols; + const z = + Math.sin(a * Math.PI * u) * Math.sin(b * Math.PI * v) + + Math.sin(b * Math.PI * u) * Math.sin(a * Math.PI * v); + const strength = Math.min(1, Math.abs(z) * (DENSITY / 6)); + const i = Math.floor((1 - strength) * CHARS.length); + out += CHARS[Math.max(0, Math.min(last, i))]; + } + out += "\n"; + } + + pre.textContent = out; + }; + + const applyDrift = () => { + pre.style.transform = `translateY(${-progress() * headroom}px)`; + }; + + const loop = (now: number) => { + if (lastFrame) phase += ((now - lastFrame) / 1000) * (morph * 0.28); + lastFrame = now; + if (now - lastPaint > PAINT_INTERVAL) { + lastPaint = now; + paint(progress(), true); + } + requestAnimationFrame(loop); + }; + + const onScroll = () => { + if (scrollQueued) return; + scrollQueued = true; + requestAnimationFrame(() => { + scrollQueued = false; + applyDrift(); + if (morph === 0) paint(progress(), false); + }); + }; + + measure(); + paint(0, true); + + window.addEventListener("resize", () => { + measure(); + paint(progress(), true); + if (!reduceMotion) applyDrift(); + }); + + if (reduceMotion) return; + + applyDrift(); + window.addEventListener("scroll", onScroll, { passive: true }); + if (morph > 0) requestAnimationFrame(loop); +};