feat: add four storefront identities

This commit is contained in:
2026-06-29 12:06:18 +01:00
parent e4bcd7578a
commit 561e948cb4
14 changed files with 391 additions and 0 deletions

30
config/store-config.ts Normal file
View File

@@ -0,0 +1,30 @@
/** Shape of a storefront's content config. Same shape for every store —
* the values differ, the structure never does. */
export type Review = { q: string; a: string };
export type StoreConfig = {
/** NEXT_PUBLIC_STORE_CODE; also the @store / shop-system id. */
code: string;
/** Switcher tab label in the showcase headbar. */
label: string;
/** Storefront wordmark. */
brandName: string;
/** Small line under / beside the wordmark. */
brandSub: string;
nav: string[];
cartLabel: string;
ctaLabel: string;
hero: {
eyebrow: string;
title: string;
body: string;
cta: string;
};
collection: { title: string; meta: string };
featured: { kicker: string; title: string; body: string; cta: string };
reviews: Review[];
footer: { links: string[]; note: string };
/** cents × multiplier ÷ 100 → display dollars (exec 16, crypto 3, collector 28, supper 1). */
priceMultiplier: number;
priceStyle: 'usd' | 'crypto' | 'appraisal' | 'plain';
};

34
config/store_collector.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { StoreConfig } from './store-config';
export const store: StoreConfig = {
code: 'collector',
label: 'Collector',
brandName: 'The Collectors Den',
brandSub: 'Graded · Vaulted · Appreciating',
nav: ['The Index', 'Provenance', 'The Vault', 'Appraisals'],
cartLabel: 'Portfolio',
ctaLabel: 'Acquire lot',
hero: {
eyebrow: 'Alternative Assets · Duck Class',
title: 'Ducks that have only ever appreciated.',
body: 'Independently graded, fully insured, sealed in the vault. Every lot in the catalogue has held — or grown — its value across every market on record.',
cta: 'View the Index',
},
collection: { title: 'The Standing Catalogue', meta: 'Six lots · all appreciating' },
featured: {
kicker: 'The Vault',
title: 'Climate-controlled, fully insured, never sold short.',
body: 'Every acquisition is graded by an independent panel, sealed with a certificate of authenticity, and held in the vault under your name. Liquidate any lot in forty-eight hours.',
cta: 'Request an appraisal',
},
reviews: [
{
q: 'Eleven years, two hundred and forty lots, and not one of them underwater. Try that with equities.',
a: 'Private collector, portfolio appraised at seven figures',
},
{ q: 'I no longer keep my faith in banks. I keep it in graded ducks, sealed in the vault.', a: 'Member, Assoc. of Duck Numismatics' },
],
footer: { links: ['The Index', 'The Vault', 'Appraisals', 'Provenance'], note: 'Member · Assoc. of Duck Numismatics' },
priceMultiplier: 28,
priceStyle: 'appraisal',
};

31
config/store_crypto.ts Normal file
View File

@@ -0,0 +1,31 @@
import type { StoreConfig } from './store-config';
export const store: StoreConfig = {
code: 'crypto',
label: 'Crypto',
brandName: 'DEGEN//DUCKS',
brandSub: 'Floor 0.012Ξ · 4200 minted',
nav: ['Drops', 'Rarity', 'Roadmap', 'Whitepaper'],
cartLabel: 'Stash',
ctaLabel: 'PUMP IT',
hero: {
eyebrow: 'LIVE · FLOOR 0.012Ξ',
title: 'Your unshakeable companion through every dip.',
body: 'When the chart bleeds red, your duck stays diamond. gm. buy the duck. hold the line. wagmi.',
cta: 'Mint Yours →',
},
collection: { title: '🔥 Latest Drops', meta: 'Floor 0.012Ξ · last sale 0.069Ξ' },
featured: {
kicker: 'Stake · Squeak · Earn',
title: 'Put your duck to work in the pond.',
body: 'Stake your duck in the pond and farm $QUACK while you sleep. Mint, stake, earn — wagmi.',
cta: 'Enter the pond',
},
reviews: [
{ q: 'down 60% this week but my HODL Duck hasnt blinked once. we are so back.', a: '@degenpond · ♥ 4.2K' },
{ q: 'talked to my Moon Duck during the crash instead of my PM. healthier honestly. gm.', a: '@onlyupjpeg · ♥ 9.1K' },
],
footer: { links: ['Discord', 'X', 'OpenSea', 'Docs'], note: 'not financial advice · gm' },
priceMultiplier: 3,
priceStyle: 'crypto',
};

