Brunswick Hills Tennis Center | Premier Tennis Academy in New Jersey

REFRESH

import React, { useEffect, useMemo, useState } from "react";

// BHTC Tennis — Single‑Pair / Two‑Set Scheduler
// -----------------------------------------------------------------------------
// FIX: Wrapped adjacent JSX properly and cleaned up mismatched tags/attributes.
// Also removed stray escape characters in JSX attributes and ensured all
// conditionals close correctly. This should resolve the error:
//   "Adjacent JSX elements must be wrapped ... (538:4)"
// -----------------------------------------------------------------------------
// Features:
// 1) Upload Excel/CSV (sheet "Schedule") with: Date, Day, Start, End, Location,
//    Players (4), Pairing, Notes
// 2) Filter by player (chips + search)
// 3) Scores: SAME pairing plays both sets (two sets total per night)
// 4) Leaderboard tallies sets won by player
// 5) Download Outlook‑compatible .ics for each night
// 6) Responsive UI: mobile cards + desktop table
// 7) Local storage for scores; export/import JSON
// -----------------------------------------------------------------------------

// ---- Utilities ----
const SEASON_KEY = "bhtc-2025-2026"; // storage namespace
const SCORE_KEY = `${SEASON_KEY}-scores-v1`;

function clsx(...xs) {
  return xs.filter(Boolean).join(" ");
}

function parsePlayers(str) {
  if (!str) return [];
  return String(str)
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
}

