11import { memo , useCallback , useMemo } from "react" ;
22
33import { commands as analyticsCommands } from "@hypr/plugin-analytics" ;
4+ import {
5+ Tooltip ,
6+ TooltipContent ,
7+ TooltipTrigger ,
8+ } from "@hypr/ui/components/ui/tooltip" ;
49import { cn } from "@hypr/utils" ;
510
611import * 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