Skip to content

Commit ddaab65

Browse files
committed
feat: curr time
1 parent 838d0c4 commit ddaab65

11 files changed

Lines changed: 248 additions & 70 deletions

platforms/calendar/app/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ export default function RootLayout({
4444
disableTransitionOnChange
4545
>
4646
<AuthProvider>
47-
<Header />
48-
{children}
47+
<div className="flex flex-col h-screen">
48+
<Header />
49+
<main className="flex-1 min-h-0 flex flex-col">
50+
{children}
51+
</main>
52+
</div>
4953
</AuthProvider>
5054
</ThemeProvider>
5155
</body>

platforms/calendar/components/calendar-demo.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function mapApiToEvent(e: {
2626
export default function CalendarDemo() {
2727
const { isAuthenticated, isReady } = useAuth()
2828
const [events, setEvents] = useState<CalendarEvent[]>([])
29-
const [mode, setMode] = useState<Mode>('month')
29+
const [mode, setMode] = useState<Mode>('week')
3030
const [date, setDate] = useState<Date>(new Date())
3131
const [eventsLoading, setEventsLoading] = useState(false)
3232
const [eventsError, setEventsError] = useState<string | null>(null)
@@ -63,26 +63,28 @@ export default function CalendarDemo() {
6363
}
6464

6565
return (
66-
<>
66+
<div className="flex flex-col flex-1 min-h-0">
6767
{eventsLoading && events.length === 0 && (
68-
<div className="flex justify-center p-4">
68+
<div className="flex justify-center p-4 shrink-0">
6969
<p className="text-muted-foreground">Loading events…</p>
7070
</div>
7171
)}
7272
{eventsError && (
73-
<div className="bg-destructive/10 text-destructive mx-4 rounded-md p-3 text-sm">
73+
<div className="bg-destructive/10 text-destructive mx-4 rounded-md p-3 text-sm shrink-0">
7474
{eventsError}
7575
</div>
7676
)}
77-
<Calendar
78-
events={events}
79-
setEvents={setEvents}
80-
mode={mode}
81-
setMode={setMode}
82-
date={date}
83-
setDate={setDate}
84-
refetchEvents={refetchEvents}
85-
/>
86-
</>
77+
<div className="flex-1 min-h-0 flex flex-col">
78+
<Calendar
79+
events={events}
80+
setEvents={setEvents}
81+
mode={mode}
82+
setMode={setMode}
83+
date={date}
84+
setDate={setDate}
85+
refetchEvents={refetchEvents}
86+
/>
87+
</div>
88+
</div>
8789
)
8890
}

platforms/calendar/components/calendar/body/calendar-body.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ export default function CalendarBody() {
77
const { mode } = useCalendarContext()
88

99
return (
10-
<>
10+
<div className="flex flex-col flex-1 min-h-0">
1111
{mode === 'day' && <CalendarBodyDay />}
1212
{mode === 'week' && <CalendarBodyWeek />}
13-
{mode === 'month' && <CalendarBodyMonth />}
14-
</>
13+
{mode === 'month' && (
14+
<div className="flex-1 min-h-0 overflow-auto">
15+
<CalendarBodyMonth />
16+
</div>
17+
)}
18+
</div>
1519
)
1620
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { format } from 'date-fns'
5+
import { getCurrentTimeOffsetTop } from './day/calendar-body-margin-day-margin'
6+
7+
export function CurrentTimeLine() {
8+
const [now, setNow] = useState(new Date())
9+
10+
useEffect(() => {
11+
const interval = setInterval(() => {
12+
setNow(new Date())
13+
}, 60000) // Update every minute
14+
return () => clearInterval(interval)
15+
}, [])
16+
17+
const top = getCurrentTimeOffsetTop()
18+
19+
return (
20+
<div
21+
className="absolute left-0 right-0 z-20 pointer-events-none flex items-center"
22+
style={{ top: `${top}px`, transform: 'translateY(-50%)' }}
23+
>
24+
<div className="flex items-center shrink-0 bg-red-500 text-white text-xs font-medium px-2 py-0.5 rounded shadow-sm">
25+
{format(now, 'h:mm a')}
26+
</div>
27+
<div className="flex-1 h-0.5 bg-red-500 min-w-0" />
28+
</div>
29+
)
30+
}
31+
32+
export { getCurrentTimeOffsetTop }

platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ import { hours } from './calendar-body-margin-day-margin'
44
import CalendarBodyHeader from '../calendar-body-header'
55
import CalendarEvent from '../../calendar-event'
66

7-
export default function CalendarBodyDayContent({ date }: { date: Date }) {
7+
export default function CalendarBodyDayContent({
8+
date,
9+
hideHeader = false,
10+
}: {
11+
date: Date
12+
hideHeader?: boolean
13+
}) {
814
const { events } = useCalendarContext()
915

1016
const dayEvents = events.filter((event) => isSameDay(event.start, date))
1117

1218
return (
13-
<div className="flex flex-col flex-grow">
14-
<CalendarBodyHeader date={date} />
19+
<div className="flex flex-col flex-grow min-h-0">
20+
{!hideHeader && <CalendarBodyHeader date={date} />}
1521

16-
<div className="flex-1 relative">
22+
<div className="flex-1 relative min-h-0">
1723
{hours.map((hour) => (
1824
<div key={hour} className="h-32 border-b border-border/50 group" />
1925
))}

platforms/calendar/components/calendar/body/day/calendar-body-day.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,78 @@
1+
'use client'
2+
3+
import { useEffect, useRef } from 'react'
4+
import { isSameDay } from 'date-fns'
15
import CalendarBodyDayCalendar from './calendar-body-day-calendar'
26
import CalendarBodyDayEvents from './calendar-body-day-events'
37
import { useCalendarContext } from '../../calendar-context'
48
import CalendarBodyDayContent from './calendar-body-day-content'
59
import CalendarBodyMarginDayMargin from './calendar-body-margin-day-margin'
10+
import CalendarBodyHeader from '../calendar-body-header'
11+
import {
12+
CurrentTimeLine,
13+
getCurrentTimeOffsetTop,
14+
} from '../current-time-line'
15+
import {
16+
GRID_HEADER_HEIGHT,
17+
PIXELS_PER_HOUR,
18+
} from './calendar-body-margin-day-margin'
19+
20+
const TOTAL_GRID_HEIGHT = 24 * PIXELS_PER_HOUR // Header is now outside the scrollable area
621

722
export default function CalendarBodyDay() {
823
const { date } = useCalendarContext()
24+
const scrollRef = useRef<HTMLDivElement>(null)
25+
const showTimeline = isSameDay(date, new Date())
26+
27+
useEffect(() => {
28+
if (!scrollRef.current) return
29+
const el = scrollRef.current
30+
const scrollToCenter = () => {
31+
if (!el.clientHeight || !el.scrollHeight) return
32+
const top = getCurrentTimeOffsetTop()
33+
const center = el.clientHeight / 2
34+
const scrollTop = Math.max(
35+
0,
36+
Math.min(top - center, el.scrollHeight - el.clientHeight)
37+
)
38+
el.scrollTop = scrollTop
39+
}
40+
41+
// Try multiple times to ensure DOM is ready
42+
const timeouts: NodeJS.Timeout[] = []
43+
timeouts.push(setTimeout(scrollToCenter, 0))
44+
timeouts.push(setTimeout(scrollToCenter, 50))
45+
timeouts.push(setTimeout(scrollToCenter, 150))
46+
47+
return () => timeouts.forEach(clearTimeout)
48+
}, [showTimeline, date])
49+
950
return (
10-
<div className="flex divide-x flex-grow overflow-hidden">
11-
<div className="flex flex-col flex-grow divide-y overflow-hidden">
12-
<div className="flex flex-col flex-1 overflow-y-auto">
13-
<div className="relative flex flex-1 divide-x">
51+
<div className="flex divide-x flex-1 min-h-0 overflow-hidden">
52+
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
53+
{/* Sticky: single day header */}
54+
<div className="flex shrink-0 border-b bg-background sticky top-0 z-30">
55+
<div className="w-12 shrink-0 bg-background" aria-hidden />
56+
<div className="flex-1 min-w-0">
57+
<CalendarBodyHeader date={date} />
58+
</div>
59+
</div>
60+
{/* Scrollable: time grid only */}
61+
<div
62+
ref={scrollRef}
63+
className="flex-1 min-h-0 overflow-y-auto"
64+
>
65+
<div
66+
className="relative flex flex-1 divide-x"
67+
style={{ minHeight: TOTAL_GRID_HEIGHT }}
68+
>
1469
<CalendarBodyMarginDayMargin />
15-
<CalendarBodyDayContent date={date} />
70+
<CalendarBodyDayContent date={date} hideHeader />
71+
{showTimeline && <CurrentTimeLine />}
1672
</div>
1773
</div>
1874
</div>
19-
<div className="lg:flex hidden flex-col flex-grow divide-y max-w-[276px]">
75+
<div className="lg:flex hidden flex-col flex-1 min-h-0 divide-y max-w-[276px] overflow-hidden">
2076
<CalendarBodyDayCalendar />
2177
<CalendarBodyDayEvents />
2278
</div>

platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { format } from 'date-fns'
22
import { cn } from '@/lib/utils'
33

44
export const hours = Array.from({ length: 24 }, (_, i) => i)
5+
export const PIXELS_PER_HOUR = 128
6+
export const GRID_HEADER_HEIGHT = 48 // Legacy: for calculating total grid height
7+
8+
export function getCurrentTimeOffsetTop(): number {
9+
const now = new Date()
10+
const hours = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600
11+
// Header is now outside the scrollable area, so start from 0
12+
return hours * PIXELS_PER_HOUR
13+
}
514

615
export default function CalendarBodyMarginDayMargin({
716
className,
@@ -15,18 +24,15 @@ export default function CalendarBodyMarginDayMargin({
1524
className
1625
)}
1726
>
18-
<div className="sticky top-0 left-0 h-[33px] bg-background z-20 border-b" />
19-
<div className="sticky left-0 w-12 bg-background z-10 flex flex-col">
20-
{hours.map((hour) => (
21-
<div key={hour} className="relative h-32 first:mt-0">
22-
{hour !== 0 && (
23-
<span className="absolute text-xs text-muted-foreground -top-2.5 left-2">
24-
{format(new Date().setHours(hour, 0, 0, 0), 'h a')}
25-
</span>
26-
)}
27-
</div>
28-
))}
29-
</div>
27+
{hours.map((hour) => (
28+
<div key={hour} className="relative h-32 first:mt-0">
29+
{hour !== 0 && (
30+
<span className="absolute text-xs text-muted-foreground -top-2.5 left-2">
31+
{format(new Date().setHours(hour, 0, 0, 0), 'h a')}
32+
</span>
33+
)}
34+
</div>
35+
))}
3036
</div>
3137
)
3238
}

platforms/calendar/components/calendar/body/week/calendar-body-week.tsx

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,85 @@
1+
'use client'
2+
3+
import { useEffect, useRef } from 'react'
14
import { useCalendarContext } from '../../calendar-context'
2-
import { startOfWeek, addDays } from 'date-fns'
5+
import { startOfWeek, addDays, isSameDay } from 'date-fns'
36
import CalendarBodyMarginDayMargin from '../day/calendar-body-margin-day-margin'
47
import CalendarBodyDayContent from '../day/calendar-body-day-content'
8+
import CalendarBodyHeader from '../calendar-body-header'
9+
import {
10+
CurrentTimeLine,
11+
getCurrentTimeOffsetTop,
12+
} from '../current-time-line'
13+
import {
14+
GRID_HEADER_HEIGHT,
15+
PIXELS_PER_HOUR,
16+
} from '../day/calendar-body-margin-day-margin'
17+
18+
const TOTAL_GRID_HEIGHT = 24 * PIXELS_PER_HOUR // Header is now outside the scrollable area
19+
520
export default function CalendarBodyWeek() {
621
const { date } = useCalendarContext()
22+
const scrollRef = useRef<HTMLDivElement>(null)
723

824
const weekStart = startOfWeek(date, { weekStartsOn: 1 })
925
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
26+
const today = new Date()
27+
const showTimeline = weekDays.some((day) => isSameDay(day, today))
28+
29+
useEffect(() => {
30+
if (!scrollRef.current) return
31+
const el = scrollRef.current
32+
const scrollToCenter = () => {
33+
if (!el.clientHeight || !el.scrollHeight) return
34+
const top = getCurrentTimeOffsetTop()
35+
const center = el.clientHeight / 2
36+
const scrollTop = Math.max(
37+
0,
38+
Math.min(top - center, el.scrollHeight - el.clientHeight)
39+
)
40+
el.scrollTop = scrollTop
41+
}
42+
43+
// Try multiple times to ensure DOM is ready
44+
const timeouts: NodeJS.Timeout[] = []
45+
timeouts.push(setTimeout(scrollToCenter, 0))
46+
timeouts.push(setTimeout(scrollToCenter, 50))
47+
timeouts.push(setTimeout(scrollToCenter, 150))
48+
49+
return () => timeouts.forEach(clearTimeout)
50+
}, [showTimeline, date])
1051

1152
return (
12-
<div className="flex divide-x flex-grow overflow-hidden">
13-
<div className="flex flex-col flex-grow divide-y overflow-hidden">
14-
<div className="flex flex-col flex-1 overflow-y-auto">
15-
<div className="relative flex flex-1 divide-x flex-col md:flex-row">
16-
<CalendarBodyMarginDayMargin className="hidden md:block" />
17-
{weekDays.map((day) => (
18-
<div
19-
key={day.toISOString()}
20-
className="flex flex-1 divide-x md:divide-x-0"
21-
>
22-
<CalendarBodyMarginDayMargin className="block md:hidden" />
23-
<CalendarBodyDayContent date={day} />
24-
</div>
25-
))}
53+
<div className="flex flex-col flex-1 min-h-0 divide-x overflow-hidden">
54+
{/* Sticky: day headers row */}
55+
<div className="flex shrink-0 border-b bg-background sticky top-0 z-30">
56+
<div className="w-12 shrink-0 hidden md:block bg-background" aria-hidden />
57+
{weekDays.map((day) => (
58+
<div key={day.toISOString()} className="flex-1 min-w-0">
59+
<CalendarBodyHeader date={day} />
2660
</div>
61+
))}
62+
</div>
63+
{/* Scrollable: time grid only */}
64+
<div
65+
ref={scrollRef}
66+
className="flex-1 min-h-0 overflow-y-auto"
67+
>
68+
<div
69+
className="relative flex divide-x flex-col md:flex-row"
70+
style={{ minHeight: TOTAL_GRID_HEIGHT }}
71+
>
72+
<CalendarBodyMarginDayMargin className="hidden md:block" />
73+
{weekDays.map((day) => (
74+
<div
75+
key={day.toISOString()}
76+
className="flex flex-1 divide-x md:divide-x-0"
77+
>
78+
<CalendarBodyMarginDayMargin className="block md:hidden" />
79+
<CalendarBodyDayContent date={day} hideHeader />
80+
</div>
81+
))}
82+
{showTimeline && <CurrentTimeLine />}
2783
</div>
2884
</div>
2985
</div>

0 commit comments

Comments
 (0)