function toISODate(d) {
  // YYYY-MM-DD from Date
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

function formatLongDate(d) {
  return d.toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
}

function parseExcelDate(value) {
  // Attempt to parse cells that might be Excel serials or strings
  if (value == null) return null;
  if (value instanceof Date) return value;
  if (typeof value === "number") {
    // Excel serial date: days since 1899-12-30
    const epoch = new Date(Date.UTC(1899, 11, 30));
    const ms = value * 24 * 60 * 60 * 1000;
    return new Date(epoch.getTime() + ms);
  }
  // Try ISO-ish or "September 9, 2025"
  const d = new Date(value);
  return isNaN(d.getTime()) ? null : d;
}

// Single pairing per night, same pairing plays both sets
function computeRotations(players, pairing) {
  function parsePairing(text) {
    if (!text || typeof text !== "string") return null;
    const parts = text.split(/vs/i);
    if (parts.length !== 2) return null;
    const left = parts[0].split("&").map((s) => s.trim()).filter(Boolean);
    const right = parts[1].split("&").map((s) => s.trim()).filter(Boolean);
    if (left.length !== 2 || right.length !== 2) return null;
    return [ { left, right }, { left, right } ];
  }
  const parsed = parsePairing(pairing);
  if (parsed) return parsed;
  const ps = [...players].sort();
  if (ps.length !== 4) return [];
  const [A, B, C, D] = ps;
  return [ { left: [A, B], right: [C, D] }, { left: [A, B], right: [C, D] } ];
}

function makeICS({ title, description, location, start, end, tzid = "America/New_York" }) {
  // start/end are Date objects in local time; we will render DTSTART;TZID and DTEND;TZID
  function fmt(d) {
    // 20250909T203000
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    const hh = String(d.getHours()).padStart(2, "0");
    const mm = String(d.getMinutes()).padStart(2, "0");
    const ss = String(d.getSeconds()).padStart(2, "0");
    return `${y}${m}${day}T${hh}${mm}${ss}`;
  }

  const uid = `${toISODate(start)}-${Math.random().toString(36).slice(2)}@bhtc-scheduler`;

  // Minimal VTIMEZONE for America/New_York
  const vtimezone = `BEGIN:VTIMEZONE\nTZID:${tzid}\nBEGIN:STANDARD\nDTSTART:19701101T020000\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\nTZOFFSETFROM:-0400\nTZOFFSETTO:-0500\nEND:STANDARD\nBEGIN:DAYLIGHT\nDTSTART:19700308T020000\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\nTZOFFSETFROM:-0500\nTZOFFSETTO:-0400\nEND:DAYLIGHT\nEND:VTIMEZONE`;

  const ics = [
    "BEGIN:VCALENDAR",
    "VERSION:2.0",
    "PRODID:-//BHTC Tennis//Schedule//EN",
    vtimezone,
    "BEGIN:VEVENT",
    `UID:${uid}`,
    `SUMMARY:${title}`,
    `DESCRIPTION:${(description || "").replace(/\n/g, "\\n")}`,
    `LOCATION:${(location || "").replace(/\n/g, "\\n")}`,
    `DTSTART;TZID=${tzid}:${fmt(start)}`,
    `DTEND;TZID=${tzid}:${fmt(end)}`,
    "END:VEVENT",
    "END:VCALENDAR",
  ].join("\n");

  return ics;
}

function download(filename, text) {
  const blob = new Blob([text], { type: "text/calendar;charset=utf-8" });
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

// ---- Types ----
/** @typedef {{ date: Date, day?: string, start?: string, end?: string, location?: string, players: string[], pairing?: string, notes?: string }} ScheduleItem */

// ---- Main App ----
export default function App() {
  const [schedule, setSchedule] = useState([]); // ScheduleItem[]
  const [playerQuery, setPlayerQuery] = useState("");
  const [scores, setScores] = useState({}); // { [isoDate]: { sets: [ { winners: string[] } ] } }
  const [scoreModal, setScoreModal] = useState(null); // ScheduleItem | null
  const [importError, setImportError] = useState("");
  const [selfTest, setSelfTest] = useState("");

  // Load scores from storage
  useEffect(() => {
    try {
      const saved = localStorage.getItem(SCORE_KEY);
      if (saved) setScores(JSON.parse(saved));
    } catch {}
  }, []);

  useEffect(() => {
    try {
      localStorage.setItem(SCORE_KEY, JSON.stringify(scores));
    } catch {}
  }, [scores]);

  const playersList = useMemo(() => {
    const set = new Set();
    schedule.forEach((e) => e.players.forEach((p) => set.add(p)));
    return Array.from(set).sort();
  }, [schedule]);

  const filtered = useMemo(() => {
    const q = playerQuery.trim().toLowerCase();
    if (!q) return [...schedule].sort((a, b) => a.date - b.date);
    return schedule
      .filter((e) => e.players.some((p) => p.toLowerCase().includes(q)))
      .sort((a, b) => a.date - b.date);
  }, [schedule, playerQuery]);

  const winsByPlayer = useMemo(() => {
    const w = Object.create(null);
    playersList.forEach((p) => (w[p] = 0));
    for (const rec of Object.values(scores)) {
      if (!rec || !Array.isArray(rec.sets)) continue;
      rec.sets.forEach((s) => {
        if (!s || !Array.isArray(s.winners)) return;
        s.winners.forEach((name) => {
          w[name] = (w[name] || 0) + 1;
        });
      });
    }
    return w;
  }, [scores, playersList]);

  function onDownloadICS(item) {
    const title = `BHTC Tennis — Doubles`;
    const description = `Players: ${item.players.join(", ")}${item.notes ? `\nNotes: ${item.notes}` : ""}`;
    const location = item.location || "BHTC";

    // Build start/end in local time. Times from sheet, default 8:30–10:00 PM.
    const [startHour, startMinute, startMeridiem] = parseTime(item.start || "8:30 PM");
    const [endHour, endMinute, endMeridiem] = parseTime(item.end || "10:00 PM");
    const start = new Date(item.date);
    setTimeLocal(start, startHour, startMinute, startMeridiem);
    const end = new Date(item.date);
    setTimeLocal(end, endHour, endMinute, endMeridiem);

    const ics = makeICS({ title, description, location, start, end });
    download(`${toISODate(item.date)}-BHTC.ics`, ics);
  }

  function parseTime(t) {
    // "8:30 PM" -> [8,30,"PM"]
    if (!t) return [20, 30, "PM"]; // fallback
    const m = String(t).trim().match(/^(\d{1,2})(?::(\d{2}))?\s*([AP]M)$/i);
    if (!m) return [20, 30, "PM"]; // fallback to 8:30 PM
    let hh = parseInt(m[1], 10);
    const mm = parseInt(m[2] || "0", 10);
    const mer = (m[3] || "PM").toUpperCase();
    return [hh, mm, mer];
  }

  function setTimeLocal(d, hh12, mm, mer) {
    let h = hh12 % 12;
    if (mer === "PM") h += 12;
    d.setHours(h, mm, 0, 0);
  }

  function openScores(item) {
    setScoreModal(item);
  }

  function saveScores(isoDate, setsState) {
    setScores((prev) => ({ ...prev, [isoDate]: { sets: setsState } }));
    setScoreModal(null);
  }

  function resetScores() {
    if (!confirm("Reset all saved scores for this season?")) return;
    setScores({});
  }

  function exportScores() {
    const blob = new Blob([JSON.stringify(scores, null, 2)], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `${SEASON_KEY}-scores.json`;
    a.click();
  }

  function importScores(e) {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const data = JSON.parse(String(reader.result));
        setScores(data);
      } catch (err) {
        alert("Invalid scores JSON");
      }
    };
    reader.readAsText(file);
  }

  async function onUploadFile(file) {
    setImportError("");
    const name = file.name.toLowerCase();
    if (name.endsWith(".csv")) {
      const text = await file.text();
      const rows = parseCSV(text);
      const parsed = rowsToSchedule(rows);
      setSchedule(parsed);
    } else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
      try {
        const XLSXMod = await import("xlsx");
        const XLSX = (XLSXMod && XLSXMod.default) ? XLSXMod.default : XLSXMod;
        const buf = await file.arrayBuffer();
        const wb = XLSX.read(buf, { type: "array" });
        const sheet = wb.Sheets["Schedule"] || wb.Sheets[wb.SheetNames[0]];
        const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
        const parsed = rowsToSchedule(rows);
        setSchedule(parsed);
      } catch (err) {
        console.error(err);
        setImportError("XLSX parsing failed. Please export your Excel as CSV and upload the CSV.");
      }
    } else {
      setImportError("Unsupported file type. Please upload .xlsx or .csv");
    }
  }

  function parseCSV(text) {
    // lightweight CSV parser (no quotes/escapes). For best results, export simple CSV from Excel.
    const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
    return lines.map((line) => line.split(","));
  }

  function rowsToSchedule(rows) {
    // Expect header row. Try to map columns by names.
    const header = rows[0].map((h) => String(h).trim());
    const idx = (name) => header.findIndex((h) => h.toLowerCase() === name.toLowerCase());
    const colDate = idx("Date");
    const colDay = idx("Day");
    const colStart = idx("Start");
    const colEnd = idx("End");
    const colLoc = header.findIndex((h) => h.toLowerCase().startsWith("location"));
    const colPlayers = header.findIndex((h) => h.toLowerCase().startsWith("players"));
    const colPair = header.findIndex((h) => h.toLowerCase().startsWith("pairing"));
    const colNotes = idx("Notes");

    const out = [];
    for (let r = 1; r < rows.length; r++) {
      const row = rows[r];
      if (!row || row.length === 0) continue;
      const dRaw = row[colDate];
      const d = parseExcelDate(dRaw);
      if (!d) continue;
      const item = {
        date: d,
        day: colDay >= 0 ? String(row[colDay] ?? "") : "",
        start: colStart >= 0 ? String(row[colStart] ?? "") : "8:30 PM",
        end: colEnd >= 0 ? String(row[colEnd] ?? "") : "10:00 PM",
        location: colLoc >= 0 ? String(row[colLoc] ?? "BHTC") : "BHTC",
        players: parsePlayers(colPlayers >= 0 ? row[colPlayers] : ""),
        pairing: colPair >= 0 ? String(row[colPair] ?? "") : "",
        notes: colNotes >= 0 ? String(row[colNotes] ?? "") : "",
      };
      out.push(item);
    }
    // sort by date
    out.sort((a, b) => a.date - b.date);
    return out;
  }

  function loadDemo() {
    // A tiny inline demo (replace with your real XLSX/CSV upload)
    const demo = [
      {
        date: new Date("2025-09-09T00:00:00"),
        day: "Tuesday",
        start: "8:30 PM",
        end: "10:00 PM",
        location: "BHTC",
        players: ["Evan", "Steve", "Howie", "Robbie"],
        pairing: "Evan & Howie vs Robbie & Steve",
        notes: "",
      },
      {
        date: new Date("2025-09-16T00:00:00"),
        day: "Tuesday",
        start: "8:30 PM",
        end: "10:00 PM",
        location: "BHTC",
        players: ["Arun", "Eric", "Joseph", "Phil"],
        pairing: "Arun & Eric vs Joseph & Phil",
        notes: "",
      },
    ];
    setSchedule(demo);
  }

  // -------------------- Self Tests (console + UI badge) --------------------
  useEffect(() => {
    const results = [];
    // Test 1: computeRotations with explicit pairing
    const sets1 = computeRotations(["A","B","C","D"], "A & B vs C & D");
    results.push(sets1 && sets1.length === 2 && sets1[0].left.join("|") === "A|B" && sets1[0].right.join("|") === "C|D" ? "T1✓" : "T1✗");
    // Test 2: computeRotations fallback alphabetical
    const sets2 = computeRotations(["D","C","B","A"], "");
    results.push(sets2 && sets2[1].left.join("|") === "A|B" && sets2[1].right.join("|") === "C|D" ? "T2✓" : "T2✗");
    // Test 3: parseTime
    const t3 = JSON.stringify(parseTime("8:30 PM"));
    results.push(t3 === JSON.stringify([8,30,"PM"]) ? "T3✓" : "T3✗");
    // Test 4: parseExcelDate string
    const d4 = parseExcelDate("September 9, 2025");
    results.push(d4 instanceof Date ? "T4✓" : "T4✗");
    setSelfTest(results.join(" · "));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div className="min-h-screen bg-neutral-50 text-neutral-900 p-6">
      <div className="max-w-6xl mx-auto">
        <header className="mb-6">
          <h1 className="text-2xl md:text-3xl font-bold flex items-center gap-2">🎾 BHTC Tennis — 2025–2026</h1>
          <p className="text-neutral-600">Tuesdays 8:30–10:00 PM · East Brunswick, NJ · Doubles</p>
          {selfTest && (<div className="mt-2 text-xs text-neutral-500">Self‑tests: {selfTest}</div>)}
        </header>

        {/* Upload + Controls */}
        <div className="grid md:grid-cols-2 gap-4 mb-6">
          <div className="p-4 bg-white rounded-2xl shadow">
            <h2 className="font-semibold mb-2">Upload Schedule</h2>
            <p className="text-sm text-neutral-600 mb-3">Upload the Excel (<em>Schedule</em> sheet) or a CSV exported from Excel.</p>
            <input
              type="file"
              accept=".xlsx,.xls,.csv"
              onChange={(e) => {
                const f = e.target.files && e.target.files[0];
                if (f) onUploadFile(f);
              }}
              className="block w-full border rounded p-2"
            />
            {importError ? <p className="text-red-600 text-sm mt-2">{importError}</p> : null}
            <button onClick={loadDemo} className="mt-3 px-3 py-2 rounded-xl bg-neutral-900 text-white">Load demo</button>
          </div>

          <div className="p-4 bg-white rounded-2xl shadow flex flex-col gap-3">
            <div>
              <label className="block text-sm font-medium mb-1">Filter by player</label>
              <input
                type="text"
                placeholder="Type a name (e.g., Arun)"
                value={playerQuery}
                onChange={(e) => setPlayerQuery(e.target.value)}
                className="w-full border rounded p-2"
              />
              {playersList.length > 0 && (
                <div className="mt-2 overflow-x-auto">
                  <div className="flex gap-2 pb-1">
                    {playersList.map((name) => (
                      <button
                        key={name}
                        onClick={() => setPlayerQuery(name)}
                        className={clsx("px-3 py-1.5 rounded-full border", playerQuery.toLowerCase() === name.toLowerCase() && "bg-neutral-900 text-white")}
                        aria-label={`Filter by ${name}`}
                      >
                        {name}
                      </button>
                    ))}
                  </div>
                </div>
              )}
            </div>

            <div className="flex gap-2 items-center">
              <button
                onClick={exportScores}
                className="px-3 py-2 rounded-xl bg-neutral-900 text-white"
              >Export scores</button>
              <label className="px-3 py-2 rounded-xl border cursor-pointer">
                Import scores
                <input type="file" accept="application/json" className="hidden" onChange={importScores} />
              </label>
              <button onClick={resetScores} className="ml-auto text-red-700 hover:underline">Reset scores</button>
            </div>
          </div>
        </div>

        {/* Table */}
        <div className="bg-white rounded-2xl shadow overflow-hidden">
          <div className="px-4 py-3 border-b font-semibold sticky top-0 bg-white z-10">Schedule</div>

          {/* Mobile cards (≤ md) */}
          <div className="md:hidden divide-y">
            {filtered.map((item, idx) => {
              const disabled = item.players.length !== 4;
              const pairingText = item.pairing || (item.players.length === 4
                ? `${[...item.players].sort()[0]} & ${[...item.players].sort()[1]} vs ${[...item.players].sort()[2]} & ${[...item.players].sort()[3]}`
                : "");
              return (
                <div key={idx} className="p-4">
                  <div className="text-sm text-neutral-500">{formatLongDate(item.date)}</div>
                  <div className="font-medium mt-1">{item.players.length ? item.players.join(", ") : <span className="text-neutral-400">—</span>}</div>
                  <div className="text-sm text-neutral-700 mt-1">{pairingText}</div>
                  {item.notes ? <div className="text-xs text-neutral-500 mt-1">{item.notes}</div> : null}
                  <div className="flex gap-2 mt-3">
                    <button
                      aria-label={disabled ? "No players assigned" : "Download Outlook invite"}
                      onClick={() => onDownloadICS(item)}
                      className="px-3 py-2 rounded-xl border"
                      disabled={disabled}
                    >Add to Outlook</button>
                    <button
                      aria-label={disabled ? "No players assigned" : "Enter scores"}
                      onClick={() => openScores(item)}
                      className="px-3 py-2 rounded-xl border"
                      disabled={disabled}
                    >Enter scores</button>
                  </div>
                </div>
              );
            })}
          </div>

          {/* Desktop table (≥ md) */}
          <div className="overflow-x-auto hidden md:block">
            <table className="min-w-full text-sm">
              <thead className="bg-neutral-100 text-neutral-700">
                <tr>
                  <th className="text-left p-3">Date</th>
                  <th className="text-left p-3">Players</th>
                  <th className="text-left p-3">Pairing</th>
                  <th className="text-left p-3">Notes</th>
                  <th className="text-left p-3">Actions</th>
                </tr>
              </thead>
              <tbody>
                {filtered.map((item, idx) => {
                  const disabled = item.players.length !== 4;
                  return (
                    <tr key={idx} className={clsx(idx % 2 ? "bg-neutral-50" : "bg-white")}>
                      <td className="p-3 whitespace-nowrap">{formatLongDate(item.date)}</td>
                      <td className="p-3">{item.players.length ? item.players.join(", ") : <span className="text-neutral-400">—</span>}</td>
                      <td className="p-3">{item.pairing || (item.players.length === 4 ? `${[...item.players].sort()[0]} & ${[...item.players].sort()[1]} vs ${[...item.players].sort()[2]} & ${[...item.players].sort()[3]}` : "")}</td>
                      <td className="p-3">{item.notes || ""}</td>
                      <td className="p-3">
                        <div className="flex flex-wrap gap-2">
                          <button
                            aria-label={disabled ? "No players assigned" : "Download Outlook invite"}
                            onClick={() => onDownloadICS(item)}
                            className="px-3 py-1.5 rounded-xl border"
                            disabled={disabled}
                            title={disabled ? "No players assigned" : "Download .ics"}
                          >Add to Outlook</button>
                          <button
                            aria-label={disabled ? "No players assigned" : "Enter scores"}
                            onClick={() => openScores(item)}
                            className="px-3 py-1.5 rounded-xl border"
                            disabled={disabled}
                          >Enter scores</button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>

        {/* Leaderboard */}
        <div className="mt-6 grid md:grid-cols-2 gap-4">
          <div className="bg-white rounded-2xl shadow p-4">
            <h3 className="font-semibold mb-2">Leaderboard (sets won)</h3>
            <ul className="space-y-1">
              {Object.entries(winsByPlayer)
                .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
                .map(([name, wins]) => (
                  <li key={name} className="flex items-center justify-between">
                    <span className="flex items-center gap-2">🏆 {name}</span>
                    <span className="font-semibold">{wins}</span>
                  </li>
                ))}
            </ul>
          </div>

          <div className="bg-white rounded-2xl shadow p-4">
            <h3 className="font-semibold mb-2">How scoring works</h3>
            <p className="text-sm text-neutral-700">Each night has <strong>2 sets with the same pairing</strong>. Pick the <strong>winning pair</strong> for each set. Wins are tallied per player across the season.</p>
            <p className="text-sm text-neutral-700 mt-2">Tip: Use the filter to type your name and see only your dates.</p>
          </div>
        </div>

        {/* Score Modal */}
        {scoreModal && (
          <ScoreDialog
            item={scoreModal}
            initial={scores[toISODate(scoreModal.date)]}
            onClose={() => setScoreModal(null)}
            onSave={saveScores}
          />
        )}

        <footer className="text-xs text-neutral-500 mt-10 mb-4 text-center">BHTC Tennis Scheduler — client‑side only · data stored in your browser</footer>
      </div>
    </div>
  );
}

function ScoreDialog({ item, initial, onClose, onSave }) {
  const iso = toISODate(item.date);
  const sets = useMemo(() => computeRotations(item.players, item.pairing) || [], [item.players, item.pairing]);
  const [setsState, setSetsState] = useState(() => {
    const prev = initial && Array.isArray(initial.sets) ? initial.sets : [];
    // Normalize to 2 sets shape
    const base = sets.map(() => ({ winners: [] }));
    // merge prev
    for (let i = 0; i < base.length; i++) {
      if (prev[i] && Array.isArray(prev[i].winners)) base[i].winners = prev[i].winners;
    }
    return base;
  });

  function toggleWinner(setIdx, name) {
    setSetsState((curr) => {
      const next = curr.map((s) => ({ winners: [...s.winners] }));
      const w = next[setIdx].winners;
      const i = w.indexOf(name);
      if (i >= 0) w.splice(i, 1);
      else w.push(name);
      // limit to two winners (a pair)
      if (w.length > 2) w.shift();
      return next;
    });
  }

  function pairLabel(pair) {
    return `${pair[0]} & ${pair[1]}`;
  }

  return (
    <div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50">
      <div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg md:max-w-2xl h-[85vh] md:h-auto overflow-y-auto">
        <div className="p-4 border-b flex items-center justify-between">
          <div>
            <div className="text-sm text-neutral-500">{formatLongDate(item.date)}</div>
            <div className="text-lg font-semibold">Enter scores</div>
          </div>
          <button onClick={onClose} className="text-neutral-500 hover:text-neutral-700">✖</button>
        </div>

        <div className="p-4 space-y-4">
          {sets.length === 0 ? (
            <div className="text-neutral-600">This night has no assigned players.</div>
          ) : (
            sets.map((s, i) => (
              <div key={i} className="border rounded-xl p-3">
                <div className="font-semibold mb-2">Set {i + 1}</div>
                <div className="grid md:grid-cols-2 gap-3">
                  <div className="border rounded-xl p-3">
                    <div className="text-sm text-neutral-600 mb-1">Left Pair</div>
                    <div className="font-medium mb-2">{pairLabel(s.left)}</div>
                    <div className="flex flex-wrap gap-2">
                      {s.left.map((name) => (
                        <button
                          key={name}
                          onClick={() => toggleWinner(i, name)}
                          className={clsx(
                            "px-3 py-1.5 rounded-xl border",
                            setsState[i].winners.includes(name) && "bg-neutral-900 text-white"
                          )}
                        >{name}</button>
                      ))}
                    </div>
                  </div>
                  <div className="border rounded-xl p-3">
                    <div className="text-sm text-neutral-600 mb-1">Right Pair</div>
                    <div className="font-medium mb-2">{pairLabel(s.right)}</div>
                    <div className="flex flex-wrap gap-2">
                      {s.right.map((name) => (
                        <button
                          key={name}
                          onClick={() => toggleWinner(i, name)}
                          className={clsx(
                            "px-3 py-1.5 rounded-xl border",
                            setsState[i].winners.includes(name) && "bg-neutral-900 text-white"
                          )}
                        >{name}</button>
                      ))}
                    </div>
                  </div>
                </div>
                <div className="text-xs text-neutral-500 mt-2">Select exactly two winners (one pair). You can click again to deselect.</div>
              </div>
            ))
          )}
        </div>

        <div className="p-4 border-t flex items-center justify-end gap-2">
          <button onClick={onClose} className="px-4 py-2 rounded-xl border">Cancel</button>
          <button onClick={() => onSave(iso, setsState)} className="px-4 py-2 rounded-xl bg-neutral-900 text-white">Save</button>
        </div>
      </div>
    </div>
  );
}