From 5d37fb899d9d4ccb5f03d51a88f38252a23d826a Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 9 Jul 2026 15:13:40 -0400 Subject: [PATCH] feat(calendar): weekly list view with grid toggle, defaults to list --- web/src/components/CalendarView.tsx | 104 ++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/web/src/components/CalendarView.tsx b/web/src/components/CalendarView.tsx index 8ca58e2..656d80a 100644 --- a/web/src/components/CalendarView.tsx +++ b/web/src/components/CalendarView.tsx @@ -33,6 +33,7 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) { const [end, setEnd] = useState(''); const [description, setDescription] = useState(''); const [selectedEvent, setSelectedEvent] = useState(null); + const [view, setView] = useState<'grid' | 'list'>('list'); const monthStart = useMemo(() => new Date(date.getFullYear(), date.getMonth(), 1), [date]); const monthEnd = useMemo(() => new Date(date.getFullYear(), date.getMonth() + 1, 0), [date]); @@ -65,6 +66,29 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) { }); }; + const weeks = useMemo(() => { + const sorted = [...events].sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()); + const grouped: { label: string; events: CalendarEvent[] }[] = []; + let currentKey = ''; + for (const ev of sorted) { + const d = new Date(ev.start_time); + // week key = Monday of that week + const dayOfWeek = d.getDay(); + const monday = new Date(d); + monday.setDate(d.getDate() - ((dayOfWeek + 6) % 7)); + const key = monday.toISOString().slice(0, 10); + if (key !== currentKey) { + const sun = new Date(monday); + sun.setDate(monday.getDate() + 6); + const label = `${monday.toLocaleDateString('default', { month: 'short', day: 'numeric' })} \u2013 ${sun.toLocaleDateString('default', { month: 'short', day: 'numeric' })}`; + grouped.push({ label, events: [] }); + currentKey = key; + } + grouped[grouped.length - 1].events.push(ev); + } + return grouped; + }, [events]); + const handleCreate = async () => { if (!title || !start) return; const payload = { title, start_time: start, end_time: end || undefined, description: description || undefined }; @@ -89,7 +113,11 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
○ {channelName} - +
+ + + +
{showForm && (
@@ -107,30 +135,66 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
{loading &&
[loading...]
}
-
- {['S','M','T','W','T','F','S'].map((d) =>
{d}
)} -
-
- {days.map((day, idx) => ( -
- {day !== null && ( - <> -
{day}
- {eventsForDay(day).map((ev) => ( + {view === 'grid' ? ( + <> +
+ {['S','M','T','W','T','F','S'].map((d) =>
{d}
)} +
+
+ {days.map((day, idx) => ( +
+ {day !== null && ( + <> +
{day}
+ {eventsForDay(day).map((ev) => ( + + ))} + + )} +
+ ))} +
+ + ) : ( +
+ {weeks.length === 0 && !loading && ( +
No events this month.
+ )} + {weeks.map((week) => ( +
+
+ {week.label} +
+ {week.events.map((ev) => { + const s = new Date(ev.start_time); + const e = ev.end_time ? new Date(ev.end_time) : null; + const dayName = s.toLocaleDateString('default', { weekday: 'short' }); + const dateStr = s.toLocaleDateString('default', { month: 'short', day: 'numeric' }); + const startStr = s.toLocaleTimeString('default', { hour: 'numeric', minute: '2-digit' }); + const endStr = e ? e.toLocaleTimeString('default', { hour: 'numeric', minute: '2-digit' }) : ''; + return ( - ))} - - )} -
- ))} -
+ ); + })} +
+ ))} +
+ )}
{selectedEvent && (
setSelectedEvent(null)}>