Files
calendar/frontend(experimental)/newchat/components/week-view.tsx
T
ben bf601846db new frontend
ik ben zo cool
2026-08-19 09:33:48 +02:00

226 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useMemo } from "react"
import type { CalendarEvent } from "@/lib/types"
import { getRoomColor } from "@/lib/room-theme"
import {
addDays,
isSameDay,
parseDateTime,
WEEKDAY_LABELS,
} from "@/lib/date-utils"
const HOUR_HEIGHT = 64 // px per hour
interface WeekViewProps {
weekStart: Date
events: CalendarEvent[]
showRoomColors: boolean
onEventClick: (event: CalendarEvent) => void
}
interface PositionedEvent {
event: CalendarEvent
start: Date
end: Date
top: number
height: number
lane: number
lanes: number
}
// Assign overlapping events to side-by-side lanes within a single day.
function layoutDayEvents(
events: { event: CalendarEvent; start: Date; end: Date }[],
hoursStart: number,
): PositionedEvent[] {
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime())
const result: PositionedEvent[] = []
let cluster: typeof sorted = []
let clusterEnd = 0
const flush = () => {
if (cluster.length === 0) return
// Greedy lane assignment within the cluster.
const laneEnds: number[] = []
const withLanes = cluster.map((item) => {
let lane = laneEnds.findIndex((end) => end <= item.start.getTime())
if (lane === -1) {
lane = laneEnds.length
laneEnds.push(item.end.getTime())
} else {
laneEnds[lane] = item.end.getTime()
}
return { ...item, lane }
})
const lanes = laneEnds.length
for (const item of withLanes) {
const startHrs = item.start.getHours() + item.start.getMinutes() / 60
const endHrs = item.end.getHours() + item.end.getMinutes() / 60
const safeEnd = Math.max(endHrs, startHrs + 0.5)
result.push({
event: item.event,
start: item.start,
end: item.end,
top: (startHrs - hoursStart) * HOUR_HEIGHT,
height: (safeEnd - startHrs) * HOUR_HEIGHT,
lane: item.lane,
lanes,
})
}
cluster = []
clusterEnd = 0
}
for (const item of sorted) {
if (cluster.length === 0 || item.start.getTime() < clusterEnd) {
cluster.push(item)
clusterEnd = Math.max(clusterEnd, item.end.getTime())
} else {
flush()
cluster.push(item)
clusterEnd = item.end.getTime()
}
}
flush()
return result
}
export function WeekView({ weekStart, events, showRoomColors, onEventClick }: WeekViewProps) {
const days = useMemo(
() => Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)),
[weekStart],
)
// Determine visible hour range from events (clamped to a sensible default).
const { hoursStart, hoursEnd } = useMemo(() => {
let min = 8
let max = 18
for (const e of events) {
const s = parseDateTime(e.start_time)
const en = parseDateTime(e.end_time)
if (!isNaN(s.getTime())) min = Math.min(min, s.getHours())
if (!isNaN(en.getTime())) {
const endHr = en.getMinutes() > 0 ? en.getHours() + 1 : en.getHours()
max = Math.max(max, endHr)
}
}
return { hoursStart: Math.max(0, min), hoursEnd: Math.min(24, Math.max(max, min + 6)) }
}, [events])
const timeSlots = useMemo(
() => Array.from({ length: hoursEnd - hoursStart }, (_, i) => hoursStart + i),
[hoursStart, hoursEnd],
)
const positionedByDay = useMemo(() => {
return days.map((day) => {
const dayEvents = events
.map((event) => ({
event,
start: parseDateTime(event.start_time),
end: parseDateTime(event.end_time),
}))
.filter((x) => !isNaN(x.start.getTime()) && isSameDay(x.start, day))
return layoutDayEvents(dayEvents, hoursStart)
})
}, [days, events, hoursStart])
const today = new Date()
const gridHeight = (hoursEnd - hoursStart) * HOUR_HEIGHT
const formatHour = (h: number) => {
if (h === 0) return "12 AM"
if (h === 12) return "12 PM"
return h > 12 ? `${h - 12} PM` : `${h} AM`
}
return (
<div className="h-full overflow-auto rounded-xl border border-white/20 bg-white/20 shadow-xl backdrop-blur-lg">
{/* Week Header */}
<div className="sticky top-0 z-10 grid grid-cols-[4rem_repeat(7,1fr)] border-b border-white/20 bg-white/10 backdrop-blur-lg">
<div className="p-2" />
{days.map((date, i) => {
const isToday = isSameDay(date, today)
return (
<div key={i} className="border-l border-white/20 p-2 text-center">
<div className="text-xs font-medium text-white/70">{WEEKDAY_LABELS[i]}</div>
<div
className={`mx-auto mt-1 flex h-8 w-8 items-center justify-center text-lg font-medium text-white ${
isToday ? "rounded-full bg-blue-500" : ""
}`}
>
{date.getDate()}
</div>
</div>
)
})}
</div>
{/* Time grid */}
<div className="grid grid-cols-[4rem_repeat(7,1fr)]">
{/* Time labels */}
<div>
{timeSlots.map((hour) => (
<div
key={hour}
className="border-b border-white/10 pr-2 text-right text-xs text-white/70"
style={{ height: HOUR_HEIGHT }}
>
{formatHour(hour)}
</div>
))}
</div>
{/* Day columns */}
{days.map((day, dayIndex) => (
<div
key={dayIndex}
className="relative border-l border-white/20"
style={{ height: gridHeight }}
>
{timeSlots.map((hour) => (
<div
key={hour}
className="border-b border-white/10"
style={{ height: HOUR_HEIGHT }}
/>
))}
{positionedByDay[dayIndex].map((pe, i) => {
const color = showRoomColors ? getRoomColor(pe.event.room_id) : { bg: "bg-blue-500" }
const cancelled = pe.event.status === "cancelled"
const tentative = pe.event.status === "draft" || pe.event.status === "proposed"
const widthPct = 100 / pe.lanes
return (
<button
key={pe.event.id + "-" + i}
onClick={() => onEventClick(pe.event)}
className={`absolute overflow-hidden rounded-md p-1.5 text-left text-xs text-white shadow-md transition-all duration-150 ease-in-out hover:z-20 hover:-translate-y-0.5 hover:shadow-lg ${color.bg} ${
cancelled ? "opacity-50 line-through" : ""
} ${tentative ? "border border-dashed border-white/70" : ""}`}
style={{
top: pe.top,
height: pe.height,
left: `calc(${pe.lane * widthPct}% + 2px)`,
width: `calc(${widthPct}% - 4px)`,
}}
>
<div className="truncate font-medium">{pe.event.title}</div>
{pe.height > 34 && (
<div className="mt-0.5 truncate text-[10px] opacity-80">
{pe.start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{" "}
{pe.end.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</div>
)}
</button>
)
})}
</div>
))}
</div>
</div>
)
}