// screens-today.jsx — landing screen for returning members // // Goal: in under 30 seconds a member should be able to (a) see/cancel their // next booked class, (b) book the next available class on the schedule, or // (c) re-book something they've taken before. const { fmtIDR, findClass, findCoach, findFriend, minutesUntil, fmtCountdown, DAYS_SHORT, DAYS_LONG, DATES_THIS_WEEK, TODAY_DAY } = window.NADI_HELPERS; function TodayScreen({ user, setUser, openClassSheet, setTab, openCheckout, showToast, onCancelBooking, upcomingBookings }) { const getAppToday = window.NADI_HELPERS.getAppToday; const parseLocalDate = window.NADI_HELPERS.parseLocalDate; const parseLocalDateTime = (dateStr, timeStr) => { if (!dateStr || !timeStr) return null; const [year, month, day] = dateStr.split('-').map(Number); const [hours, minutes] = timeStr.split(':').map(Number); return new Date(year, month - 1, day, hours, minutes); }; // Their booked sessions mapped to session details and sorted by actual date-time const bookedSessions = (upcomingBookings || []) .map(b => { const s = window.NADI.SCHEDULE.find(x => x.id === b.session_id); if (!s) return null; return { ...s, bookingId: b.id, date: b.date, }; }) .filter(Boolean) .filter(s => { const d = parseLocalDateTime(s.date, s.time); return d && d >= getAppToday(); }) .sort((a, b) => parseLocalDateTime(a.date, a.time) - parseLocalDateTime(b.date, b.time)); const next = bookedSessions[0]; // "Next available" — first class after simulated now const nextAvailable = window.NADI.SCHEDULE .filter((s) => { if (user.booked.includes(s.id)) return false; if (s.capacity - s.booked <= 0) return false; const d = parseLocalDateTime(s.date, s.time); return d && d >= getAppToday(); }) .sort((a, b) => parseLocalDateTime(a.date, a.time) - parseLocalDateTime(b.date, b.time))[0]; // Quick rebook: distinct class+coach combos from last 3 history items const quickRebook = (user.history || []).slice(0, 3).map((h) => ({ classId: h.classId, coachId: h.coachId, })); // Pack about to expire (urgent if <= 15 days) const packs = (user.creditPacks || []).slice().sort((a, b) => new Date(a.expires) - new Date(b.expires)); const urgentPack = packs.find((p) => { const getStartOfDay = (dateObj) => new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate()); const expiryDate = parseLocalDate(p.expires); const todayDate = getStartOfDay(getAppToday()); const days = Math.max(0, Math.round((expiryDate - todayDate) / 86400000)); return days <= 15 && p.credits > 0; }); return (
{/* Greeting */}
{DAYS_LONG[TODAY_DAY].toLowerCase()} · {String(DATES_THIS_WEEK[TODAY_DAY]).padStart(2, "0")} {new Date().toLocaleDateString("en-GB", { month: "short" }).toLowerCase()} {String(window.NADI_HELPERS.TODAY_HOUR).padStart(2, "0")}:{String(window.NADI_HELPERS.TODAY_MIN).padStart(2, "0")} · jakarta
Hello, {user ? user.name.split(" ")[0] : "Member"}.

{next ? `You're on the mat ${fmtCountdown(minutesUntil(next))}.` : nextAvailable ? `Nothing booked yet — the next class is ${fmtCountdown(minutesUntil(nextAvailable))}.` : "All quiet on the schedule."}

