mirror of
https://github.com/UiO-Gaming/uiogaming.no.git
synced 2026-11-05 13:56:47 +01:00
Share event date handling and stop depending on the server timezone
Event dates are stored as Oslo wall-clock strings, but were parsed in the server's local timezone, so times were only correct because the container sets TZ=Europe/Oslo. Move date parsing and formatting into lib/dates, interpret those strings as Oslo time, and also accept ISO timestamps in case the Studio writes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35c175c1ad
commit
5c612d995d
4 files changed
+85
-65
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
import Back from "@/components/ui/back"
|
||||
|
||||
import { formatEventDate } from "@/lib/dates"
|
||||
import { getEvent } from "@/lib/sanity"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
@@ -33,25 +34,11 @@ export async function generateMetadata({
|
||||
|
||||
const t = await getTranslations({ locale, namespace: "metaTags" })
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
return generateSeoMetadata({
|
||||
locale,
|
||||
title: data.title,
|
||||
description: [
|
||||
`${t("time")}: ${formatDate(data.date)}`,
|
||||
`${t("time")}: ${formatEventDate(data.date, locale)}`,
|
||||
data.location && `${t("location")}: ${data.location}`,
|
||||
data.description && `\n${data.description}`,
|
||||
]
|
||||
@@ -71,21 +58,6 @@ const Event = async ({ params }: EventPageParams) => {
|
||||
|
||||
if (!data) return notFound()
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Back />
|
||||
@@ -97,7 +69,7 @@ const Event = async ({ params }: EventPageParams) => {
|
||||
</div>
|
||||
<div className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>{formatDate(data.date)}</p>
|
||||
<p>{formatEventDate(data.date, locale)}</p>
|
||||
</div>
|
||||
<p className={styles.description}>{data.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatEventDate } from "@/lib/dates"
|
||||
import { getUpcomingEvents } from "@/lib/sanity"
|
||||
|
||||
import { getLocale, getTranslations } from "next-intl/server"
|
||||
@@ -27,21 +28,6 @@ const Events = async () => {
|
||||
|
||||
const events: Event[] = await getUpcomingEvents(2)
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
const previewDescription = (description: string | null) => {
|
||||
if (!description) return null
|
||||
|
||||
@@ -75,7 +61,7 @@ const Events = async () => {
|
||||
</div>
|
||||
<div className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>{formatDate(event.date)}</p>
|
||||
<p>{formatEventDate(event.date, locale)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{previewDescription(event.description)}</p>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Locale } from "next-intl"
|
||||
|
||||
const TIME_ZONE = "Europe/Oslo"
|
||||
|
||||
// Event dates are usually stored as Oslo wall-clock time without an offset
|
||||
// ("2026-11-05 17:00"). The Studio's datetime field stores ISO timestamps
|
||||
// instead, so both formats have to be handled
|
||||
const LOCAL_DATE_FORMAT = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/
|
||||
|
||||
const osloPartsFormatter = new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: TIME_ZONE,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
})
|
||||
|
||||
const getOsloParts = (date: Date) =>
|
||||
Object.fromEntries(
|
||||
osloPartsFormatter
|
||||
.formatToParts(date)
|
||||
.map(({ type, value }) => [type, value])
|
||||
)
|
||||
|
||||
/** Formats a date as Oslo wall-clock time, e.g. "2026-11-05 17:00" */
|
||||
export const toOsloDateString = (date: Date) => {
|
||||
const { year, month, day, hour, minute } = getOsloParts(date)
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// How far Oslo's clock is ahead of UTC at the given moment (1 or 2 hours)
|
||||
const getOsloOffset = (timestamp: number) => {
|
||||
const { year, month, day, hour, minute } = getOsloParts(new Date(timestamp))
|
||||
const osloAsUtc = Date.UTC(+year, +month - 1, +day, +hour, +minute)
|
||||
return osloAsUtc - Math.floor(timestamp / 60_000) * 60_000
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an event date. Oslo wall-clock strings are interpreted in Oslo time
|
||||
* regardless of the server's timezone
|
||||
*/
|
||||
export const parseEventDate = (value: string) => {
|
||||
const match = value.match(LOCAL_DATE_FORMAT)
|
||||
if (!match) return new Date(value)
|
||||
|
||||
const [year, month, day, hour, minute] = match.slice(1).map(Number)
|
||||
const wallClockAsUtc = Date.UTC(year, month - 1, day, hour, minute)
|
||||
|
||||
// The offset depends on daylight saving time at the resulting moment, so
|
||||
// guess with the offset at the wall-clock time, then correct
|
||||
const guess = wallClockAsUtc - getOsloOffset(wallClockAsUtc)
|
||||
return new Date(wallClockAsUtc - getOsloOffset(guess))
|
||||
}
|
||||
|
||||
/** Formats an event date like "torsdag 5. november 2026 kl. 17:00" */
|
||||
export const formatEventDate = (value: string, locale: Locale) =>
|
||||
parseEventDate(value).toLocaleString(locale, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: TIME_ZONE,
|
||||
})
|
||||
+13
-18
@@ -1,5 +1,7 @@
|
||||
import { createClient } from "@sanity/client"
|
||||
|
||||
import { toOsloDateString } from "./dates"
|
||||
|
||||
export const sanityClient = createClient({
|
||||
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
|
||||
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
|
||||
@@ -72,26 +74,19 @@ export async function getEvent(slug: string) {
|
||||
}
|
||||
|
||||
export async function getUpcomingEvents(limit: number) {
|
||||
// Event dates are stored as Oslo local time strings ("2026-11-05 17:00"),
|
||||
// which sort correctly as text, so compare against the current time in the
|
||||
// same format
|
||||
const parts = Object.fromEntries(
|
||||
new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: "Europe/Oslo",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
})
|
||||
.formatToParts(new Date())
|
||||
.map(({ type, value }) => [type, value])
|
||||
)
|
||||
const now = `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`
|
||||
// Most event dates are Oslo wall-clock strings ("2026-11-05 17:00"), which
|
||||
// sort correctly as text, so compare them against the current Oslo time in
|
||||
// the same format. ISO timestamps from the Studio are compared as datetimes
|
||||
const now = toOsloDateString(new Date())
|
||||
|
||||
const query = `
|
||||
*[_type == "event" && date >= $now] | order(date asc)[0...$limit] {
|
||||
*[
|
||||
_type == "event" &&
|
||||
select(
|
||||
defined(dateTime(date)) => dateTime(date) >= dateTime(now()),
|
||||
date >= $now
|
||||
)
|
||||
] | order(date asc)[0...$limit] {
|
||||
_id,
|
||||
title,
|
||||
location,
|
||||
|
||||
Reference in new issue
Block a user