web: show event dates in Oslo time whatever the visitor's timezone
This commit is contained in:
4 files changed
+219
-48
No files matched your search
@@ -26,13 +26,12 @@ import {
|
|||||||
import { isTodayOrFuture } from "@/lib/date";
|
import { isTodayOrFuture } from "@/lib/date";
|
||||||
import { useToday } from "@/components/general/Today";
|
import { useToday } from "@/components/general/Today";
|
||||||
import styles from "./eventContainer.module.scss";
|
import styles from "./eventContainer.module.scss";
|
||||||
import { formatDate, formatYearMonth } from "@/lib/date";
|
|
||||||
import {
|
import {
|
||||||
startOfWeek,
|
calendarWeekPadding,
|
||||||
endOfWeek,
|
formatDay,
|
||||||
differenceInCalendarDays,
|
formatYearMonth,
|
||||||
parse,
|
parseDay,
|
||||||
} from "date-fns";
|
} from "@/lib/date";
|
||||||
import { Icon } from "../general/Icon";
|
import { Icon } from "../general/Icon";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { VenueFragment } from "@/gql/graphql";
|
import { VenueFragment } from "@/gql/graphql";
|
||||||
@@ -191,7 +190,7 @@ const CalendarDay = ({
|
|||||||
<div
|
<div
|
||||||
className={`${styles.calendarDay} ${events.length === 0 && styles.empty}`}
|
className={`${styles.calendarDay} ${events.length === 0 && styles.empty}`}
|
||||||
>
|
>
|
||||||
<h3>{formatDate(parse(day, "yyyy-MM-dd", new Date(0)), "eeee dd.MM.")}</h3>
|
<h3>{formatDay(day, "eeee dd.MM.")}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<EventItem
|
<EventItem
|
||||||
@@ -210,15 +209,8 @@ const CalendarWeek = ({
|
|||||||
}: {
|
}: {
|
||||||
days: Record<string, SingularEvent<EventOverviewItemFragment>[]>;
|
days: Record<string, SingularEvent<EventOverviewItemFragment>[]>;
|
||||||
}) => {
|
}) => {
|
||||||
const daysInWeek = Object.keys(days);
|
const { before: emptyCellsBefore, after: emptyCellsAfter } =
|
||||||
const firstDay = daysInWeek[0];
|
calendarWeekPadding(Object.keys(days));
|
||||||
const lastDay = daysInWeek[daysInWeek.length - 1];
|
|
||||||
|
|
||||||
const weekStart = startOfWeek(firstDay, { weekStartsOn: 1 });
|
|
||||||
const weekEnd = endOfWeek(lastDay, { weekStartsOn: 1 });
|
|
||||||
|
|
||||||
const emptyCellsBefore = differenceInCalendarDays(firstDay, weekStart);
|
|
||||||
const emptyCellsAfter = differenceInCalendarDays(weekEnd, lastDay);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.calendarWeek}>
|
<div className={styles.calendarWeek}>
|
||||||
@@ -237,7 +229,7 @@ const CalendarWeek = ({
|
|||||||
|
|
||||||
function srOnlyYear(yearMonthString: string, today: Date) {
|
function srOnlyYear(yearMonthString: string, today: Date) {
|
||||||
// formatYearMonth only shows the year for non-current years; always expose it to screen readers
|
// formatYearMonth only shows the year for non-current years; always expose it to screen readers
|
||||||
const yearMonth = new Date(yearMonthString);
|
const yearMonth = parseDay(yearMonthString);
|
||||||
const isCurrentYear = yearMonth.getFullYear() === today.getFullYear();
|
const isCurrentYear = yearMonth.getFullYear() === today.getFullYear();
|
||||||
if (!isCurrentYear) {
|
if (!isCurrentYear) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+58
-21
@@ -1,10 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
differenceInCalendarDays,
|
||||||
|
endOfWeek,
|
||||||
|
format as formatWithoutTimeZone,
|
||||||
isBefore,
|
isBefore,
|
||||||
parse,
|
parse,
|
||||||
|
startOfWeek,
|
||||||
compareAsc,
|
compareAsc,
|
||||||
addDays,
|
addDays,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
startOfDay,
|
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { nb } from "date-fns/locale";
|
import { nb } from "date-fns/locale";
|
||||||
import { toZonedTime, format, formatInTimeZone } from "date-fns-tz";
|
import { toZonedTime, format, formatInTimeZone } from "date-fns-tz";
|
||||||
@@ -26,9 +29,43 @@ export function osloDay(now: Date = new Date()): string {
|
|||||||
return formatInTimeZone(now, timeZone, "yyyy-MM-dd");
|
return formatInTimeZone(now, timeZone, "yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two kinds of Date are used here. An instant is a real point in time (what
|
||||||
|
// the CMS sends). A wall-clock Date has its own fields set to what a clock in
|
||||||
|
// Oslo shows, whatever timezone the code runs in: toLocalTime() makes one
|
||||||
|
// from an instant, parseDay() from a day string. Convert once, then stay on
|
||||||
|
// wall-clock Dates: formatDate() takes instants, formatWallClock() the rest
|
||||||
|
|
||||||
|
// "yyyy-MM-dd" or "yyyy-MM" as a wall-clock Date (midnight, first of month)
|
||||||
|
export function parseDay(day: string): Date {
|
||||||
|
return parse(day, day.length === 7 ? "yyyy-MM" : "yyyy-MM-dd", new Date(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWallClock(date: Date, formatStr: string) {
|
||||||
|
return formatWithoutTimeZone(date, formatStr, { locale: nb });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a "yyyy-MM-dd" or "yyyy-MM" string
|
||||||
|
export function formatDay(day: string, formatStr: string) {
|
||||||
|
return formatWallClock(parseDay(day), formatStr);
|
||||||
|
}
|
||||||
|
|
||||||
// Start of an Oslo day, comparable with toLocalTime() values
|
// Start of an Oslo day, comparable with toLocalTime() values
|
||||||
export function startOfOsloDay(day: string): Date {
|
export function startOfOsloDay(day: string): Date {
|
||||||
return parse(day, "yyyy-MM-dd", new Date(0));
|
return parseDay(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many empty cells a calendar week needs before its first and after its
|
||||||
|
// last day ("yyyy-MM-dd", in order) to span mandag–søndag
|
||||||
|
export function calendarWeekPadding(days: string[]) {
|
||||||
|
const first = parseDay(days[0]);
|
||||||
|
const last = parseDay(days[days.length - 1]);
|
||||||
|
return {
|
||||||
|
before: differenceInCalendarDays(
|
||||||
|
first,
|
||||||
|
startOfWeek(first, { weekStartsOn: 1 })
|
||||||
|
),
|
||||||
|
after: differenceInCalendarDays(endOfWeek(last, { weekStartsOn: 1 }), last),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date: Date | string | number, formatStr: string) {
|
export function formatDate(date: Date | string | number, formatStr: string) {
|
||||||
@@ -37,11 +74,11 @@ export function formatDate(date: Date | string | number, formatStr: string) {
|
|||||||
|
|
||||||
export function formatYearMonth(yearMonth: string, today: Date) {
|
export function formatYearMonth(yearMonth: string, today: Date) {
|
||||||
// full name of month if year is current year, otherwise name of month + year
|
// full name of month if year is current year, otherwise name of month + year
|
||||||
const parsed = parse(yearMonth, "yyyy-MM", new Date(0));
|
const parsed = parseDay(yearMonth);
|
||||||
if (parsed.getFullYear() === today.getFullYear()) {
|
if (parsed.getFullYear() === today.getFullYear()) {
|
||||||
return formatDate(parsed, "MMMM");
|
return formatWallClock(parsed, "MMMM");
|
||||||
}
|
}
|
||||||
return formatDate(parsed, "MMMM yyyy");
|
return formatWallClock(parsed, "MMMM yyyy");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatExtendedDateTime(
|
export function formatExtendedDateTime(
|
||||||
@@ -76,16 +113,15 @@ export function isConsecutiveDays(a: Date, b: Date): boolean {
|
|||||||
|
|
||||||
export function groupConsecutiveDates(dates: string[]): string[][] {
|
export function groupConsecutiveDates(dates: string[]): string[][] {
|
||||||
const groupedDates: string[][] = [];
|
const groupedDates: string[][] = [];
|
||||||
const uniqueDays = unique(
|
const uniqueDays: string[] = unique(
|
||||||
dates.map((date) => format(startOfDay(toLocalTime(date)), "yyyy-MM-dd"))
|
dates.map((date) => formatWallClock(toLocalTime(date), "yyyy-MM-dd"))
|
||||||
).sort();
|
).sort();
|
||||||
let group: string[] = [];
|
let group: string[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < uniqueDays.length; i++) {
|
for (let i = 0; i < uniqueDays.length; i++) {
|
||||||
if (
|
if (
|
||||||
i === 0 ||
|
i === 0 ||
|
||||||
isSameDay(uniqueDays[i - 1], uniqueDays[i]) ||
|
isConsecutiveDays(parseDay(uniqueDays[i - 1]), parseDay(uniqueDays[i]))
|
||||||
isConsecutiveDays(uniqueDays[i - 1], uniqueDays[i])
|
|
||||||
) {
|
) {
|
||||||
group.push(uniqueDays[i]);
|
group.push(uniqueDays[i]);
|
||||||
} else {
|
} else {
|
||||||
@@ -125,38 +161,39 @@ export function formatOccurrenceMonths(starts: string[]): string {
|
|||||||
|
|
||||||
return groups
|
return groups
|
||||||
.map((g) => {
|
.map((g) => {
|
||||||
const first = parse(g[0], "yyyy-MM", new Date(0));
|
const first = parseDay(g[0]);
|
||||||
if (g.length === 1) {
|
if (g.length === 1) {
|
||||||
return formatDate(first, "MMMM yyyy");
|
return formatWallClock(first, "MMMM yyyy");
|
||||||
}
|
}
|
||||||
const last = parse(g[g.length - 1], "yyyy-MM", new Date(0));
|
const last = parseDay(g[g.length - 1]);
|
||||||
const firstFmt =
|
const firstFmt =
|
||||||
first.getFullYear() === last.getFullYear()
|
first.getFullYear() === last.getFullYear()
|
||||||
? formatDate(first, "MMMM")
|
? formatWallClock(first, "MMMM")
|
||||||
: formatDate(first, "MMMM yyyy");
|
: formatWallClock(first, "MMMM yyyy");
|
||||||
return `${firstFmt} – ${formatDate(last, "MMMM yyyy")}`;
|
return `${firstFmt} – ${formatWallClock(last, "MMMM yyyy")}`;
|
||||||
})
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes "yyyy-MM-dd" days in order, as grouped by groupConsecutiveDates
|
||||||
export function formatDateRange(dates: string[]): string {
|
export function formatDateRange(dates: string[]): string {
|
||||||
if (dates.length === 1) {
|
if (dates.length === 1) {
|
||||||
return formatDate(dates[0], "d. MMMM");
|
return formatDay(dates[0], "d. MMMM");
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstDate = toLocalTime(dates[0]);
|
const firstDate = parseDay(dates[0]);
|
||||||
const lastDate = toLocalTime(dates[dates.length - 1]);
|
const lastDate = parseDay(dates[dates.length - 1]);
|
||||||
|
|
||||||
if (firstDate.getMonth() === lastDate.getMonth()) {
|
if (firstDate.getMonth() === lastDate.getMonth()) {
|
||||||
// same month
|
// same month
|
||||||
return `${firstDate.getDate()}.—${lastDate.getDate()}. ${formatDate(
|
return `${firstDate.getDate()}.—${lastDate.getDate()}. ${formatWallClock(
|
||||||
firstDate,
|
firstDate,
|
||||||
"MMMM"
|
"MMMM"
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// continues into next month
|
// continues into next month
|
||||||
const formattedFirstDate = formatDate(firstDate, "d. MMMM");
|
const formattedFirstDate = formatWallClock(firstDate, "d. MMMM");
|
||||||
const formattedLastDate = formatDate(lastDate, "d. MMMM");
|
const formattedLastDate = formatWallClock(lastDate, "d. MMMM");
|
||||||
return `${formattedFirstDate}—${formattedLastDate}`;
|
return `${formattedFirstDate}—${formattedLastDate}`;
|
||||||
}
|
}
|
||||||
+12
-10
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import {
|
import {
|
||||||
toLocalTime,
|
toLocalTime,
|
||||||
formatDate,
|
formatWallClock,
|
||||||
compareDates,
|
compareDates,
|
||||||
} from "./date";
|
} from "./date";
|
||||||
import { graphql, unmaskFragment } from "@/gql";
|
import { graphql, unmaskFragment } from "@/gql";
|
||||||
@@ -266,28 +266,30 @@ export function organizeEventsInCalendar<E extends EventListable>(
|
|||||||
|
|
||||||
const calendar: EventCalendar<E> = {};
|
const calendar: EventCalendar<E> = {};
|
||||||
|
|
||||||
const minDate = new Date(sortedEvents[0]?.occurrence.start);
|
// wall-clock dates from here on: weeks and days are Oslo's, not the
|
||||||
const maxDate = new Date(
|
// runtime's
|
||||||
|
const minDate = toLocalTime(sortedEvents[0]?.occurrence.start);
|
||||||
|
const maxDate = toLocalTime(
|
||||||
sortedEvents[sortedEvents.length - 1]?.occurrence.start
|
sortedEvents[sortedEvents.length - 1]?.occurrence.start
|
||||||
);
|
);
|
||||||
|
|
||||||
let currentDate = startOfWeek(minDate, { weekStartsOn: 1 });
|
let currentDate = startOfWeek(minDate, { weekStartsOn: 1 });
|
||||||
while (currentDate <= maxDate) {
|
while (currentDate <= maxDate) {
|
||||||
const week = formatDate(currentDate, "w");
|
const week = formatWallClock(currentDate, "w");
|
||||||
const daysOfWeek = eachDayOfInterval({
|
const daysOfWeek = eachDayOfInterval({
|
||||||
start: currentDate,
|
start: currentDate,
|
||||||
end: endOfWeek(currentDate, { weekStartsOn: 1 }),
|
end: endOfWeek(currentDate, { weekStartsOn: 1 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
daysOfWeek.forEach((day) => {
|
daysOfWeek.forEach((day) => {
|
||||||
const yearMonth = formatDate(day, "yyyy-MM");
|
const yearMonth = formatWallClock(day, "yyyy-MM");
|
||||||
if (!calendar[yearMonth]) {
|
if (!calendar[yearMonth]) {
|
||||||
calendar[yearMonth] = {};
|
calendar[yearMonth] = {};
|
||||||
}
|
}
|
||||||
if (!calendar[yearMonth][week]) {
|
if (!calendar[yearMonth][week]) {
|
||||||
calendar[yearMonth][week] = {};
|
calendar[yearMonth][week] = {};
|
||||||
}
|
}
|
||||||
const formattedDay = formatDate(day, "yyyy-MM-dd");
|
const formattedDay = formatWallClock(day, "yyyy-MM-dd");
|
||||||
if (!calendar[yearMonth][week][formattedDay]) {
|
if (!calendar[yearMonth][week][formattedDay]) {
|
||||||
calendar[yearMonth][week][formattedDay] = [];
|
calendar[yearMonth][week][formattedDay] = [];
|
||||||
}
|
}
|
||||||
@@ -298,9 +300,9 @@ export function organizeEventsInCalendar<E extends EventListable>(
|
|||||||
|
|
||||||
sortedEvents.forEach((event) => {
|
sortedEvents.forEach((event) => {
|
||||||
const start = toLocalTime(event.occurrence.start);
|
const start = toLocalTime(event.occurrence.start);
|
||||||
const yearMonth = formatDate(start, "yyyy-MM");
|
const yearMonth = formatWallClock(start, "yyyy-MM");
|
||||||
const week = formatDate(start, "w");
|
const week = formatWallClock(start, "w");
|
||||||
const day = formatDate(start, "yyyy-MM-dd");
|
const day = formatWallClock(start, "yyyy-MM-dd");
|
||||||
if (!calendar[yearMonth]) {
|
if (!calendar[yearMonth]) {
|
||||||
calendar[yearMonth] = {};
|
calendar[yearMonth] = {};
|
||||||
}
|
}
|
||||||
@@ -341,7 +343,7 @@ export function organizeEventsByDate<E extends EventListable>(
|
|||||||
|
|
||||||
sortedEvents.forEach((event) => {
|
sortedEvents.forEach((event) => {
|
||||||
const start = toLocalTime(event.occurrence.start);
|
const start = toLocalTime(event.occurrence.start);
|
||||||
const day = formatDate(start, "yyyy-MM-dd");
|
const day = formatWallClock(start, "yyyy-MM-dd");
|
||||||
if (!eventsByDate[day]) {
|
if (!eventsByDate[day]) {
|
||||||
eventsByDate[day] = [];
|
eventsByDate[day] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
calendarWeekPadding,
|
||||||
|
formatDateRange,
|
||||||
|
formatDay,
|
||||||
|
formatOccurrenceMonths,
|
||||||
|
formatYearMonth,
|
||||||
|
groupConsecutiveDates,
|
||||||
|
isTodayOrFuture,
|
||||||
|
parseDay,
|
||||||
|
startOfOsloDay,
|
||||||
|
} from "./date.ts";
|
||||||
|
import {
|
||||||
|
getFutureOccurrences,
|
||||||
|
getSingularEvents,
|
||||||
|
organizeEventsByDate,
|
||||||
|
organizeEventsInCalendar,
|
||||||
|
} from "./event.ts";
|
||||||
|
|
||||||
|
// Much of this code also runs in the visitor's browser, in whatever timezone
|
||||||
|
// that is set to. Everything shown is Oslo time, so the result must not
|
||||||
|
// depend on the runtime's timezone
|
||||||
|
const TIMEZONES = [
|
||||||
|
"Europe/Oslo",
|
||||||
|
"UTC",
|
||||||
|
"Asia/Tokyo",
|
||||||
|
"Pacific/Auckland",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
];
|
||||||
|
|
||||||
|
const originalTz = process.env.TZ;
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.TZ = originalTz;
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeEvent = (id: string, starts: string[]) => ({
|
||||||
|
id,
|
||||||
|
occurrences: starts.map((start, i) => ({ id: `${id}-${i}`, start, end: null })),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 00:30 on onsdag 1. juli 2026 in Oslo; still 30. juni in UTC and the Americas
|
||||||
|
const JUST_PAST_MIDNIGHT = "2026-06-30T22:30:00Z";
|
||||||
|
|
||||||
|
describe.each(TIMEZONES)("with the runtime in %s", (tz) => {
|
||||||
|
const inTz = (name: string, fn: () => void) =>
|
||||||
|
it(name, () => {
|
||||||
|
process.env.TZ = tz;
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("formats day and month strings as written", () => {
|
||||||
|
expect(formatDay("2026-07-01", "eeee dd.MM.")).toBe("onsdag 01.07.");
|
||||||
|
expect(formatDay("2026-01", "MMMM yyyy")).toBe("januar 2026");
|
||||||
|
expect(parseDay("2026-01").getFullYear()).toBe(2026);
|
||||||
|
expect(parseDay("2026-01-01").getDate()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("formats month headings", () => {
|
||||||
|
const today = startOfOsloDay("2026-07-07");
|
||||||
|
expect(formatYearMonth("2026-07", today)).toBe("juli");
|
||||||
|
expect(formatYearMonth("2026-01", today)).toBe("januar");
|
||||||
|
expect(formatYearMonth("2027-01", today)).toBe("januar 2027");
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("formats occurrence months and date ranges", () => {
|
||||||
|
expect(formatOccurrenceMonths([JUST_PAST_MIDNIGHT])).toBe("juli 2026");
|
||||||
|
expect(
|
||||||
|
formatOccurrenceMonths(["2026-12-05T12:00:00Z", "2027-01-05T12:00:00Z"])
|
||||||
|
).toBe("desember 2026 – januar 2027");
|
||||||
|
expect(formatDateRange(["2026-07-01"])).toBe("1. juli");
|
||||||
|
expect(formatDateRange(["2026-07-01", "2026-07-03"])).toBe("1.—3. juli");
|
||||||
|
expect(formatDateRange(["2026-06-30", "2026-07-01"])).toBe(
|
||||||
|
"30. juni—1. juli"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("groups consecutive Oslo days", () => {
|
||||||
|
expect(
|
||||||
|
groupConsecutiveDates([
|
||||||
|
JUST_PAST_MIDNIGHT,
|
||||||
|
"2026-07-02T18:00:00Z",
|
||||||
|
"2026-07-04T18:00:00Z",
|
||||||
|
])
|
||||||
|
).toEqual([["2026-07-01", "2026-07-02"], ["2026-07-04"]]);
|
||||||
|
// across the 25-hour DST fall-back day
|
||||||
|
expect(
|
||||||
|
groupConsecutiveDates([
|
||||||
|
"2026-10-24T18:00:00Z",
|
||||||
|
"2026-10-25T18:00:00Z",
|
||||||
|
"2026-10-26T18:00:00Z",
|
||||||
|
])
|
||||||
|
).toEqual([["2026-10-24", "2026-10-25", "2026-10-26"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("places events on their Oslo day in the calendar", () => {
|
||||||
|
const events = getSingularEvents([makeEvent("a", [JUST_PAST_MIDNIGHT])]);
|
||||||
|
|
||||||
|
const calendar = organizeEventsInCalendar(events);
|
||||||
|
|
||||||
|
// the seeded week starts mandag 29. juni; June holds only padding
|
||||||
|
expect(Object.keys(calendar)).toEqual(["2026-07"]);
|
||||||
|
const [week] = Object.values(calendar["2026-07"]);
|
||||||
|
expect(Object.keys(week)).toEqual([
|
||||||
|
"2026-07-01",
|
||||||
|
"2026-07-02",
|
||||||
|
"2026-07-03",
|
||||||
|
"2026-07-04",
|
||||||
|
"2026-07-05",
|
||||||
|
]);
|
||||||
|
expect(week["2026-07-01"].map((e) => e.id)).toEqual(["a"]);
|
||||||
|
|
||||||
|
expect(Object.keys(organizeEventsByDate(events))).toEqual(["2026-07-01"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("pads a partial calendar week to mandag and søndag", () => {
|
||||||
|
// onsdag 1. – søndag 5. juli
|
||||||
|
expect(
|
||||||
|
calendarWeekPadding(["2026-07-01", "2026-07-02", "2026-07-05"])
|
||||||
|
).toEqual({ before: 2, after: 0 });
|
||||||
|
// mandag 27. – tirsdag 30. juni
|
||||||
|
expect(calendarWeekPadding(["2026-06-29", "2026-06-30"])).toEqual({
|
||||||
|
before: 0,
|
||||||
|
after: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
inTz("judges past and future by the Oslo day", () => {
|
||||||
|
const today = startOfOsloDay("2026-07-01");
|
||||||
|
expect(isTodayOrFuture(JUST_PAST_MIDNIGHT, today)).toBe(true);
|
||||||
|
expect(isTodayOrFuture("2026-06-30T21:30:00Z", today)).toBe(false);
|
||||||
|
expect(
|
||||||
|
getFutureOccurrences(
|
||||||
|
makeEvent("a", ["2026-06-30T21:30:00Z", JUST_PAST_MIDNIGHT]),
|
||||||
|
today
|
||||||
|
).map((o) => o.start)
|
||||||
|
).toEqual([JUST_PAST_MIDNIGHT]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user