UMBRA/frontend/src/components/calendar/CalendarSidebar.tsx
Kyle Pope 5b056cf674 Add calendar redesign frontend with multi-calendar UI
- Custom toolbar replacing FullCalendar defaults (nav, today, view switcher)
- Calendar sidebar with visibility toggles, color dots, add/edit support
- CalendarForm dialog for creating/editing calendars with color swatches
- EventForm updated to use calendar dropdown instead of color picker
- CSS overrides: accent-tinted today highlight, now indicator, rounded event pills
- Types updated for Calendar interface and mixed id types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:14:06 +08:00

100 lines
3.4 KiB
TypeScript

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Pencil } from 'lucide-react';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
import type { Calendar } from '@/types';
import { useCalendars } from '@/hooks/useCalendars';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import CalendarForm from './CalendarForm';
export default function CalendarSidebar() {
const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars();
const [showForm, setShowForm] = useState(false);
const [editingCalendar, setEditingCalendar] = useState<Calendar | null>(null);
const toggleMutation = useMutation({
mutationFn: async ({ id, is_visible }: { id: number; is_visible: boolean }) => {
await api.put(`/calendars/${id}`, { is_visible });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendars'] });
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
},
onError: (error) => {
toast.error(getErrorMessage(error, 'Failed to update calendar'));
},
});
const handleToggle = (calendar: Calendar) => {
toggleMutation.mutate({ id: calendar.id, is_visible: !calendar.is_visible });
};
const handleEdit = (calendar: Calendar) => {
setEditingCalendar(calendar);
setShowForm(true);
};
const handleCloseForm = () => {
setShowForm(false);
setEditingCalendar(null);
};
return (
<div className="w-56 shrink-0 border-r bg-card flex flex-col">
<div className="p-4 border-b flex items-center justify-between">
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => { setEditingCalendar(null); setShowForm(true); }}
>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-0.5">
{calendars.map((cal) => (
<div
key={cal.id}
className="group flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-card-elevated transition-colors duration-150"
>
<Checkbox
checked={cal.is_visible}
onChange={() => handleToggle(cal)}
className="shrink-0"
style={{
accentColor: cal.color,
borderColor: cal.is_visible ? cal.color : undefined,
backgroundColor: cal.is_visible ? cal.color : undefined,
}}
/>
<span
className="h-2.5 w-2.5 rounded-full shrink-0"
style={{ backgroundColor: cal.color }}
/>
<span className="text-sm text-foreground truncate flex-1">{cal.name}</span>
{!cal.is_system && (
<button
onClick={() => handleEdit(cal)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
>
<Pencil className="h-3.5 w-3.5" />
</button>
)}
</div>
))}
</div>
{showForm && (
<CalendarForm
calendar={editingCalendar}
onClose={handleCloseForm}
/>
)}
</div>
);
}