Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/calendar-web/src/calendars/calendar.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isSentinelCalendarList,
synthesizeLocalCalendar,
} from "@web/calendars/local-calendar.sentinel";
import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint";

export const calendarQueryKeys = {
all: ["calendars"] as const,
Expand All @@ -36,10 +37,14 @@ export function calendarsQueryOptions(authenticated: boolean) {
// covers the gap before React re-renders after the session store flips,
// so a still-mounted anonymous observer cannot write the sentinel back.
if (!authenticated && !readAuthenticated()) {
return [synthesizeLocalCalendar(getLocalCalendarSentinelId())];
const local = [synthesizeLocalCalendar(getLocalCalendarSentinelId())];
syncCalendarTimeZoneHintFromCalendars(local);
return local;
}

return CalendarApi.list();
const calendars = await CalendarApi.list();
syncCalendarTimeZoneHintFromCalendars(calendars);
return calendars;
},
staleTime: 60_000,
});
Expand Down Expand Up @@ -97,6 +102,7 @@ export async function ensureRemoteCalendars(

const calendars = await client.fetchQuery(calendarsQueryOptions(true));
if (isSentinelCalendarList(calendars)) return undefined;
syncCalendarTimeZoneHintFromCalendars(calendars);
return calendars;
}

Expand Down
39 changes: 38 additions & 1 deletion apps/calendar-web/src/routers/loaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
createRouter,
isRedirect,
} from "@tanstack/react-router";
import { TimeZoneSchema } from "@core/types/domain-primitives";
import { createMockCalendar } from "@web/__tests__/utils/factories/calendar.factory";
import { queryClient } from "@web/api/query-client";
import { calendarQueryKeys } from "@web/calendars/calendar.query";
import { ROOT_ROUTES } from "@web/common/constants/routes";
import * as prefetchWeekEvents from "@web/events/queries/prefetch-week-events";
import {
Expand All @@ -16,7 +20,18 @@ import {
redirectToToday,
validateWeekDateParam,
} from "@web/routers/loaders";
import { afterEach, describe, expect, it, spyOn } from "bun:test";
import {
resetEffectiveTimeZoneStoreForTests,
setBrowserTimeZoneForTests,
} from "@web/timezone/effective-timezone.store";
import {
afterEach,
describe,
expect,
it,
setSystemTime,
spyOn,
} from "bun:test";

function getRedirect(fn: () => unknown) {
try {
Expand Down Expand Up @@ -126,6 +141,28 @@ describe("router redirects", () => {
});
});

describe("loadTodayData", () => {
afterEach(() => {
resetEffectiveTimeZoneStoreForTests();
queryClient.clear();
setSystemTime();
});

it("applies a cached calendar hint before picking today at the merge-queue boundary", () => {
setBrowserTimeZoneForTests("UTC");
setSystemTime(new Date("2026-11-12T00:23:00.000Z"));
queryClient.setQueryData(calendarQueryKeys.all, [
createMockCalendar({
name: "miscellaneous",
timeZone: TimeZoneSchema.parse("America/Denver"),
isPrimary: true,
}),
]);

expect(loadTodayData().dateString).toBe("2026-11-11");
});
});

describe("loadDateParam", () => {
it("shapes the loader data for an already-valid dateString param", () => {
const result = loadDateParam({
Expand Down
12 changes: 12 additions & 0 deletions apps/calendar-web/src/routers/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { redirect } from "@tanstack/react-router";
import { type Calendar } from "@core/types/calendar.contracts";
import { zYearMonthDayString } from "@core/types/type.utils";
import dayjs, { type Dayjs } from "@core/util/date/dayjs";
import { queryClient } from "@web/api/query-client";
import { calendarQueryKeys } from "@web/calendars/calendar.query";
import {
DEFAULT_CALENDAR_ROUTE,
ROOT_ROUTES,
Expand All @@ -11,6 +14,7 @@ import {
prefetchWeekEventsQuery,
weekDateStringFromPathname,
} from "@web/events/queries/prefetch-week-events";
import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint";
import { getEffectiveTimeZone } from "@web/timezone/effective-timezone.store";

export interface DayLoaderData {
Expand Down Expand Up @@ -38,7 +42,15 @@ export async function loadAuthenticated() {
return { authenticated };
}

function syncCachedCalendarTimeZoneHintBeforeToday(): void {
const calendars = queryClient.getQueryData<Calendar[]>(calendarQueryKeys.all);
if (calendars?.length) {
syncCalendarTimeZoneHintFromCalendars(calendars);
}
}

export function loadTodayData(): DayLoaderData {
syncCachedCalendarTimeZoneHintBeforeToday();
const dateInView = dayjs().tz(getEffectiveTimeZone());
const dateFormat = dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT;

Expand Down
7 changes: 2 additions & 5 deletions apps/calendar-web/src/timezone/CalendarTimeZoneHintSync.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { useLayoutEffect } from "react";
import { useCalendarsQuery } from "@web/calendars/calendar.query";
import { pickCalendarTimeZoneHint } from "@web/timezone/calendar-timezone-hint";
import { setCalendarTimeZoneHint } from "@web/timezone/effective-timezone.store";
import { syncCalendarTimeZoneHintFromCalendars } from "@web/timezone/calendar-timezone-hint";

/** Keeps the Auto effective zone aligned with connected calendar timezones. */
export function CalendarTimeZoneHintSync() {
const { data: calendars } = useCalendarsQuery();

useLayoutEffect(() => {
setCalendarTimeZoneHint(
calendars ? pickCalendarTimeZoneHint(calendars) : null,
);
syncCalendarTimeZoneHintFromCalendars(calendars);
}, [calendars]);

return null;
Expand Down
36 changes: 34 additions & 2 deletions apps/calendar-web/src/timezone/calendar-timezone-hint.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
import { TimeZoneSchema } from "@core/types/domain-primitives";
import dayjs from "@core/util/date/dayjs";
import { createMockCalendar } from "@web/__tests__/utils/factories/calendar.factory";
import { pickCalendarTimeZoneHint } from "@web/timezone/calendar-timezone-hint";
import { describe, expect, it } from "bun:test";
import {
pickCalendarTimeZoneHint,
syncCalendarTimeZoneHintFromCalendars,
} from "@web/timezone/calendar-timezone-hint";
import {
getEffectiveTimeZone,
resetEffectiveTimeZoneStoreForTests,
setBrowserTimeZoneForTests,
} from "@web/timezone/effective-timezone.store";
import { afterEach, describe, expect, it, setSystemTime } from "bun:test";

describe("syncCalendarTimeZoneHintFromCalendars", () => {
afterEach(() => {
resetEffectiveTimeZoneStoreForTests();
setSystemTime();
});

it("applies denver today before navigation when the browser is UTC at the merge-queue boundary", () => {
setBrowserTimeZoneForTests("UTC");
setSystemTime(new Date("2026-11-12T00:23:00.000Z"));
syncCalendarTimeZoneHintFromCalendars([
createMockCalendar({
name: "miscellaneous",
timeZone: TimeZoneSchema.parse("America/Denver"),
isPrimary: true,
}),
]);
expect(getEffectiveTimeZone()).toBe("America/Denver");
expect(dayjs().tz(getEffectiveTimeZone()).format("YYYY-MM-DD")).toBe(
"2026-11-11",
);
});
});

describe("pickCalendarTimeZoneHint", () => {
it("prefers the primary calendar zone", () => {
Expand Down
10 changes: 10 additions & 0 deletions apps/calendar-web/src/timezone/calendar-timezone-hint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Calendar } from "@core/types/calendar.contracts";
import { isValidTimeZone } from "@web/timezone/browser-timezone";
import { setCalendarTimeZoneHint } from "@web/timezone/effective-timezone.store";
import { isUtcLikeTimeZone } from "@web/timezone/utc-like-timezone";

/**
Expand Down Expand Up @@ -32,3 +33,12 @@ export function pickCalendarTimeZoneHint(

return null;
}

/** Keep Auto effective zone aligned with connected calendars (see CalendarTimeZoneHintSync). */
export function syncCalendarTimeZoneHintFromCalendars(
calendars: readonly Calendar[] | null | undefined,
): void {
setCalendarTimeZoneHint(
calendars ? pickCalendarTimeZoneHint(calendars) : null,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,68 @@ describe("calendarIdsWithVisibleEventsOnDay", () => {
resetEffectiveTimeZoneStoreForTests();
});

it("shows every stress calendar column when viewed day matches denver event day", () => {
setBrowserTimeZoneForTests("UTC");
const stressCalendars = Array.from({ length: 12 }, (_, index) =>
makeCalendar({
id: `f${String(index).padStart(23, "0")}` as Calendar["id"],
name: `stress-${index}`,
isPrimary: index === 0,
}),
);
const dateInViewDenver = dayjs.tz("2026-11-11T17:23:00", "America/Denver");
const timed = stressCalendars.map((calendar, index) =>
gridEvent({
_id: String(50 + index),
calendarId: calendar.id,
startDate: "2026-11-11 09:00:00.000",
endDate: "2026-11-11 10:00:00.000",
timeZone: "America/Denver",
}),
);

expect(
getDayViewCalendars({
allDayEvents: [],
calendars: stressCalendars,
dateInView: dateInViewDenver,
hiddenEventIds: new Set(),
timedEvents: timed,
}),
).toHaveLength(12);
});

it("falls back to primary when viewed utc day mismatches denver stress events", () => {
setBrowserTimeZoneForTests("UTC");
const stressCalendars = Array.from({ length: 12 }, (_, index) =>
makeCalendar({
id: `f${String(index).padStart(23, "0")}` as Calendar["id"],
name: `stress-${index}`,
isPrimary: index === 0,
}),
);
const dateInViewUtc = dayjs.tz("2026-11-12T00:23:00", "UTC");
const timed = stressCalendars.map((calendar, index) =>
gridEvent({
_id: String(50 + index),
calendarId: calendar.id,
startDate: "2026-11-11 09:00:00.000",
endDate: "2026-11-11 10:00:00.000",
timeZone: "America/Denver",
}),
);

expect(
getDayViewCalendars({
allDayEvents: [],
calendars: stressCalendars,
dateInView: dateInViewUtc,
hiddenEventIds: new Set(),
timedEvents: timed,
}),
).toHaveLength(1);
});

it("keeps both day columns at the merge-queue UTC boundary (browser UTC, no calendar hint)", () => {
setBrowserTimeZoneForTests("UTC");
const primary = makeCalendar({
Expand Down
Loading