{/* Hero: the next class (booked OR suggested next) */} {next ? : nextAvailable ? : null} {/* Credits / pack rail */} {/* Quick rebook */} {quickRebook.length > 0 && (
book again
{quickRebook.map((q, i) => { const c = findClass(q.classId), co = findCoach(q.coachId); const matching = window.NADI.SCHEDULE.find((s) => s.classId === q.classId && s.coachId === q.coachId && minutesUntil(s) > 0); return ( ); })}
)} {/* This week peek */} {/* Friends going */} {/* Streak / breath signature */}
); } // ── Hero: a booked class ───────────────────────────────────────────── function NextBookedHero({ s, user, setUser, showToast, setTab, onCancel }) { const c = findClass(s.classId), co = findCoach(s.coachId); const mins = minutesUntil(s); const friends = (s.friends || []).map(findFriend).filter(Boolean); const canCancel = mins > 60; const privacy = (user.bookingPrivacy && user.bookingPrivacy[s.id]) || (user.visibleByDefault ? "self" : "anon"); const isAnon = privacy === "anon"; const flipPrivacy = () => { if (!setUser) return; const next = isAnon ? "self" : "anon"; setUser({ ...user, bookingPrivacy: { ...(user.bookingPrivacy || {}), [s.id]: next } }); showToast && showToast(next === "anon" ? "Going anonymously." : "Visible to your circle."); }; const dateStr = s.date ? window.NADI_HELPERS.parseLocalDate(s.date).toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short" }).toLowerCase() : DAYS_LONG[s.day].toLowerCase(); return (
{/* Background breath dot — subtle */}
your next class {fmtCountdown(mins).toLowerCase()}

{c.name}

{dateStr} · {s.time} · with {co.name} · {s.roomShort}
{friends.length > 0 && (
going
)}
); } // ── Hero: a suggested-next class (no booking yet) ──────────────────── function NextSuggestedHero({ s, openClassSheet }) { const c = findClass(s.classId), co = findCoach(s.coachId); const mins = minutesUntil(s); return (
); } // ── Credits Rail ───────────────────────────────────────────────────── function CreditsRail({ user, setTab, urgentPack, openCheckout }) { const total = user.credits != null ? user.credits : (user.creditPacks || []).reduce((s, p) => s + p.credits, 0); const nextPack = (user.creditPacks || []).slice().sort((a, b) => new Date(a.expires) - new Date(b.expires))[0]; const getStartOfDay = (dateObj) => new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate()); const expiryDate = nextPack ? window.NADI_HELPERS.parseLocalDate(nextPack.expires) : null; const todayDate = getStartOfDay(window.NADI_HELPERS.getAppToday()); const daysLeft = nextPack ? Math.max(0, Math.round((expiryDate - todayDate) / 86400000)) : 0; return (
); } // ── This week peek (compact day strip) ─────────────────────────────── function ThisWeekPeek({ setTab, openClassSheet, user }) { // Today onward, max 5 sessions across next 3 days const sessions = window.NADI.SCHEDULE .filter((s) => minutesUntil(s) > 0 && !user.booked.includes(s.id)) .sort((a, b) => minutesUntil(a) - minutesUntil(b)) .slice(0, 4); return (
this week
{sessions.map((s, i) => { const c = findClass(s.classId), co = findCoach(s.coachId); const left = s.capacity - s.booked; const friends = s.friends.map(findFriend).filter(Boolean); return ( ); })}
); } // ── Friends going this week ───────────────────────────────────────── function FriendsThisWeek({ openClassSheet, user }) { // Sessions with ≥ 2 friends going const popular = window.NADI.SCHEDULE .filter((s) => s.friends.length >= 3 && minutesUntil(s) > 0 && !user.booked.includes(s.id)) .sort((a, b) => b.friends.length - a.friends.length) .slice(0, 3); if (popular.length === 0) return null; return (
your circle

Where they're going.

{popular.map((s) => { const c = findClass(s.classId), co = findCoach(s.coachId); const friends = s.friends.map(findFriend).filter(Boolean); return ( ); })}
); } // ── Breath strip (streak + brand signature) ───────────────────────── function BreathStrip({ user }) { return (
{user.streak} weeks
your steady rhythm

breathe in, four counts.
breathe out, four counts. the mat will wait.

); } Object.assign(window, { TodayScreen });