Brunswick Hills Tennis Center | Premier Tennis Academy in New Jersey
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>
);
}