feat(calendar): weekly list view with grid toggle, defaults to list
This commit is contained in:
@@ -33,6 +33,7 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
|
|||||||
const [end, setEnd] = useState('');
|
const [end, setEnd] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
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 monthStart = useMemo(() => new Date(date.getFullYear(), date.getMonth(), 1), [date]);
|
||||||
const monthEnd = useMemo(() => new Date(date.getFullYear(), date.getMonth() + 1, 0), [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 () => {
|
const handleCreate = async () => {
|
||||||
if (!title || !start) return;
|
if (!title || !start) return;
|
||||||
const payload = { title, start_time: start, end_time: end || undefined, description: description || undefined };
|
const payload = { title, start_time: start, end_time: end || undefined, description: description || undefined };
|
||||||
@@ -89,8 +113,12 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
|
|||||||
<div className="flex flex-col h-full bg-gb-bg">
|
<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">
|
<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>
|
<span>○ {channelName}</span>
|
||||||
|
<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>
|
<button onClick={() => setShowForm((p) => !p)} className="text-xs text-gb-fg-f hover:text-gb-orange font-mono">[NEW EVENT]</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="p-3 border-b border-gb-bg-t space-y-2 font-mono text-xs">
|
<div className="p-3 border-b border-gb-bg-t space-y-2 font-mono text-xs">
|
||||||
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="title" className="terminal-input w-full" />
|
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="title" className="terminal-input w-full" />
|
||||||
@@ -107,6 +135,8 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
{loading && <div className="p-3 text-gb-fg-f text-xs font-mono">[loading...]</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="flex-1 overflow-y-auto p-3">
|
||||||
|
{view === 'grid' ? (
|
||||||
|
<>
|
||||||
<div className="grid grid-cols-7 gap-1 text-center text-xs font-mono text-gb-fg-s mb-1">
|
<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>)}
|
{['S','M','T','W','T','F','S'].map((d) => <div key={d}>{d}</div>)}
|
||||||
</div>
|
</div>
|
||||||
@@ -131,6 +161,40 @@ export function CalendarView({ channelId, channelName }: CalendarViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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 px-2 py-1 hover:bg-gb-bg-t rounded-sm flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gb-bg/90 p-4" onClick={() => setSelectedEvent(null)}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gb-bg/90 p-4" onClick={() => setSelectedEvent(null)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user