diff --git a/apps/web/src/atoms/calendar-preferences.ts b/apps/web/src/atoms/calendar-preferences.ts new file mode 100644 index 00000000..b8e01784 --- /dev/null +++ b/apps/web/src/atoms/calendar-preferences.ts @@ -0,0 +1,19 @@ +import { useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; + +export interface CalendarPreference { + hidden?: boolean; + order?: number; + color?: string; +} + +export type CalendarPreferences = Record; + +export const calendarPreferencesAtom = atomWithStorage( + "analog-calendar-preferences", + {}, +); + +export function useCalendarPreferences() { + return useAtom(calendarPreferencesAtom); +} diff --git a/apps/web/src/atoms/calendars-visibility.ts b/apps/web/src/atoms/calendars-visibility.ts deleted file mode 100644 index 321d8b87..00000000 --- a/apps/web/src/atoms/calendars-visibility.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useAtom } from "jotai"; -import { atomWithStorage } from "jotai/utils"; - -export interface CalendarsVisibility { - hiddenCalendars: string[]; -} - -export const calendarsVisibilityAtom = atomWithStorage( - "analog-calendars-visibility", - { hiddenCalendars: [] }, -); - -export function useCalendarsVisibility() { - return useAtom(calendarsVisibilityAtom); -} diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 2cabbe0e..dc0165a6 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -7,10 +7,10 @@ export { } from "./view-preferences"; export { - calendarsVisibilityAtom, - type CalendarsVisibility, - useCalendarsVisibility, -} from "./calendars-visibility"; + calendarPreferencesAtom, + type CalendarPreferences, + useCalendarPreferences, +} from "./calendar-preferences"; export { calendarSettingsAtom, diff --git a/apps/web/src/components/calendar-layout.tsx b/apps/web/src/components/calendar-layout.tsx index e2d3bfe5..ce0ede2c 100644 --- a/apps/web/src/components/calendar-layout.tsx +++ b/apps/web/src/components/calendar-layout.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect } from "react"; -import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { @@ -13,8 +12,8 @@ import { CalendarView } from "@/components/calendar-view"; import { EventForm } from "@/components/event-form/event-form"; import { RightSidebar } from "@/components/right-sidebar"; import { SidebarInset } from "@/components/ui/sidebar"; +import { useCalendars } from "@/hooks/use-calendars"; import { EventHotkeys } from "@/lib/hotkeys/event-hotkeys"; -import { useTRPC } from "@/lib/trpc/client"; import { useOptimisticEvents } from "./event-calendar/hooks/use-optimistic-events"; export function CalendarLayout() { @@ -36,8 +35,7 @@ export function CalendarLayout() { } function IsolatedCalendarLayout() { - const trpc = useTRPC(); - const query = useQuery(trpc.calendars.list.queryOptions()); + const query = useCalendars(); const { events, selectedEvents, dispatchAction, dispatchAsyncAction } = useOptimisticEvents(); diff --git a/apps/web/src/components/calendar-picker.tsx b/apps/web/src/components/calendar-picker.tsx index 0183aa32..523c4369 100644 --- a/apps/web/src/components/calendar-picker.tsx +++ b/apps/web/src/components/calendar-picker.tsx @@ -2,9 +2,8 @@ import * as React from "react"; import { useResizeObserver } from "@react-hookz/web"; -import { useQuery } from "@tanstack/react-query"; -import { useCalendarsVisibility } from "@/atoms"; +import { useCalendarPreferences } from "@/atoms"; import { Button } from "@/components/ui/button"; import { Command, @@ -19,9 +18,9 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { useCalendars } from "@/hooks/use-calendars"; import { Calendar } from "@/lib/interfaces"; import { RouterOutputs } from "@/lib/trpc"; -import { useTRPC } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { CalendarToggle } from "./calendar-toggle"; @@ -50,14 +49,12 @@ function VisibleCalendars({ calendars }: VisibleCalendarProps) { } function useCalendarList() { - const trpc = useTRPC(); - - return useQuery(trpc.calendars.list.queryOptions()); + return useCalendars(); } function CalendarListItem({ calendar }: { calendar: Calendar }) { - const [calendarsVisibility, setCalendarsVisibility] = - useCalendarsVisibility(); + const [calendarPreferences, setCalendarPreferences] = + useCalendarPreferences(); const textRef = React.useRef(null); const [isTextTruncated, setIsTextTruncated] = React.useState(false); @@ -70,26 +67,25 @@ function CalendarListItem({ calendar }: { calendar: Calendar }) { }); const handleCalendarVisibilityChange = React.useCallback( - (checked: boolean, calendarId: string) => { - const newHiddenCalendars = checked - ? calendarsVisibility.hiddenCalendars.filter((id) => id !== calendarId) - : [...calendarsVisibility.hiddenCalendars, calendarId]; - - setCalendarsVisibility({ - hiddenCalendars: newHiddenCalendars, - }); + (checked: boolean, cal: Calendar) => { + const key = `${cal.accountId}.${cal.id}`; + setCalendarPreferences((prev) => ({ + ...prev, + [key]: { ...prev[key], hidden: !checked }, + })); }, - [calendarsVisibility.hiddenCalendars, setCalendarsVisibility], + [setCalendarPreferences], ); - const checked = !calendarsVisibility.hiddenCalendars.includes(calendar.id); + const pref = calendarPreferences[`${calendar.accountId}.${calendar.id}`]; + const checked = !pref?.hidden; return ( { - handleCalendarVisibilityChange(!checked, calendar.id); + handleCalendarVisibilityChange(!checked, calendar); }} > { - handleCalendarVisibilityChange(checked, calendar.id); + handleCalendarVisibilityChange(checked, calendar); }} primaryCalendar={calendar.primary} /> @@ -116,15 +112,17 @@ export function CalendarPicker() { const { data } = useCalendarList(); - const [calendarVisibility] = useCalendarsVisibility(); + const [calendarPreferences] = useCalendarPreferences(); const visibleCalendars = React.useMemo(() => { return data?.accounts .flatMap((account) => account.calendars) - .filter( - (calendar) => !calendarVisibility.hiddenCalendars.includes(calendar.id), - ); - }, [data, calendarVisibility]); + .filter((calendar) => { + const pref = + calendarPreferences[`${calendar.accountId}.${calendar.id}`]; + return !pref?.hidden; + }); + }, [data, calendarPreferences]); if (!data) { return null; diff --git a/apps/web/src/components/calendar-view.tsx b/apps/web/src/components/calendar-view.tsx index 2c21114c..205ff07b 100644 --- a/apps/web/src/components/calendar-view.tsx +++ b/apps/web/src/components/calendar-view.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef } from "react"; import { useHotkeysContext } from "react-hotkeys-hook"; -import { useCalendarsVisibility, useViewPreferences } from "@/atoms"; +import { useCalendarPreferences, useViewPreferences } from "@/atoms"; import { CalendarHeader, EventGap, @@ -109,7 +109,7 @@ export function CalendarView({ dispatchAction, }: CalendarViewProps) { const viewPreferences = useViewPreferences(); - const [calendarVisibility] = useCalendarsVisibility(); + const [calendarPreferences] = useCalendarPreferences(); // const isDragging = useAtomValue(isDraggingAtom); const scrollContainerRef = useRef(null); const headerRef = useRef(null); @@ -121,13 +121,9 @@ export function CalendarView({ () => filterVisibleEvents( filterPastEvents(events, viewPreferences.showPastEvents), - calendarVisibility.hiddenCalendars, + calendarPreferences, ), - [ - events, - viewPreferences.showPastEvents, - calendarVisibility.hiddenCalendars, - ], + [events, viewPreferences.showPastEvents, calendarPreferences], ); const { enableScope } = useHotkeysContext(); diff --git a/apps/web/src/components/calendars.tsx b/apps/web/src/components/calendars.tsx index 65310ab8..49ed3421 100644 --- a/apps/web/src/components/calendars.tsx +++ b/apps/web/src/components/calendars.tsx @@ -2,7 +2,6 @@ import { Fragment, useMemo, useRef, useState } from "react"; import { useResizeObserver } from "@react-hookz/web"; -import { useQuery } from "@tanstack/react-query"; import { ChevronRight } from "lucide-react"; import { @@ -19,7 +18,7 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar"; import { Skeleton } from "@/components/ui/skeleton"; -import { useTRPC } from "@/lib/trpc/client"; +import { useCalendars } from "@/hooks/use-calendars"; import { CalendarToggle } from "./calendar-toggle"; export type CalendarItem = { @@ -30,9 +29,7 @@ export type CalendarItem = { }; function useCalendarList() { - const trpc = useTRPC(); - - return useQuery(trpc.calendars.list.queryOptions()); + return useCalendars(); } export function Calendars() { diff --git a/apps/web/src/components/event-calendar/event-context-menu.tsx b/apps/web/src/components/event-calendar/event-context-menu.tsx index 653a8fa3..d9e5fa81 100644 --- a/apps/web/src/components/event-calendar/event-context-menu.tsx +++ b/apps/web/src/components/event-calendar/event-context-menu.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; -import { useQuery } from "@tanstack/react-query"; import { CheckIcon } from "lucide-react"; import { CalendarEvent } from "@/components/event-calendar/types"; @@ -16,7 +15,7 @@ import { } from "@/components/ui/context-menu"; import { KeyboardShortcut } from "@/components/ui/keyboard-shortcut"; import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; -import { useTRPC } from "@/lib/trpc/client"; +import { useCalendars } from "@/hooks/use-calendars"; import { cn } from "@/lib/utils"; import { Action } from "./hooks/use-optimistic-events"; @@ -61,8 +60,7 @@ interface EventContextMenuCalendarListProps { function EventContextMenuCalendarList({ disabled, }: EventContextMenuCalendarListProps) { - const trpc = useTRPC(); - const calendarQuery = useQuery(trpc.calendars.list.queryOptions()); + const calendarQuery = useCalendars(); return (
diff --git a/apps/web/src/components/event-calendar/utils/event.ts b/apps/web/src/components/event-calendar/utils/event.ts index 44ad2e93..5d3b457d 100644 --- a/apps/web/src/components/event-calendar/utils/event.ts +++ b/apps/web/src/components/event-calendar/utils/event.ts @@ -10,6 +10,7 @@ import { import { toDate } from "@repo/temporal"; +import type { CalendarPreferences } from "@/atoms/calendar-preferences"; import type { CalendarEvent } from "../types"; // ============================================================================ @@ -63,9 +64,12 @@ export function filterPastEvents( export function filterVisibleEvents( events: CalendarEvent[], - hiddenCalendars: string[], + preferences: CalendarPreferences, ): CalendarEvent[] { - return events.filter((event) => !hiddenCalendars.includes(event.calendarId)); + return events.filter((event) => { + const key = `${event.accountId}.${event.calendarId}`; + return !preferences[key]?.hidden; + }); } export function getEventsStartingOnDay( diff --git a/apps/web/src/components/event-form/event-form.tsx b/apps/web/src/components/event-form/event-form.tsx index 5a40cf81..a8c061cd 100644 --- a/apps/web/src/components/event-form/event-form.tsx +++ b/apps/web/src/components/event-form/event-form.tsx @@ -1,7 +1,6 @@ "use client"; import * as React from "react"; -import { useQuery } from "@tanstack/react-query"; import { RepeatIcon } from "lucide-react"; import { @@ -20,8 +19,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; +import { useCalendars } from "@/hooks/use-calendars"; import { Calendar, CalendarEvent, DraftEvent } from "@/lib/interfaces"; -import { useTRPC } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { createEventId, isDraftEvent } from "@/lib/utils/calendar"; import { @@ -81,8 +80,7 @@ export function EventForm({ }: EventFormProps) { const settings = useCalendarSettings(); - const trpc = useTRPC(); - const query = useQuery(trpc.calendars.list.queryOptions()); + const query = useCalendars(); const [event, setEvent] = React.useState(selectedEvent); diff --git a/apps/web/src/components/settings-dialog/tabs/accounts.tsx b/apps/web/src/components/settings-dialog/tabs/accounts.tsx index 824cfc80..b60afcca 100644 --- a/apps/web/src/components/settings-dialog/tabs/accounts.tsx +++ b/apps/web/src/components/settings-dialog/tabs/accounts.tsx @@ -18,6 +18,7 @@ import { } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; +import { useCalendars } from "@/hooks/use-calendars"; import { RouterOutputs } from "@/lib/trpc"; import { useTRPC } from "@/lib/trpc/client"; import { @@ -57,7 +58,7 @@ function DefaultCalendarPicker() { const trpc = useTRPC(); const queryClient = useQueryClient(); - const query = useQuery(trpc.calendars.list.queryOptions()); + const query = useCalendars(); const mutation = useMutation( trpc.calendars.setDefault.mutationOptions({ onSuccess: () => { diff --git a/apps/web/src/hooks/use-calendars.ts b/apps/web/src/hooks/use-calendars.ts new file mode 100644 index 00000000..02f20b50 --- /dev/null +++ b/apps/web/src/hooks/use-calendars.ts @@ -0,0 +1,50 @@ +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; + +import { calendarPreferencesAtom } from "@/atoms/calendar-preferences"; +import { useTRPC } from "@/lib/trpc/client"; + +export function useCalendars() { + const trpc = useTRPC(); + const preferences = useAtomValue(calendarPreferencesAtom); + + return useQuery({ + ...trpc.calendars.list.queryOptions(), + select: (data) => { + const accounts = data.accounts.map((account) => ({ + ...account, + calendars: account.calendars + .map((calendar, index) => { + const key = `${calendar.accountId}.${calendar.id}`; + const pref = preferences[key]; + return { + ...calendar, + color: pref?.color ?? calendar.color, + hidden: pref?.hidden ?? false, + order: pref?.order, + _idx: index, + }; + }) + .sort((a, b) => { + const orderA = a.order ?? Number.POSITIVE_INFINITY; + const orderB = b.order ?? Number.POSITIVE_INFINITY; + if (orderA === orderB) return a._idx - b._idx; + return orderA - orderB; + }) + .map(({ _idx, ...cal }) => cal), + })); + + const defaultKey = `${data.defaultCalendar.accountId}.${data.defaultCalendar.id}`; + const defaultPref = preferences[defaultKey]; + + return { + ...data, + defaultCalendar: { + ...data.defaultCalendar, + color: defaultPref?.color ?? data.defaultCalendar.color, + }, + accounts, + }; + }, + }); +}