/* ============================================================ Screens: Login, Dashboard, Booking, MyBookings, Profile ============================================================ */ /* -------------------- LOGIN -------------------- */ function LoginScreen({ supabaseReady }) { const db = window.YCCdb; const [mode, setMode] = useState("signin"); // signin | signup const [email, setEmail] = useState(""); const [pw, setPw] = useState(""); const [err, setErr] = useState(""); const [msg, setMsg] = useState(""); const [busy, setBusy] = useState(false); function google() { setErr(""); setMsg(""); if (!supabaseReady) { setErr("Sign-in isn't configured yet — add your Supabase keys."); return; } db.auth.signInWithGoogle().catch(() => setErr("Couldn't start Google sign-in.")); } function submit(e) { e.preventDefault(); setErr(""); setMsg(""); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setErr("Enter a valid email address."); return; } if (pw.length < 6) { setErr("Password must be at least 6 characters."); return; } if (!supabaseReady) { setErr("Sign-in isn't configured yet — add your Supabase keys."); return; } setBusy(true); const p = mode === "signup" ? db.auth.signUpEmail(email, pw) : db.auth.signInEmailPassword(email, pw); let settled = false; const timer = setTimeout(() => { if (!settled) { setBusy(false); setErr("Sign-in is taking too long — refresh the page and try again."); } }, 9000); p.then((res) => { settled = true; clearTimeout(timer); const error = res && res.error; if (error) { setBusy(false); setErr(error.message || "Sign-in failed."); return; } if (mode === "signup") { const user = res.data && res.data.user; const session = res.data && res.data.session; // Supabase returns a user with an EMPTY identities array when the email already exists. if (user && Array.isArray(user.identities) && user.identities.length === 0) { setBusy(false); setErr("That email already has an account. Use “Sign in” instead — or have the manager delete it in Supabase → Authentication → Users to start over."); return; } if (session) { setBusy(false); return; } // confirmation OFF + auto-session → listener swaps in // No session: either confirmation is ON, or we can sign in now. const confirmed = user && (user.email_confirmed_at || user.confirmed_at); if (!confirmed) { setBusy(false); setErr("Your Supabase project still requires email confirmation. Turn OFF “Confirm email” (Authentication → Sign In / Providers → Email → Save), delete this user under Authentication → Users, then create the account again."); return; } db.auth.signInEmailPassword(email, pw).then((r2) => { setBusy(false); const sess = r2 && (r2.session || (r2.data && r2.data.session)); if (sess) return; setMsg("Account created — sign in with your email and password."); }).catch(() => { setBusy(false); setMsg("Account created — now sign in."); }); return; } setBusy(false); // Sign-in success with a session → the auth listener swaps to the app automatically. }).catch((e) => { settled = true; clearTimeout(timer); setBusy(false); setErr((e && e.message) || "Something went wrong. Try again."); }); } return (
{/* left: brand panel */}
YCC
Yardley Country Club
DIV Tour · Yardley, PA
DIV Tour · Member Portal

Your weekend on the fairway, booked in seconds.

Reserve weekly tee times, manage your group, and run the Saturday skins game — all from one place.

Championship 18 USGA / GHIN
{/* right: form */}

{mode === "signup" ? "Create your account" : "Welcome"}

{mode === "signup" ? "Sign up with your email to join the DIV Tour." : "Sign in with your email to access the DIV Tour."}

{ setEmail(e.target.value); setErr(""); }} />
setPw(e.target.value)} /> {err &&
{err}
} {msg &&
{msg}
}

{mode === "signup" ? "Already have an account? " : "New to the DIV Tour? "} { setMode(mode === "signup" ? "signin" : "signup"); setErr(""); setMsg(""); }}> {mode === "signup" ? "Sign in" : "Create one"}

