// screens-book.jsx — schedule with day-swipe & session cards // // Day strip across the top (today + next 6). Tap or swipe horizontally to // change the active day. A filter row lets the user pin a class type or a // teacher. Sessions render as scrollable cards under sticky time bands. const { fmtIDR, findClass, findCoach, findFriend, minutesUntil, fmtCountdown, DAYS_SHORT, DAYS_LONG, DATES_THIS_WEEK, TODAY_DAY } = window.NADI_HELPERS; function BookScreen({ user, openClassSheet, setTab, upcomingBookings }) { const getAppToday = window.NADI_HELPERS.getAppToday; const parseLocalDate = window.NADI_HELPERS.parseLocalDate; const formatLocalDateISO = window.NADI_HELPERS.formatLocalDateISO; const minWS = new Date(2026, 4, 25); // May 25, 2026 const [activeDate, setActiveDate] = useState(() => { return getAppToday(); }); const [weekStart, setWeekStart] = useState(() => { const d = new Date(activeDate); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday return new Date(d.setDate(diff)); }); useEffect(() => { const d = new Date(activeDate); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); setWeekStart(new Date(d.setDate(diff))); }, [activeDate]); const weekDates = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(weekStart.getDate() + i); return d; }); const activeDateStr = formatLocalDateISO(activeDate); const weekEnd = weekDates[6]; const prevWeek = () => { const nextWS = new Date(weekStart.getTime() - 7 * 24 * 60 * 60 * 1000); if (nextWS < minWS) return; setWeekStart(nextWS); setActiveDate(nextWS); }; const nextWeek = () => { const nextWS = new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000); setWeekStart(nextWS); setActiveDate(nextWS); }; const handlePrevDay = () => { setActiveDate((curr) => { const prev = new Date(curr.getTime() - 24 * 60 * 60 * 1000); if (prev < minWS) return curr; return prev; }); }; const handleNextDay = () => { setActiveDate((curr) => { const next = new Date(curr.getTime() + 24 * 60 * 60 * 1000); return next; }); }; const [filterClass, setFilterClass] = useState(null); const [filterCoach, setFilterCoach] = useState(null); const [filterFav, setFilterFav] = useState(false); const activeDayIndex = (activeDate.getDay() + 6) % 7; // Monday = 0 // Sessions on this day const all = window.NADI.SCHEDULE.filter((s) => s.date === activeDateStr); const filtered = all.filter((s) => { if (filterClass && s.classId !== filterClass) return false; if (filterCoach && s.coachId !== filterCoach) return false; if (filterFav) { const f1 = user.savedClasses.includes(s.classId); const f2 = user.savedCoaches.includes(s.coachId); if (!f1 && !f2) return false; } return true; }).sort((a, b) => a.time.localeCompare(b.time)); // Filter sheet state const [filtersOpen, setFiltersOpen] = useState(false); // Swipe gestures on the schedule body const swipeRef = useRef(null); useEffect(() => { const el = swipeRef.current; if (!el) return; let startX = 0, startY = 0, deltaX = 0, locked = false, captured = false; const onStart = (e) => { const t = e.touches ? e.touches[0] : e; startX = t.clientX; startY = t.clientY; deltaX = 0; locked = false; captured = false; }; const onMove = (e) => { const t = e.touches ? e.touches[0] : e; const dx = t.clientX - startX, dy = t.clientY - startY; if (!locked) { if (Math.abs(dx) > 14 || Math.abs(dy) > 14) { locked = true; captured = Math.abs(dx) > Math.abs(dy); } } if (captured) { deltaX = dx; if (e.cancelable) e.preventDefault(); } }; const onEnd = () => { if (captured && Math.abs(deltaX) > 60) { if (deltaX < 0) { handleNextDay(); } else { handlePrevDay(); } } captured = false; locked = false; }; el.addEventListener("touchstart", onStart, { passive: true }); el.addEventListener("touchmove", onMove, { passive: false }); el.addEventListener("touchend", onEnd); return () => { el.removeEventListener("touchstart", onStart); el.removeEventListener("touchmove", onMove); el.removeEventListener("touchend", onEnd); }; }, [activeDate, weekStart]); // Filter active count const activeFilters = (filterClass ? 1 : 0) + (filterCoach ? 1 : 0) + (filterFav ? 1 : 0); const activeDayIsToday = activeDateStr === formatLocalDateISO(getAppToday()); return (
{/* Header */}
{weekStart.getDate()} {weekStart.toLocaleDateString("en-GB", { month: "short" }).toLowerCase()} — {weekEnd.getDate()} {weekEnd.toLocaleDateString("en-GB", { month: "short" }).toLowerCase()}

