diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx
index 285344e..fb6da5a 100644
--- a/src/TennisMatchApp.jsx
+++ b/src/TennisMatchApp.jsx
@@ -745,6 +745,7 @@ const TennisMatchApp = () => {
skillLevel: null,
format: "Doubles",
dateTime: "",
+ availabilityOptions: [],
location: "",
latitude: null,
longitude: null,
@@ -2724,6 +2725,24 @@ const TennisMatchApp = () => {
});
};
+ const formatAlternateTimes = (options) => {
+ if (!Array.isArray(options)) return [];
+ const formatted = [];
+ options.forEach((option) => {
+ if (!option) return;
+ const parsed = new Date(option);
+ if (Number.isNaN(parsed.getTime())) return;
+ formatted.push(formatDateTime(parsed));
+ });
+ return formatted;
+ };
+
+ const buildAlternateTimesNote = (options) => {
+ const formatted = formatAlternateTimes(options);
+ if (formatted.length === 0) return "";
+ return `Alternate options:\n- ${formatted.join("\n- ")}`;
+ };
+
const formatHoursUntilStart = useCallback((hours) => {
if (hours === null || hours === undefined) return null;
if (!Number.isFinite(hours)) return null;
@@ -4811,6 +4830,8 @@ const TennisMatchApp = () => {
const CreateMatchScreen = () => {
const [recentLocations, setRecentLocations] = useState(() => loadStoredLocations());
+ const [alternateDateTime, setAlternateDateTime] = useState("");
+ const [alternateError, setAlternateError] = useState("");
useEffect(() => {
const syncRecentLocations = () => {
@@ -4896,6 +4917,15 @@ const TennisMatchApp = () => {
}
}
+ const alternateTimesNote =
+ matchData.type === "closed"
+ ? buildAlternateTimesNote(matchData.availabilityOptions)
+ : "";
+ const combinedNotes = [matchData.notes, alternateTimesNote]
+ .map((entry) => entry?.trim())
+ .filter(Boolean)
+ .join("\n\n");
+
const privacy = matchData.type === "closed" ? "private" : "open";
const skillLevelValue =
matchData.type === "closed"
@@ -4917,7 +4947,7 @@ const TennisMatchApp = () => {
skillLevel: skillLevelValue,
match_format: matchData.format || undefined,
format: matchData.format || undefined,
- notes: matchData.notes || undefined,
+ notes: combinedNotes || undefined,
};
return Object.fromEntries(
@@ -4996,6 +5026,20 @@ const TennisMatchApp = () => {
{formatDateTime(matchData.dateTime)}
+ {matchData.availabilityOptions.length > 0 && (
+
@@ -5700,6 +5845,10 @@ const TennisMatchApp = () => {
parts.push("You're invited to a tennis match!");
if (host) parts.push(`Host: ${host}`);
if (matchData.dateTime) parts.push(`When: ${formatDateTime(matchData.dateTime)}`);
+ const alternateTimes = formatAlternateTimes(matchData.availabilityOptions);
+ if (alternateTimes.length > 0) {
+ parts.push(`Other options: ${alternateTimes.join("; ")}`);
+ }
if (matchData.location) parts.push(`Where: ${matchData.location}`);
if (matchData.format) parts.push(`Format: ${matchData.format}`);
if (matchData.skillLevel && matchData.skillLevel !== "Any Level") {
diff --git a/src/components/MatchCreatorFlow.jsx b/src/components/MatchCreatorFlow.jsx
index 0578c86..68df5e8 100644
--- a/src/components/MatchCreatorFlow.jsx
+++ b/src/components/MatchCreatorFlow.jsx
@@ -86,6 +86,7 @@ const initialMatchData = () => {
skillLevel: "4.0",
format: "Doubles",
notes: "",
+ alternateTimes: [],
invitedPlayers: [],
manualInvitees: [],
listingVisibility: "listed",
@@ -151,6 +152,22 @@ const formatDateDisplay = (dateStr) => {
});
};
+const formatAlternateSlotDisplay = (slot) => {
+ if (!slot?.date || !slot?.startTime) return "";
+ return `${formatDateDisplay(slot.date)} at ${formatTimeDisplay(slot.startTime)}`;
+};
+
+const formatAlternateSlots = (slots) =>
+ (Array.isArray(slots) ? slots : [])
+ .map((slot) => formatAlternateSlotDisplay(slot))
+ .filter(Boolean);
+
+const buildAlternateTimesNote = (slots) => {
+ const formatted = formatAlternateSlots(slots);
+ if (formatted.length === 0) return "";
+ return `Alternate options:\n- ${formatted.join("\n- ")}`;
+};
+
const formatRelativeDate = (isoValue) => {
if (!isoValue) return "Recently active";
const target = new Date(isoValue);
@@ -265,6 +282,9 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
const [contactName, setContactName] = useState("");
const [contactPhone, setContactPhone] = useState("");
const [contactError, setContactError] = useState("");
+ const [alternateDate, setAlternateDate] = useState("");
+ const [alternateTime, setAlternateTime] = useState("");
+ const [alternateError, setAlternateError] = useState("");
const [isFormatManuallySelected, setIsFormatManuallySelected] = useState(false);
const [recentLocations, setRecentLocations] = useState(() => loadStoredLocations());
const [recentPlayers, setRecentPlayers] = useState(() => loadStoredRecentPlayers());
@@ -337,6 +357,9 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
setContactName("");
setContactPhone("");
setContactError("");
+ setAlternateDate("");
+ setAlternateTime("");
+ setAlternateError("");
setIsFormatManuallySelected(false);
}, []);
@@ -416,6 +439,50 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
[setMatchData],
);
+ const handleAlternateTimeChange = useCallback((value) => {
+ setAlternateTime(value ? clampTimeToRange(value) : "");
+ }, []);
+
+ const handleAddAlternateSlot = useCallback(() => {
+ setAlternateError("");
+ if (!alternateDate || !alternateTime) {
+ setAlternateError("Add a date and time for this option.");
+ return;
+ }
+ const normalizedTime = clampTimeToRange(alternateTime);
+ const optionIso = combineDateAndTimeToIso(alternateDate, normalizedTime);
+ if (!optionIso) {
+ setAlternateError("That option time isn't valid.");
+ return;
+ }
+ const mainIso = combineDateAndTimeToIso(matchData.date, matchData.startTime);
+ if (mainIso && optionIso === mainIso) {
+ setAlternateError("That matches your main date/time.");
+ return;
+ }
+ const existingIso = (matchData.alternateTimes || []).some((slot) => {
+ const slotIso = combineDateAndTimeToIso(slot.date, slot.startTime);
+ return slotIso && slotIso === optionIso;
+ });
+ if (existingIso) {
+ setAlternateError("That option is already added.");
+ return;
+ }
+ if ((matchData.alternateTimes || []).length >= 5) {
+ setAlternateError("You can add up to 5 alternate options.");
+ return;
+ }
+ setMatchData((prev) => ({
+ ...prev,
+ alternateTimes: [
+ ...(prev.alternateTimes || []),
+ { date: alternateDate, startTime: normalizedTime },
+ ],
+ }));
+ setAlternateDate("");
+ setAlternateTime("");
+ }, [alternateDate, alternateTime, matchData, setMatchData]);
+
const recordRecentLocation = useCallback(
(locationLabel, latitude, longitude) => {
const next = persistRecentLocation(locationLabel, latitude, longitude);
@@ -520,6 +587,15 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
return;
}
+ const alternateTimesNote =
+ matchData.type === "private"
+ ? buildAlternateTimesNote(matchData.alternateTimes)
+ : "";
+ const combinedNotes = [matchData.notes, alternateTimesNote]
+ .map((entry) => entry?.trim())
+ .filter(Boolean)
+ .join("\n\n");
+
const payload = {
status: "upcoming",
match_type: matchData.type === "private" ? "private" : "open",
@@ -529,7 +605,7 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
longitude: matchData.longitude ?? undefined,
player_limit: matchData.totalPlayers,
match_format: matchData.format,
- notes: matchData.notes || undefined,
+ notes: combinedNotes || undefined,
};
if (matchData.type === "open" && matchData.skillLevel) {
@@ -664,11 +740,21 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
};
const buildShareMessage = useCallback(
- () =>
- `Join my ${matchData.format} match on ${formatDateDisplay(matchData.date)} at ${formatTimeDisplay(
- matchData.startTime,
- )} at ${matchData.location}.`,
- [matchData.format, matchData.date, matchData.startTime, matchData.location]
+ () => {
+ const primary = `Join my ${matchData.format} match on ${formatDateDisplay(
+ matchData.date,
+ )} at ${formatTimeDisplay(matchData.startTime)} at ${matchData.location}.`;
+ const alternateOptions = formatAlternateSlots(matchData.alternateTimes);
+ if (alternateOptions.length === 0) return primary;
+ return `${primary} Other options: ${alternateOptions.join("; ")}.`;
+ },
+ [
+ matchData.format,
+ matchData.date,
+ matchData.startTime,
+ matchData.location,
+ matchData.alternateTimes,
+ ]
);
const handleShare = (method) => {
@@ -719,7 +805,12 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
if (matchData.type === "open" && matchData.skillLevel) {
description += ` Skill level: NTRP ${matchData.skillLevel}.`;
}
- if (matchData.notes) description += ` ${matchData.notes}`;
+ const alternateTimesNote = buildAlternateTimesNote(matchData.alternateTimes);
+ const combinedNotes = [matchData.notes, alternateTimesNote]
+ .map((entry) => entry?.trim())
+ .filter(Boolean)
+ .join("\n\n");
+ if (combinedNotes) description += ` ${combinedNotes}`;
const details = {
title,
@@ -827,7 +918,13 @@ const MatchCreatorFlow = ({ onCancel, onReturnHome, onMatchCreated, currentUser
+ {matchData.type === "private" && (
+