Compare commits
3 Commits
901c766ced
...
c5adc316ef
| Author | SHA1 | Date | |
|---|---|---|---|
| c5adc316ef | |||
| d5080f59af | |||
| a352a50b63 |
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import FullCalendar from '@fullcalendar/react';
|
||||
@ -12,9 +12,12 @@ import EventForm from './EventForm';
|
||||
|
||||
export default function CalendarPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const calendarRef = useRef<FullCalendar>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||
const [selectedStart, setSelectedStart] = useState<string | null>(null);
|
||||
const [selectedEnd, setSelectedEnd] = useState<string | null>(null);
|
||||
const [selectedAllDay, setSelectedAllDay] = useState(false);
|
||||
|
||||
const { data: events = [] } = useQuery({
|
||||
queryKey: ['calendar-events'],
|
||||
@ -24,8 +27,43 @@ export default function CalendarPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const eventDropMutation = useMutation({
|
||||
mutationFn: async ({ id, start, end, allDay }: { id: number; start: string; end: string; allDay: boolean }) => {
|
||||
const toLocalDatetime = (d: Date): string => {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
};
|
||||
|
||||
const updateEventTimes = (
|
||||
id: number,
|
||||
start: string,
|
||||
end: string,
|
||||
allDay: boolean,
|
||||
revert: () => void,
|
||||
) => {
|
||||
// Optimistically update cache so re-renders don't snap back
|
||||
queryClient.setQueryData<CalendarEvent[]>(['calendar-events'], (old) =>
|
||||
old?.map((e) =>
|
||||
e.id === id
|
||||
? { ...e, start_datetime: start, end_datetime: end, all_day: allDay }
|
||||
: e,
|
||||
),
|
||||
);
|
||||
|
||||
eventMutation.mutate({ id, start, end, allDay, revert });
|
||||
};
|
||||
|
||||
const eventMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
allDay,
|
||||
}: {
|
||||
id: number;
|
||||
start: string;
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
revert: () => void;
|
||||
}) => {
|
||||
const response = await api.put(`/events/${id}`, {
|
||||
start_datetime: start,
|
||||
end_datetime: end,
|
||||
@ -35,11 +73,12 @@ export default function CalendarPage() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
||||
toast.success('Event moved');
|
||||
toast.success('Event updated');
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error, variables) => {
|
||||
variables.revert();
|
||||
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
||||
toast.error(getErrorMessage(error, 'Failed to move event'));
|
||||
toast.error(getErrorMessage(error, 'Failed to update event'));
|
||||
},
|
||||
});
|
||||
|
||||
@ -61,31 +100,50 @@ export default function CalendarPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const toLocalDatetime = (d: Date): string => {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
};
|
||||
|
||||
const handleEventDrop = (info: EventDropArg) => {
|
||||
const id = parseInt(info.event.id);
|
||||
const start = info.event.allDay
|
||||
? info.event.startStr
|
||||
: info.event.start ? toLocalDatetime(info.event.start) : info.event.startStr;
|
||||
: info.event.start
|
||||
? toLocalDatetime(info.event.start)
|
||||
: info.event.startStr;
|
||||
const end = info.event.allDay
|
||||
? info.event.endStr || info.event.startStr
|
||||
: info.event.end ? toLocalDatetime(info.event.end) : start;
|
||||
eventDropMutation.mutate({ id, start, end, allDay: info.event.allDay });
|
||||
: info.event.end
|
||||
? toLocalDatetime(info.event.end)
|
||||
: start;
|
||||
updateEventTimes(id, start, end, info.event.allDay, info.revert);
|
||||
};
|
||||
|
||||
const handleEventResize = (info: { event: EventDropArg['event']; revert: () => void }) => {
|
||||
const id = parseInt(info.event.id);
|
||||
const start = info.event.allDay
|
||||
? info.event.startStr
|
||||
: info.event.start
|
||||
? toLocalDatetime(info.event.start)
|
||||
: info.event.startStr;
|
||||
const end = info.event.allDay
|
||||
? info.event.endStr || info.event.startStr
|
||||
: info.event.end
|
||||
? toLocalDatetime(info.event.end)
|
||||
: start;
|
||||
updateEventTimes(id, start, end, info.event.allDay, info.revert);
|
||||
};
|
||||
|
||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||
setSelectedDate(selectInfo.startStr);
|
||||
setSelectedStart(selectInfo.startStr);
|
||||
setSelectedEnd(selectInfo.endStr);
|
||||
setSelectedAllDay(selectInfo.allDay);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
calendarRef.current?.getApi().unselect();
|
||||
setShowForm(false);
|
||||
setEditingEvent(null);
|
||||
setSelectedDate(null);
|
||||
setSelectedStart(null);
|
||||
setSelectedEnd(null);
|
||||
setSelectedAllDay(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -97,6 +155,7 @@ export default function CalendarPage() {
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="bg-card rounded-lg border p-4">
|
||||
<FullCalendar
|
||||
ref={calendarRef}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
headerToolbar={{
|
||||
@ -108,10 +167,12 @@ export default function CalendarPage() {
|
||||
editable={true}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
unselectAuto={false}
|
||||
dayMaxEvents={true}
|
||||
weekends={true}
|
||||
eventClick={handleEventClick}
|
||||
eventDrop={handleEventDrop}
|
||||
eventResize={handleEventResize}
|
||||
select={handleDateSelect}
|
||||
height="auto"
|
||||
/>
|
||||
@ -119,7 +180,13 @@ export default function CalendarPage() {
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<EventForm event={editingEvent} initialDate={selectedDate} onClose={handleCloseForm} />
|
||||
<EventForm
|
||||
event={editingEvent}
|
||||
initialStart={selectedStart}
|
||||
initialEnd={selectedEnd}
|
||||
initialAllDay={selectedAllDay}
|
||||
onClose={handleCloseForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -20,7 +20,9 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
interface EventFormProps {
|
||||
event: CalendarEvent | null;
|
||||
initialDate?: string | null;
|
||||
initialStart?: string | null;
|
||||
initialEnd?: string | null;
|
||||
initialAllDay?: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@ -54,11 +56,11 @@ function formatForInput(dt: string, allDay: boolean, fallbackTime: string = '09:
|
||||
return allDay ? toDateOnly(dt) : toDatetimeLocal(dt, fallbackTime);
|
||||
}
|
||||
|
||||
export default function EventForm({ event, initialDate, onClose }: EventFormProps) {
|
||||
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, onClose }: EventFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isAllDay = event?.all_day ?? false;
|
||||
const rawStart = event?.start_datetime || initialDate || '';
|
||||
const rawEnd = event?.end_datetime || initialDate || '';
|
||||
const isAllDay = event?.all_day ?? initialAllDay ?? false;
|
||||
const rawStart = event?.start_datetime || initialStart || '';
|
||||
const rawEnd = event?.end_datetime || initialEnd || '';
|
||||
const [formData, setFormData] = useState({
|
||||
title: event?.title || '',
|
||||
description: event?.description || '',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user