{!supabaseReady && (

Sign-in requires Supabase keys in config.js.

)}
); } /* -------------------- DASHBOARD -------------------- */ function Dashboard({ ctx }) { const { member, bookings, slotOf, go, daysUntil, tour, gameConfig } = ctx; const Y = window.YCC; const Gm = window.YCC_GAMES, Eng = window.YCC_SCORING; const satG = Gm.gameById(Eng.whichGame(gameConfig, tour.satISO, tour.satISO)); const sunG = Gm.gameById(Eng.whichGame(gameConfig, tour.sunISO, tour.satISO)); const sameGame = satG.id === sunG.id; const sorted = [...bookings].sort((a, b) => a.dateISO.localeCompare(b.dateISO)); const nextB = sorted[0] || null; const nextSlot = nextB ? slotOf(nextB) : null; const nextDateLabel = nextB ? Y.fmtDate(new Date(nextB.dateISO + "T00:00:00")) : ""; const hour = new Date().getHours(); const greet = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening"; return (
{window.YCC.fmtDate(window.YCC.TODAY)}

{greet}, {member.first}.

The DIV Tour entry for the weekend of {tour.weekendLabel} is published. Claim your spot and your handicap travels with you.

{/* next tee time */}
{nextSlot ? (
Your next tee time
{daysUntil(nextB.dateISO)}
{nextSlot.time}
{nextDateLabel}
Championship 18 · DIV Tour
Your group · {nextSlot.players.length} of 4
{nextSlot.players.map((p, i) => (
{p.name === member.name ? p.name + " (you)" : p.name}
HI {p.hi} · GHIN {p.ghin}
))}
) : (

You haven't claimed a spot yet

The DIV Tour entry is open — claim an open spot for Saturday or Sunday.

)} {/* conditions */}
{/* right column */}
Handicap · GHIN
{member.handicapIndex ?? "—"} Index
GHIN number {member.ghin || "—"}
Course handicap {member.handicapIndex == null ? "—" : window.YCC.courseHandicapForTee(member.handicapIndex, member.tee || "Blue")} · {member.tee || "Blue"}
{member.handicapIndex == null && (

Add your GHIN and handicap in your profile.

)}
This weekend's games
{sameGame ? ( <>

{satG.name}

{satG.blurb}

) : (
{[["Saturday", satG], ["Sunday", sunG]].map(([d, gm]) => (
{d} {gm.name}
))}
)}
); } /* -------------------- BOOKING (claim a spot in the DIV Tour entry) -------------------- */ function SeatFilled({ p, you, onRemove, onMove, onTransport, approved }) { return (
{you ? p.name + " (you)" : p.name} {p.guest && Off-app} {approved ? Approved : Pending HCP}
HI {p.hi} · GHIN {p.ghin} {onTransport ? : }
{onMove && ( )} {onRemove && ( )}
); } function SeatOpen({ onClaim, canClaim, manager }) { if (manager) { return ( ); } return ( ); } function BookingScreen({ ctx }) { const Y = window.YCC; const { tour, dayKeys, member, requestClaim, role, t, go, activeManager, removeSignup, requestManagerMove, requestAddPlayer, setTransport, managerSwapPlayers, managerMovePlayer, phase, entry, confirmedNames, requestManagerEmail, primaryManager } = ctx; const [activeDay, setActiveDay] = useState(dayKeys[0].iso); const dragRef = useRef(null); const [dragName, setDragName] = useState(null); const slots = Y.sortByTime(tour.slots[activeDay] || []); const dayNote = (tour.notes || {})[activeDay]; const flow = t.bookingFlow || "sheet"; const isManager = role === "manager"; const myDaySlot = slots.find(s => s.players.some(p => p.name === member.name)); const memberOpen = phase === "open"; // members can self-serve only while open const managerLocked = false; // managers keep control through game day const isApproved = (p) => p.guest || (confirmedNames && confirmedNames.has(p.name)); const fmtET = (d) => d.toLocaleString("en-US", { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); // game-day drag/drop: drop A onto B → swap; drop A onto an open seat → move. function handleSeatDrop(dstSlotId, dstName) { const src = dragRef.current; dragRef.current = null; setDragName(null); if (!src) return; if (dstName) managerSwapPlayers(activeDay, src.slotId, src.name, dstSlotId, dstName); else if (src.slotId !== dstSlotId) managerMovePlayer(activeDay, src.slotId, dstSlotId, src.name); } if (!tour.published && !isManager) { return (

This weekend isn't published yet

{activeManager.name} hasn't released the DIV Tour tee times for {tour.weekendLabel}. You'll be notified by email when sign-up opens.

); } return (
DIV Tour · Weekend of {tour.weekendLabel}

{isManager ? "Tee sheet" : "Tee times"}

{isManager ? "Live view of who has claimed a spot in this weekend's published entry. Edit the times in Tour Setup." : "Claim an open spot in any group. Your handicap and GHIN are attached automatically when you join."}

Published by {activeManager.name}
{tour.publishedAt}
{/* sign-up window banner */} {(() => { const map = { open: { bg: "var(--brand-soft)", fg: "var(--brand)", icon: "calendar", text: <>Open sign-ups — claim, move, or release your own spot until Wednesday 11:59 PM ET ({fmtET(entry.wed)}). After that, changes go through your manager. }, managerOnly: { bg: "color-mix(in srgb,var(--accent) 14%,var(--surface))", fg: "var(--accent-deep)", icon: "shield", text: <>Member sign-ups are closed. Your manager can add, release, or move players until the final lock Friday 9:00 AM ET ({fmtET(entry.fri)}). Ask your manager for any changes. }, locked: { bg: "var(--surface-2)", fg: "var(--ink-soft)", icon: "clock", text: <>Groups are final — the Friday 9:00 AM ET lock has passed. The tee sheet is set for the weekend. }, }[phase]; return (
{map.text}
{!isManager && phase !== "open" && ( )}
); })()} {/* day tabs */}
{dayKeys.map((d) => { const daySlots = tour.slots[d.iso] || []; const open = daySlots.reduce((n, s) => n + (4 - s.players.length), 0); const active = activeDay === d.iso; return ( ); })} {myDaySlot && !isManager && ( You're in the {myDaySlot.time} group )}
{dayNote && slots.length > 0 && (
Club event this day. {dayNote}
)} {isManager && slots.length > 0 && (
Drag a player onto another to swap their groups, or onto an open seat to move them.
)} {slots.length === 0 ? (

{dayNote ? "No DIV Tour group times this day" : "No tee times set for this day"}

{dayNote || (isManager ? "Add times in Tour Setup." : "Check back once the manager publishes them.")}

) : flow === "grid" ? (
{slots.map((slot) => { const mine = slot.players.some(p => p.name === member.name); const open = 4 - slot.players.length; const canClaim = !isManager && open > 0 && !mine && !myDaySlot && memberOpen; return (
{slot.time}
{mine ? "You're in" : open ? `${open} open` : "Full"}
{[0, 1, 2, 3].map(i => { const p = slot.players[i]; return ( {p ? p.initials : ""} ); })}
{isManager ? ( managerLocked ? (
{slot.players.length} of 4 · final
) : open ? ( ) : (
Group full · 4 of 4
) ) : mine ? ( ) : !memberOpen ? ( ) : myDaySlot ? ( ) : open ? ( ) : ( )}
); })}
) : (
{slots.map((slot) => { const mine = slot.players.some(p => p.name === member.name); const open = 4 - slot.players.length; const canClaim = !isManager && open > 0 && !mine && !myDaySlot && memberOpen; return (
{slot.time} {mine ? "You're in this group" : open ? `${open} open spot${open > 1 ? "s" : ""}` : "Group full"}
{isManager ? ( {slot.players.length} / 4 claimed ) : mine ? ( ) : !memberOpen ? ( Sign-ups closed ) : myDaySlot ? ( One spot per day ) : open ? ( ) : ( Full )}
{[0, 1, 2, 3].map(i => { const p = slot.players[i]; const dropZone = isManager ? { onDragOver: (e) => { if (dragRef.current) e.preventDefault(); }, onDrop: (e) => { e.preventDefault(); handleSeatDrop(slot.id, p ? p.name : null); }, } : {}; if (p) { const isYou = p.name === member.name; const key = slot.id + "|" + p.name; return (
{ dragRef.current = { slotId: slot.id, name: p.name }; setDragName(key); } : undefined} onDragEnd={() => { dragRef.current = null; setDragName(null); }} style={{ cursor: isManager ? "grab" : "default", opacity: dragName === key ? .45 : 1 }}> removeSignup(activeDay, slot.id, p.name) : undefined} onMove={isManager ? () => requestManagerMove(activeDay, slot, p) : undefined} onTransport={(isManager || isYou) ? () => setTransport(activeDay, slot.id, p.name, (p.transport || "walk") === "walk" ? "ride" : "walk") : undefined} />
); } if (isManager) return
requestAddPlayer(activeDay, slot)} />
; return requestClaim(activeDay, slot)} />; })}
); })}
)}
); } /* -------------------- WEATHER / CONDITIONS -------------------- */ function ConditionsCard() { const Y = window.YCC; const [wx, setWx] = useState(null); const [state, setState] = useState("loading"); // loading | ok | error useEffect(() => { let alive = true; Y.fetchWeather().then((w) => { if (!alive) return; if (w && w.tempNow != null && !isNaN(w.tempNow)) { setWx(w); setState("ok"); } else setState("error"); }); return () => { alive = false; }; }, []); const tiles = wx ? [ [`${wx.tempNow}°`, "Now"], [isNaN(wx.high) ? "—" : `${wx.high}°`, "Today's high"], [`${wx.windMph}`, `mph ${wx.windDir}`.trim()], [wx.condition, "Conditions"], ] : [["—", "Now"], ["—", "Today's high"], ["—", "Wind"], ["—", "Conditions"]]; return (

Course conditions

Cart path: open
{tiles.map(([v, l], i) => (
6 ? 18 : 26, fontWeight: 700, color: "var(--brand)", lineHeight: 1.05 }}>{v}
{l}
))}
{state === "ok" ? `${Y.YARDLEY_GEO.label} · live, ${wx.source}` : state === "loading" ? "Loading live weather…" : `${Y.YARDLEY_GEO.label} · weather unavailable`}
); } Object.assign(window, { LoginScreen, Dashboard, BookingScreen, SeatFilled, SeatOpen, ConditionsCard });