From 90c30f462e6d3f0090136d6fe7b4318a6bc3ca93 Mon Sep 17 00:00:00 2001 From: Prism Codex Date: Wed, 10 Jun 2026 14:27:06 +0000 Subject: [PATCH] Show session times in local timezone --- .../(frontend)/_components/PortalShell.tsx | 31 +++- .../_components/SessionDateTime.tsx | 168 ++++++++++++++++++ src/app/(frontend)/events/[id]/page.tsx | 19 +- src/app/(frontend)/events/page.tsx | 32 ++-- src/app/(frontend)/projects/[slug]/page.tsx | 24 +-- src/app/(frontend)/threads/[slug]/page.tsx | 8 +- 6 files changed, 234 insertions(+), 48 deletions(-) create mode 100644 src/app/(frontend)/_components/SessionDateTime.tsx diff --git a/src/app/(frontend)/_components/PortalShell.tsx b/src/app/(frontend)/_components/PortalShell.tsx index 3480a53..a587615 100644 --- a/src/app/(frontend)/_components/PortalShell.tsx +++ b/src/app/(frontend)/_components/PortalShell.tsx @@ -27,6 +27,7 @@ import type { import { Button } from '@/components/ui/button' import type { ProductPageCopy } from '@/utilities/pageCopy' import { toSafeURL } from '@/utilities/safeURL' +import { SessionDateTime } from './SessionDateTime' import { VibeCheckButton } from './VibeCheckButton' type PortalHomeProps = { @@ -137,7 +138,12 @@ export const PortalPublicHome: React.FC = ({ {nextEvent ? (
-

{formatDateTime(nextEvent.startsAt)}

+

{nextEvent.title}

{nextEvent.locationLabel ? (

{nextEvent.locationLabel}

@@ -206,7 +212,12 @@ export const PortalPublicHome: React.FC = ({ {upcomingEvents.length ? ( upcomingEvents.slice(0, 3).map((event) => (
-

{formatDateTime(event.startsAt)}

+

{event.title}

{event.summary ? (

@@ -489,9 +500,12 @@ export const PortalDashboard: React.FC = ({

Next session

{nextEvent.title}

-

- {formatDateTime(nextEvent.startsAt)} -

+ {nextEvent.locationLabel ? (

{nextEvent.locationLabel}

) : null} @@ -592,7 +606,12 @@ export const PortalDashboard: React.FC = ({
{upcomingEvents.slice(0, 3).map((event) => (
-

{formatDateTime(event.startsAt)}

+

{event.title}

{event.locationLabel ? (

{event.locationLabel}

diff --git a/src/app/(frontend)/_components/SessionDateTime.tsx b/src/app/(frontend)/_components/SessionDateTime.tsx new file mode 100644 index 0000000..eb51006 --- /dev/null +++ b/src/app/(frontend)/_components/SessionDateTime.tsx @@ -0,0 +1,168 @@ +'use client' + +import { useEffect, useState } from 'react' +import type { FC } from 'react' + +type DateStyle = 'full' | 'long' | 'medium' | 'short' + +type SessionDateTimeProps = { + className?: string + dateStyle?: DateStyle + endsAt?: string | null + startsAt?: string | null +} + +type SessionDateBadgeProps = { + dateClassName?: string + dayClassName?: string + startsAt?: string | null +} + +const fallbackTimeZone = 'UTC' + +export const SessionDateTime: FC = ({ + className, + dateStyle = 'long', + endsAt, + startsAt, +}) => { + const [timeZone, setTimeZone] = useState(fallbackTimeZone) + const label = formatSessionDateTime({ dateStyle, endsAt, startsAt, timeZone }) + + useEffect(() => { + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone + + if (localTimeZone) setTimeZone(localTimeZone) + }, []) + + if (!startsAt || !label) return null + + return ( + + ) +} + +export const SessionDateBadge: FC = ({ + dateClassName, + dayClassName, + startsAt, +}) => { + const [timeZone, setTimeZone] = useState(fallbackTimeZone) + const badge = formatSessionDateBadge(startsAt, timeZone) + + useEffect(() => { + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone + + if (localTimeZone) setTimeZone(localTimeZone) + }, []) + + if (!startsAt || !badge) return null + + return ( + <> +

+ {badge.day} +

+

+ {badge.date} +

+ + ) +} + +const formatSessionDateTime = ({ + dateStyle, + endsAt, + startsAt, + timeZone, +}: { + dateStyle: DateStyle + endsAt?: string | null + startsAt?: string | null + timeZone: string +}): string | null => { + if (!startsAt) return null + + const start = toValidDate(startsAt) + const end = endsAt ? toValidDate(endsAt) : null + + if (!start) return null + + const timeZoneLabel = getTimeZoneLabel(start, timeZone) + const dateFormatter = new Intl.DateTimeFormat('en-US', { + dateStyle, + timeZone, + }) + const timeFormatter = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + timeZone, + }) + const startDate = dateFormatter.format(start) + const startTime = timeFormatter.format(start) + + if (!end) return `${startDate}, ${startTime} ${timeZoneLabel}` + + const endDate = dateFormatter.format(end) + const endTime = timeFormatter.format(end) + const range = + startDate === endDate + ? `${startDate}, ${startTime}-${endTime}` + : `${startDate}, ${startTime}-${endDate}, ${endTime}` + + return `${range} ${timeZoneLabel}` +} + +const formatSessionDateBadge = ( + startsAt: string | null | undefined, + timeZone: string, +): { date: string; day: string } | null => { + if (!startsAt) return null + + const start = toValidDate(startsAt) + + if (!start) return null + + return { + date: new Intl.DateTimeFormat('en-US', { + day: '2-digit', + timeZone, + }).format(start), + day: new Intl.DateTimeFormat('en-US', { + timeZone, + weekday: 'short', + }).format(start), + } +} + +const getTimeZoneLabel = (date: Date, timeZone: string): string => { + const label = + getTimeZoneName(date, timeZone, 'longGeneric') || + getTimeZoneName(date, timeZone, 'long') || + timeZone + + return simplifyTimeZoneLabel(label) +} + +const getTimeZoneName = ( + date: Date, + timeZone: string, + timeZoneName: Intl.DateTimeFormatOptions['timeZoneName'], +): string | null => + new Intl.DateTimeFormat('en-US', { + timeZone, + timeZoneName, + }) + .formatToParts(date) + .find((part) => part.type === 'timeZoneName')?.value || null + +const simplifyTimeZoneLabel = (label: string): string => + label.replace(/\s+(Standard|Daylight) Time$/, '').replace(/\s+Time$/, '') + +const toValidDate = (value: string): Date | null => { + const date = new Date(value) + + return Number.isNaN(date.getTime()) ? null : date +} diff --git a/src/app/(frontend)/events/[id]/page.tsx b/src/app/(frontend)/events/[id]/page.tsx index fdb0c36..0188884 100644 --- a/src/app/(frontend)/events/[id]/page.tsx +++ b/src/app/(frontend)/events/[id]/page.tsx @@ -20,6 +20,7 @@ import type { import { createGoogleCalendarURL } from '@/utilities/calendarLinks' import { getCurrentUser } from '@/utilities/getCurrentUser' import { toSafeURL } from '@/utilities/safeURL' +import { SessionDateTime } from '../../_components/SessionDateTime' export const dynamic = 'force-dynamic' @@ -32,15 +33,6 @@ type Args = { const relationDocs = (items?: (number | T)[] | null): T[] => items?.filter((item): item is T => item !== null && typeof item === 'object') || [] -const formatDateTime = (date?: string | null) => { - if (!date) return null - - return new Intl.DateTimeFormat('en', { - dateStyle: 'full', - timeStyle: 'short', - }).format(new Date(date)) -} - const formatDate = (date?: string | null) => { if (!date) return null @@ -180,9 +172,12 @@ export default async function SessionDetailPage({ params: paramsPromise }: Args)