/* Oakmere Club — mockup. React via CDN + Babel, no build step. Edit + refresh (auto-reloads). Fake data in data/data.js. Single file on purpose: in-browser Babel makes multi-file scope fragile and un-testable headlessly here, so we keep one reliable file until it hurts. */ const { useState, useMemo } = React; /* ============================ icons ============================ */ function Icon({ name, className = "w-6 h-6" }) { const p = { tennis: <>, golf: <>, dining: <>, cafe: <>, bell: <>, search: <>, chevron: , back: , home: <>, calendar: <>, activity: , user: <>, clock: <>, pin: <>, users: <>, plus: <>, check: , close: , cart: <>, walk: <>, flag: <>, }[name]; return {p}; } /* ============================ shared bits ============================ */ function Chip({ children, tone = "neutral" }) { const tones = { open: "bg-emerald-50 text-emerald-700", closed: "bg-stone-100 text-stone-500", neutral: "bg-stone-100 text-stone-600", gold: "bg-amber-50 text-amber-700", club: "bg-club-50 text-club-700", }; return {children}; } function Avatar({ person, className = "w-9 h-9 text-xs" }) { return
{person.initials}
; } function Segmented({ options, value, onChange }) { return (
{options.map((o) => ( ))}
); } /* ============================ navigation ============================ */ /* tiny built-in router — no library for a 4-screen mockup */ function BottomNav({ navigate, active }) { const items = [["home", "Home"], ["calendar", "Bookings"], ["activity", "Activity"], ["user", "Profile"]]; return ( ); } /* ============================ HOME ============================ */ function TopBar({ club }) { return (
O
{club.name}
{club.tagline}
); } function UpcomingCard({ item }) { return (
{item.title}
{item.when}
{item.meta}
); } function AmenityCard({ a, onClick }) { const [c1, c2] = a.gradient; return ( ); } function EventCard({ e }) { return (
{e.spots}
{e.title}
{e.when} {e.where}
); } function SectionTitle({ children, action, onAction }) { return (

{children}

{action && }
); } function HomeScreen({ navigate }) { const c = window.CLUB; const openAmenity = (a) => a.id === "golf" ? navigate("golf") : alert(`(${a.name}) flow — coming next!`); return (

Good morning, {c.member.first}

{c.today}

Search courts, tee times, tables…
Your reservations
{c.upcoming.map((u) => )}
Book an amenity
{c.amenities.map((a) => openAmenity(a)} />)}
Happening at the club {c.events.map((e) => )}
); } /* ============================ GOLF ============================ */ /* deterministic pseudo-random so availability is stable across reloads */ function seed(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } function rint(str, n) { return seed(str) % n; } function fmtTime(h, m) { const ap = h >= 12 ? "PM" : "AM"; const hh = ((h + 11) % 12) + 1; return { h: hh, m: m.toString().padStart(2, "0"), ap }; } function dayStrip() { const base = new Date("2026-06-05T00:00:00"); const wk = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]; return Array.from({ length: 10 }, (_, i) => { const d = new Date(base); d.setDate(base.getDate() + i); return { idx: i, wd: wk[d.getDay()], day: d.getDate(), key: `d${i}` }; }); } function genTeeTimes(courseId, dateKey) { const G = window.CLUB.golf.golfers; const times = []; for (let h = 7; h <= 9; h++) for (let m = 0; m < 60; m += 10) times.push([h, m, "morning"]); for (let h = 11; h <= 12; h++) for (let m = 0; m < 60; m += 20) times.push([h, m, "midday"]); for (let h = 14; h <= 16; h++) for (let m = 0; m < 60; m += 20) times.push([h, m, "afternoon"]); return times.map(([h, m, period]) => { const key = `${courseId}|${dateKey}|${h}:${m}`; const fill = rint(key, 5); // 0..4 already in the group const off = rint(key + "p", G.length); const players = Array.from({ length: fill }, (_, k) => G[(off + k) % G.length]); return { id: key, h, m, period, players, open: 4 - fill, ...fmtTime(h, m) }; }); } function TeeTimeCard({ t, onSelect }) { const full = t.open === 0; return ( ); } function BookingSheet({ slot, course, dateLabel, holes, onClose }) { const [done, setDone] = useState(false); const me = { name: `${window.CLUB.member.first} ${window.CLUB.member.last}`, initials: window.CLUB.member.initials }; const [transport, setTransport] = useState("cart"); const [hole, setHole] = useState(holes); const group = [me, ...slot.players]; const emptySlots = Math.max(0, 4 - group.length); return (
{done ? (
Tee time requested
{course.name} · {dateLabel} · {slot.h}:{slot.m} {slot.ap}
) : ( <>
{slot.h}:{slot.m} {slot.ap}
{course.name} · {dateLabel}
Your group
{group.map((p, i) => (
{p.name}{i === 0 && " (you)"} {p.hcp != null && HCP {p.hcp}}
))} {Array.from({ length: emptySlots }).map((_, i) => ( ))}
Holes
Getting around
)}
); } function GolfScreen({ navigate }) { const golf = window.CLUB.golf; const days = useMemo(dayStrip, []); const [courseId, setCourseId] = useState(golf.courses[0].id); const [dateIdx, setDateIdx] = useState(0); const [period, setPeriod] = useState("all"); const [holes, setHoles] = useState(18); const [slot, setSlot] = useState(null); const course = golf.courses.find((c) => c.id === courseId); const day = days[dateIdx]; const all = useMemo(() => genTeeTimes(courseId, day.key), [courseId, day.key]); const list = all.filter((t) => period === "all" || t.period === period); const periods = [["all", "All"], ["morning", "Morning"], ["midday", "Midday"], ["afternoon", "Afternoon"]]; return (
Golf · Tee times
{course.note}
({ value: c.id, label: c.name }))} />
{/* date strip */}
{days.map((d) => ( ))}
{/* period filter + holes preference */}
{periods.map(([v, l]) => ( ))}
{list.map((t) => )}
{slot && setSlot(null)} />}
); } /* ============================ ROOT ============================ */ function App() { const [route, setRoute] = useState({ name: "home" }); const navigate = (name) => setRoute({ name }); return (
{route.name === "home" && } {route.name === "golf" && }
); } ReactDOM.createRoot(document.getElementById("root")).render();