• package.json • next.config.mjs • tsconfig.json • postcss.config.js • tailwind.config.ts • .env.example • middleware.ts • app/ o layout.tsx o globals.css o page.tsx (Home) o competitions/  [slug]/  page.tsx (Competition page)  checkout/  page.tsx (Ticket purchase flow) o transparency/  page.tsx (Transparency hub) o api/  competitions/  route.ts (list competitions)  competitions/  [slug]/  progress/  route.ts (live counters/threshold/NCR)  purchase/  route.ts (create intent stub)  ack/  route.ts (store material disclosure acknowledgement)  verify-skill/  route.ts (skill check)  finalize/  route.ts (finalize order after webhook in real backend) o winners/  page.tsx o auth/  age-check/  page.tsx • lib/ o models.ts (TypeScript entities) o store.ts (in-memory store for staging) o calc.ts (threshold/NCR/SDLT/fallback engine) o skill.ts (hash/check of skill answers) o audit.ts (append-only audit log writer) o config.ts (competition configuration, thresholds, max entries) o compliance.ts (disclosure text and validators) • components/ o Header.tsx, Footer.tsx, DisclosurePanel.tsx, ProgressWidget.tsx, Countdown.tsx, TicketSelector.tsx, Acknowledge.tsx, SkillQuestion.tsx, EligibilityGate.tsx, TrustStrip.tsx, Tabs.tsx, Layout primitives (Container, Button, Card) • public/ o robots.txt (Disallow) • pages/ o api/stripe/webhook.ts (placeholder handler if you later host on Node; for Cloudflare Pages Workers you’ll port to functions) Install dependencies (next, react, tailwind, zod, date-fns, jose for hashing) Run: npm install Code — key files (trimmed to essentials but fully deployable) package.json { "name": "bricks-and-breeches-mvp", "private": true, "scripts": { "dev": "next dev", "build": "next build && next export", "start": "next start", "lint": "next lint" }, "dependencies": { "date-fns": "^3.6.0", "next": "14.2.4", "react": "18.2.0", "react-dom": "18.2.0", "zod": "^3.23.8" }, "devDependencies": { "autoprefixer": "^10.4.16", "postcss": "^8.4.35", "tailwindcss": "^3.4.4", "typescript": "^5.4.5" } } next.config.mjs /** @type {import('next').NextConfig} / const nextConfig = { reactStrictMode: true, experimental: { appDir: true }, headers: async () => { return [ { source: "/:path", headers: [ { key: "X-Robots-Tag", value: "noindex, nofollow" }, { key: "Cache-Control", value: "no-store" } ] } ]; } }; export default nextConfig; tailwind.config.ts import type { Config } from "tailwindcss"; const config: Config = { content: ["./app//*.{ts,tsx}", "./components//*.{ts,tsx}"], theme: { extend: { colors: { navy: "#0B1C2E", green: "#23523D", cream: "#F4EFE6", stone: "#D6CEC2", earth: "#A9947A", gold: "#C9A65B", slate: "#6B8A99", coral: "#C46857" }, borderRadius: { md: "10px", lg: "12px" } } }, plugins: [] }; export default config; postcss.config.js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }; tsconfig.json { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["node"], "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "jsx": "preserve", "baseUrl": ".", "paths": { "@/": ["./"] }, "allowJs": false, "esModuleInterop": true, "resolveJsonModule": true }, "include": ["app", "components", "lib", "pages", "next-env.d.ts"] } app/globals.css @tailwind base; @tailwind components; @tailwind utilities; :root{ --cta-radius:10px; } html,body{ @apply bg-cream text-navy; } a:focus{ outline:2px solid #C9A65B; outline-offset:2px; } lib/models.ts export type CompetitionStatus = "draft"|"live"|"ended"|"settlement"; export interface Competition { id: string; slug: string; title: string; description: string; ticketPricePence: number; maxTickets: number; maxEntriesPerUser: number; propertyValuePence: number; grossThresholdPence: number; // e.g., 32_500_000 startAt: string; // ISO endAt: string; // ISO status: CompetitionStatus; sdlTReservePence: number; // SDLT covered by promoter allowPostal: boolean; } export interface EntryAck { competitionId: string; userHash: string; textHash: string; at: string; } export interface Entry { id: string; competitionId: string; userId: string; quantity: number; type: "paid"|"postal"; status: "pending"|"confirmed"|"refunded"; createdAt: string; skillCorrect: boolean; } export interface Payment { id: string; entryId: string; amountPence: number; feesPence: number; netPence: number; status: "succeeded"|"failed"|"processing"|"refunded"; createdAt: string; } export interface AuditLog { id: string; actor: "system"|"user"; entity: string; entityId: string; action: string; data: Record; ts: string; } lib/config.ts export const COMPETITION_1 = { slug: "comp-1", title: "UK Property Competition (Concept) — £9.99 Ticket", ticketPricePence: 999, maxTickets: 500000, // scalable maxEntriesPerUser: 100, grossThresholdPence: 32_500_000, // £325,000 propertyValuePence: 12_000_000, // ~£120,000 sdlTReservePence: 400_000, // £4,000 buffer startAt: new Date().toISOString(), endAt: new Date(Date.now()+302460601000).toISOString() }; lib/calc.ts // NCR = gross - (VAT if any) - payment fees - direct comp ads - direct legal/compliance - prize transfer // Fallback = 50% * max(0, NCR - 10_000 pounds) export interface NCRInputs { grossPence: number; vatPence: number; feesPence: number; adsPence: number; legalPence: number; prizeTransferPence: number; } export function calcNCR(i: NCRInputs){ const ncr = Math.max(0, i.grossPence - i.vatPence - i.feesPence - i.adsPence - i.legalPence - i.prizeTransferPence); return ncr; } export function calcFallback(ncrPence: number){ const base = Math.max(0, ncrPence - 1_000_000); // £10,000 in pence return Math.floor(base * 0.5); } export function thresholdReached(grossPence: number, thresholdPence: number){ return grossPence >= thresholdPence; } lib/skill.ts import { createHash } from "crypto"; export function hashAnswer(ans: string){ return createHash("sha256").update(ans.trim().toLowerCase()).digest("hex"); } export function verifyAnswer(ans: string, hash: string){ return hashAnswer(ans) === hash; } // Example question export const SKILL_Q = { q: "What is the 7th prime number?", // answer = 17 hash: hashAnswer("17") }; lib/audit.ts let seq = 0; const logs: import("./models").AuditLog[] = []; export function writeAudit(actor: "system"|"user", entity: string, entityId: string, action: string, data: Record){ const id = log_${++seq}; const ts = new Date().toISOString(); logs.push({ id, actor, entity, entityId, action, data, ts }); return id; } export function getAudit(){ return logs.slice(-200); } lib/store.ts // Staging in-memory store. Replace with DB later (Supabase/Postgres). import { Competition, Entry, Payment } from "./models"; import { COMPETITION_1 } from "./config"; const comp: Competition = { id: "c1", slug: COMPETITION_1.slug, title: COMPETITION_1.title, description: "Staging MVP", ticketPricePence: COMPETITION_1.ticketPricePence, maxTickets: COMPETITION_1.maxTickets, maxEntriesPerUser: COMPETITION_1.maxEntriesPerUser, propertyValuePence: COMPETITION_1.propertyValuePence, grossThresholdPence: COMPETITION_1.grossThresholdPence, startAt: COMPETITION_1.startAt, endAt: COMPETITION_1.endAt, status: "live", sdlTReservePence: COMPETITION_1.sdlTReservePence, allowPostal: true }; const competitions = [comp]; const entries: Entry[] = []; const payments: Payment[] = []; export function listCompetitions(){ return competitions; } export function getCompetitionBySlug(slug: string){ return competitions.find(c=>c.slug===slug)||null; } export function addEntry(e: Entry){ entries.push(e); return e; } export function listEntriesForCompetition(competitionId: string){ return entries.filter(e=>e.competitionId===competitionId && e.status!=="refunded"); } export function totalTicketsSold(competitionId: string){ return listEntriesForCompetition(competitionId).reduce((s,e)=>s+e.quantity,0); } export function addPayment(p: Payment){ payments.push(p); return p; } export function listPaymentsForCompetition(competitionId: string){ const ids = new Set(listEntriesForCompetition(competitionId).map(e=>e.id)); return payments.filter(p => ids.has(p.entryId) && p.status==="succeeded"); } export function grossRevenuePence(competitionId: string){ return listEntriesForCompetition(competitionId).reduce((sum,e)=>sum + e.quantity*comp.ticketPricePence,0); }