Skip to content
Open

wip #446

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
41 changes: 41 additions & 0 deletions apps/web/src/atoms/selected-display-items.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { atom } from "jotai";

export const selectedDisplayItemIdsAtom = atom<string[]>([]);

export const isDisplayItemSelected = (displayItemId: string) =>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This function creates a new atom instance on every call. When used in components, this causes a new atom to be created on each render, leading to unnecessary re-subscriptions and potential performance issues.

Consider using atomFamily from jotai/utils which memoizes atoms by their parameters:

import { atomFamily } from "jotai/utils";

export const isDisplayItemSelected = atomFamily((displayItemId: string) =>
  atom((get) => get(selectedDisplayItemIdsAtom).includes(displayItemId))
);
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/atoms/selected-display-items.ts, line 5:

<comment>This function creates a new atom instance on every call. When used in components, this causes a new atom to be created on each render, leading to unnecessary re-subscriptions and potential performance issues.

Consider using `atomFamily` from `jotai/utils` which memoizes atoms by their parameters:

```ts
import { atomFamily } from "jotai/utils";

export const isDisplayItemSelected = atomFamily((displayItemId: string) =>
  atom((get) => get(selectedDisplayItemIdsAtom).includes(displayItemId))
);
```</comment>

<file context>
@@ -0,0 +1,35 @@
+
+export const selectedDisplayItemIdsAtom = atom<string[]>([]);
+
+export const isDisplayItemSelected = (displayItemId: string) =>
+  atom((get) => {
+    const items = get(selectedDisplayItemIdsAtom);
</file context>
Fix with Cubic

atom((get) => {
const items = get(selectedDisplayItemIdsAtom);

return items.includes(displayItemId);
});

export const isEventSelected = (eventId: string) =>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Same issue as above - this creates a new atom instance on every call. Consider using atomFamily:

export const isEventSelected = atomFamily((eventId: string) =>
  atom((get) => get(selectedDisplayItemIdsAtom).includes(`event_${eventId}`))
);
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/atoms/selected-display-items.ts, line 12:

<comment>Same issue as above - this creates a new atom instance on every call. Consider using `atomFamily`:

```ts
export const isEventSelected = atomFamily((eventId: string) =>
  atom((get) => get(selectedDisplayItemIdsAtom).includes(`event_${eventId}`))
);
```</comment>

<file context>
@@ -0,0 +1,35 @@
+    return items.includes(displayItemId);
+  });
+
+export const isEventSelected = (eventId: string) =>
+  atom((get) => {
+    const items = get(selectedDisplayItemIdsAtom);
</file context>
Fix with Cubic

atom((get) => {
const items = get(selectedDisplayItemIdsAtom);

return items.includes(`event_${eventId}`);
});

export const selectedEventIdsAtom = atom(
(get) => {
return get(selectedDisplayItemIdsAtom)
.filter((id) => id.startsWith("event_"))
.map((id) => id.slice(6));
},
(get, set, update: string[] | ((prev: string[]) => string[])) => {
const prev = get(selectedDisplayItemIdsAtom)
.filter((id) => id.startsWith("event_"))
.map((id) => id.slice(6));

const eventIds = typeof update === "function" ? update(prev) : update;

const otherItems = get(selectedDisplayItemIdsAtom).filter(
(id) => !id.startsWith("event_"),
);

set(selectedDisplayItemIdsAtom, [
...otherItems,
...eventIds.map((id) => `event_${id}`),
]);
},
);
12 changes: 0 additions & 12 deletions apps/web/src/atoms/selected-events.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/web/src/atoms/window-stack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { atom } from "jotai";

import { StackWindowEntry } from "@/components/command-bar/stacked-window";
import { selectedEventIdsAtom } from "./selected-events";
import { selectedEventIdsAtom } from "./selected-display-items";

function createWindowId() {
return `window-${crypto.randomUUID()}`;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/atoms/window-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { atom } from "jotai";

import { selectedEventIdsAtom } from "@/atoms/selected-events";
import { selectedEventIdsAtom } from "@/atoms/selected-display-items";

export const windowStateAtom = atom<"default" | "expanded">((get) => {
const events = get(selectedEventIdsAtom);
Expand Down
33 changes: 20 additions & 13 deletions apps/web/src/components/calendar-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { CalendarHeader } from "@/components/calendar/header/calendar-header";
import { MonthView } from "@/components/calendar/month-view/month-view";
import { WeekView } from "@/components/calendar/week-view/week-view";
import { db, mapEventQueryInput } from "@/lib/db";
import { DisplayItem, isEvent } from "@/lib/display-item";
import { cn } from "@/lib/utils";
import { applyOptimisticActions } from "./calendar/hooks/apply-optimistic-actions";
import { optimisticActionsByEventIdAtom } from "./calendar/hooks/optimistic-actions";
import { useEventsForDisplay } from "./calendar/hooks/use-events";
import { filterPastEvents } from "./calendar/utils/event";
import { filterPastItems } from "./calendar/utils/event";

interface CalendarContentProps {
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
Expand Down Expand Up @@ -54,22 +55,26 @@ function CalendarContent({ scrollContainerRef }: CalendarContentProps) {
);
}, [data?.events, data?.recurringMasterEvents]);

const events = React.useMemo(() => {
const events = applyOptimisticActions({
const displayItems = React.useMemo(() => {
const eventItems = applyOptimisticActions({
items: data?.events ?? [],
timeZone: defaultTimeZone,
optimisticActions,
});

const pastFiltered = showPastEvents
? events
: filterPastEvents(events, defaultTimeZone);
const pastFiltered: DisplayItem[] = showPastEvents
? eventItems
: filterPastItems(eventItems, defaultTimeZone);

return pastFiltered.filter((item) => {
if (!isEvent(item)) {
return true;
}

return pastFiltered.filter((eventItem) => {
const preference = getCalendarPreference(
calendarPreferences,
eventItem.event.calendar.provider.accountId,
eventItem.event.calendar.id,
item.event.calendar.provider.accountId,
item.event.calendar.id,
);

return !(preference?.hidden === true);
Expand All @@ -83,24 +88,26 @@ function CalendarContent({ scrollContainerRef }: CalendarContentProps) {
]);

if (view === "month") {
return <MonthView currentDate={currentDate} events={events} />;
return <MonthView currentDate={currentDate} items={displayItems} />;
}

if (view === "week") {
return <WeekView events={events} scrollContainerRef={scrollContainerRef} />;
return (
<WeekView items={displayItems} scrollContainerRef={scrollContainerRef} />
);
}

if (view === "day") {
return (
<DayView
currentDate={currentDate}
events={events}
items={displayItems}
scrollContainerRef={scrollContainerRef}
/>
);
}

return <AgendaView currentDate={currentDate} items={events} />;
return <AgendaView currentDate={currentDate} items={displayItems} />;
}

interface CalendarViewProps {
Expand Down
20 changes: 11 additions & 9 deletions apps/web/src/components/calendar/agenda-view/agenda-view-day.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Temporal } from "temporal-polyfill";
import { isToday } from "@repo/temporal";

import { calendarSettingsAtom } from "@/atoms/calendar-settings";
import { eventOverlapsDay } from "@/components/calendar/utils/positioning";
import { EventCollectionItem } from "../hooks/event-collection";
import { AgendaViewEvent } from "./agenda-view-event";
import { displayItemOverlapsDay } from "@/components/calendar/utils/positioning";
import { DisplayItem, isInlineItem } from "@/lib/display-item";
import { AgendaViewItem } from "./agenda-view-event";

interface AgendaViewDayHeaderProps {
day: Temporal.PlainDate;
Expand Down Expand Up @@ -51,24 +51,26 @@ function AgendaViewDayContainer({ children }: AgendaViewDayContainerProps) {

interface AgendaViewDayProps {
day: Temporal.PlainDate;
items: EventCollectionItem[];
items: DisplayItem[];
}

export function AgendaViewDay({ day, items }: AgendaViewDayProps) {
const events = React.useMemo(() => {
return items.filter((item) => eventOverlapsDay(item, day));
const dayItems = React.useMemo(() => {
return items.filter(
(item) => isInlineItem(item) && displayItemOverlapsDay(item, day),
);
}, [day, items]);

if (events.length === 0) {
if (dayItems.length === 0) {
return null;
}

return (
<AgendaViewDayContainer>
<AgendaViewDayHeader day={day} />
<AgendaViewDayContent>
{events.map((event) => (
<AgendaViewEvent key={event.event.id} item={event} />
{dayItems.map((item) => (
<AgendaViewItem key={item.id} item={item} />
))}
</AgendaViewDayContent>
</AgendaViewDayContainer>
Expand Down
37 changes: 28 additions & 9 deletions apps/web/src/components/calendar/agenda-view/agenda-view-event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,47 @@

import * as React from "react";

import { EventItem } from "@/components/calendar/event/event-item";
import type { EventCollectionItem } from "@/components/calendar/hooks/event-collection";
import { DisplayItemComponent } from "@/components/calendar/display-item/display-item";
import { useSelectAction } from "@/components/calendar/hooks/use-optimistic-mutations";
import {
DisplayItem,
InlineDisplayItem,
isEvent,
isInlineItem,
} from "@/lib/display-item";

interface AgendaViewEventProps {
item: EventCollectionItem;
interface AgendaViewItemProps {
item: DisplayItem;
}

export function AgendaViewEvent({ item }: AgendaViewEventProps) {
export function AgendaViewItem({ item }: AgendaViewItemProps) {
if (!isInlineItem(item)) {
return null;
}

return <AgendaViewInlineItem item={item} />;
}

interface AgendaViewInlineItemProps {
item: InlineDisplayItem;
}

function AgendaViewInlineItem({ item }: AgendaViewInlineItemProps) {
const selectAction = useSelectAction();

const onClick = React.useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
selectAction(item.event);
if (isEvent(item)) {
selectAction(item.event);
}
},
[selectAction, item.event],
[selectAction, item],
);

return (
<EventItem
key={item.event.id}
<DisplayItemComponent
key={item.id}
item={item}
view="agenda"
onClick={onClick}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/calendar/agenda-view/agenda-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RiCalendarEventLine } from "@remixicon/react";
import { Temporal } from "temporal-polyfill";

import { AgendaDaysToShow } from "@/components/calendar/constants";
import type { EventCollectionItem } from "@/components/calendar/hooks/event-collection";
import type { DisplayItem } from "@/lib/display-item";
import { AgendaViewDay } from "./agenda-view-day";

function AgendaViewEmpty() {
Expand All @@ -15,17 +15,17 @@ function AgendaViewEmpty() {
size={32}
className="mb-2 text-muted-foreground/50"
/>
<h3 className="text-lg font-medium">No events found</h3>
<h3 className="text-lg font-medium">No items found</h3>
<p className="text-muted-foreground">
There are no events scheduled for this time period.
There are no items scheduled for this time period.
</p>
</div>
);
}

interface AgendaViewProps {
currentDate: Temporal.PlainDate;
items: EventCollectionItem[];
items: DisplayItem[];
}

export function AgendaView({ currentDate, items }: AgendaViewProps) {
Expand Down
Loading