11"use client" ;
22
3+ import { KeyboardEvent } from "react" ;
34import { Checklist , Item } from "@/app/_types" ;
45import { useCalendarView } from "@/app/_hooks/kanban/useCalendarView" ;
56import { Button } from "@/app/_components/GlobalComponents/Buttons/Button" ;
@@ -10,7 +11,11 @@ import {
1011 Download04Icon ,
1112} from "hugeicons-react" ;
1213import { cn } from "@/app/_utils/global-utils" ;
13- import { getPriorityDotColor } from "@/app/_utils/kanban/index" ;
14+ import { getPriorityBarStyle } from "@/app/_utils/kanban/index" ;
15+ import {
16+ getMaxBarLanes ,
17+ getWeekBarSegments ,
18+ } from "@/app/_utils/kanban/calendar-utils" ;
1419import { useTranslations } from "next-intl" ;
1520
1621interface CalendarViewProps {
@@ -33,16 +38,18 @@ const _toLocalDate = (d: Date): string => {
3338 return `${ d . getFullYear ( ) } -${ pad ( d . getMonth ( ) + 1 ) } -${ pad ( d . getDate ( ) ) } ` ;
3439} ;
3540
41+ const _BAR_HEIGHT = 18 ;
42+
3643export const CalendarView = ( { checklist, onItemClick } : CalendarViewProps ) => {
3744 const t = useTranslations ( ) ;
3845 const {
3946 currentDate,
4047 calendarGrid,
48+ events,
4149 goToPreviousMonth,
4250 goToNextMonth,
4351 goToToday,
4452 exportICS,
45- getEventsForDate,
4653 unscheduledItems,
4754 } = useCalendarView ( checklist ) ;
4855
@@ -53,6 +60,11 @@ export const CalendarView = ({ checklist, onItemClick }: CalendarViewProps) => {
5360 year : "numeric" ,
5461 } ) ;
5562
63+ const _openItem = ( itemId : string ) => {
64+ const item = checklist . items . find ( ( entry ) => entry . id === itemId ) ;
65+ if ( item && onItemClick ) onItemClick ( item ) ;
66+ } ;
67+
5668 return (
5769 < div className = "space-y-4 min-w-0" >
5870 < div className = "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between min-w-0" >
@@ -88,78 +100,99 @@ export const CalendarView = ({ checklist, onItemClick }: CalendarViewProps) => {
88100 ) ) }
89101 </ div >
90102
91- { calendarGrid . map ( ( week , weekIndex ) => (
92- < div key = { weekIndex } className = "grid grid-cols-7" >
93- { week . map ( ( day , dayIndex ) => {
94- if ( ! day ) {
95- return (
96- < div
97- key = { `empty-${ dayIndex } ` }
98- className = "min-h-[100px] p-1 border-b border-r border-border bg-muted/20"
99- />
100- ) ;
101- }
103+ { calendarGrid . map ( ( week , weekIndex ) => {
104+ const segments = getWeekBarSegments ( week , events ) ;
105+ const maxLanes = getMaxBarLanes ( segments ) ;
106+ const barAreaHeight = maxLanes * _BAR_HEIGHT ;
102107
103- const dateStr = _toLocalDate ( day ) ;
104- const dayEvents = getEventsForDate ( day ) ;
105- const isToday = dateStr === today ;
108+ return (
109+ < div key = { weekIndex } className = "relative grid grid-cols-7" >
110+ { week . map ( ( day , dayIndex ) => {
111+ if ( ! day ) {
112+ return (
113+ < div
114+ key = { `empty-${ dayIndex } ` }
115+ className = "border-b border-r border-border bg-muted/20"
116+ style = { { minHeight : 72 + barAreaHeight } }
117+ />
118+ ) ;
119+ }
106120
107- return (
108- < div
109- key = { dateStr }
110- className = { cn (
111- "min-h-[100px] p-1 border-b border-r border-border transition-colors" ,
112- isToday && "bg-primary/5" ,
113- ) }
114- >
121+ const dateStr = _toLocalDate ( day ) ;
122+ const isToday = dateStr === today ;
123+
124+ return (
115125 < div
126+ key = { dateStr }
116127 className = { cn (
117- "text-xs font-medium mb-1 w-6 h-6 flex items-center justify-center rounded-full " ,
118- isToday && "bg-primary text-primary-foreground " ,
128+ "border-b border-r border-border p-1 transition-colors " ,
129+ isToday && "bg-primary/5 " ,
119130 ) }
131+ style = { { minHeight : 72 + barAreaHeight } }
120132 >
121- { day . getDate ( ) }
122- </ div >
123- < div className = "space-y-0.5" >
124- { dayEvents . slice ( 0 , 3 ) . map ( ( event ) => (
125- < div
126- key = { event . id }
127- onClick = { ( ) => {
128- const item = checklist . items . find (
129- ( i ) => i . id === event . itemId ,
130- ) ;
131- if ( item && onItemClick ) onItemClick ( item ) ;
132- } }
133- className = { cn (
134- "text-[10px] px-1 py-0.5 rounded truncate cursor-pointer hover:opacity-80 transition-opacity flex items-center gap-1 bg-muted text-muted-foreground border-l-2" ,
135- event . completed && "line-through opacity-60" ,
136- ) }
137- style = { {
138- borderLeftColor : event . completed
139- ? "#22c55e"
140- : getPriorityDotColor (
141- event . priority as Parameters <
142- typeof getPriorityDotColor
143- > [ 0 ] ,
144- ) ,
145- } }
146- >
147- { event . title }
148- </ div >
149- ) ) }
150- { dayEvents . length > 3 && (
151- < div className = "text-[10px] text-muted-foreground px-1" >
152- { t ( "kanban.moreEvents" , {
153- count : dayEvents . length - 3 ,
154- } ) }
155- </ div >
156- ) }
133+ < div
134+ className = { cn (
135+ "text-xs font-medium mb-1 w-6 h-6 flex items-center justify-center rounded-full" ,
136+ isToday && "bg-primary text-primary-foreground" ,
137+ ) }
138+ >
139+ { day . getDate ( ) }
140+ </ div >
157141 </ div >
142+ ) ;
143+ } ) }
144+
145+ { maxLanes > 0 && (
146+ < div
147+ className = "absolute inset-x-0 top-7 grid grid-cols-7 gap-y-0.5 px-0.5 pointer-events-none"
148+ style = { { gridTemplateRows : `repeat(${ maxLanes } , ${ _BAR_HEIGHT } px)` } }
149+ >
150+ { segments . map ( ( segment ) => {
151+ const barStyle = getPriorityBarStyle (
152+ segment . event . priority as Parameters < typeof getPriorityBarStyle > [ 0 ] ,
153+ ) ;
154+
155+ return (
156+ < div
157+ key = { `${ segment . event . id } -${ segment . colStart } -${ segment . lane } ` }
158+ { ...( onItemClick
159+ ? {
160+ role : "button" as const ,
161+ tabIndex : 0 ,
162+ "aria-label" : segment . event . title ,
163+ onClick : ( ) => _openItem ( segment . event . itemId ) ,
164+ onKeyDown : ( e : KeyboardEvent ) => {
165+ if ( e . key === "Enter" || e . key === " " ) {
166+ e . preventDefault ( ) ;
167+ _openItem ( segment . event . itemId ) ;
168+ }
169+ } ,
170+ }
171+ : { } ) }
172+ className = { cn (
173+ "pointer-events-auto h-4 text-[10px] font-medium leading-4 px-1.5 truncate transition-[filter,opacity]" ,
174+ onItemClick &&
175+ "cursor-pointer hover:brightness-95 dark:hover:brightness-110" ,
176+ ! barStyle . backgroundColor && "bg-muted/80 text-muted-foreground" ,
177+ segment . continuesPrev ? "rounded-l-none ml-0" : "rounded-l-jotty ml-0.5" ,
178+ segment . continuesNext ? "rounded-r-none mr-0" : "rounded-r-jotty mr-0.5" ,
179+ segment . event . completed && "line-through opacity-70" ,
180+ ) }
181+ style = { {
182+ ...barStyle ,
183+ gridColumn : `${ segment . colStart + 1 } / span ${ segment . colSpan } ` ,
184+ gridRow : segment . lane + 1 ,
185+ } }
186+ >
187+ { ! segment . continuesPrev ? segment . event . title : "\u00a0" }
188+ </ div >
189+ ) ;
190+ } ) }
158191 </ div >
159- ) ;
160- } ) }
161- </ div >
162- ) ) }
192+ ) }
193+ </ div >
194+ ) ;
195+ } ) }
163196 </ div >
164197
165198 { unscheduledItems . length > 0 && (
0 commit comments