import React, { useEffect, useMemo, useRef, useState } from "react";
// ---- Simple, dependency-free v1 SPA for The Crafters Roadshow ----
// Notes:
// - Single-file React component, responsive, production-ready structure
// - Tailwind-style utility classes are mimicked with inline CSS below to ensure portability
// - Replace copy/images/venues as needed. All copy respects “showcase, not markets”.
// - Countdown auto-targets the next future event from the events list.
// ---------- Data ----------
const VENUES = [
{
id: "metrocentre",
name: "Metrocentre · Gateshead",
city: "Newcastle upon Tyne",
blurb:
"Prime indoor retail powerhouse. Start of Christmas footfall surges — perfect for brand discovery and gifting.",
splitWeekends: false,
events: [
{ start: "2025-11-03", end: "2025-11-16", status: "confirmed" },
{ start: "2025-12-18", end: "2025-12-23", status: "tentative" }, // Do not overpromise — clearly labelled.
],
},
{
id: "unionsquare",
name: "Union Square · Aberdeen",
city: "Aberdeen",
blurb:
"Aberdeen’s busiest destination — warm, dry, and indoors. Big gift-seeking audiences + loyal regulars.",
splitWeekends: true, // Split weekends available here.
events: [
{ start: "2025-12-04", end: "2025-12-21", status: "confirmed" },
],
},
{
id: "braehead",
name: "Braehead · Glasgow",
city: "Glasgow",
blurb:
"Placed among headline brands. Premium positioning for serious small businesses.",
splitWeekends: false,
events: [
{ start: "2025-09-25", end: "2025-09-28", status: "planned" },
],
},
{
id: "silverburn",
name: "Silverburn · Glasgow",
city: "Glasgow",
blurb:
"High-energy shoppers. Perfect for bold showcases and new product launches.",
splitWeekends: false,
events: [
{ start: "2025-10-16", end: "2025-10-19", status: "planned" },
],
},
{
id: "livingston",
name: "The Centre · Livingston",
city: "Livingston",
blurb:
"Steady footfall + strong family audiences. Great for repeat customers.",
splitWeekends: false,
events: [
{ start: "2025-09-11", end: "2025-09-14", status: "planned" },
],
},
];
const TESTIMONIALS = [
{
quote:
"We’ve joined for years at Union Square. It’s warm, busy, and full of gift-shoppers. Perfect for us.",
name: "Angela — Angelic Aromas Aberdeen",
},
{
quote:
"Metrocentre gave us huge brand visibility. People saw us, chatted with us, and came back the next day.",
name: "Mike for Suzanne Thomson Art",
},
{
quote:
"I don’t do ‘markets’. This is a professional showcase. Customers treat your brand that way.",
name: "Sophie Photography",
},
];
// ---------- Helpers ----------
function parse(d) {
const [y, m, dd] = d.split("-").map(Number);
return new Date(y, m - 1, dd);
}
function formatRange(start, end) {
const s = parse(start);
const e = parse(end);
const month = s.toLocaleString(undefined, { month: "short" });
const dayS = s.getDate();
const dayE = e.getDate();
const year = s.getFullYear();
return `${month} ${dayS}–${dayE}, ${year}`;
}
function nextEventFrom(venues) {
const now = new Date();
const future = [];
for (const v of venues) {
for (const ev of v.events) {
const s = parse(ev.start);
if (s.getTime() >= now.getTime() - 24 * 3600 * 1000) {
future.push({ ...ev, venue: v });
}
}
}
future.sort((a, b) => parse(a.start) - parse(b.start));
return future[0] || null;
}
function plural(n, a, b) {
return n === 1 ? a : b;
}
// ---------- UI ----------
export default function App() {
const [menuOpen, setMenuOpen] = useState(false);
const [filter, setFilter] = useState("");
const [form, setForm] = useState({ name: "", email: "", product: "", venue: "", dates: "" });
const [submitted, setSubmitted] = useState(false);
const [newsletterEmail, setNewsletterEmail] = useState("");
const [newsletterOk, setNewsletterOk] = useState(false);
const target = useMemo(() => nextEventFrom(VENUES), []);
// Countdown
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
return () => clearInterval(id);
}, []);
const countdown = useMemo(() => {
if (!target) return null;
const end = parse(target.start);
const now = new Date();
const diff = Math.max(0, end.getTime() - now.getTime());
const days = Math.floor(diff / (24 * 3600 * 1000));
const hours = Math.floor((diff % (24 * 3600 * 1000)) / (3600 * 1000));
const mins = Math.floor((diff % (3600 * 1000)) / (60 * 1000));
const secs = Math.floor((diff % (60 * 1000)) / 1000);
return { days, hours, mins, secs };
}, [target, tick]);
const venuesFiltered = useMemo(() => {
const q = filter.toLowerCase();
return VENUES.filter(
(v) => v.name.toLowerCase().includes(q) || v.city.toLowerCase().includes(q)
);
}, [filter]);
function onSubmit(e) {
e.preventDefault();
setSubmitted(true);
}
function onNewsletter(e) {
e.preventDefault();
setNewsletterOk(true);
}
return (
{/* Top Bar */}
{/* Hero */}
Professional Showcases in the UK’s Busiest Shopping Centres
Put your small business where great brands belong.
Our showcases aren’t “markets.” They’re brand stages. Meet real shoppers, tell your story, and spark the
kind of word‑of‑mouth that online can’t touch.
{target && countdown && (
Next Showcase
{target.venue.name}
{formatRange(target.start, target.end)} {target.status !== "confirmed" && {target.status} }
)}
{/* Venues */}
Our Venues
High‑footfall, indoor retail destinations. Click a venue to see dates.
setFilter(e.target.value)}
placeholder="Search city or venue (e.g., Aberdeen, Metrocentre)"
aria-label="Filter venues"
/>
{venuesFiltered.map((v) => (
))}
{/* Join as a Maker */}
Join as a Maker
Your presence is the unfair advantage. Customers don’t just buy the product; they buy the person behind it.
Join us in a venue that matches your brand, and turn browsers into fans.
Premium placement among leading brands
Face‑to‑face momentum you can’t get online
Friendly, organised team and clear communication
"Split weekends" available at Union Square (Sat or Sun options)
Psst—New to our showcases? Ask about the New Maker Intro options we run occasionally.
{!submitted ? (
) : (
Thanks! 🎉
Your request has been noted. We’ll get back to you by email with venue availability and booking details.
Explore more venues
)}
{/* Why In-Person */}
Why in‑person beats online
Online is great. But it’s missing the secret ingredient: you . When shoppers meet you,
hear your story, and try your product, conversion skyrockets—and they tell their friends.
People who meet you come back again
People who talk to you tell their friends
People who gift your work become promoters
Positioning matters: our showcases sit among top brands—because that’s where great
small businesses belong.
Make your space pay for itself
Our upcoming £270 Pre‑Paid Event Playbook shows simple ways to generate sales before day one—so
you arrive already in profit.
Pre‑event teaser posts that actually drive DMs
Bundle ideas shoppers say “yes” to
Fast track repeat customers with a simple VIP card
Get the playbook when it drops
{/* Testimonials */}
What makers say
{TESTIMONIALS.map((t, i) => (
“{t.quote}”
— {t.name}
))}
{/* Newsletter */}
The Weekly Maker Playbook
Inspired by Social Media Examiner’s clean, useful format. Short, practical wins each week to grow your
brand—plus occasional invites to premium showcase dates.
Pro tips that actually move the needle
Examples from real makers (no fluff)
Fresh angles for content, offers, and display
{!newsletterOk ? (
) : (
Welcome aboard!
You’re subscribed. Watch your inbox for the next issue.
Book a space
)}
{/* FAQ */}
Things we get asked all the time
Do you supply a table?
We’ll confirm per venue. Many makers bring their own to match their brand display.
Do I have to book all 4 days?
Full weekend is most effective. Split weekends are available only at Union Square.
How much is a space?
Pricing varies by venue and date. As a guide, many weekends total around £270–£290. Ask for the latest.
Is this a market?
No. It’s a professional showcase —designed for serious small businesses.
{/* Footer */}
The Crafters Roadshow
Showcases in Metrocentre, Union Square, Braehead, Silverburn, and The Centre Livingston.
© {new Date().getFullYear()} The Crafters Roadshow
Please do not refer to our events as “markets” in any advertising or posts.
{/* Floating CTA */}
Book a Space
);
}
function VenueCard({ venue }) {
const [open, setOpen] = useState(false);
return (
setOpen(!open)} role="button" aria-expanded={open}>
{venue.name}
{venue.city}
{venue.splitWeekends && Split weekends }
{venue.blurb}
setOpen(!open)}>{open ? "Hide" : "View"} dates
{open && (
{venue.events.map((ev, i) => (
{formatRange(ev.start, ev.end)}
{ev.status}
))}
)}
);
}
function CTChunk({ n, label }) {
return (
{String(n).padStart(2, "0")}
{label}
);
}
function HeroScene() {
return (
The Makers Christmas Showcase
Union Square · Aberdeen
);
}
function BigRed({ markOnly = false }) {
return (
{/* Gold ring */}
{/* Button */}
{/* Shine */}
{/* Eyes */}
{/* Smile */}
{!markOnly && (
<>
{/* Arms */}
{/* Legs */}
{/* Shoes */}
>
)}
);
}
// ---------- CSS (scoped) ----------
const globalCss = `
:root{
--bg: #0b0b0f; /* deep space */
--panel: #121219;
--ink: #ededf1;
--muted: #b6b7c4;
--brand: #e11d48; /* rose-600 */
--brand-2:#d4af37; /* gold ring */
--ok: #16a34a;
--card: #141420;
--ghost: rgba(255,255,255,.06);
--border: rgba(255,255,255,.10);
}
*{ box-sizing: border-box }
html, body, #root { height: 100% }
body{ margin:0; background: radial-gradient(1200px 600px at 20% 0%, #161626 0%, #0b0b0f 40%), #0b0b0f; color: var(--ink); font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial }
a{ color: inherit }
.container{ width: min(1100px, 92vw); margin: 0 auto }
.row{ display:flex; gap:16px }
.between{ justify-content: space-between }
.center{ align-items: center }
.top{ align-items: flex-start }
.grid2{ display:grid; grid-template-columns: 1.1fr 0.9fr; gap: 28px }
@media (max-width: 900px){ .grid2{ grid-template-columns: 1fr; } }
.tcr-header{ position: sticky; top: 0; z-index: 40; background: rgba(11,11,15,.7); backdrop-filter: saturate(120%) blur(10px); border-bottom: 1px solid var(--border) }
.tcr-header .brand{ display:flex; align-items:center; gap:10px; text-decoration:none; font-weight:700 }
.tcr-header .brand span{ letter-spacing:.2px }
.bigred{ width: 28px; height:28px }
.bigred.mark{ width: 22px; height:22px }
.nav{ display:flex; gap: 18px }
.nav a{ text-decoration:none; opacity:.9 }
.nav a:hover{ opacity:1 }
.menu{ display:none; width:40px; height:40px; background:transparent; border:0 }
.menu span{ display:block; height:2px; background:#fff; margin:6px 0 }
@media (max-width: 860px){
.nav{ display:none; position:absolute; right:16px; top:64px; flex-direction:column; background: var(--panel); padding:12px 10px; border:1px solid var(--border); border-radius:12px }
.nav.open{ display:flex }
.menu{ display:block }
}
.hero{ padding: 64px 0 28px; background:
radial-gradient(600px 300px at 10% 10%, rgba(225,29,72,.18), transparent 60%),
radial-gradient(800px 380px at 90% 0%, rgba(212,175,55,.20), transparent 60%);
}
.hero-copy h1{ font-size: clamp(34px, 5vw, 54px); line-height:1.05; margin: 10px 0 8px }
.hero .eyebrow{ text-transform:uppercase; letter-spacing:.12em; font-size:12px; color:var(--muted) }
.lead{ color:#d7d7e5; font-size: 18px; max-width: 60ch }
.cta-row{ display:flex; gap:12px; margin: 18px 0 8px }
.btn{ display:inline-flex; align-items:center; justify-content:center; height:44px; padding:0 16px; border-radius:12px; border:1px solid var(--border); background: var(--ghost); color:#fff; text-decoration:none; cursor:pointer; transition: .2s ease }
.btn:hover{ transform: translateY(-1px) }
.btn-solid{ background: linear-gradient(180deg, #ff6b6b, var(--brand)); border-color: transparent; box-shadow: 0 10px 26px rgba(225,29,72,.35) }
.btn-ghost{ background: transparent }
.btn.small{ height:36px; padding:0 12px }
.hero-art{ display:flex; align-items:center; justify-content:center }
.scene{ position:relative; height: 360px; background: linear-gradient(180deg, #0f0f18, #0b0b0f); border:1px solid var(--border); border-radius: 18px; overflow:hidden }
.stage{ position:absolute; inset:0; display:grid; place-items:center }
.poster{ position:absolute; bottom:58px; background: #1b1b29; padding:10px 14px; border-radius:999px; border:1px solid var(--border); font-weight:700 }
.city{ position:absolute; bottom:22px; opacity:.75 }
.glow{ position:absolute; width:400px; height:400px; border-radius:50%; filter: blur(42px); background: radial-gradient(circle at 50% 40%, rgba(212,175,55,.25), rgba(225,29,72,.22), transparent 70%) }
.section{ padding: 56px 0 }
.section.alt{ background: linear-gradient(180deg, #0b0b0f, #10101a) }
.section-head h2{ font-size: clamp(24px, 3vw, 34px); margin:0 0 8px }
.section-head p{ color: var(--muted) }
.filter{ margin: 16px 0 8px }
.filter input{ width:100%; background:#0f0f18; color:#fff; border:1px solid var(--border); border-radius:12px; padding:12px 14px }
.cards{ display:grid; grid-template-columns: repeat(3, 1fr); gap: 16px }
@media (max-width: 1100px){ .cards{ grid-template-columns: repeat(2, 1fr) } }
@media (max-width: 680px){ .cards{ grid-template-columns: 1fr } }
.card{ background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 16px }
.venue .v-head{ display:flex; justify-content:space-between; gap:16px; cursor:pointer }
.venue .v-name{ font-weight: 700 }
.venue .v-city{ color: var(--muted); font-size: 14px }
.venue .v-blurb{ color:#d8d9e5; margin: 8px 0 10px }
.v-dates{ margin-top:12px; display:grid; gap:10px }
.date-row{ display:flex; align-items:center; justify-content:space-between; gap:12px; background:#0f0f18; border:1px solid var(--border); border-radius:12px; padding:10px 12px }
.date{ font-weight:600 }
.status{ display:flex; align-items:center; gap:8px; color:var(--muted) }
.dot{ width:10px; height:10px; border-radius:50% }
.dot.confirmed{ background: var(--ok) }
.dot.planned{ background: #3b82f6 }
.dot.tentative{ background: #f59e0b }
.cap{ text-transform:capitalize }
.tag{ display:inline-block; padding:2px 8px; font-size:12px; background:#1f1f2b; border:1px solid var(--border); border-radius:999px; margin-left:8px }
.form .form-row{ display:grid; gap:6px; margin: 10px 0 }
.form input, .form select, .form textarea{ background:#0f0f18; color:#fff; border:1px solid var(--border); border-radius:12px; padding:12px 14px; resize:vertical }
.form-foot{ color:var(--muted); font-size:12px; margin-top:8px }
.success h3{ margin: 6px 0 }
.ticks{ list-style:none; padding:0; margin: 10px 0; display:grid; gap:8px }
.ticks li{ position:relative; padding-left:28px }
.ticks li:before{ content:"✔"; position:absolute; left:0; top:0; color:var(--ok) }
.pill-grid{ display:flex; gap:10px; flex-wrap:wrap; margin: 12px 0 }
.pill{ border:1px solid var(--border); background:#0f0f18; padding:8px 12px; border-radius:999px }
.callout{ border-left:4px solid var(--brand); padding:10px 12px; background:#121222; margin-top:12px }
.testi{ display:grid; gap:12px; grid-template-columns: repeat(3, 1fr) }
@media (max-width: 900px){ .testi{ grid-template-columns: 1fr } }
.quote{ background:#0f0f18; border:1px solid var(--border); border-radius:16px; padding:16px }
.quote blockquote{ margin:0 0 8px; font-size:18px; color:#e6e7f2 }
.quote figcaption{ color:var(--muted) }
.faq{ display:grid; gap:10px }
.faq details{ background:#0f0f18; border:1px solid var(--border); border-radius:12px; padding:10px 12px }
.faq summary{ cursor:pointer; font-weight:600 }
.footer{ padding: 28px 0; border-top:1px solid var(--border); background: #0a0a0e }
.footer .brand{ display:flex; align-items:center; gap:10px; font-weight:700 }
.footer .b2{ align-items:flex-start; flex-direction:column }
.footer .sub{ color:var(--muted); margin-top:6px }
.footer .links{ display:grid; gap:8px }
.footer .meta{ color:var(--muted) }
.footer .tiny{ font-size:12px; margin-top:6px }
.float-cta{ position: fixed; right: 16px; bottom: 16px; background: linear-gradient(180deg, #ff6b6b, var(--brand)); padding: 10px 14px; border-radius: 999px; color:#fff; text-decoration:none; border:0; box-shadow:0 12px 30px rgba(225,29,72,.45) }
.countdown{ margin-top: 16px; padding: 12px; border:1px solid var(--border); background:#0f0f18; border-radius: 16px; max-width: 520px }
.ct-head{ color:var(--muted); text-transform:uppercase; letter-spacing:.12em; font-size:12px }
.ct-venue{ font-weight:700; margin-top:2px }
.ct-dates{ color:#d7d7e5; margin-bottom:8px }
.ct-timer{ display:flex; gap:10px }
.ct-chunk{ background:#151526; border:1px solid var(--border); border-radius:12px; padding: 8px 10px; text-align:center; min-width:80px }
.ct-chunk .num{ font-size: 24px; font-weight:800 }
.ct-chunk .lab{ color:var(--muted); font-size:12px }
`;