feat(calendar): weekly list view with grid toggle, defaults to list

This commit is contained in:
2026-07-09 15:13:40 -04:00
parent af913b9923
commit 5d37fb899d
+84 -20
View File
@@ -33,6 +33,7 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
const [end, setEnd] = useState('');
const [description, setDescription] = useState('');
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(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) {
<div className="flex flex-col h-full bg-gb-bg">
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s flex items-center justify-between">
<span> {channelName}</span>
<button onClick={() => setShowForm((p) => !p)} className="text-xs text-gb-fg-f hover:text-gb-orange font-mono">[NEW EVENT]</button>
<div className="flex gap-2">
<button onClick={() => setView('list')} className={`text-xs font-mono ${view === 'list' ? 'text-gb-orange' : 'text-gb-fg-f hover:text-gb-orange'}`}>[LIST]</button>
<button onClick={() => setView('grid')} className={`text-xs font-mono ${view === 'grid' ? 'text-gb-orange' : 'text-gb-fg-f hover:text-gb-orange'}`}>[GRID]</button>
<button onClick={() => setShowForm((p) => !p)} className="text-xs text-gb-fg-f hover:text-gb-orange font-mono">[NEW EVENT]</button>
</div>
</div>
{showForm && (
<div className="p-3 border-b border-gb-bg-t space-y-2 font-mono text-xs">
@@ -107,30 +135,66 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
</div>
{loading && <div className="p-3 text-gb-fg-f text-xs font-mono">[loading...]</div>}
<div className="flex-1 overflow-y-auto p-3">
<div className="grid grid-cols-7 gap-1 text-center text-xs font-mono text-gb-fg-s mb-1">
{['S','M','T','W','T','F','S'].map((d) => <div key={d}>{d}</div>)}
</div>
<div className="grid grid-cols-7 gap-1">
{days.map((day, idx) => (
<div key={idx} className="min-h-16 border border-gb-bg-t p-1 text-xs">
{day !== null && (
<>
<div className="text-gb-fg-f font-mono">{day}</div>
{eventsForDay(day).map((ev) => (
{view === 'grid' ? (
<>
<div className="grid grid-cols-7 gap-1 text-center text-xs font-mono text-gb-fg-s mb-1">
{['S','M','T','W','T','F','S'].map((d) => <div key={d}>{d}</div>)}
</div>
<div className="grid grid-cols-7 gap-1">
{days.map((day, idx) => (
<div key={idx} className="min-h-16 border border-gb-bg-t p-1 text-xs">
{day !== null && (
<>
<div className="text-gb-fg-f font-mono">{day}</div>
{eventsForDay(day).map((ev) => (
<button
key={ev.id}
onClick={() => setSelectedEvent(ev)}
className="w-full text-left mt-1 px-1 py-0.5 truncate text-gb-bg font-mono"
style={{ backgroundColor: ev.color || '#b8bb26' }}
>
{ev.title}
</button>
))}
</>
)}
</div>
))}
</div>
</>
) : (
<div className="space-y-4 font-mono text-xs">
{weeks.length === 0 && !loading && (
<div className="text-gb-fg-f">No events this month.</div>
)}
{weeks.map((week) => (
<div key={week.label}>
<div className="text-gb-orange font-bold mb-1 border-b border-gb-bg-t pb-1">
{week.label}
</div>
{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 (
<button
key={ev.id}
onClick={() => setSelectedEvent(ev)}
className="w-full text-left mt-1 px-1 py-0.5 truncate text-gb-bg font-mono"
style={{ backgroundColor: ev.color || '#b8bb26' }}
className="w-full text-left px-2 py-1 hover:bg-gb-bg-t rounded-sm flex items-center gap-3"
>
{ev.title}
<span className="text-gb-fg-f w-20 shrink-0">{dayName} {dateStr}</span>
<span className="text-gb-fg-s w-28 shrink-0">{startStr}{endStr ? ` \u2013 ${endStr}` : ''}</span>
<span className="text-gb-fg truncate">{ev.title}</span>
</button>
))}
</>
)}
</div>
))}
</div>
);
})}
</div>
))}
</div>
)}
</div>
{selectedEvent && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gb-bg/90 p-4" onClick={() => setSelectedEvent(null)}>