{activeDayIsToday ? ( <>Today, {DAYS_LONG[activeDayIndex].toLowerCase()}. ) : ( <>{DAYS_LONG[activeDayIndex]}, {String(activeDate.getDate()).padStart(2, "0")} {activeDate.toLocaleDateString("en-GB", { month: "short" }).toLowerCase()}. )}

{/* Day strip */}
{/* Schedule body */}
{filtered.length === 0 ? (
nothing scheduled for this day.
) : ( )}
{/* Filter sheet */} setFiltersOpen(false)} maxHeight="80%"> setFiltersOpen(false)} user={user} />
); } // ── Day strip ───────────────────────────────────────────────────────── function DayStrip({ activeDate, setActiveDate, weekDates }) { const getAppToday = window.NADI_HELPERS.getAppToday; const formatLocalDateISO = window.NADI_HELPERS.formatLocalDateISO; const todayStr = formatLocalDateISO(getAppToday()); return (
{weekDates.map((d, i) => { const dStr = formatLocalDateISO(d); const isToday = dStr === todayStr; const active = dStr === formatLocalDateISO(activeDate); return ( ); })}
); } // ── Time-band groups ───────────────────────────────────────────────── function ScheduleBands({ sessions, user, upcomingBookings, activeDateStr, openClassSheet }) { const bands = [ { label: "morning", range: ["00:00", "11:59"] }, { label: "afternoon", range: ["12:00", "16:59"] }, { label: "evening", range: ["17:00", "23:59"] }, ]; return (
{bands.map((b) => { const items = sessions.filter((s) => s.time >= b.range[0] && s.time <= b.range[1]); if (items.length === 0) return null; return (
{b.label} {b.range[0]} — {b.range[1]}
{items.map((s) => openClassSheet({ ...s, date: activeDateStr })} />)}
); })}
); } // ── Session card (the heart of Book) ───────────────────────────────── function SessionCard({ s, user, upcomingBookings, activeDateStr, onClick }) { const c = findClass(s.classId), co = findCoach(s.coachId); const left = s.capacity - s.booked; const full = left === 0; const fav = user.savedClasses.includes(s.classId) || user.savedCoaches.includes(s.coachId); const friends = s.friends.map(findFriend).filter(Boolean); const booked = (upcomingBookings || []).some(b => b.session_id === s.id && b.date === activeDateStr); const mine = booked; // Accent line color based on class color token const accent = c.color === "primary" ? "var(--primary)" : c.color === "accent" ? "var(--accent)" : "var(--muted)"; return ( ); } function FavIcon({ size = 12, active }) { return ( ); } // ── Filter sheet ───────────────────────────────────────────────────── function FilterSheet({ filterClass, setFilterClass, filterCoach, setFilterCoach, filterFav, setFilterFav, onClose, user }) { return (
filter

Refine the week.

show only my favourites
class type
setFilterClass(null)}>all classes {window.NADI.CLASS_TYPES.map((c) => ( setFilterClass(c.id === filterClass ? null : c.id)}> {c.name.toLowerCase()} ))}
teacher
setFilterCoach(null)}>all teachers {window.NADI.COACHES.map((co) => ( setFilterCoach(co.id === filterCoach ? null : co.id)}> {co.firstName.toLowerCase()} ))}
); } Object.assign(window, { BookScreen, SessionCard, FavIcon });