/* ============================================================ Yardley Country Club — mock data + golf helpers Exposed on window.YCC for the React app. ============================================================ */ // --- The signed-in member ---------------------------------- // Fallback/guest shape — real identity comes from the signed-in account. const MEMBER = { id: null, first: "", last: "", name: "", email: "", ghin: "", handicapIndex: null, memberSince: null, homeClub: "Yardley Country Club", tier: "Member", tee: "White", phone: "", initials: "", }; // No demo roster — members come from the database / real sign-ups. const ROSTER = []; // --- Date helpers: find this coming weekend ---------------- function fmtDate(d) { return d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }); } function shortDate(d) { return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }); } function isoDay(d) { return d.toISOString().slice(0, 10); } // Use the actual current date; the weekend is computed from today. const TODAY = (() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; })(); function nextDow(from, dow) { const d = new Date(from); const diff = (dow - d.getDay() + 7) % 7 || 7; d.setDate(d.getDate() + diff); return d; } const SAT = nextDow(TODAY, 6); const SUN = nextDow(TODAY, 0); // --- Tee sheet generator ----------------------------------- // Deterministic pseudo-random so reloads look the same. function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function timeLabel(h, m) { const ap = h >= 12 ? "PM" : "AM"; const hh = h % 12 === 0 ? 12 : h % 12; return `${hh}:${m.toString().padStart(2, "0")} ${ap}`; } function buildTeeSheet(date, seed) { const rand = mulberry32(seed); const slots = []; let h = 7, m = 0, idx = 0; while (h < 11 || (h === 11 && m === 0)) { const fillCount = Math.floor(rand() * 5); // 0..4 occupancy const players = []; const used = new Set(); for (let i = 0; i < fillCount; i++) { let p; do { p = ROSTER[Math.floor(rand() * ROSTER.length)]; } while (used.has(p.name) && used.size < ROSTER.length); used.add(p.name); players.push({ ...p }); } slots.push({ id: `${isoDay(date)}-${idx}`, time: timeLabel(h, m), hour: h, min: m, players, }); idx++; m += 9; if (m >= 60) { m -= 60; h += 1; } } return slots; } // --- Time helpers ------------------------------------------ function parseTime(label) { const m = label.match(/(\d+):(\d+)\s*(AM|PM)/i); if (!m) return 0; let h = parseInt(m[1], 10) % 12; if (/pm/i.test(m[3])) h += 12; return h * 60 + parseInt(m[2], 10); } function sortByTime(slots) { return [...slots].sort((a, b) => parseTime(a.time) - parseTime(b.time)); } // --- Sign-up windows (Eastern Time) ------------------------ // Open to all members until WED 11:59 PM ET; managers-only until FRI 9 AM ET; // then final lock. Deadlines are relative to that weekend's Saturday. function nowEastern() { return new Date(new Date().toLocaleString("en-US", { timeZone: "America/New_York" })); } function entryWindow(satDate) { const y = satDate.getFullYear(), mo = satDate.getMonth(), da = satDate.getDate(); const wed = new Date(y, mo, da - 3, 23, 59, 59); // Wednesday 11:59 PM ET const fri = new Date(y, mo, da - 1, 9, 0, 0); // Friday 9:00 AM ET const now = nowEastern(); let phase = "open"; if (now > fri) phase = "locked"; else if (now > wed) phase = "managerOnly"; return { phase, wed, fri }; } // Was a member's handicap confirmed for THIS week (within 7 days before Saturday)? function confirmedForWeek(confirmedAt, satDate) { if (!confirmedAt) return false; const cutoff = new Date(satDate); cutoff.setDate(cutoff.getDate() - 7); return new Date(confirmedAt) >= cutoff; } // --- Phone formatting: (XXX) XXX-XXXX ---------------------- function formatPhone(v) { const d = (v || "").replace(/\D/g, "").slice(0, 10); if (d.length < 4) return d; if (d.length < 7) return `(${d.slice(0, 3)}) ${d.slice(3)}`; return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`; } // --- Live weather for Yardley, PA 19067 -------------------- // Coordinates for the 19067 ZIP (Yardley / Lower Makefield, PA). const YARDLEY_GEO = { lat: 40.2445, lon: -74.8463, label: "Yardley, PA 19067" }; const WMO = { 0: "Clear", 1: "Mostly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Fog", 51: "Drizzle", 53: "Drizzle", 55: "Drizzle", 61: "Light rain", 63: "Rain", 65: "Heavy rain", 66: "Freezing rain", 67: "Freezing rain", 71: "Light snow", 73: "Snow", 75: "Heavy snow", 77: "Snow grains", 80: "Showers", 81: "Showers", 82: "Heavy showers", 85: "Snow showers", 86: "Snow showers", 95: "Thunderstorm", 96: "Thunderstorm", 99: "Thunderstorm", }; function compass(deg) { if (deg == null || isNaN(deg)) return ""; const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; return dirs[Math.round(deg / 45) % 8]; } // Returns a promise of { tempNow, high, low, windMph, windDir, condition, source } or null. // Uses Open-Meteo (free, no key, sourced from national weather services incl. NWS/NOAA). // Set window.YCC_CONFIG.weatherProxy to an AccuWeather-backed endpoint to override. async function fetchWeather() { const cfg = (window.YCC_CONFIG || {}); try { if (cfg.weatherProxy) { const r = await fetch(cfg.weatherProxy); if (r.ok) return await r.json(); } const { lat, lon } = YARDLEY_GEO; const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` + `¤t=temperature_2m,wind_speed_10m,wind_direction_10m,weather_code` + `&daily=temperature_2m_max,temperature_2m_min` + `&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=America%2FNew_York&forecast_days=1`; const r = await fetch(url); if (!r.ok) return null; const j = await r.json(); const c = j.current || {}; const d = j.daily || {}; return { tempNow: Math.round(c.temperature_2m), high: Math.round((d.temperature_2m_max || [])[0]), low: Math.round((d.temperature_2m_min || [])[0]), windMph: Math.round(c.wind_speed_10m), windDir: compass(c.wind_direction_10m), condition: WMO[c.weather_code] || "—", source: "Open-Meteo · NWS data", }; } catch (e) { return null; } } // --- The DIV Tour weekly entry (manager-published) --------- // The manager publishes specific tee times for Saturday and Sunday. // Members claim individual open spots; their handicap (HI + GHIN) attaches. const ADMIN_EMAIL = "grahamat6@gmail.com"; // Emails that ALWAYS have tour-manager access (independent of the database). const ADMIN_EMAILS = ["grahamat6@gmail.com", "dapaoni3@gmail.com"]; function isAdminEmail(email) { const e = (email || "").trim().toLowerCase(); return !!e && ADMIN_EMAILS.some((a) => a.trim().toLowerCase() === e); } // The DIV Tour primary manager — change-request emails go here. // David Paoni is the app's primary tour manager (and a tour member). const PRIMARY_MANAGER = { name: "David Paoni", initials: "DP", email: "dapaoni3@gmail.com", phone: "(267) 421-1003", title: "DIV Tour Manager" }; // Additional club inboxes members can also notify on a change request. const CLUB_CONTACTS = [ { id: "golfshop", name: "YCC Golf Shop", email: "golfshop@yardleycc.com" }, { id: "headpro", name: "Head Pro · Yardley Country Club", email: "HeadPro@yardleycc.com" }, ]; const DEFAULT_MANAGERS = [ { name: PRIMARY_MANAGER.name, initials: PRIMARY_MANAGER.initials, role: "Primary", title: PRIMARY_MANAGER.title, email: PRIMARY_MANAGER.email, phone: PRIMARY_MANAGER.phone, available: true }, { name: "Graham", initials: "G", role: "Backup", title: "Administrator", email: ADMIN_EMAIL, phone: "", available: true, admin: true }, ]; // Season default tee times + tournament exceptions (no DIV Tour group times). // Dates are the affected calendar day (ISO). Through end of October 2026. const TEE_EXCEPTIONS = { "2026-06-06": "Women's Member Guest — no group times. Course opens 2:30 PM; members make times online.", "2026-06-13": "Men's & Women's Senior Championship — no group times before 11 AM; members make times online.", "2026-06-14": "Men's & Women's Senior Championship — no group times before 11 AM; members make times online.", "2026-06-27": "Men's Member Guest — no times available all day.", "2026-07-04": "Red, White & Blue Tournament — no group times. Members sign up on ForeTees.", }; const SEASON_END = "2026-10-31"; function defaultTeeTimes(dayISO, dow) { if (TEE_EXCEPTIONS[dayISO] || dayISO > SEASON_END) return []; return dow === 6 ? ["11:34 AM", "11:43 AM"] : ["11:45 AM", "11:54 AM"]; } function buildDivTour(member) { const satISO = isoDay(SAT), sunISO = isoDay(SUN); const satTimes = defaultTeeTimes(satISO, 6); const sunTimes = defaultTeeTimes(sunISO, 0); const mk = (times, dayISO) => times.map((time, i) => ({ id: `${dayISO}-${i}`, time, players: [] })); const slots = { [satISO]: mk(satTimes, satISO), [sunISO]: mk(sunTimes, sunISO) }; const notes = {}; if (TEE_EXCEPTIONS[satISO]) notes[satISO] = TEE_EXCEPTIONS[satISO]; if (TEE_EXCEPTIONS[sunISO]) notes[sunISO] = TEE_EXCEPTIONS[sunISO]; const weekendLabel = `${SAT.toLocaleDateString("en-US", { month: "long", day: "numeric" })} – ${SUN.toLocaleDateString("en-US", { day: "numeric" })}`; return { satISO, sunISO, slots, notes, bookings: [], weekendLabel, published: true, managers: DEFAULT_MANAGERS.map(m => ({ ...m })), publishedAt: null, }; } // --- Holes / scorecard for the skins game ------------------ // Yardley-style par 71, with stroke index (handicap) per hole. // --- Yardley Country Club scorecard (real card) ------------ // Par 72. Five tee sets, each with its own rating/slope and stroke-index row: // Blue & White share one index; Green & Gold share another; Red has its own. const HOLE_PAR = [4, 4, 4, 4, 4, 4, 5, 4, 3, 5, 4, 4, 5, 4, 3, 4, 3, 4]; const SI_SETS = { BlueWhite: [13, 1, 5, 11, 3, 17, 7, 9, 15, 10, 8, 6, 16, 2, 12, 18, 4, 14], GreenGold: [11, 1, 5, 9, 3, 13, 17, 7, 15, 6, 4, 10, 12, 2, 8, 18, 16, 14], Red: [13, 7, 3, 9, 1, 15, 5, 11, 17, 2, 14, 4, 12, 8, 18, 10, 16, 6], }; const TEES = [ { name: "Blue", rating: 70.6, slope: 132, si: "BlueWhite", total: 6379, yards: [328, 387, 408, 329, 380, 309, 557, 352, 148, 551, 388, 370, 490, 436, 159, 262, 217, 308] }, { name: "White", rating: 69.2, slope: 130, si: "BlueWhite", total: 6057, yards: [320, 376, 397, 307, 356, 292, 538, 346, 129, 509, 370, 347, 471, 415, 142, 255, 185, 302] }, { name: "Green", rating: 67.6, slope: 127, si: "GreenGold", total: 5714, yards: [320, 376, 353, 307, 340, 292, 429, 346, 129, 478, 370, 284, 471, 408, 142, 255, 112, 302] }, { name: "Gold", rating: 66.0, slope: 124, si: "GreenGold", total: 5356, yards: [274, 321, 353, 292, 340, 273, 429, 300, 110, 478, 314, 284, 423, 408, 106, 248, 112, 291] }, { name: "Red", rating: 70.9, slope: 124, si: "Red", total: 5304, yards: [309, 318, 348, 283, 336, 265, 426, 297, 106, 472, 309, 280, 417, 400, 99, 244, 108, 287] }, ]; function teeByName(name) { return TEES.find((t) => t.name === name) || TEES[0]; } function siForTee(name, holeIdx) { return SI_SETS[teeByName(name).si][holeIdx]; } function yardsForTee(name, holeIdx) { return teeByName(name).yards[holeIdx]; } // HOLES default to Blue yards + Blue/White stroke index for generic display. const HOLES = HOLE_PAR.map((par, i) => ({ n: i + 1, par, yards: TEES[0].yards[i], si: SI_SETS.BlueWhite[i] })); // --- The weekend skins game -------------------------------- // The weekend skins game is built from real scores entered during play. const GAME_PLAYERS = []; const GAME_SCORES = []; // Course rating / slope to turn HI into a course handicap (default Blue tees). const COURSE = { rating: 70.6, slope: 132, par: 72, tee: "Blue", yards: 6379 }; function courseHandicap(hi) { return Math.round((hi * COURSE.slope) / 113 + (COURSE.rating - COURSE.par)); } // Course handicap from a specific set of tees (Blue/White/Green/Red). function courseHandicapForTee(hi, teeName) { const tee = TEES.find((t) => t.name === teeName) || COURSE; return Math.round((hi * tee.slope) / 113 + (tee.rating - COURSE.par)); } // strokes a player receives on a given hole (by stroke index) function strokesOnHole(courseHcp, si) { let s = Math.floor(courseHcp / 18); if (si <= courseHcp % 18) s += 1; return s; } // strokes a player gets on a hole RELATIVE to the low player in the group. // The low course handicap plays to scratch (0); everyone else gets the // difference, allocated by stroke index. This is what the scorecard "dots" show. function strokesVsLow(courseHcp, lowCourseHcp, si) { const diff = Math.max(0, Math.round(courseHcp) - Math.round(lowCourseHcp)); return strokesOnHole(diff, si); } /* Compute skins. mode: "gross" | "net" carryover: boolean skinValue: dollars per skin returns { holes:[{n, winnerIdx|null, value, carriedIn}], totals:[{idx,skins,money}] } */ function computeSkins({ players, scores, holes, mode, carryover, skinValue, courseHcps }) { const cHcps = courseHcps || players.map((p) => courseHandicap(p.hi)); const out = []; let pot = 0; const totals = players.map(() => ({ skins: 0, money: 0 })); holes.forEach((hole, hIdx) => { const vals = players.map((p, pIdx) => { const gross = scores[pIdx][hIdx]; if (mode === "net") return gross - strokesOnHole(cHcps[pIdx], hole.si); return gross; }); const min = Math.min(...vals); const winners = vals.map((v, i) => (v === min ? i : -1)).filter((i) => i >= 0); const carriedIn = pot; if (winners.length === 1) { const value = skinValue + (carryover ? pot : 0); out.push({ n: hole.n, winnerIdx: winners[0], value, carriedIn, scores: vals }); totals[winners[0]].skins += 1 + (carryover ? carriedIn / skinValue : 0); totals[winners[0]].money += value; pot = 0; } else { // tie if (carryover) { pot += skinValue; out.push({ n: hole.n, winnerIdx: null, value: 0, carriedIn, scores: vals }); } else { out.push({ n: hole.n, winnerIdx: null, value: 0, carriedIn: 0, scores: vals }); } } }); return { holes: out, totals, potLeft: pot }; } window.YCC = { MEMBER, ROSTER, ADMIN_EMAIL, ADMIN_EMAILS, isAdminEmail, PRIMARY_MANAGER, CLUB_CONTACTS, DEFAULT_MANAGERS, TODAY, SAT, SUN, fmtDate, shortDate, isoDay, buildDivTour, parseTime, sortByTime, formatPhone, nowEastern, entryWindow, confirmedForWeek, fetchWeather, YARDLEY_GEO, compass, HOLES, TEES, SI_SETS, teeByName, siForTee, yardsForTee, COURSE, GAME_PLAYERS, GAME_SCORES, courseHandicap, courseHandicapForTee, strokesOnHole, strokesVsLow, computeSkins, timeLabel, };