Repository: analogdotnow/Analog
Pull request #54: chore(web): improve type safety calendar mutations
Author: login: JeanMeijer
Created: 2025-05-25T22:49:37Z | Merged: 2025-05-27T12:55:23Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved calendar event management by centralizing event creation, updating, and deletion logic for a more modular and maintainable experience.
  - Enhanced type safety and clarity in event handling, reducing the risk of errors.
  - Streamlined calendar event listing and updating for more predictable behavior when no calendar IDs are provided.

- **Bug Fixes**
  - Ensured consistent handling of calendar event data and improved error recovery during event operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/web/src/components/calendar-view.tsx b/apps/web/src/components/calendar-view.tsx
index 1266aa0e..ea601ddd 100644
--- a/apps/web/src/components/calendar-view.tsx
+++ b/apps/web/src/components/calendar-view.tsx
@@ -10,6 +10,7 @@ import {
 import { useTRPC } from "@/lib/trpc/client";
 import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
 import { dateHelpers } from "@/lib/date-helpers";
+import { RouterOutputs } from "@/lib/trpc";
 
 interface CalendarViewProps {
   className?: string;
@@ -35,31 +36,33 @@ const CALENDAR_CONFIG = {
   DEFAULT_CALENDAR_ID: "primary",
 };
 
-export function CalendarView({ className }: CalendarViewProps) {
+type Event = RouterOutputs["events"]["list"]["events"][number];
+
+function useCalendarActions() {
   const trpc = useTRPC();
   const queryClient = useQueryClient();
 
   const timeMin = useMemo(
     () =>
       subDays(new Date(), CALENDAR_CONFIG.TIME_RANGE_DAYS_PAST).toISOString(),
-    []
+    [],
   );
   const timeMax = useMemo(
     () =>
       addDays(new Date(), CALENDAR_CONFIG.TIME_RANGE_DAYS_FUTURE).toISOString(),
-    []
+    [],
   );
 
   const eventsQueryKey = useMemo(
     () => trpc.events.list.queryOptions({ timeMin, timeMax }).queryKey,
-    [trpc.events.list, timeMin, timeMax]
+    [trpc.events.list, timeMin, timeMax],
   );
 
   const { data } = useQuery(
     trpc.events.list.queryOptions({
       timeMin,
       timeMax,
-    })
+    }),
   );
 
   const transformedEvents = useMemo(() => {
@@ -70,7 +73,7 @@ export function CalendarView({ className }: CalendarViewProps) {
       const endDate = dateHelpers.adjustEndDateForDisplay(
         startDate,
         new Date(event.end),
-        event.allDay
+        event.allDay,
       );
 
       return {
@@ -86,122 +89,150 @@ export function CalendarView({ className }: CalendarViewProps) {
     });
   }, [data]);
 
-  const { mutate: createEvent, isPending: isCreating } = useMutation({
-    ...trpc.events.create.mutationOptions(),
-    onMutate: async (newEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        const tempEvent = {
-          id: `temp-${Date.now()}`,
-          title: newEvent.title,
-          description: newEvent.description,
-          start: newEvent.start,
-          end: newEvent.end,
-          allDay: newEvent.allDay || false,
-          location: newEvent.location,
-          colorId: "1",
-        };
-
-        return {
-          ...old,
-          events: [...(old.events || []), tempEvent].sort(
-            (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
-          ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, newEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: updateEvent, isPending: isUpdating } = useMutation({
-    ...trpc.events.update.mutationOptions(),
-    onMutate: async (updatedEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events
-            .map((event: any) =>
-              event.id === updatedEvent.eventId
-                ? {
-                    ...event,
-                    title: updatedEvent.title ?? event.title,
-                    description: updatedEvent.description ?? event.description,
-                    start: updatedEvent.start ?? event.start,
-                    end: updatedEvent.end ?? event.end,
-                    allDay: updatedEvent.allDay ?? event.allDay,
-                    location: updatedEvent.location ?? event.location,
-                  }
-                : event
-            )
-            .sort(
-              (a: any, b: any) =>
-                new Date(a.start).getTime() - new Date(b.start).getTime()
+  const { mutate: createEvent, isPending: isCreating } = useMutation(
+    trpc.events.create.mutationOptions({
+      onMutate: async (newEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          const tempEvent: Event = {
+            id: `temp-${Date.now()}`,
+            title: newEvent.title,
+            description: newEvent.description,
+            start: newEvent.start,
+            end: newEvent.end,
+            allDay: newEvent.allDay || false,
+            location: newEvent.location,
+            colorId: "1",
+            status: undefined,
+            htmlLink: undefined,
+          };
+
+          return {
+            ...old,
+            events: [...(old.events || []), tempEvent].sort(
+              (a, b) =>
+                new Date(a.start).getTime() - new Date(b.start).getTime(),
             ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, updatedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: deleteEvent, isPending: isDeleting } = useMutation({
-    ...trpc.events.delete.mutationOptions(),
-    onMutate: async ({ eventId }) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events.filter((event: any) => event.id !== eventId),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, deletedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
+          };
+        });
+
+        return { previousEvents };
+      },
+      onError: (err, _, context) => {
+        // TODO: error message
+
+        if (context?.previousEvents) {
+          queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+        }
+      },
+      onSuccess: () => {},
+      onSettled: () => {
+        queryClient.invalidateQueries({ queryKey: eventsQueryKey });
+      },
+    }),
+  );
+
+  const { mutate: updateEvent, isPending: isUpdating } = useMutation(
+    trpc.events.update.mutationOptions({
+      onMutate: async (updatedEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          return {
+            ...old,
+            events: old.events
+              .map((event) =>
+                event.id === updatedEvent.eventId
+                  ? {
+                      ...event,
+                      title: updatedEvent.title ?? event.title,
+                      description:
+                        updatedEvent.description ?? event.description,
+                      start: updatedEvent.start ?? event.start,
+                      end: updatedEvent.end ?? event.end,
+                      allDay: updatedEvent.allDay ?? event.allDay,
+                      location: updatedEvent.location ?? event.location,
+                    }
+                  : event,
+              )
+              .sort(
+                (a, b) =>
+                  new Date(a.start).getTime() - new Date(b.start).getTime(),
+              ),
+          };
+        });
+
+        return { previousEvents };
+      },
+      onError: (error, _, context) => {
+        // TODO: error message
+
+        if (context?.previousEvents) {
+          queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+        }
+      },
+      onSuccess: () => {},
+      onSettled: () => {
+        queryClient.invalidateQueries({ queryKey: eventsQueryKey });
+      },
+    }),
+  );
+
+  const { mutate: deleteEvent, isPending: isDeleting } = useMutation(
+    trpc.events.delete.mutationOptions({
+      onMutate: async ({ eventId }) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          return {
+            ...old,
+            events: old.events.filter((event) => event.id !== eventId),
+          };
+        });
+
+        return { previousEvents };
+      },
+      onError: (error, _, context) => {
+        // TODO: error message
+
+        if (context?.previousEvents) {
+          queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+        }
+      },
+      onSuccess: () => {},
+      onSettled: () => {
+        queryClient.invalidateQueries({ queryKey: eventsQueryKey });
+      },
+    }),
+  );
+
+  return {
+    events: transformedEvents,
+    createEvent,
+    updateEvent,
+    deleteEvent,
+    isCreating,
+    isUpdating,
+    isDeleting,
+  };
+}
+
+export function CalendarView({ className }: CalendarViewProps) {
+  const { events, createEvent, updateEvent, deleteEvent } =
+    useCalendarActions();
 
   const handleEventAdd = (event: CalendarEvent) => {
     createEvent({
@@ -222,11 +253,11 @@ export function CalendarView({ className }: CalendarViewProps) {
       title: updatedEvent.title,
       start: dateHelpers.formatDateForAPI(
         updatedEvent.start,
-        updatedEvent.allDay || false
+        updatedEvent.allDay || false,
       ),
       end: dateHelpers.formatDateForAPI(
         updatedEvent.end,
-        updatedEvent.allDay || false
+        updatedEvent.allDay || false,
       ),
       allDay: updatedEvent.allDay,
       description: updatedEvent.description,
@@ -243,7 +274,7 @@ export function CalendarView({ className }: CalendarViewProps) {
 
   return (
     <EventCalendar
-      events={transformedEvents}
+      events={events}
       onEventAdd={handleEventAdd}
       onEventUpdate={handleEventUpdate}
       onEventDelete={handleEventDelete}
diff --git a/packages/api/src/providers/google-calendar.ts b/packages/api/src/providers/google-calendar.ts
index 4be1a119..9dc10c30 100644
--- a/packages/api/src/providers/google-calendar.ts
+++ b/packages/api/src/providers/google-calendar.ts
@@ -1,6 +1,7 @@
 import { GoogleCalendar } from "@repo/google-calendar";
 import { dateHelpers } from "../utils/date-helpers";
 import { CALENDAR_DEFAULTS } from "../constants/calendar";
+import type { CreateEventOptions, UpdateEventOptions } from "./interfaces";
 
 interface GoogleCalendarProviderOptions {
   accessToken: string;
@@ -32,7 +33,10 @@ export class GoogleCalendarProvider {
 
   async events(calendarId: string, timeMin?: string, timeMax?: string) {
     const defaultTimeMin = new Date();
-    const defaultTimeMax = new Date(Date.now() + CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000);
+    const defaultTimeMax = new Date(
+      Date.now() +
+        CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000,
+    );
 
     const { items } = await this.client.calendars.events.list(calendarId, {
       timeMin: timeMin || defaultTimeMin.toISOString(),
@@ -45,13 +49,23 @@ export class GoogleCalendarProvider {
     return items?.map((event) => this.transformGoogleEvent(event)) ?? [];
   }
 
-  async createEvent(calendarId: string, params: any) {
-    const googleEvent = await this.client.calendars.events.create(calendarId, params);
+  async createEvent(calendarId: string, params: CreateEventOptions) {
+    const googleEvent = await this.client.calendars.events.create(
+      calendarId,
+      params,
+    );
     return this.transformGoogleEvent(googleEvent);
   }
 
-  async updateEvent(calendarId: string, eventId: string, params: any) {
-    const googleEvent = await this.client.calendars.events.update(eventId, { calendarId, ...params });
+  async updateEvent(
+    calendarId: string,
+    eventId: string,
+    params: UpdateEventOptions,
+  ) {
+    const googleEvent = await this.client.calendars.events.update(eventId, {
+      calendarId,
+      ...params,
+    });
     return this.transformGoogleEvent(googleEvent);
   }
 
@@ -59,10 +73,15 @@ export class GoogleCalendarProvider {
     await this.client.calendars.events.delete(eventId, { calendarId });
   }
 
-  private transformGoogleEvent(googleEvent: any) {
+  private transformGoogleEvent(
+    googleEvent: GoogleCalendar.Calendars.Events.Event,
+  ) {
     const isAllDay = !googleEvent.start?.dateTime;
 
-    const start = dateHelpers.parseGoogleDate(googleEvent.start || {}, isAllDay);
+    const start = dateHelpers.parseGoogleDate(
+      googleEvent.start || {},
+      isAllDay,
+    );
     const end = dateHelpers.parseGoogleDate(googleEvent.end || {}, isAllDay);
 
     return {
diff --git a/packages/api/src/providers/interfaces.ts b/packages/api/src/providers/interfaces.ts
new file mode 100644
index 00000000..31a766fc
--- /dev/null
+++ b/packages/api/src/providers/interfaces.ts
@@ -0,0 +1,11 @@
+import { GoogleCalendar } from "@repo/google-calendar";
+
+export type CreateEventOptions = Omit<
+  GoogleCalendar.Calendars.Events.EventCreateParams,
+  "calendarId"
+>;
+
+export type UpdateEventOptions = Omit<
+  GoogleCalendar.Calendars.Events.EventUpdateParams,
+  "calendarId"
+>;
diff --git a/packages/api/src/routers/events.ts b/packages/api/src/routers/events.ts
index ae32955d..591fba2b 100644
--- a/packages/api/src/routers/events.ts
+++ b/packages/api/src/routers/events.ts
@@ -9,10 +9,10 @@ export const eventsRouter = createTRPCRouter({
   list: protectedProcedure
     .input(
       z.object({
-        calendarIds: z.array(z.string()).optional(),
+        calendarIds: z.array(z.string()).default([]),
         timeMin: z.string().optional(),
         timeMax: z.string().optional(),
-      })
+      }),
     )
     .query(async ({ ctx, input }) => {
       const { accessToken } = await auth.api.getAccessToken({
@@ -31,10 +31,15 @@ export const eventsRouter = createTRPCRouter({
       });
 
       let calendarIds = input.calendarIds;
-      if (!calendarIds || calendarIds.length === 0) {
+
+      if (calendarIds.length === 0) {
         const calendars = await client.calendars();
+
         calendarIds = calendars
-          .filter((cal) => cal.primary || cal.id?.includes("@group.calendar.google.com"))
+          .filter(
+            (cal) =>
+              cal.primary || cal.id?.includes("@group.calendar.google.com"),
+          )
           .map((cal) => cal.id)
           .filter(Boolean);
       }
@@ -42,16 +47,26 @@ export const eventsRouter = createTRPCRouter({
       const allEvents = await Promise.all(
         calendarIds.map(async (calendarId) => {
           try {
-            const events = await client.events(calendarId, input.timeMin, input.timeMax);
-            return events;
+            return await client.events(
+              calendarId,
+              input.timeMin,
+              input.timeMax,
+            );
           } catch (error) {
-            console.error(`Failed to fetch events for calendar ${calendarId}:`, error);
+            console.error(
+              `Failed to fetch events for calendar ${calendarId}:`,
+              error,
+            );
             return [];
           }
-        })
+        }),
       );
 
-      const events = allEvents.flat().sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
+      const events = allEvents
+        .flat()
+        .sort(
+          (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(),
+        );
 
       return { events };
     }),
@@ -66,7 +81,7 @@ export const eventsRouter = createTRPCRouter({
         allDay: z.boolean().optional(),
         description: z.string().optional(),
         location: z.string().optional(),
-      })
+      }),
     )
     .mutation(async ({ ctx, input }) => {
       const { accessToken } = await auth.api.getAccessToken({
@@ -100,7 +115,7 @@ export const eventsRouter = createTRPCRouter({
         allDay: z.boolean().optional(),
         description: z.string().optional(),
         location: z.string().optional(),
-      })
+      }),
     )
     .mutation(async ({ ctx, input }) => {
       const { accessToken } = await auth.api.getAccessToken({
@@ -119,7 +134,11 @@ export const eventsRouter = createTRPCRouter({
       });
 
       const googleParams = dateHelpers.prepareGoogleParams(input);
-      const event = await client.updateEvent(input.calendarId, input.eventId, googleParams);
+      const event = await client.updateEvent(
+        input.calendarId,
+        input.eventId,
+        googleParams,
+      );
 
       return { event };
     }),
@@ -129,7 +148,7 @@ export const eventsRouter = createTRPCRouter({
       z.object({
         calendarId: z.string(),
         eventId: z.string(),
-      })
+      }),
     )
     .mutation(async ({ ctx, input }) => {
       const { accessToken } = await auth.api.getAccessToken({


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>apps/web/src/components/calendar-view.tsx (3)</summary><blockquote>

`45-54`: **Compute time range dynamically to avoid stale data**  
`timeMin` / `timeMax` are memo-ised once with an empty dependency array, so if the component stays mounted overnight the query window will not roll forward and users will miss new events. Consider recalculating these values daily (or on each mount) by adding `Date.now()` (or a simple `new Date()` value) to the dependency array, or by wrapping the hook in a timer.

---

`124-135`: **Surface mutation errors to the UI**  
`onError` currently contains a `// TODO` placeholder. Silent failures make debugging hard for users. A minimal improvement is to trigger a toast or log the error to Sentry (or both).  
```diff
- // TODO: error message
+ import { toast } from "@/lib/toast";
+ ...
+ onError: (err, _, context) => {
+   toast.error("Failed to create event, reverting…");
+   if (context?.previousEvents) {
+     queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+   }
+ }
```

---

`222-231`: **Leverage the is* flags for better UX**  
`useCalendarActions` already exposes `isCreating`, `isUpdating` and `isDeleting`, but `CalendarView` doesn’t consume them. Forwarding these flags to `EventCalendar` (or a surrounding spinner) would let the UI give feedback while mutations are in flight.

</blockquote></details>
<details>
<summary>packages/api/src/routers/events.ts (2)</summary><blockquote>

`33-44`: **Minor: avoid mutating `calendarIds` variable**  
`calendarIds` is reassigned after the fallback fetch. While harmless, using a new constant (e.g. `resolvedCalendarIds`) would make the immutable intent clearer and avoid accidental side-effects later.

---

`65-69`: **Potentially large concurrent requests**  
`Promise.all` fires a request per calendar without concurrency limits. If a user owns dozens of calendars this could hit provider rate limits. Consider `p-limit` or batching requests.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 06be1582d5f0019210be63ba91cbfd89eb127fd9 and c0e86ddf7ba665226732fd72e1690112ae130a42.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/web/src/components/calendar-view.tsx` (6 hunks)
* `packages/api/src/providers/google-calendar.ts` (2 hunks)
* `packages/api/src/routers/events.ts` (6 hunks)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>packages/api/src/providers/google-calendar.ts (2)</summary>

`51-62`: **Type-safe refactor looks good**  
Switching from `any` to the explicit `Omit<...EventCreateParams, "calendarId">` greatly reduces the chance of passing invalid fields. Nice!

---

`73-77`: **Verify Google client argument order**  
`events.update` is called with `(eventId, { calendarId, ...params })`, whereas `events.create` is `(calendarId, params)`. If the underlying SDK expects `(calendarId, eventId, params)` (many do), this could 404. Please double-check the SDK signature or add a unit test.

</details>
<details>
<summary>packages/api/src/routers/events.ts (1)</summary>

`12-16`: **Defaulting `calendarIds` removes unnecessary null checks – good improvement**  
The schema change simplifies the downstream logic and makes the intent clearer.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-26T13:41:34Z
user: login: a-rebets
body: Good job! Some trivial requests from my side
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T17:30:47Z
user: login: russssl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T19:36:41Z
user: login: JeanMeijer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T12:38:20Z
user: login: JeanMeijer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T12:39:36Z
user: login: russssl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T12:40:22Z

Review comments:
diff_hunk: @@ -32,7 +32,10 @@ export class GoogleCalendarProvider {
 
   async events(calendarId: string, timeMin?: string, timeMax?: string) {
     const defaultTimeMin = new Date();
-    const defaultTimeMax = new Date(Date.now() + CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000);
+    const defaultTimeMax = new Date(
+      Date.now() +
+        CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000,
+    );
path: packages/api/src/providers/google-calendar.ts
user: login: a-rebets
body: I think if we have `date-fns` in the project this can be simplified to `addDays(new Date(), CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE);`

It's better to avoid manual time math in the code
created_at: 2025-05-26T17:10:26Z
author_association: COLLABORATOR
diff_hunk: @@ -45,24 +48,48 @@ export class GoogleCalendarProvider {
     return items?.map((event) => this.transformGoogleEvent(event)) ?? [];
   }
 
-  async createEvent(calendarId: string, params: any) {
-    const googleEvent = await this.client.calendars.events.create(calendarId, params);
+  async createEvent(
+    calendarId: string,
+    params: Omit<
+      GoogleCalendar.Calendars.Events.EventCreateParams,
+      "calendarId"
+    >,
path: packages/api/src/providers/google-calendar.ts
user: login: a-rebets
body: Make this a type at the start of the file or in a separate `types` file, I see this can be reused
created_at: 2025-05-26T17:11:31Z
author_association: COLLABORATOR
diff_hunk: @@ -86,122 +89,150 @@ export function CalendarView({ className }: CalendarViewProps) {
     });
   }, [data]);
 
-  const { mutate: createEvent, isPending: isCreating } = useMutation({
-    ...trpc.events.create.mutationOptions(),
-    onMutate: async (newEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        const tempEvent = {
-          id: `temp-${Date.now()}`,
-          title: newEvent.title,
-          description: newEvent.description,
-          start: newEvent.start,
-          end: newEvent.end,
-          allDay: newEvent.allDay || false,
-          location: newEvent.location,
-          colorId: "1",
-        };
-
-        return {
-          ...old,
-          events: [...(old.events || []), tempEvent].sort(
-            (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
-          ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, newEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: updateEvent, isPending: isUpdating } = useMutation({
-    ...trpc.events.update.mutationOptions(),
-    onMutate: async (updatedEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events
-            .map((event: any) =>
-              event.id === updatedEvent.eventId
-                ? {
-                    ...event,
-                    title: updatedEvent.title ?? event.title,
-                    description: updatedEvent.description ?? event.description,
-                    start: updatedEvent.start ?? event.start,
-                    end: updatedEvent.end ?? event.end,
-                    allDay: updatedEvent.allDay ?? event.allDay,
-                    location: updatedEvent.location ?? event.location,
-                  }
-                : event
-            )
-            .sort(
-              (a: any, b: any) =>
-                new Date(a.start).getTime() - new Date(b.start).getTime()
+  const { mutate: createEvent, isPending: isCreating } = useMutation(
+    trpc.events.create.mutationOptions({
+      onMutate: async (newEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          const tempEvent: Event = {
+            id: `temp-${Date.now()}`,
+            title: newEvent.title,
+            description: newEvent.description,
+            start: newEvent.start,
+            end: newEvent.end,
+            allDay: newEvent.allDay || false,
+            location: newEvent.location,
+            colorId: "1",
+            status: undefined,
+            htmlLink: undefined,
+          };
+
+          return {
+            ...old,
+            events: [...(old.events || []), tempEvent].sort(
+              (a, b) =>
+                new Date(a.start).getTime() - new Date(b.start).getTime(),
             ),
path: apps/web/src/components/calendar-view.tsx
user: login: a-rebets
body: this part is hard to comprehend / prune to introduction of bugs. `date-fns` can help with dates and the array concatenation / object destructuring can be replaced by simpler expressions
created_at: 2025-05-26T17:14:35Z
author_association: COLLABORATOR
diff_hunk: @@ -86,122 +89,150 @@ export function CalendarView({ className }: CalendarViewProps) {
     });
   }, [data]);
 
-  const { mutate: createEvent, isPending: isCreating } = useMutation({
-    ...trpc.events.create.mutationOptions(),
-    onMutate: async (newEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        const tempEvent = {
-          id: `temp-${Date.now()}`,
-          title: newEvent.title,
-          description: newEvent.description,
-          start: newEvent.start,
-          end: newEvent.end,
-          allDay: newEvent.allDay || false,
-          location: newEvent.location,
-          colorId: "1",
-        };
-
-        return {
-          ...old,
-          events: [...(old.events || []), tempEvent].sort(
-            (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
-          ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, newEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: updateEvent, isPending: isUpdating } = useMutation({
-    ...trpc.events.update.mutationOptions(),
-    onMutate: async (updatedEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events
-            .map((event: any) =>
-              event.id === updatedEvent.eventId
-                ? {
-                    ...event,
-                    title: updatedEvent.title ?? event.title,
-                    description: updatedEvent.description ?? event.description,
-                    start: updatedEvent.start ?? event.start,
-                    end: updatedEvent.end ?? event.end,
-                    allDay: updatedEvent.allDay ?? event.allDay,
-                    location: updatedEvent.location ?? event.location,
-                  }
-                : event
-            )
-            .sort(
-              (a: any, b: any) =>
-                new Date(a.start).getTime() - new Date(b.start).getTime()
+  const { mutate: createEvent, isPending: isCreating } = useMutation(
+    trpc.events.create.mutationOptions({
+      onMutate: async (newEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          const tempEvent: Event = {
+            id: `temp-${Date.now()}`,
+            title: newEvent.title,
+            description: newEvent.description,
+            start: newEvent.start,
+            end: newEvent.end,
+            allDay: newEvent.allDay || false,
+            location: newEvent.location,
+            colorId: "1",
+            status: undefined,
+            htmlLink: undefined,
+          };
+
+          return {
+            ...old,
+            events: [...(old.events || []), tempEvent].sort(
+              (a, b) =>
+                new Date(a.start).getTime() - new Date(b.start).getTime(),
             ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, updatedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: deleteEvent, isPending: isDeleting } = useMutation({
-    ...trpc.events.delete.mutationOptions(),
-    onMutate: async ({ eventId }) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events.filter((event: any) => event.id !== eventId),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, deletedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
+          };
+        });
+
+        return { previousEvents };
+      },
+      onError: (err, _, context) => {
+        // TODO: error message
+
+        if (context?.previousEvents) {
+          queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+        }
+      },
+      onSuccess: () => {},
+      onSettled: () => {
+        queryClient.invalidateQueries({ queryKey: eventsQueryKey });
+      },
+    }),
+  );
+
+  const { mutate: updateEvent, isPending: isUpdating } = useMutation(
+    trpc.events.update.mutationOptions({
+      onMutate: async (updatedEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          return {
+            ...old,
+            events: old.events
+              .map((event) =>
+                event.id === updatedEvent.eventId
+                  ? {
+                      ...event,
+                      title: updatedEvent.title ?? event.title,
+                      description:
+                        updatedEvent.description ?? event.description,
+                      start: updatedEvent.start ?? event.start,
+                      end: updatedEvent.end ?? event.end,
+                      allDay: updatedEvent.allDay ?? event.allDay,
+                      location: updatedEvent.location ?? event.location,
+                    }
+                  : event,
+              )
+              .sort(
+                (a, b) =>
+                  new Date(a.start).getTime() - new Date(b.start).getTime(),
+              ),
+          };
+        });
path: apps/web/src/components/calendar-view.tsx
user: login: a-rebets
body: again here we have a destructured object with two chained lambdas and a ternary with a big object. this can be written much simpler. like:
```ts
// Helper function to apply updates to a single event
const applyEventUpdates = (event: Event): Event => {
    if (event.id !== updatedEventInput.eventId) {
        return event;
    }
    return {
        ...event,
        title: updatedEventInput.title ?? event.title,
        description: updatedEventInput.description ?? event.description,
        start: updatedEventInput.start ?? event.start,
        end: updatedEventInput.end ?? event.end,
        allDay: updatedEventInput.allDay ?? event.allDay,
        location: updatedEventInput.location ?? event.location,
    };
};
```
and
```ts
const sortPredicate = (a, b) => {
    // isBefore from date-fns
    const dateA = new Date(a.start);
    const dateB = new Date(b.start);
    if (isBefore(dateA, dateB)) return -1;
    if (isBefore(dateB, dateA)) return 1;
    return 0;
}
```

allows you to do
```ts
{
    ...old,
    events: old.events.map(applyEventUpdates)
             .sort(sortPredicate),
};
```
created_at: 2025-05-26T17:26:23Z
author_association: COLLABORATOR
diff_hunk: @@ -32,7 +32,10 @@ export class GoogleCalendarProvider {
 
   async events(calendarId: string, timeMin?: string, timeMax?: string) {
     const defaultTimeMin = new Date();
-    const defaultTimeMax = new Date(Date.now() + CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000);
+    const defaultTimeMax = new Date(
+      Date.now() +
+        CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000,
+    );
path: packages/api/src/providers/google-calendar.ts
user: login: russssl
body: wouldnt it be better to use luxon ? imo more readable and flexible
created_at: 2025-05-26T18:32:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,7 +32,10 @@ export class GoogleCalendarProvider {
 
   async events(calendarId: string, timeMin?: string, timeMax?: string) {
     const defaultTimeMin = new Date();
-    const defaultTimeMax = new Date(Date.now() + CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000);
+    const defaultTimeMax = new Date(
+      Date.now() +
+        CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000,
+    );
path: packages/api/src/providers/google-calendar.ts
user: login: JeanMeijer
body: I'll do this in another pr. 

@russssl I don't know `luxon`, `date-fns` was already used in the event calendar component. Although, I personally prefer `@formkit/tempo` and `@internationalized/date`. We'll have to see, still annoyed the Temporal API has been taking so long.
created_at: 2025-05-27T12:38:20Z
author_association: COLLABORATOR
diff_hunk: @@ -86,122 +89,150 @@ export function CalendarView({ className }: CalendarViewProps) {
     });
   }, [data]);
 
-  const { mutate: createEvent, isPending: isCreating } = useMutation({
-    ...trpc.events.create.mutationOptions(),
-    onMutate: async (newEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        const tempEvent = {
-          id: `temp-${Date.now()}`,
-          title: newEvent.title,
-          description: newEvent.description,
-          start: newEvent.start,
-          end: newEvent.end,
-          allDay: newEvent.allDay || false,
-          location: newEvent.location,
-          colorId: "1",
-        };
-
-        return {
-          ...old,
-          events: [...(old.events || []), tempEvent].sort(
-            (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
-          ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, newEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: updateEvent, isPending: isUpdating } = useMutation({
-    ...trpc.events.update.mutationOptions(),
-    onMutate: async (updatedEvent) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events
-            .map((event: any) =>
-              event.id === updatedEvent.eventId
-                ? {
-                    ...event,
-                    title: updatedEvent.title ?? event.title,
-                    description: updatedEvent.description ?? event.description,
-                    start: updatedEvent.start ?? event.start,
-                    end: updatedEvent.end ?? event.end,
-                    allDay: updatedEvent.allDay ?? event.allDay,
-                    location: updatedEvent.location ?? event.location,
-                  }
-                : event
-            )
-            .sort(
-              (a: any, b: any) =>
-                new Date(a.start).getTime() - new Date(b.start).getTime()
+  const { mutate: createEvent, isPending: isCreating } = useMutation(
+    trpc.events.create.mutationOptions({
+      onMutate: async (newEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          const tempEvent: Event = {
+            id: `temp-${Date.now()}`,
+            title: newEvent.title,
+            description: newEvent.description,
+            start: newEvent.start,
+            end: newEvent.end,
+            allDay: newEvent.allDay || false,
+            location: newEvent.location,
+            colorId: "1",
+            status: undefined,
+            htmlLink: undefined,
+          };
+
+          return {
+            ...old,
+            events: [...(old.events || []), tempEvent].sort(
+              (a, b) =>
+                new Date(a.start).getTime() - new Date(b.start).getTime(),
             ),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, updatedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
-
-  const { mutate: deleteEvent, isPending: isDeleting } = useMutation({
-    ...trpc.events.delete.mutationOptions(),
-    onMutate: async ({ eventId }) => {
-      await queryClient.cancelQueries({ queryKey: eventsQueryKey });
-
-      const previousEvents = queryClient.getQueryData(eventsQueryKey);
-
-      queryClient.setQueryData(eventsQueryKey, (old: any) => {
-        if (!old) return old;
-
-        return {
-          ...old,
-          events: old.events.filter((event: any) => event.id !== eventId),
-        };
-      });
-
-      return { previousEvents };
-    },
-    onError: (err, deletedEvent, context) => {
-      if (context?.previousEvents) {
-        queryClient.setQueryData(eventsQueryKey, context.previousEvents);
-      }
-    },
-    onSuccess: () => {},
-    onSettled: () => {
-      queryClient.invalidateQueries({ queryKey: eventsQueryKey });
-    },
-  });
+          };
+        });
+
+        return { previousEvents };
+      },
+      onError: (err, _, context) => {
+        // TODO: error message
+
+        if (context?.previousEvents) {
+          queryClient.setQueryData(eventsQueryKey, context.previousEvents);
+        }
+      },
+      onSuccess: () => {},
+      onSettled: () => {
+        queryClient.invalidateQueries({ queryKey: eventsQueryKey });
+      },
+    }),
+  );
+
+  const { mutate: updateEvent, isPending: isUpdating } = useMutation(
+    trpc.events.update.mutationOptions({
+      onMutate: async (updatedEvent) => {
+        await queryClient.cancelQueries({ queryKey: eventsQueryKey });
+
+        const previousEvents = queryClient.getQueryData(eventsQueryKey);
+
+        queryClient.setQueryData(eventsQueryKey, (old) => {
+          if (!old) return old;
+
+          return {
+            ...old,
+            events: old.events
+              .map((event) =>
+                event.id === updatedEvent.eventId
+                  ? {
+                      ...event,
+                      title: updatedEvent.title ?? event.title,
+                      description:
+                        updatedEvent.description ?? event.description,
+                      start: updatedEvent.start ?? event.start,
+                      end: updatedEvent.end ?? event.end,
+                      allDay: updatedEvent.allDay ?? event.allDay,
+                      location: updatedEvent.location ?? event.location,
+                    }
+                  : event,
+              )
+              .sort(
+                (a, b) =>
+                  new Date(a.start).getTime() - new Date(b.start).getTime(),
+              ),
+          };
+        });
path: apps/web/src/components/calendar-view.tsx
user: login: JeanMeijer
body: This was existing code. Can be done in separate PR.
created_at: 2025-05-27T12:39:36Z
author_association: COLLABORATOR
diff_hunk: @@ -32,7 +32,10 @@ export class GoogleCalendarProvider {
 
   async events(calendarId: string, timeMin?: string, timeMax?: string) {
     const defaultTimeMin = new Date();
-    const defaultTimeMax = new Date(Date.now() + CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000);
+    const defaultTimeMax = new Date(
+      Date.now() +
+        CALENDAR_DEFAULTS.TIME_RANGE_DAYS_FUTURE * 24 * 60 * 60 * 1000,
+    );
path: packages/api/src/providers/google-calendar.ts
user: login: russssl
body: @JeanMeijer honestly after working with luxon for ~2 years I can say it is amazing, although the decision is yours
created_at: 2025-05-27T12:40:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-25T22:49:41Z
body: [vc]: #dF4+RW/L9P2uS4zwJcS4RsVjWDDq3PaDppvonGEXxZ8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhbmFsb2ciLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYW5hbG9nLWdpdC1jaG9yZS1pbXByb3ZlLXR5cGUtc2FmZXR5LWMtMWJkNzhmLWFuYWxvZ2ludGVyZmFjZWlvLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FuYWxvZ2ludGVyZmFjZWlvL2FuYWxvZy9IaDl0aVc4aDRxVGh3UWtWZXQ3WFZxZ3JoUmRtIiwicHJldmlld1VybCI6ImFuYWxvZy1naXQtY2hvcmUtaW1wcm92ZS10eXBlLXNhZmV0eS1jLTFiZDc4Zi1hbmFsb2dpbnRlcmZhY2Vpby52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd2ViIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **analog** | ✅ Ready ([Inspect](https://vercel.com/analoginterfaceio/analog/Hh9tiW8h4qThwQkVet7XVqgrhRdm)) | [Visit Preview](https://analog-git-chore-improve-type-safety-c-1bd78f-analoginterfaceio.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/analog-git-chore-improve-type-safety-c-1bd78f-analoginterfaceio.vercel.app?via=pr-comment-feedback-link) | May 27, 2025 0:46am |


author_association: NONE
user: login: JeanMeijer
created_at: 2025-05-26T13:39:50Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-26T13:39:55Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-26T13:39:58Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes refactor calendar event management by introducing a custom React hook for event mutations, improving modularity in the frontend. On the backend, type safety is enhanced for Google Calendar provider methods, and input schemas are updated to use default array values, simplifying logic and improving code clarity without altering core functionalities.

## Changes

| Files/Paths                                      | Change Summary                                                                                                                      |
|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| apps/web/src/components/calendar-view.tsx        | Refactored to use a custom `useCalendarActions` hook for event create, update, and delete mutations with optimistic updates.         |
| packages/api/src/providers/google-calendar.ts    | Refined method signatures for `createEvent`, `updateEvent`, and `transformGoogleEvent` to use explicit, stricter types.             |
| packages/api/src/routers/events.ts               | Updated input schema for `list` to default `calendarIds` to an empty array; simplified related logic and improved code formatting.  |

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=analogdotnow/Analog&utm_content=54):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE