diff --git a/config/store-config.ts b/config/store-config.ts new file mode 100644 index 0000000..935a06a --- /dev/null +++ b/config/store-config.ts @@ -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'; +}; diff --git a/config/store_collector.ts b/config/store_collector.ts new file mode 100644 index 0000000..e6a1d50 --- /dev/null +++ b/config/store_collector.ts @@ -0,0 +1,34 @@ +import type { StoreConfig } from './store-config'; + +export const store: StoreConfig = { + code: 'collector', + label: 'Collector', + brandName: 'The Collector’s 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', +}; diff --git a/config/store_crypto.ts b/config/store_crypto.ts new file mode 100644 index 0000000..9a3226f --- /dev/null +++ b/config/store_crypto.ts @@ -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', +}; diff --git a/config/store_executive.ts b/config/store_executive.ts new file mode 100644 index 0000000..e6f4982 --- /dev/null +++ b/config/store_executive.ts @@ -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', +}; diff --git a/config/store_supper.ts b/config/store_supper.ts new file mode 100644 index 0000000..3cf9c73 --- /dev/null +++ b/config/store_supper.ts @@ -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. There’s 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', +}; diff --git a/config/stores.ts b/config/stores.ts new file mode 100644 index 0000000..ddb45bc --- /dev/null +++ b/config/stores.ts @@ -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_.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' }, +]; diff --git a/src/data/overrides/collector.ts b/src/data/overrides/collector.ts new file mode 100644 index 0000000..c9e8cb6 --- /dev/null +++ b/src/data/overrides/collector.ts @@ -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'], +}; diff --git a/src/data/overrides/crypto.ts b/src/data/overrides/crypto.ts new file mode 100644 index 0000000..54e18d8 --- /dev/null +++ b/src/data/overrides/crypto.ts @@ -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'], +}; diff --git a/src/data/overrides/executive.ts b/src/data/overrides/executive.ts new file mode 100644 index 0000000..3a692be --- /dev/null +++ b/src/data/overrides/executive.ts @@ -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'], +}; diff --git a/src/data/overrides/index.ts b/src/data/overrides/index.ts new file mode 100644 index 0000000..d80ae31 --- /dev/null +++ b/src/data/overrides/index.ts @@ -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 = { + executive, + crypto, + supper, + collector, +}; + +export type { StoreMeta } from './types'; diff --git a/src/data/overrides/supper.ts b/src/data/overrides/supper.ts new file mode 100644 index 0000000..eda869c --- /dev/null +++ b/src/data/overrides/supper.ts @@ -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'], +}; diff --git a/src/data/overrides/types.ts b/src/data/overrides/types.ts new file mode 100644 index 0000000..3a85c37 --- /dev/null +++ b/src/data/overrides/types.ts @@ -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[]; +}; diff --git a/src/styles/fonts.css b/src/styles/fonts.css new file mode 100644 index 0000000..f45f386 --- /dev/null +++ b/src/styles/fonts.css @@ -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'); diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..ba7ba20 --- /dev/null +++ b/src/styles/globals.css @@ -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 Collector’s 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; + } +}