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