+ const handleDatesSet = (arg: DatesSetArg) => {
+ setCalendarTitle(arg.view.title);
+ setCurrentView(arg.view.type as CalendarView);
+ };
-
-
-
+ const navigatePrev = () => calendarRef.current?.getApi().prev();
+ const navigateNext = () => calendarRef.current?.getApi().next();
+ const navigateToday = () => calendarRef.current?.getApi().today();
+ const changeView = (view: CalendarView) => calendarRef.current?.getApi().changeView(view);
+
+ return (
+
+
+
+
+ {/* Custom toolbar */}
+
+
+
+
+
+
+
{calendarTitle}
+
+ {(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
+
+ ))}
+
+
+
+ {/* Calendar grid */}
+
diff --git a/frontend/src/components/calendar/CalendarSidebar.tsx b/frontend/src/components/calendar/CalendarSidebar.tsx
new file mode 100644
index 0000000..0dce1e1
--- /dev/null
+++ b/frontend/src/components/calendar/CalendarSidebar.tsx
@@ -0,0 +1,99 @@
+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
(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 (
+
+
+
Calendars
+
+
+
+ {calendars.map((cal) => (
+
+
handleToggle(cal)}
+ className="shrink-0"
+ style={{
+ accentColor: cal.color,
+ borderColor: cal.is_visible ? cal.color : undefined,
+ backgroundColor: cal.is_visible ? cal.color : undefined,
+ }}
+ />
+
+ {cal.name}
+ {!cal.is_system && (
+
+ )}
+
+ ))}
+
+
+ {showForm && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/calendar/EventForm.tsx b/frontend/src/components/calendar/EventForm.tsx
index 6e3dd13..0d16b43 100644
--- a/frontend/src/components/calendar/EventForm.tsx
+++ b/frontend/src/components/calendar/EventForm.tsx
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
import type { CalendarEvent, Location } from '@/types';
+import { useCalendars } from '@/hooks/useCalendars';
import {
Dialog,
DialogContent,
@@ -26,31 +27,17 @@ interface EventFormProps {
onClose: () => void;
}
-const colorPresets = [
- { name: 'Accent', value: '' },
- { name: 'Red', value: '#ef4444' },
- { name: 'Orange', value: '#f97316' },
- { name: 'Yellow', value: '#eab308' },
- { name: 'Green', value: '#22c55e' },
- { name: 'Blue', value: '#3b82f6' },
- { name: 'Purple', value: '#8b5cf6' },
- { name: 'Pink', value: '#ec4899' },
-];
-
-// Extract just the date portion (YYYY-MM-DD) from any date/datetime string
function toDateOnly(dt: string): string {
if (!dt) return '';
return dt.split('T')[0];
}
-// Ensure a datetime string is in datetime-local format (YYYY-MM-DDThh:mm)
function toDatetimeLocal(dt: string, fallbackTime: string = '09:00'): string {
if (!dt) return '';
- if (dt.includes('T')) return dt.slice(0, 16); // trim seconds/timezone
+ if (dt.includes('T')) return dt.slice(0, 16);
return `${dt}T${fallbackTime}`;
}
-// Format a date/datetime string for the correct input type
function formatForInput(dt: string, allDay: boolean, fallbackTime: string = '09:00'): string {
if (!dt) return '';
return allDay ? toDateOnly(dt) : toDatetimeLocal(dt, fallbackTime);
@@ -58,9 +45,14 @@ function formatForInput(dt: string, allDay: boolean, fallbackTime: string = '09:
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, onClose }: EventFormProps) {
const queryClient = useQueryClient();
+ const { data: calendars = [] } = useCalendars();
const isAllDay = event?.all_day ?? initialAllDay ?? false;
const rawStart = event?.start_datetime || initialStart || '';
const rawEnd = event?.end_datetime || initialEnd || '';
+
+ const defaultCalendar = calendars.find((c) => c.is_default);
+ const initialCalendarId = event?.calendar_id?.toString() || defaultCalendar?.id?.toString() || '';
+
const [formData, setFormData] = useState({
title: event?.title || '',
description: event?.description || '',
@@ -68,7 +60,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
end_datetime: formatForInput(rawEnd, isAllDay, '10:00'),
all_day: isAllDay,
location_id: event?.location_id?.toString() || '',
- color: event?.color || '',
+ calendar_id: initialCalendarId,
recurrence_rule: event?.recurrence_rule || '',
is_starred: event?.is_starred || false,
});
@@ -81,11 +73,15 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
},
});
+ // Filter out system calendars (Birthdays) from the dropdown
+ const selectableCalendars = calendars.filter((c) => !c.is_system);
+
const mutation = useMutation({
mutationFn: async (data: typeof formData) => {
const payload = {
...data,
location_id: data.location_id ? parseInt(data.location_id) : null,
+ calendar_id: data.calendar_id ? parseInt(data.calendar_id) : null,
};
if (event) {
const response = await api.put(`/events/${event.id}`, payload);
@@ -196,6 +192,21 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
+
+
+
+
+
-
-
-
-
-