From f8f4594900934e958ea0ad1d1c25ed5cebabd595 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Sun, 8 Feb 2026 11:25:06 -0800 Subject: [PATCH] Add calendar links for hosts on match details --- src/pages/MatchPage.jsx | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/pages/MatchPage.jsx b/src/pages/MatchPage.jsx index e047d52..09d4463 100644 --- a/src/pages/MatchPage.jsx +++ b/src/pages/MatchPage.jsx @@ -46,6 +46,13 @@ import { } from "../utils/matchOptions"; import { combineDateAndTimeToIso } from "../utils/datetime"; import { isPrivateMatch } from "../utils/matchPrivacy"; +import { + DEFAULT_EVENT_DURATION_MINUTES, + downloadICSFile, + ensureEventEnd, + openGoogleCalendar, + openOutlookCalendar, +} from "../utils/calendar"; import { buildMatchUpdatePayload, getMatchPlayerLimit, @@ -482,6 +489,33 @@ const toTimeInput = (value) => { const combineDateTime = (date, time) => combineDateAndTimeToIso(date, time); +const toSafeDate = (value) => { + if (!value) return null; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? null : date; +}; + +const deriveDurationMinutes = (match) => { + const candidates = [ + match?.duration_minutes, + match?.durationMinutes, + match?.duration, + ]; + if (match?.duration_hours !== undefined && match?.duration_hours !== null) { + const hours = Number(match.duration_hours); + if (Number.isFinite(hours)) { + candidates.push(hours * 60); + } + } + for (const candidate of candidates) { + const numeric = Number(candidate); + if (Number.isFinite(numeric) && numeric > 0) { + return numeric; + } + } + return DEFAULT_EVENT_DURATION_MINUTES; +}; + const buildInitialForm = (match) => { if (!match) return { ...DEFAULT_FORM }; const latitude = @@ -653,6 +687,26 @@ export default function MatchPage() { originalForm.date !== formState.date || originalForm.time !== formState.time || originalForm.location.trim() !== formState.location.trim(); + const startDate = useMemo( + () => toSafeDate(match?.start_date_time || match?.startDateTime), + [match?.startDateTime, match?.start_date_time], + ); + const eventDetails = useMemo(() => { + if (!startDate) return null; + const durationMinutes = deriveDurationMinutes(match); + const endDate = ensureEventEnd( + startDate, + toSafeDate(match?.end_date_time || match?.endDateTime), + durationMinutes, + ); + return { + title: match?.title || match?.name || `Tennis Match - ${match?.match_format || match?.format || "Play"}`, + description: match?.notes || "", + location: match?.location_text || match?.location || "", + start: startDate, + end: endDate || startDate, + }; + }, [match, startDate]); const updateMatchMutation = useMutation({ mutationFn: async (updates) => { @@ -728,6 +782,25 @@ export default function MatchPage() { setIsEditing((prev) => !prev); }; + const handleCalendarAction = (type) => { + if (!eventDetails) { + setFeedback({ type: "error", message: "Match start time not available yet." }); + return; + } + try { + if (type === "google") { + openGoogleCalendar(eventDetails); + } else if (type === "outlook") { + openOutlookCalendar(eventDetails); + } else { + downloadICSFile(eventDetails); + } + } catch (error) { + console.error(error); + setFeedback({ type: "error", message: "Unable to open calendar. Please try again." }); + } + }; + const handleLocationInputChange = useCallback((value) => { setFormState((prev) => ({ ...prev, @@ -1315,6 +1388,47 @@ export default function MatchPage() { {match.notes} )} + {isHost && ( +
+

+ Add to calendar +

+

+ {eventDetails + ? "Share this match with your calendar." + : "Set a match date and time to enable calendar links."} +

+
+ + + +
+
+ )} )}