34
config/store_executive.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { StoreConfig } from './store-config';
export const store: StoreConfig = {
code: 'executive',
label: 'Executive',
brandName: 'Onyx',
brandSub: 'Private Collection',
nav: ['The Collection', 'Bespoke', 'Concierge'],
cartLabel: 'Bag',
ctaLabel: 'Acquire',
hero: {
eyebrow: 'Est. MMXXIV · Private Collection',
title: 'Rehearse like the room is already watching.',
body: 'Hand-finished executive ducks for the founders, chairs and closers who refuse to wing the hard conversation.',
cta: 'Enter the Boardroom',
},
collection: { title: 'The Collection', meta: 'Six pieces · made to order' },
featured: {
kicker: 'The Atelier',
title: 'Monogram, hand-set into the base.',
body: 'A two-week commission. Your initials, your firm, the date of the round. Strictly limited, never repeated.',
cta: 'Commission a piece',
},
reviews: [
{
q: 'I rehearsed the layoffs speech in front of The Chairman for a week. Delivered it without flinching.',
a: 'Managing Partner, a fund you have heard of',
},
{ q: 'My board calls are sharper. The duck does not nod along — and that is the point.', a: 'Founder & CEO, Series C' },
],
footer: { links: ['Concierge', 'The Atelier', 'Press', 'Terms'], note: '© MMXXVI Onyx Executive' },
priceMultiplier: 16,
priceStyle: 'usd',
};

31
config/store_supper.ts Normal file
View File

@@ -0,0 +1,31 @@
import type { StoreConfig } from './store-config';
export const store: StoreConfig = {
code: 'supper',
label: 'Supper',
brandName: 'Rubber Dubber',
brandSub: 'Supper Club · Since 1960',
nav: ['Shop', 'The Table', 'Recipes', 'Our Story'],
cartLabel: 'Basket',
ctaLabel: 'Add to table',
hero: {
eyebrow: 'A seat at the table',
title: 'Nobody should have to eat supper alone.',
body: 'Meet the little companions who pull up a chair, keep you company, and never once steal your fries.',
cta: 'Set the Table →',
},
collection: { title: 'Pull up a chair', meta: 'Six companions, each with their own appetite.' },
featured: {
kicker: 'This week at the table',
title: 'Tomato Soup pairs beautifully with a thunderstorm.',
body: 'And grilled cheese, naturally. Theres a recipe card tucked into every box — handwritten, a little smudged.',
cta: 'See the pairing',
},
reviews: [
{ q: 'I set a place for Sunday Roast every week now. Supper feels a little less quiet.', a: 'Marta, eats at seven' },
{ q: 'My kid insists Tomato Soup gets its own spoon. We oblige.', a: 'Dev, table for three' },
],
footer: { links: ['Shop', 'Recipes', 'Journal', 'Stockists'], note: 'Made warm · est. 1960' },
priceMultiplier: 1,
priceStyle: 'plain',
};

19
config/stores.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Single source of truth for the storefronts that get built, deployed and
* linked from the showcase headbar switcher.
*
* code — NEXT_PUBLIC_STORE_CODE; the @store / shop-system id. Must match a
* config/store_<code>.ts file.
* label — switcher tab label.
* worker — the Cloudflare Worker this storefront deploys to.
* url — public URL the switcher tab links to (each storefront is its own
* deployment). Override per environment as needed.
*/
export type StoreEntry = { code: string; label: string; worker: string; url: string };
export const stores: StoreEntry[] = [
{ code: 'executive', label: 'Executive', worker: 'rubberquack-executive', url: 'https://executive.rubberquack.dev' },
{ code: 'crypto', label: 'Crypto', worker: 'rubberquack-crypto', url: 'https://crypto.rubberquack.dev' },
{ code: 'supper', label: 'Supper', worker: 'rubberquack-supper', url: 'https://supper.rubberquack.dev' },
{ code: 'collector', label: 'Collector', worker: 'rubberquack-collector', url: 'https://collector.rubberquack.dev' },
];

