From acbd3c07378abbdb99a3e40f3e0f43f56bdffde6 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Tue, 19 May 2026 10:45:11 +0100 Subject: [PATCH] feat(events): render public detail page in event timezone Event detail page formatted dates in viewer-local time, so a Lisbon viewer of a Toronto ET event saw the time in WEST. Format with event.displayTimezone instead so attendees see the event's intended wall-clock + zone suffix ("6:00 AM EDT") regardless of where they sit. useEventDateUtils.formatDate / formatTime / formatDateRange / isToday now accept a { timeZone } option and pass it to Intl.DateTimeFormat. Existing call sites that don't pass timeZone fall through to viewer- local, matching prior behaviour. --- app/composables/useEventDateUtils.js | 83 ++++++++++++++++------------ app/pages/events/[slug].vue | 8 +++ 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/app/composables/useEventDateUtils.js b/app/composables/useEventDateUtils.js index ab536b4..2aec85e 100644 --- a/app/composables/useEventDateUtils.js +++ b/app/composables/useEventDateUtils.js @@ -1,85 +1,98 @@ -// Utility composable for event date handling with timezone support +// Utility composable for event date handling with timezone support. +// Pass `{ timeZone: event.displayTimezone }` to render in the event's TZ. export const useEventDateUtils = () => { - const TIMEZONE = "America/Toronto"; + const DEFAULT_TIMEZONE = "America/Toronto"; - // Format a date to a specific format const formatDate = (date, options = {}) => { + if (!date) return ""; const dateObj = date instanceof Date ? date : new Date(date); - const { month = "short", day = "numeric", year = "numeric" } = options; + if (isNaN(dateObj.getTime())) return ""; + const { + month = "short", + day = "numeric", + year = "numeric", + weekday, + timeZone, + } = options; return new Intl.DateTimeFormat("en-US", { + ...(weekday && { weekday }), month, day, year, + ...(timeZone && { timeZone }), }).format(dateObj); }; - // Format event date range - const formatDateRange = (startDate, endDate, compact = false) => { + const formatDateRange = (startDate, endDate, compact = false, timeZone) => { if (!startDate || !endDate) return "No dates"; const start = new Date(startDate); const end = new Date(endDate); - const startMonth = start.toLocaleDateString("en-US", { month: "short" }); - const endMonth = end.toLocaleDateString("en-US", { month: "short" }); - const startDay = start.getDate(); - const endDay = end.getDate(); - const year = end.getFullYear(); + const tzOpts = timeZone ? { timeZone } : {}; + const startMonth = start.toLocaleDateString("en-US", { month: "short", ...tzOpts }); + const endMonth = end.toLocaleDateString("en-US", { month: "short", ...tzOpts }); + const startDay = Number( + start.toLocaleDateString("en-US", { day: "numeric", ...tzOpts }), + ); + const endDay = Number( + end.toLocaleDateString("en-US", { day: "numeric", ...tzOpts }), + ); + const year = Number( + end.toLocaleDateString("en-US", { year: "numeric", ...tzOpts }), + ); + const startMonthIdx = startMonth; // compared as label string + const endMonthIdx = endMonth; + const startYear = Number( + start.toLocaleDateString("en-US", { year: "numeric", ...tzOpts }), + ); if (compact) { - if ( - start.getMonth() === end.getMonth() && - start.getFullYear() === end.getFullYear() - ) { + if (startMonthIdx === endMonthIdx && startYear === year) { return `${startMonth} ${startDay}-${endDay}`; } return `${startMonth} ${startDay} - ${endMonth} ${endDay}`; } - if ( - start.getMonth() === end.getMonth() && - start.getFullYear() === end.getFullYear() - ) { + if (startMonthIdx === endMonthIdx && startYear === year) { return `${startMonth} ${startDay}-${endDay}, ${year}`; - } else if (start.getFullYear() === end.getFullYear()) { + } else if (startYear === year) { return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`; } else { - return `${formatDate(startDate)} - ${formatDate(endDate)}`; + return `${formatDate(startDate, { timeZone })} - ${formatDate(endDate, { timeZone })}`; } }; - // Check if a date is in the past const isPastDate = (date) => { const dateObj = date instanceof Date ? date : new Date(date); - const now = new Date(); - return dateObj < now; + return dateObj < new Date(); }; - // Check if a date is today - const isToday = (date) => { + const isToday = (date, timeZone) => { const dateObj = date instanceof Date ? date : new Date(date); const today = new Date(); + const opts = { year: "numeric", month: "2-digit", day: "2-digit", ...(timeZone && { timeZone }) }; return ( - dateObj.getDate() === today.getDate() && - dateObj.getMonth() === today.getMonth() && - dateObj.getFullYear() === today.getFullYear() + dateObj.toLocaleDateString("en-US", opts) === + today.toLocaleDateString("en-US", opts) ); }; - // Get a readable time string - const formatTime = (date, includeSeconds = false) => { + const formatTime = (date, includeSeconds = false, timeZone) => { const dateObj = date instanceof Date ? date : new Date(date); - const options = { + return new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", ...(includeSeconds && { second: "2-digit" }), - }; - return new Intl.DateTimeFormat("en-US", options).format(dateObj); + ...(timeZone && { timeZone }), + }).format(dateObj); }; return { - TIMEZONE, + DEFAULT_TIMEZONE, + // Legacy alias for callers that hard-coded the constant. + TIMEZONE: DEFAULT_TIMEZONE, formatDate, formatDateRange, isPastDate, diff --git a/app/pages/events/[slug].vue b/app/pages/events/[slug].vue index 8dc05d2..1befb81 100644 --- a/app/pages/events/[slug].vue +++ b/app/pages/events/[slug].vue @@ -197,21 +197,29 @@ onMounted(async () => { } }); +const eventTimeZone = computed( + () => event.value?.displayTimezone || "America/Toronto", +); + const formatDate = (dateStr) => { + if (!dateStr) return ""; const d = new Date(dateStr); return new Intl.DateTimeFormat("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", + timeZone: eventTimeZone.value, }).format(d); }; const formatTime = (start, end) => { + if (!start || !end) return ""; const fmt = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", timeZoneName: "short", + timeZone: eventTimeZone.value, }); return `${fmt.format(new Date(start))} – ${fmt.format(new Date(end))}`; };