Prepare product layout and reusable Flutter theme

Relocate the existing Astro website and preserve its assets and hosting configuration. Add the consumer/backend scaffold and themed Flutter package with consumer tests. Related to #1.
This commit is contained in:
Stephan D committed 2026-09-11 15:36:58 +02:00
1 parent ef65a40213
commit d9318053e8
46 files changed
+703 -1

No files matched your search

+296
View File
@@ -0,0 +1,296 @@
---
import Layout from '../../layouts/Layout.astro';
import Footer from '../../components/Footer.astro';
import AccountDialog from '../../components/AccountDialog.astro';
import { appLinks } from '../../lib/app-links';
import { copy, languages, type Lang } from '../../i18n/copy';
export function getStaticPaths() {
return languages.map((lang) => ({ params: { lang } }));
}
const lang = Astro.params.lang as Lang;
const t = copy[lang];
---
<Layout lang={lang}>
<div
class="hero-shell"
data-register-url={appLinks.register}
data-login-url={appLinks.login}
>
<header class="site-header wrap">
<a class="wordmark" href={`/${lang}/`} aria-label="SXCHNGE"
><span class="brand-icon" aria-hidden="true">↗</span> sxchnge<span
class="brand-dot">®</span
></a
>
<nav
class="desktop-nav"
aria-label={lang === 'en' ? 'Main navigation' : 'Основная навигация'}
>
{
t.nav.map((label, i) => (
<a href={['#how', '#why', '#faq'][i]}>{label}</a>
))
}
</nav>
<div class="header-actions">
<div
class="language-switch"
aria-label={lang === 'en' ? 'Language' : 'Язык'}
>
<a
href="/en/"
lang="en"
aria-current={lang === 'en' ? 'page' : undefined}>EN</a
><span>/</span><a
href="/ru/"
lang="ru"
aria-current={lang === 'ru' ? 'page' : undefined}>RU</a>
</div><button class="login-button" data-auth="login"
>{t.login}<span aria-hidden="true">↗</span></button>
</div>
</header>
<main id="main">
<noscript
><p class="no-script">
{
lang === 'en'
? 'Enable JavaScript to use the demo calculator and account preview. All information and policies remain available below.'
: 'Включите JavaScript для демо-калькулятора и просмотра регистрации. Вся информация и документы доступны ниже.'
}
</p></noscript
>
<section class="hero wrap" aria-labelledby="hero-title">
<div class="hero-copy">
<p class="eyebrow"><span class="status-dot"></span>{t.eyebrow}</p><h1
id="hero-title"
>
{t.headline[0]}<br /><span>{t.headline[1]}</span><span
class="headline-star"
aria-hidden="true">✳</span>
</h1><p class="hero-intro">{t.intro}</p><div class="hero-actions">
<button class="button button-dark" data-auth="register"
>{t.register}<span aria-hidden="true">↗</span></button
><a class="text-link" href="#how"
>{t.subcta}<span aria-hidden="true">↓</span></a>
</div><div class="hero-signoff">
<span class="mini-line" aria-hidden="true"></span>{t.heroNote}
</div>
</div>
<div class="exchange-stage">
<div class="orbit" aria-hidden="true"></div><div
class="stage-caption"
aria-hidden="true"
>
MAKE YOUR MOVE ↗
</div><section class="exchange-card" aria-labelledby="exchange-title">
<div class="exchange-heading">
<h2 id="exchange-title">{t.preview}</h2><span class="demo-badge"
>{t.demo}</span>
</div><div class="amount-box">
<label for="send-amount">{t.send}</label><div class="amount-row">
<input
id="send-amount"
type="text"
inputmode="decimal"
value="1"
autocomplete="off"
aria-describedby="amount-error quote-disclaimer"
/><div class="currency-picker">
<span class="coin coin-btc" id="coin-icon" aria-hidden="true"
>₿</span
><select id="crypto" aria-label={t.crypto}
><option value="BTC">BTC</option><option value="ETH"
>ETH</option
><option value="USDT">USDT</option></select>
</div>
</div>
</div><div class="direction-divider" aria-hidden="true">
<span>↓</span>
</div><div class="amount-box receive-box">
<label for="receive-amount">{t.receive}</label><div
class="amount-row"
>
<output
id="receive-amount"
aria-label={t.quoteLabel}
aria-live="polite">56,925.00</output
><div class="currency-picker">
<span class="coin coin-fiat" id="fiat-icon" aria-hidden="true"
>€</span
><select id="fiat" aria-label={t.fiat}
><option value="EUR">EUR</option><option value="USD"
>USD</option
></select>
</div>
</div>
</div><p id="amount-error" class="amount-error" hidden>
{t.invalid}
</p><dl class="quote-details">
<div>
<dt>{t.sampleRate}</dt><dd id="quote-rate">
1 BTC ≈ 57,500 EUR
</dd>
</div><div>
<dt>{t.fee}</dt><dd id="quote-fee">575.00 EUR</dd>
</div><div><dt>{t.payout}</dt><dd>{t.bank}</dd></div>
</dl><button
class="button button-mint exchange-submit"
data-auth="register"
>{t.exchange}<span aria-hidden="true">↗</span></button
><p class="quote-disclaimer" id="quote-disclaimer">
{t.disclaimer}
</p>
</section><div class="stage-bottom" aria-hidden="true">
<span>₿</span><span>→</span><span>€</span><div class="stage-line">
</div><span>01 — ∞</span>
</div>
</div>
</section>
<div class="ticker" aria-hidden="true">
<div>
{
[...t.strip, ...t.strip].map((s) => (
<span>
{s}
<b>✳</b>
</span>
))
}
</div>
</div>
<section class="section how-section" id="how" aria-labelledby="how-title">
<div class="wrap">
<div class="section-heading">
<div>
<p class="eyebrow">{t.howLabel}</p><h2 id="how-title">
{t.howTitle}
</h2>
</div><p>{t.howIntro}</p>
</div><div class="steps">
{
t.steps.map(([title, body], i) => (
<article class="step">
<>
<div class="step-top">
<>
<span class="step-number">0{i + 1}</span>
<span class="step-arrow" aria-hidden="true">
{i === 2 ? '↗' : '→'}
</span>
</>
</div>
<h3>{title}</h3>
<p>{body}</p>
</>
</article>
))
}
</div>
</div>
</section>
<section class="section why-section" id="why" aria-labelledby="why-title">
<div class="wrap">
<div class="section-heading">
<div>
<p class="eyebrow">{t.whyLabel}</p><h2 id="why-title">
{t.whyTitle}
</h2>
</div><p>{t.whyIntro}</p>
</div><div class="feature-grid">
<article class="feature-card clarity-card">
<div class="feature-top">
<span class="feature-symbol" aria-hidden="true">↗</span><span
class="feature-index">/ 01</span>
</div><div class="clarity-visual" aria-hidden="true">
<div>
<span>BTC</span><span class="visual-line"></span><span
>EUR</span>
</div><div class="clarity-flow">
<span>₿</span><span>⟶</span><span>€</span>
</div><div class="visual-ruler">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i
></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i
></i><i></i><i></i><i></i>
</div>
</div><h3>{t.features[0][0]}</h3><p>{t.features[0][1]}</p>
</article><article class="feature-card choice-card">
<div class="feature-top">
<span class="feature-symbol" aria-hidden="true">↔</span><span
class="feature-index">/ 02</span>
</div><div class="currency-visual" aria-hidden="true">
<span>₿</span><span>Ξ</span><span>₮</span><span>€</span><span
>$</span>
</div><span class="pair-note">{t.pairNote}</span><h3>
{t.features[1][0]}
</h3><p>{t.features[1][1]}</p>
</article><article class="verification-card">
<div class="verification-mark" aria-hidden="true">
<svg viewBox="0 0 64 64" fill="none"
><path
d="M32 6 52 14v17c0 13-20 25-20 25S12 44 12 31V14L32 6Z"
stroke="currentColor"
stroke-width="2"></path><path
d="m23 30 6 6 13-14"
stroke="currentColor"
stroke-width="2"></path></svg>
</div><div class="verification-copy">
<p class="eyebrow">{t.verification}</p><h3>
{t.features[2][0]}
</h3><p>{t.features[2][1]}</p>
</div><div class="verification-links">
<a href={`/${lang}/policies/kyc/`}
>{t.kyc}<span aria-hidden="true">↗</span></a
><a href={`/${lang}/policies/aml/`}
>{t.aml}<span aria-hidden="true">↗</span></a>
</div>
</article>
</div>
</div>
</section>
<section class="section faq-section" id="faq" aria-labelledby="faq-title">
<div class="wrap faq-grid">
<div>
<p class="eyebrow">{t.faqLabel}</p><h2 id="faq-title">
{t.faqTitle}
</h2><span class="faq-asterisk" aria-hidden="true">✳</span>
</div><div class="faq-list">
{
t.faqs.map(([q, a]) => (
<details name="faq">
<>
<summary>
{q}
<span aria-hidden="true">+</span>
</summary>
<p>{a}</p>
</>
</details>
))
}
</div>
</div>
</section>
<section class="closing-section" aria-labelledby="closing-title">
<div class="wrap closing-content">
<p class="eyebrow">{t.closingLabel}</p><h2 id="closing-title">
{t.closing}<span aria-hidden="true">↗</span>
</h2><div class="closing-actions">
<button class="button button-dark" data-auth="register"
>{t.register}<span aria-hidden="true">↗</span></button
><button class="closing-login" data-auth="login"
>{t.login} ↗</button>
</div><p class="closing-note">{t.closingNote}</p><span
class="closing-star"
aria-hidden="true">✳</span>
</div>
</section>
</main>
<Footer lang={lang} />
<AccountDialog lang={lang} />
</div>
<script>
import '../../scripts/home';
</script>
</Layout>
@@ -0,0 +1,52 @@
---
import Layout from '../../../layouts/Layout.astro';
import Footer from '../../../components/Footer.astro';
import { copy, languages, type Lang } from '../../../i18n/copy';
const policySlugs = ['terms', 'privacy', 'aml', 'kyc'];
export function getStaticPaths() {
return languages.flatMap((lang) =>
['terms', 'privacy', 'aml', 'kyc'].map((policy) => ({
params: { lang, policy },
})),
);
}
const lang = Astro.params.lang as Lang;
const policy = Astro.params.policy!;
const t = copy[lang];
const index = policySlugs.indexOf(policy);
---
<Layout
lang={lang}
title={`${t.legalNames[index]} — SXCHNGE`}
path={`policies/${policy}/`}
>
<header class="site-header wrap">
<a class="wordmark" href={`/${lang}/`}
><span class="brand-icon" aria-hidden="true">↗</span> sxchnge</a
><div class="language-switch">
<a
href={`/en/policies/${policy}/`}
lang="en"
aria-current={lang === 'en' ? 'page' : undefined}>EN</a
><span>/</span><a
href={`/ru/policies/${policy}/`}
lang="ru"
aria-current={lang === 'ru' ? 'page' : undefined}>RU</a>
</div>
</header>
<main class="policy-content wrap" id="main">
<a class="text-link" href={`/${lang}/`}>← {t.policyBack}</a><p
class="eyebrow policy-label"
>
{t.policyDraft}
</p><h1>{t.legalNames[index]}</h1><p class="policy-intro">
{t.policyIntro}
</p><section class="policy-placeholder">
<h2>{t.policyNeeded}</h2><ul>
{t.policyItems[index].map((item) => <li>{item}</li>)}
</ul>
</section><p class="policy-company">{t.companyFields[0]} · {t.support}</p>
</main>
<Footer lang={lang} />
</Layout>
+3
View File
@@ -0,0 +1,3 @@
---
return Astro.redirect('/en/');
---