View File

@@ -0,0 +1,14 @@
import type { StoreMeta } from './types';
export const collector: StoreMeta = {
name: ['First Mallard', 'Black Lacquer', 'The Porcelain', 'The Bullion', 'Cobalt Reserve', 'The Vermillion'],
blurb: [
'Lot 001 of the founding run — never traded below issue.',
'Hand-lacquered shell; only nine known in this grade.',
'Kiln-pale and flawless. A blue-chip hold.',
'24-duckmark gilt. Tracks the index, lot for lot.',
'Deep cobalt, tightly held by serious hands.',
'Vermillion. Ungraded twins are simply unheard of.',
],
tag: ['MINT 70', 'GEM 68', 'PROOF 69', 'MINT 71', 'GEM 66', 'MINT 70'],
};

View File

@@ -0,0 +1,14 @@
import type { StoreMeta } from './types';
export const crypto: StoreMeta = {
name: ['HODL Duck', 'Whale Duck', 'Degen Duck', 'Lambo Duck', 'Moon Duck', 'Diamond Hands'],
blurb: [
'Refuses to sell. Ever. Not even at -82%.',
'Moves the floor just by entering the pond.',
'Aped in at the top. Still grinning.',
'Gold chrome. Engine noises sold separately.',
'Eyes locked on the chart. wen moon.',
'Grips so tight the duck squeaks diamonds.',
],
tag: ['RARE', 'LEGENDARY', 'COMMON', 'MYTHIC', 'EPIC', 'LEGENDARY'],
};

View File

@@ -0,0 +1,14 @@
import type { StoreMeta } from './types';
export const executive: StoreMeta = {
name: ['The Keynote', 'The Chairman', 'The Closer', 'The Founder', 'The Negotiator', 'The Successor'],
blurb: [
'Hand-finished matte shell for the cold open.',
'Weighted base. Holds your gaze without blinking.',
'The one you rehearse the hard "no" in front of.',
'Carry it into the raise. It has seen bigger rounds.',
'Silent, patient, unimpressed — exactly the audience you need.',
'For the succession talk you keep postponing.',
],
tag: ['No. 001 / 250', 'No. 014 / 180', 'Atelier', 'Signature', 'No. 007 / 90', 'By request'],
};

View File

@@ -0,0 +1,16 @@
import type { StoreMeta } from './types';
import { executive } from './executive';
import { crypto } from './crypto';
import { supper } from './supper';
import { collector } from './collector';
/** Per shop-system copy. Keyed by the shop system (store code up to the first
* `_`), the same rule the PostCSS plugin uses for styles. */
export const metaBySystem: Record<string, StoreMeta> = {
executive,
crypto,
supper,
collector,
};
export type { StoreMeta } from './types';

View File

@@ -0,0 +1,14 @@
import type { StoreMeta } from './types';
export const supper: StoreMeta = {
name: ['Sunday Roast', 'Midnight Snack', 'Porridge Pal', 'Honey Butter', 'Blueberry', 'Tomato Soup'],
blurb: [
'Sits patiently while the gravy settles.',
'Keeps you company at the open fridge, 1am.',
'Slow mornings, warm bowls, no rush.',
'Best friends with toast and a long Sunday.',
'Small, cheerful, smells faintly of muffins.',
'Pairs with grilled cheese and bad weather.',
],
tag: ['Serves 1', 'Serves 1', 'Best with oats', 'Best with toast', 'Brunch', 'Soup season'],
};

View File

