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() {
+ Add to calendar +
++ {eventDetails + ? "Share this match with your calendar." + : "Set a match date and time to enable calendar links."} +
+