Skip to content

Commit 1917fe8

Browse files
committed
status component in calendar tab (#2500)
1 parent 504c91b commit 1917fe8

File tree

8 files changed

+271
-253
lines changed

8 files changed

+271
-253
lines changed

apps/desktop/src/components/main/sidebar/timeline/index.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
import { useAnchor, useAutoScrollToAnchor } from "./anchor";
1818
import { TimelineItemComponent } from "./item";
1919
import { CurrentTimeIndicator, useCurrentTimeMs } from "./realtime";
20-
import { RefetchButton } from "./refetch";
2120

2221
export function TimelineView() {
2322
const buckets = useTimelineData();
@@ -105,18 +104,13 @@ export function TimelineView() {
105104
<CurrentTimeIndicator ref={setCurrentTimeIndicatorRef} />
106105
)}
107106
<div
108-
className={cn(["sticky top-0 z-10", "bg-neutral-50 px-2 py-1"])}
107+
className={cn([
108+
"sticky top-0 z-10",
109+
"bg-neutral-50 pl-3 pr-1 py-1",
110+
])}
109111
>
110-
<div
111-
className={cn([
112-
"flex items-center",
113-
isToday && "justify-between",
114-
])}
115-
>
116-
<div className="text-base font-bold text-neutral-900">
117-
{bucket.label}
118-
</div>
119-
{isToday && <RefetchButton />}
112+
<div className="text-base font-bold text-neutral-900">
113+
{bucket.label}
120114
</div>
121115
</div>
122116
{isToday ? (
Lines changed: 159 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { memo, useCallback, useMemo } from "react";
22

33
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
4+
import {
5+
Tooltip,
6+
TooltipContent,
7+
TooltipTrigger,
8+
} from "@hypr/ui/components/ui/tooltip";
49
import { cn } from "@hypr/utils";
510

611
import * as main from "../../../../store/tinybase/main";
@@ -22,38 +27,136 @@ export const TimelineItemComponent = memo(
2227
precision: TimelinePrecision;
2328
selected: boolean;
2429
}) => {
25-
const openCurrent = useTabs((state) => state.openCurrent);
26-
const openNew = useTabs((state) => state.openNew);
27-
const invalidateResource = useTabs((state) => state.invalidateResource);
28-
2930
const store = main.UI.useStore(main.STORE_ID);
3031

3132
const eventId =
3233
item.type === "event" ? item.id : item.data.event_id || undefined;
33-
3434
const title = item.data.title || "Untitled";
3535
const timestamp =
3636
item.type === "event" ? item.data.started_at : item.data.created_at;
3737

38-
const handleClick = () => {
39-
if (item.type === "event") {
40-
handleEventClick(false);
41-
} else {
42-
const tab: TabInput = { id: item.id, type: "sessions" };
43-
openCurrent(tab);
38+
const calendarId = useMemo(() => {
39+
if (!store || !eventId) {
40+
return null;
4441
}
45-
};
46-
47-
const handleCmdClick = useCallback(() => {
4842
if (item.type === "event") {
49-
handleEventClick(true);
50-
} else {
51-
const tab: TabInput = { id: item.id, type: "sessions" };
52-
openNew(tab);
43+
return item.data.calendar_id ?? null;
44+
}
45+
if (item.data.event_id) {
46+
const event = store.getRow("events", item.data.event_id);
47+
return event?.calendar_id ? String(event.calendar_id) : null;
5348
}
54-
}, [item, openNew]);
49+
return null;
50+
}, [store, eventId, item]);
51+
52+
const displayTime = useMemo(
53+
() => formatDisplayTime(timestamp, precision),
54+
[timestamp, precision],
55+
);
5556

56-
const handleEventClick = (openInNewTab: boolean) => {
57+
const { handleClick, handleCmdClick, handleDelete } =
58+
useTimelineItemActions(item, store, eventId, title);
59+
60+
const contextMenu = useMemo(
61+
() => [
62+
{ id: "open-new-tab", text: "Open in New Tab", action: handleCmdClick },
63+
{ id: "delete", text: "Delete Completely", action: handleDelete },
64+
],
65+
[handleCmdClick, handleDelete],
66+
);
67+
68+
return (
69+
<InteractiveButton
70+
onClick={handleClick}
71+
onCmdClick={handleCmdClick}
72+
contextMenu={contextMenu}
73+
className={cn([
74+
"w-full text-left px-3 py-2 rounded-lg",
75+
selected && "bg-neutral-200",
76+
!selected && "hover:bg-neutral-100",
77+
])}
78+
>
79+
<div className="flex items-center gap-2">
80+
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
81+
<div className="text-sm font-normal truncate">{title}</div>
82+
{displayTime && (
83+
<div className="text-xs text-neutral-500">{displayTime}</div>
84+
)}
85+
</div>
86+
{calendarId && <CalendarIndicator calendarId={calendarId} />}
87+
</div>
88+
</InteractiveButton>
89+
);
90+
},
91+
);
92+
93+
function formatDisplayTime(
94+
timestamp: string | null | undefined,
95+
precision: TimelinePrecision,
96+
): string {
97+
if (!timestamp) {
98+
return "";
99+
}
100+
101+
const date = new Date(timestamp);
102+
if (Number.isNaN(date.getTime())) {
103+
return "";
104+
}
105+
106+
const time = date.toLocaleTimeString([], {
107+
hour: "numeric",
108+
minute: "numeric",
109+
});
110+
111+
if (precision === "time") {
112+
return time;
113+
}
114+
115+
const sameYear = date.getFullYear() === new Date().getFullYear();
116+
const dateStr = sameYear
117+
? date.toLocaleDateString([], { month: "short", day: "numeric" })
118+
: date.toLocaleDateString([], {
119+
month: "short",
120+
day: "numeric",
121+
year: "numeric",
122+
});
123+
124+
return `${dateStr}, ${time}`;
125+
}
126+
127+
function CalendarIndicator({ calendarId }: { calendarId: string }) {
128+
const calendar = main.UI.useRow("calendars", calendarId, main.STORE_ID);
129+
130+
const name = calendar?.name ? String(calendar.name) : undefined;
131+
const color = calendar?.color ? String(calendar.color) : "#888";
132+
133+
return (
134+
<Tooltip delayDuration={0}>
135+
<TooltipTrigger asChild>
136+
<div
137+
className="size-2 rounded-full shrink-0 opacity-60"
138+
style={{ backgroundColor: color }}
139+
/>
140+
</TooltipTrigger>
141+
<TooltipContent side="right" className="text-xs">
142+
{name || "Calendar"}
143+
</TooltipContent>
144+
</Tooltip>
145+
);
146+
}
147+
148+
function useTimelineItemActions(
149+
item: TimelineItem,
150+
store: ReturnType<typeof main.UI.useStore>,
151+
eventId: string | undefined,
152+
title: string,
153+
) {
154+
const openCurrent = useTabs((state) => state.openCurrent);
155+
const openNew = useTabs((state) => state.openNew);
156+
const invalidateResource = useTabs((state) => state.invalidateResource);
157+
158+
const handleEventClick = useCallback(
159+
(openInNewTab: boolean) => {
57160
if (!eventId || !store) {
58161
return;
59162
}
@@ -68,10 +171,7 @@ export const TimelineItemComponent = memo(
68171
});
69172

70173
if (existingSessionId) {
71-
const tab: TabInput = {
72-
id: existingSessionId,
73-
type: "sessions",
74-
};
174+
const tab: TabInput = { id: existingSessionId, type: "sessions" };
75175
if (openInNewTab) {
76176
openNew(tab);
77177
} else {
@@ -95,80 +195,38 @@ export const TimelineItemComponent = memo(
95195
openCurrent(tab);
96196
}
97197
}
98-
};
99-
100-
const handleDelete = useCallback(() => {
101-
if (!store) {
102-
return;
103-
}
104-
if (item.type === "event") {
105-
invalidateResource("events", item.id);
106-
store.delRow("events", item.id);
107-
} else {
108-
invalidateResource("sessions", item.id);
109-
store.delRow("sessions", item.id);
110-
}
111-
}, [store, item.id, item.type, invalidateResource]);
112-
113-
const contextMenu = useMemo(
114-
() => [
115-
{ id: "open-new-tab", text: "Open in New Tab", action: handleCmdClick },
116-
{ id: "delete", text: "Delete Completely", action: handleDelete },
117-
],
118-
[handleCmdClick, handleDelete],
119-
);
120-
121-
const displayTime = useMemo(() => {
122-
if (!timestamp) {
123-
return "";
124-
}
125-
126-
const date = new Date(timestamp);
127-
if (Number.isNaN(date.getTime())) {
128-
return "";
129-
}
130-
131-
const time = date.toLocaleTimeString([], {
132-
hour: "numeric",
133-
minute: "numeric",
134-
});
135-
136-
if (precision === "time") {
137-
return time;
138-
}
139-
140-
const sameYear = date.getFullYear() === new Date().getFullYear();
141-
const dateStr = sameYear
142-
? date.toLocaleDateString([], {
143-
month: "short",
144-
day: "numeric",
145-
})
146-
: date.toLocaleDateString([], {
147-
month: "short",
148-
day: "numeric",
149-
year: "numeric",
150-
});
151-
return `${dateStr}, ${time}`;
152-
}, [timestamp, precision]);
153-
154-
return (
155-
<InteractiveButton
156-
onClick={handleClick}
157-
onCmdClick={handleCmdClick}
158-
contextMenu={contextMenu}
159-
className={cn([
160-
"w-full text-left px-3 py-2 rounded-lg",
161-
selected && "bg-neutral-200",
162-
!selected && "hover:bg-neutral-100",
163-
])}
164-
>
165-
<div className="flex flex-col gap-0.5">
166-
<div className="text-sm font-normal truncate">{title}</div>
167-
{displayTime && (
168-
<div className="text-xs text-neutral-500">{displayTime}</div>
169-
)}
170-
</div>
171-
</InteractiveButton>
172-
);
173-
},
174-
);
198+
},
199+
[eventId, store, title, openCurrent, openNew],
200+
);
201+
202+
const handleClick = useCallback(() => {
203+
if (item.type === "event") {
204+
handleEventClick(false);
205+
} else {
206+
openCurrent({ id: item.id, type: "sessions" });
207+
}
208+
}, [item, handleEventClick, openCurrent]);
209+
210+
const handleCmdClick = useCallback(() => {
211+
if (item.type === "event") {
212+
handleEventClick(true);
213+
} else {
214+
openNew({ id: item.id, type: "sessions" });
215+
}
216+
}, [item, handleEventClick, openNew]);
217+
218+
const handleDelete = useCallback(() => {
219+
if (!store) {
220+
return;
221+
}
222+
if (item.type === "event") {
223+
invalidateResource("events", item.id);
224+
store.delRow("events", item.id);
225+
} else {
226+
invalidateResource("sessions", item.id);
227+
store.delRow("sessions", item.id);
228+
}
229+
}, [store, item.id, item.type, invalidateResource]);
230+
231+
return { handleClick, handleCmdClick, handleDelete };
232+
}

0 commit comments

Comments
 (0)