@@ -0,0 +1,7 @@
/** Per-store copy for the six shared ducks. Arrays are index-aligned with
* `ducks` in ../products.ts — same product, different voice per storefront. */
export type StoreMeta = {
name: string[];
blurb: string[];
tag: string[];
};

6
src/styles/fonts.css Normal file
View File

@@ -0,0 +1,6 @@
/*
* All storefronts load from the same stylesheet. Each store selects its display
* / ui / mono faces purely through the --font-* tokens set in its @store block
* in globals.css. Only the active store's block survives the build.
*/
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Hanken+Grotesk:wght@400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400;1,6..72,500&family=Spectral:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap');

127
src/styles/globals.css Normal file
View File

@@ -0,0 +1,127 @@
@import './fonts.css';
@import 'tailwindcss';
/*
* Semantic brand tokens. Every component styles itself from these — the same
* markup, the same classes, across all four storefronts. Each `@store` block
* below rewrites the tokens (and a few layout switches) for one storefront; the
* postcss-storestyles plugin keeps exactly one block at build time, driven by
* NEXT_PUBLIC_STORE_CODE. The shipped CSS carries a single brand, no branching.
*/
@theme {
--color-bg: #0c0d10;
--color-surface: #15161b;
--color-surface2: #1d1f26;
--color-fg: #efeae0;
--color-muted: #9a958a;
--color-line: rgba(220, 210, 190, 0.13);
--color-accent: #c9a86a;
--color-accent2: #8a7a55;
--font-display: 'Cormorant Garamond', Georgia, serif;
--font-ui: 'Hanken Grotesk', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-card: 2px;
}
@keyframes floaty {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-9px);
}
}
@keyframes tick {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
body {
@apply bg-bg text-fg;
font-family: var(--font-ui);
}
a {
color: inherit;
text-decoration: none;
}
/* ── EXECUTIVE · Onyx — luxury, dark, gilt, Cormorant serif ── */
@store executive {
:root {
--color-bg: #0c0d10;
--color-surface: #15161b;
--color-surface2: #1d1f26;
--color-fg: #efeae0;
--color-muted: #9a958a;
--color-line: rgba(220, 210, 190, 0.13);
--color-accent: #c9a86a;
--color-accent2: #8a7a55;
--font-display: 'Cormorant Garamond', Georgia, serif;
--font-ui: 'Hanken Grotesk', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-card: 2px;
}
}
/* ── CRYPTO · DEGEN//DUCKS — neon, dark, glow, Space Grotesk ── */
@store crypto {
:root {
--color-bg: #070a0f;
--color-surface: #0d1219;
--color-surface2: #111927;
--color-fg: #e8f0ff;
--color-muted: #6f7d92;
--color-line: rgba(80, 255, 170, 0.2);
--color-accent: #33ff99;
--color-accent2: #b15bff;
--font-display: 'Space Grotesk', system-ui, sans-serif;
--font-ui: 'Space Grotesk', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-card: 16px;
}
}
/* ── SUPPER · Rubber Dubber — warm cream, cozy, Newsreader serif ── */
@store supper {
:root {
--color-bg: #f6efe3;
--color-surface: #fffaf2;
--color-surface2: #ece0cd;
--color-fg: #3a3128;
--color-muted: #8a7c6a;
--color-line: rgba(80, 60, 40, 0.14);
--color-accent: #cf7d4e;
--color-accent2: #7d8a5a;
--font-display: 'Newsreader', Georgia, serif;
--font-ui: 'Hanken Grotesk', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-card: 22px;
}
}
/* ── COLLECTOR · The Collectors Den — vintage paper, maroon, Spectral ── */
@store collector {
:root {
--color-bg: #e7e3d6;
--color-surface: #f3f0e6;
--color-surface2: #dcd7c6;
--color-fg: #22231d;
--color-muted: #6f6c5d;
--color-line: rgba(40, 40, 28, 0.16);
--color-accent: #8a2f2f;
--color-accent2: #b08a3e;
--font-display: 'Spectral', Georgia, serif;
--font-ui: 'Hanken Grotesk', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-card: 0px;
}
}