"use client" import { create } from "zustand" import { persist } from "zustand/middleware" import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage" import { DEFAULT_AGENDA_USER_SETTINGS, } from "@/lib/agenda/agenda-settings-defaults" import { migrateAutoImportIdentityKeys, normalizeAutoImportInvitationSources, normalizeInvitationImportExclusions, } from "@/lib/agenda/agenda-destination-identities" import type { AgendaAutoImportSource, AgendaCalendarView, AgendaDurationStep, AgendaExternalCalendar, AgendaInvitationExclusion, AgendaTimeFormat, AgendaUserSettings, AgendaVideoProvider, AgendaWeekStart, } from "@/lib/agenda/agenda-settings-types" import { EXTERNAL_CALENDAR_ID_PREFIX, isReservedAgendaViewName, } from "@/lib/agenda/agenda-calendar-visibility" import type { AgendaView } from "./agenda-url.ts" function newAgendaId(prefix: string): string { return `${prefix}${crypto.randomUUID()}` } type AgendaSettingsState = AgendaUserSettings & { quickSettingsOpen: boolean hiddenCalendarIds: string[] lastView: AgendaView activeCalendarViewId: string | null } type PersistedAgendaSettings = Partial< AgendaSettingsState & { autoImportInvitationIdentityKeys?: string[] } > type AgendaSettingsActions = { setQuickSettingsOpen: (open: boolean) => void setDefaultVideoProvider: (provider: AgendaVideoProvider) => void setVideoProviderApiKey: (provider: AgendaVideoProvider, key: string) => void setDefaultInvitationIdentityKey: (key: string | null) => void setAutoImportInvitationSources: (sources: AgendaAutoImportSource[]) => void setInvitationImportExclusions: (items: AgendaInvitationExclusion[]) => void setWeekStart: (weekStart: AgendaWeekStart) => void setDefaultQuickDurationMinutes: (minutes: number) => void setVisibleHoursStart: (minutes: number) => void setVisibleHoursEnd: (minutes: number) => void setTimeFormat: (format: AgendaTimeFormat) => void setDragSnapMinutes: (minutes: AgendaDurationStep) => void setButtonSnapMinutes: (minutes: AgendaDurationStep) => void toggleCalendarVisible: (id: string) => void setLastView: (view: AgendaView) => void setExternalCalendars: (calendars: AgendaExternalCalendar[]) => void addExternalCalendar: (input: Omit) => string updateExternalCalendar: ( id: string, patch: Partial>, ) => void removeExternalCalendar: (id: string) => void setCalendarViews: (views: AgendaCalendarView[]) => void addCalendarView: (input: Omit) => string updateCalendarView: ( id: string, patch: Partial>, ) => void removeCalendarView: (id: string) => void setDefaultCalendarViewId: (id: string | null) => void setActiveCalendarViewId: (id: string | null) => void setAccountEnabledCalendarIds: ( accountId: string, calendarIds: string[] | null, ) => void toggleAccountCalendar: ( accountId: string, calendarId: string, allCalendarIds: string[], ) => void } export const useAgendaSettingsStore = create< AgendaSettingsState & AgendaSettingsActions >()( persist( (set, get) => ({ ...DEFAULT_AGENDA_USER_SETTINGS, quickSettingsOpen: false, hiddenCalendarIds: [], lastView: "week", activeCalendarViewId: null, setQuickSettingsOpen: (quickSettingsOpen) => set({ quickSettingsOpen }), setDefaultVideoProvider: (defaultVideoProvider) => set({ defaultVideoProvider }), setVideoProviderApiKey: (provider, key) => set((s) => ({ videoProviderApiKeys: { ...s.videoProviderApiKeys, [provider]: key }, })), setDefaultInvitationIdentityKey: (defaultInvitationIdentityKey) => set({ defaultInvitationIdentityKey }), setAutoImportInvitationSources: (autoImportInvitationSources) => set({ autoImportInvitationSources: normalizeAutoImportInvitationSources( autoImportInvitationSources, ), }), setInvitationImportExclusions: (invitationImportExclusions) => set({ invitationImportExclusions: normalizeInvitationImportExclusions( invitationImportExclusions, ), }), setWeekStart: (weekStart) => set({ weekStart }), setDefaultQuickDurationMinutes: (defaultQuickDurationMinutes) => set({ defaultQuickDurationMinutes }), setVisibleHoursStart: (visibleHoursStart) => set({ visibleHoursStart }), setVisibleHoursEnd: (visibleHoursEnd) => set({ visibleHoursEnd }), setTimeFormat: (timeFormat) => set({ timeFormat }), setDragSnapMinutes: (dragSnapMinutes) => set({ dragSnapMinutes }), setButtonSnapMinutes: (buttonSnapMinutes) => set({ buttonSnapMinutes }), toggleCalendarVisible: (id) => { const hidden = get().hiddenCalendarIds set({ hiddenCalendarIds: hidden.includes(id) ? hidden.filter((h) => h !== id) : [...hidden, id], }) }, setLastView: (lastView) => set({ lastView }), setExternalCalendars: (externalCalendars) => set({ externalCalendars }), addExternalCalendar: (input) => { const id = newAgendaId(EXTERNAL_CALENDAR_ID_PREFIX) set((s) => ({ externalCalendars: [...s.externalCalendars, { ...input, id }], })) return id }, updateExternalCalendar: (id, patch) => set((s) => ({ externalCalendars: s.externalCalendars.map((calendar) => calendar.id === id ? { ...calendar, ...patch } : calendar, ), })), removeExternalCalendar: (id) => set((s) => ({ externalCalendars: s.externalCalendars.filter((calendar) => calendar.id !== id), calendarViews: s.calendarViews.map((view) => ({ ...view, calendar_ids: view.calendar_ids.filter((calendarId) => calendarId !== id), })), activeCalendarViewId: s.activeCalendarViewId === id ? null : s.activeCalendarViewId, hiddenCalendarIds: s.hiddenCalendarIds.filter((hiddenId) => hiddenId !== id), accountEnabledCalendarIds: Object.fromEntries( Object.entries(s.accountEnabledCalendarIds).map(([accountId, calendarIds]) => [ accountId, calendarIds.filter((calendarId) => calendarId !== id), ]), ), })), setCalendarViews: (calendarViews) => set({ calendarViews }), addCalendarView: (input) => { const name = input.name.trim() if (isReservedAgendaViewName(name)) return "" const id = newAgendaId("view:") set((s) => ({ calendarViews: [...s.calendarViews, { ...input, name, id }], })) return id }, updateCalendarView: (id, patch) => set((s) => { const nextName = patch.name?.trim() if (nextName && isReservedAgendaViewName(nextName)) return s return { calendarViews: s.calendarViews.map((view) => view.id === id ? { ...view, ...patch, ...(nextName ? { name: nextName } : {}) } : view, ), } }), removeCalendarView: (id) => set((s) => ({ calendarViews: s.calendarViews.filter((view) => view.id !== id), defaultCalendarViewId: s.defaultCalendarViewId === id ? null : s.defaultCalendarViewId, activeCalendarViewId: s.activeCalendarViewId === id ? null : s.activeCalendarViewId, })), setDefaultCalendarViewId: (defaultCalendarViewId) => set({ defaultCalendarViewId }), setActiveCalendarViewId: (activeCalendarViewId) => set({ activeCalendarViewId }), setAccountEnabledCalendarIds: (accountId, calendarIds) => set((s) => { const next = { ...s.accountEnabledCalendarIds } if (calendarIds === null) { delete next[accountId] } else { next[accountId] = calendarIds } return { accountEnabledCalendarIds: next } }), toggleAccountCalendar: (accountId, calendarId, allCalendarIds) => { set((s) => { const enabled = s.accountEnabledCalendarIds[accountId] ?? allCalendarIds const next = enabled.includes(calendarId) ? enabled.filter((id) => id !== calendarId) : [...enabled, calendarId] const nextMap = { ...s.accountEnabledCalendarIds } if ( next.length === allCalendarIds.length && allCalendarIds.every((id) => next.includes(id)) ) { delete nextMap[accountId] } else { nextMap[accountId] = next } return { accountEnabledCalendarIds: nextMap } }) }, }), { name: "agenda-settings-store", version: 3, migrate: (persisted, version) => { const state = persisted as PersistedAgendaSettings if (version < 1) { if ( !state.autoImportInvitationSources && Array.isArray(state.autoImportInvitationIdentityKeys) ) { state.autoImportInvitationSources = migrateAutoImportIdentityKeys( state.autoImportInvitationIdentityKeys, ) } delete state.autoImportInvitationIdentityKeys } if (version < 2) { state.externalCalendars ??= [] state.calendarViews ??= [] state.defaultCalendarViewId ??= null state.accountEnabledCalendarIds ??= {} state.activeCalendarViewId ??= null } if (version < 3) { state.calendarViews = (state.calendarViews ?? []).map((view) => isReservedAgendaViewName(view.name) ? { ...view, name: `${view.name} (vue)` } : view, ) } return state as AgendaSettingsState }, storage: debouncedPersistJSONStorage, partialize: (s) => ({ defaultVideoProvider: s.defaultVideoProvider, videoProviderApiKeys: s.videoProviderApiKeys, defaultInvitationIdentityKey: s.defaultInvitationIdentityKey, autoImportInvitationSources: s.autoImportInvitationSources, invitationImportExclusions: s.invitationImportExclusions, weekStart: s.weekStart, defaultQuickDurationMinutes: s.defaultQuickDurationMinutes, visibleHoursStart: s.visibleHoursStart, visibleHoursEnd: s.visibleHoursEnd, timeFormat: s.timeFormat, dragSnapMinutes: s.dragSnapMinutes, buttonSnapMinutes: s.buttonSnapMinutes, hiddenCalendarIds: s.hiddenCalendarIds, lastView: s.lastView, externalCalendars: s.externalCalendars, calendarViews: s.calendarViews, defaultCalendarViewId: s.defaultCalendarViewId, accountEnabledCalendarIds: s.accountEnabledCalendarIds, activeCalendarViewId: s.activeCalendarViewId, }), }, ), ) interface AgendaUIState { sidebarCollapsed: boolean setSidebarCollapsed: (v: boolean) => void } export const useAgendaUIStore = create((set) => ({ sidebarCollapsed: false, setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }), })) export function identityKey(email: string, accountId?: string): string { return `${accountId ?? ""}:${email}` }