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

+12
View File
@@ -0,0 +1,12 @@
// Full Flutter destinations are intentionally unset until the real app exists.
function validatedUrl(value: string | undefined) {
if (!value) return undefined;
const url = new URL(value);
if (!['https:', 'http:'].includes(url.protocol))
throw new Error('Flutter URLs must be HTTP(S) URLs');
return url.toString();
}
export const appLinks = {
register: validatedUrl(import.meta.env.PUBLIC_FLUTTER_REGISTER_URL),
login: validatedUrl(import.meta.env.PUBLIC_FLUTTER_LOGIN_URL),
};
+30
View File
@@ -0,0 +1,30 @@
export const cryptoCurrencies = ['BTC', 'ETH', 'USDT'] as const;
export const fiatCurrencies = ['EUR', 'USD'] as const;
export type Crypto = (typeof cryptoCurrencies)[number];
export type Fiat = (typeof fiatCurrencies)[number];
// Fixed, deliberately illustrative fixtures. Never use for an actual exchange.
export const demoRates: Record<Crypto, Record<Fiat, number>> = {
BTC: { EUR: 57500, USD: 62500 },
ETH: { EUR: 2300, USD: 2500 },
USDT: { EUR: 0.92, USD: 1 },
};
export function parseAmount(raw: string): number {
const normalized = raw.trim().replace(',', '.');
if (!/^\d+(?:\.\d{0,8})?$/.test(normalized)) return NaN;
return Number(normalized);
}
export function calculateQuote(amount: number, crypto: Crypto, fiat: Fiat) {
if (!Number.isFinite(amount) || amount <= 0 || amount > 1_000_000)
throw new RangeError('Invalid amount');
if (!cryptoCurrencies.includes(crypto) || !fiatCurrencies.includes(fiat))
throw new RangeError('Invalid currency');
const rate = demoRates[crypto][fiat];
const grossCents = Math.round(amount * rate * 100);
const feeCents = Math.round(grossCents * 0.01);
return {
rate,
fee: feeCents / 100,
receive: (grossCents - feeCents) / 100,
demo: true,
};
}