import { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { createRoute } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useQueryClient, useMutation } from "@tanstack/react-query"; import { api, useApiQuery, useApiMutation } from "@/lib/api"; import { useApiDomain } from "@/lib/stores/use-active-domain-store"; import { useRealtime } from "@/hooks/use-realtime"; import { Plus, Trash2, ChevronLeft, ChevronRight } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { LoadingState, EmptyState } from "@/components/state"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import type { CalendarEvent } from "@/lib/types"; import { format, parseISO, addDays, startOfWeek, getDay } from "date-fns"; // react-big-calendar import { Calendar, dateFnsLocalizer, Navigate, type View, type stringOrDate } from "react-big-calendar"; import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "react-big-calendar/lib/addons/dragAndDrop/styles.css"; const localizer = dateFnsLocalizer({ startOfWeek, getDay, format, locales: {}, }); // Mapped calendar event shape used by react-big-calendar (adds Date start/end accessors) type CalendarViewEvent = CalendarEvent & { start: Date; end: Date }; const DragAndDropCalendar = withDragAndDrop(Calendar); // Color palette: tasks=accent, events by domain const EVENT_COLORS: Record = { task: "var(--accent-hsl, 217 91% 60%)", habit: "142 71% 45%", project: "271 81% 56%", note: "24 95% 53%", default: "215 16% 47%", }; function eventColor(event: CalendarEvent): string { const hue = event.entityType ? EVENT_COLORS[event.entityType] || EVENT_COLORS.default : EVENT_COLORS.default; return `hsl(${hue})`; } function eventBg(event: CalendarEvent): string { const hue = event.entityType ? EVENT_COLORS[event.entityType] || EVENT_COLORS.default : EVENT_COLORS.default; return `hsla(${hue}, 0.15)`; } // Custom event component function EventComponent({ event }: { event: CalendarEvent }) { return (
{event.title}
); } // Custom toolbar function CustomToolbar({ date, onNavigate, label }: any) { const goToBack = () => onNavigate(Navigate.PREVIOUS); const goToNext = () => onNavigate(Navigate.NEXT); const goToToday = () => onNavigate(Navigate.TODAY); return (
{label}
); } function EventForm({ event, onClose }: { event?: CalendarEvent; onClose: () => void }) { const queryClient = useQueryClient(); const [title, setTitle] = useState(event?.title || ""); const [startTime, setStartTime] = useState(event?.startTime ? event.startTime.slice(0, 16) : ""); const [endTime, setEndTime] = useState(event?.endTime ? event.endTime.slice(0, 16) : ""); const [allDay, setAllDay] = useState(event?.allDay || false); const [color, setColor] = useState(event?.color || "#3b82f6"); const createMutation = useApiMutation("post", "/calendar/events", { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["calendar-events"] }); toast.success("Event created"); onClose(); }, onError: (err) => toast.error(err.message || "Failed to create event"), }); const updateMutation = useApiMutation("patch", event ? `/calendar/events/${event.id}` : "", { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["calendar-events"] }); toast.success("Event updated"); onClose(); }, onError: (err) => toast.error(err.message || "Failed to update event"), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) return; const data: any = { title: title.trim(), allDay, color }; if (startTime) data.startTime = new Date(startTime).toISOString(); if (endTime) data.endTime = new Date(endTime).toISOString(); if (event) updateMutation.mutate(data); else createMutation.mutate(data); }; return (
setTitle(e.target.value)} placeholder="Event title" required />
setAllDay(e.target.checked)} className="rounded" />
setStartTime(e.target.value)} />
setEndTime(e.target.value)} />
setColor(e.target.value)} />
); } function CalendarPage() { const queryClient = useQueryClient(); const [date, setDate] = useState(new Date()); const [view, setView] = useState("month"); const [createOpen, setCreateOpen] = useState(false); const [selectedEvent, setSelectedEvent] = useState(null); const [eventDetailOpen, setEventDetailOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); useRealtime({ enabled: true }); useEffect(() => { const check = () => setIsMobile(window.innerWidth < 768); check(); window.addEventListener("resize", check); return () => window.removeEventListener("resize", check); }, []); const activeDomainId = useApiDomain(); const { data: eventsData, isLoading: eventsLoading } = useApiQuery<{ items: CalendarEvent[]; totalItems: number }>( ["calendar-events", activeDomainId, date.toISOString()], `/calendar/events?from=${new Date(0).toISOString()}&to=${new Date("2100-01-01").toISOString()}` + (activeDomainId ? "&domain=" + activeDomainId : "") ); const events = eventsData?.items || []; const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete("/calendar/events/" + id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["calendar-events"] }); setEventDetailOpen(false); toast.success("Event deleted"); }, onError: (err) => toast.error(err.message || "Failed to delete event"), }); // Map API events to react-big-calendar format const calendarEvents = useMemo(() => { return events.map((evt) => ({ ...evt, start: parseISO(evt.startTime), end: evt.endTime ? parseISO(evt.endTime) : addDays(parseISO(evt.startTime), 1), })); }, [events]); const handleSelectEvent = useCallback((event: any) => { setSelectedEvent(event as CalendarEvent); setEventDetailOpen(true); }, []); const handleSelectSlot = useCallback(() => { setCreateOpen(true); }, []); const handleEventDrop = useCallback( ({ event, start, end }: { event: any; start: stringOrDate; end: stringOrDate }) => { api.patch(`/calendar/events/${event.id}`, { startTime: start instanceof Date ? start.toISOString() : new Date(start).toISOString(), endTime: end instanceof Date ? end.toISOString() : new Date(end).toISOString(), }).then(() => { queryClient.invalidateQueries({ queryKey: ["calendar-events"] }); }); }, [queryClient] ); const handleEventResize = useCallback( ({ event, start, end }: { event: any; start: stringOrDate; end: stringOrDate }) => { api.patch(`/calendar/events/${event.id}`, { startTime: start instanceof Date ? start.toISOString() : new Date(start).toISOString(), endTime: end instanceof Date ? end.toISOString() : new Date(end).toISOString(), }).then(() => { queryClient.invalidateQueries({ queryKey: ["calendar-events"] }); }); }, [queryClient] ); const handleNavigate = useCallback((newDate: Date) => { setDate(newDate); }, []); const handleViewChange = useCallback((newView: string) => { setView(newView as View); }, []); return (

Calendar

New Event setCreateOpen(false)} />
{["month", "week", "day", "agenda"].map((name) => ( ))}
{eventsLoading && events.length === 0 ? ( ) : !eventsLoading && events.length === 0 ? ( setCreateOpen(true)}> New Event } /> ) : (
, }} views={["month", "week", "work_week", "day", "agenda"]} step={30} timeslots={2} style={{ height: isMobile ? 400 : 600 }} />
)} {/* Event detail dialog */} {selectedEvent?.title || "Event"} {selectedEvent && (

Start: {new Date(selectedEvent.startTime).toLocaleString()}

{selectedEvent.endTime &&

End: {new Date(selectedEvent.endTime).toLocaleString()}

} {selectedEvent.description &&

{selectedEvent.description}

}
setEventDetailOpen(false)} /> Delete Event Are you sure you want to delete "{selectedEvent.title}"? Cancel deleteMutation.mutate(selectedEvent.id)} className="bg-destructive text-destructive-foreground">Delete
)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/calendar", component: CalendarPage, });