feat: improve calendar view mode handling with validation and defaulting

This commit is contained in:
Linus Rath
2026-03-17 23:56:10 +01:00
parent 6fe4a98b02
commit 8a92d53125
2 changed files with 35 additions and 12 deletions
+13 -11
View File
@@ -10,6 +10,7 @@ import {
format, parseISO, format, parseISO,
} from "date-fns"; } from "date-fns";
import { useCalendarStore } from "@/stores/calendar-store"; import { useCalendarStore } from "@/stores/calendar-store";
import { isCalendarViewMode } from "@/stores/calendar-store";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store"; import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
@@ -64,6 +65,7 @@ export default function CalendarPage() {
} = useCalendarStore(); } = useCalendarStore();
const { firstDayOfWeek, timeFormat } = useSettingsStore(); const { firstDayOfWeek, timeFormat } = useSettingsStore();
const { identities } = useIdentityStore(); const { identities } = useIdentityStore();
const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month";
const currentUserEmails = useMemo(() => const currentUserEmails = useMemo(() =>
identities.map(id => id.email).filter(Boolean), identities.map(id => id.email).filter(Boolean),
@@ -133,7 +135,7 @@ export default function CalendarPage() {
const dateRange = useMemo(() => { const dateRange = useMemo(() => {
const d = selectedDate; const d = selectedDate;
switch (viewMode) { switch (normalizedViewMode) {
case "month": { case "month": {
const ms = startOfMonth(d); const ms = startOfMonth(d);
const me = endOfMonth(d); const me = endOfMonth(d);
@@ -160,17 +162,17 @@ export default function CalendarPage() {
end: format(addDays(d, 30), "yyyy-MM-dd'T'23:59:59"), end: format(addDays(d, 30), "yyyy-MM-dd'T'23:59:59"),
}; };
} }
}, [selectedDate, viewMode, firstDayOfWeek]); }, [selectedDate, normalizedViewMode, firstDayOfWeek]);
useEffect(() => { useEffect(() => {
if (client && calendars.length > 0) { if (client && calendars.length > 0 && dateRange) {
fetchEvents(client, dateRange.start, dateRange.end); fetchEvents(client, dateRange.start, dateRange.end);
} }
}, [client, calendars.length, dateRange, fetchEvents]); }, [client, calendars.length, dateRange, fetchEvents]);
const navigatePrev = useCallback(() => { const navigatePrev = useCallback(() => {
let next: Date; let next: Date;
switch (viewMode) { switch (normalizedViewMode) {
case "month": next = subMonths(selectedDate, 1); break; case "month": next = subMonths(selectedDate, 1); break;
case "week": next = subWeeks(selectedDate, 1); break; case "week": next = subWeeks(selectedDate, 1); break;
case "day": next = subDays(selectedDate, 1); break; case "day": next = subDays(selectedDate, 1); break;
@@ -178,11 +180,11 @@ export default function CalendarPage() {
} }
setSelectedDate(next); setSelectedDate(next);
setMiniMonth(next); setMiniMonth(next);
}, [viewMode, selectedDate, setSelectedDate]); }, [normalizedViewMode, selectedDate, setSelectedDate]);
const navigateNext = useCallback(() => { const navigateNext = useCallback(() => {
let next: Date; let next: Date;
switch (viewMode) { switch (normalizedViewMode) {
case "month": next = addMonths(selectedDate, 1); break; case "month": next = addMonths(selectedDate, 1); break;
case "week": next = addWeeks(selectedDate, 1); break; case "week": next = addWeeks(selectedDate, 1); break;
case "day": next = addDays(selectedDate, 1); break; case "day": next = addDays(selectedDate, 1); break;
@@ -190,7 +192,7 @@ export default function CalendarPage() {
} }
setSelectedDate(next); setSelectedDate(next);
setMiniMonth(next); setMiniMonth(next);
}, [viewMode, selectedDate, setSelectedDate]); }, [normalizedViewMode, selectedDate, setSelectedDate]);
const goToToday = useCallback(() => { const goToToday = useCallback(() => {
setSelectedDate(new Date()); setSelectedDate(new Date());
@@ -222,10 +224,10 @@ export default function CalendarPage() {
setSelectedDate(date); setSelectedDate(date);
setMiniMonth(date); setMiniMonth(date);
// On mobile month view, tapping a date switches to day view // On mobile month view, tapping a date switches to day view
if (isMobile && viewMode === "month") { if (isMobile && normalizedViewMode === "month") {
setViewMode("day"); setViewMode("day");
} }
}, [setSelectedDate, isMobile, viewMode, setViewMode]); }, [setSelectedDate, isMobile, normalizedViewMode, setViewMode]);
const handleMiniMonthChange = useCallback((date: Date) => { const handleMiniMonthChange = useCallback((date: Date) => {
setMiniMonth(date); setMiniMonth(date);
@@ -625,7 +627,7 @@ export default function CalendarPage() {
} }
const viewContent = (() => { const viewContent = (() => {
switch (viewMode) { switch (normalizedViewMode) {
case "month": case "month":
return ( return (
<CalendarMonthView <CalendarMonthView
@@ -764,7 +766,7 @@ export default function CalendarPage() {
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<CalendarToolbar <CalendarToolbar
selectedDate={selectedDate} selectedDate={selectedDate}
viewMode={viewMode} viewMode={normalizedViewMode}
onPrev={navigatePrev} onPrev={navigatePrev}
onNext={navigateNext} onNext={navigateNext}
onToday={goToToday} onToday={goToToday}
+22 -1
View File
@@ -7,6 +7,12 @@ import { normalizeAllDayDuration } from '@/lib/calendar-utils';
export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda'; export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda';
const CALENDAR_VIEW_MODES: CalendarViewMode[] = ['month', 'week', 'day', 'agenda'];
export function isCalendarViewMode(value: unknown): value is CalendarViewMode {
return typeof value === 'string' && CALENDAR_VIEW_MODES.includes(value as CalendarViewMode);
}
export interface ICalSubscription { export interface ICalSubscription {
id: string; id: string;
url: string; url: string;
@@ -71,6 +77,10 @@ const initialState = {
icalSubscriptions: [] as ICalSubscription[], icalSubscriptions: [] as ICalSubscription[],
}; };
function getSafeCalendarViewMode(value: unknown): CalendarViewMode {
return isCalendarViewMode(value) ? value : 'month';
}
export const useCalendarStore = create<CalendarStore>()( export const useCalendarStore = create<CalendarStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
@@ -299,7 +309,7 @@ export const useCalendarStore = create<CalendarStore>()(
}, },
setSelectedDate: (date) => set({ selectedDate: date }), setSelectedDate: (date) => set({ selectedDate: date }),
setViewMode: (mode) => set({ viewMode: mode }), setViewMode: (mode) => set({ viewMode: getSafeCalendarViewMode(mode) }),
updateCalendar: async (client, calendarId, updates) => { updateCalendar: async (client, calendarId, updates) => {
set({ error: null }); set({ error: null });
@@ -575,6 +585,17 @@ export const useCalendarStore = create<CalendarStore>()(
}), }),
{ {
name: 'calendar-storage', name: 'calendar-storage',
merge: (persistedState, currentState) => {
const mergedState = {
...currentState,
...(persistedState as Partial<CalendarStore> | undefined),
};
return {
...mergedState,
viewMode: getSafeCalendarViewMode(mergedState.viewMode),
};
},
partialize: (state) => ({ partialize: (state) => ({
selectedCalendarIds: state.selectedCalendarIds, selectedCalendarIds: state.selectedCalendarIds,
viewMode: state.viewMode, viewMode: state.viewMode,