/* 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 ;
}
/* ============================ 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();