calendar 8

This commit is contained in:
Alma 2025-04-13 13:52:53 +02:00
parent f6b451a388
commit d76af98aec

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
@ -9,11 +9,28 @@ import frLocale from "@fullcalendar/core/locales/fr";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Loader2, Plus } from "lucide-react";
import { Loader2, Plus, Calendar as CalendarIcon, Check, X } from "lucide-react";
import { Calendar, Event } from "@prisma/client";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
// Predefined professional color palette
const colorPalette = [
"#4f46e5", // Indigo
"#0891b2", // Cyan
"#0e7490", // Teal
"#16a34a", // Green
"#65a30d", // Lime
"#ca8a04", // Amber
"#d97706", // Orange
"#dc2626", // Red
"#e11d48", // Rose
"#9333ea", // Purple
"#7c3aed", // Violet
"#2563eb", // Blue
];
interface CalendarClientProps {
initialCalendars: (Calendar & { events: Event[] })[];
@ -30,6 +47,192 @@ interface EventFormData {
calendarId?: string;
}
interface CalendarDialogProps {
open: boolean;
onClose: () => void;
onSave: (calendarData: Partial<Calendar>) => Promise<void>;
initialData?: Partial<Calendar>;
}
function CalendarDialog({ open, onClose, onSave, initialData }: CalendarDialogProps) {
const [name, setName] = useState(initialData?.name || "");
const [color, setColor] = useState(initialData?.color || "#4f46e5");
const [description, setDescription] = useState(initialData?.description || "");
const [isSubmitting, setIsSubmitting] = useState(false);
const [customColorMode, setCustomColorMode] = useState(false);
useEffect(() => {
if (open) {
setName(initialData?.name || "");
setColor(initialData?.color || "#4f46e5");
setDescription(initialData?.description || "");
setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5"));
}
}, [open, initialData]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
try {
await onSave({
id: initialData?.id,
name,
color,
description
});
resetForm();
} catch (error) {
console.error("Erreur lors de la création du calendrier:", error);
} finally {
setIsSubmitting(false);
}
};
const resetForm = () => {
setName("");
setColor("#4f46e5");
setDescription("");
setCustomColorMode(false);
onClose();
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-md rounded-xl">
<DialogHeader>
<DialogTitle className="flex items-center text-xl font-semibold text-gray-900">
<CalendarIcon className="w-5 h-5 mr-2 text-indigo-600" />
{initialData?.id ? "Modifier le calendrier" : "Créer un nouveau calendrier"}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-5 py-4">
<div className="space-y-2">
<Label htmlFor="calendar-name" className="text-gray-700">Nom</Label>
<Input
id="calendar-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Nom du calendrier"
required
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div className="space-y-3">
<Label className="text-gray-700">Couleur</Label>
<div className="flex flex-wrap gap-3 mb-3">
{colorPalette.map((paletteColor) => (
<button
key={paletteColor}
type="button"
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all ${
color === paletteColor && !customColorMode
? 'ring-2 ring-offset-2 ring-gray-400'
: 'hover:scale-110'
}`}
style={{ backgroundColor: paletteColor }}
onClick={() => {
setColor(paletteColor);
setCustomColorMode(false);
}}
>
{color === paletteColor && !customColorMode && (
<Check className="w-4 h-4 text-white" />
)}
</button>
))}
<button
type="button"
className={`w-8 h-8 rounded-full flex items-center justify-center bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 transition-all ${
customColorMode
? 'ring-2 ring-offset-2 ring-gray-400'
: 'hover:scale-110'
}`}
onClick={() => setCustomColorMode(true)}
>
{customColorMode && <Check className="w-4 h-4 text-white" />}
</button>
</div>
{customColorMode && (
<div className="flex items-center gap-4 mt-2 p-3 bg-gray-50 rounded-lg">
<div className="flex flex-1 items-center gap-3">
<Input
id="calendar-color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-10 h-10 p-1 cursor-pointer rounded border-gray-300"
/>
<Input
type="text"
value={color}
onChange={(e) => setColor(e.target.value)}
placeholder="#RRGGBB"
className="w-28 rounded-lg"
/>
</div>
<div
className="w-8 h-8 rounded-lg shadow-sm"
style={{ backgroundColor: color }}
></div>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="calendar-description" className="text-gray-700">
Description (optionnelle)
</Label>
<Textarea
id="calendar-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description du calendrier"
rows={3}
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<DialogFooter className="mt-6 border-t border-gray-100 pt-4">
<div className="flex gap-3 w-full sm:justify-end">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isSubmitting}
className="rounded-lg border-gray-300 text-gray-700 hover:bg-gray-50"
>
<X className="w-4 h-4 mr-2" />
Annuler
</Button>
<Button
type="submit"
disabled={!name || isSubmitting}
className="rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white"
>
<Check className="w-4 h-4 mr-2" />
{isSubmitting
? "Enregistrement..."
: initialData?.id
? "Mettre à jour"
: "Créer"
}
</Button>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
export function CalendarClient({ initialCalendars, userId }: CalendarClientProps) {
const [calendars, setCalendars] = useState(initialCalendars);
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
@ -37,7 +240,9 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
);
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [selectedCalendar, setSelectedCalendar] = useState<Calendar | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [eventForm, setEventForm] = useState<EventFormData>({
@ -51,6 +256,41 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
const calendarRef = useRef<any>(null);
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
try {
setLoading(true);
const response = await fetch("/api/calendars", {
method: calendarData.id ? "PUT" : "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...calendarData,
userId,
}),
});
if (!response.ok) {
throw new Error("Erreur lors de la sauvegarde du calendrier");
}
const updatedCalendar = await response.json();
if (calendarData.id) {
setCalendars(calendars.map(cal =>
cal.id === calendarData.id ? { ...cal, ...updatedCalendar } : cal
));
} else {
setCalendars([...calendars, updatedCalendar]);
}
setIsCalendarModalOpen(false);
} catch (error) {
console.error("Error saving calendar:", error);
setError(error instanceof Error ? error.message : "Une erreur est survenue");
} finally {
setLoading(false);
}
};
const handleDateSelect = (selectInfo: any) => {
const startDate = new Date(selectInfo.start);
const endDate = new Date(selectInfo.end);
@ -177,7 +417,8 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
{error}
</div>
)}
{/* Calendar filters and options */}
{/* Calendar management */}
<div className="flex flex-wrap justify-between items-center gap-4 mb-4">
<div className="flex flex-wrap gap-2">
{calendars.map((calendar) => (
@ -185,10 +426,21 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
key={calendar.id}
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
onClick={() => setSelectedCalendarId(calendar.id)}
style={{ backgroundColor: calendar.id === selectedCalendarId ? calendar.color : undefined }}
>
{calendar.name}
</Button>
))}
<Button
variant="outline"
onClick={() => {
setSelectedCalendar(null);
setIsCalendarModalOpen(true);
}}
>
<Plus className="mr-2 h-4 w-4" />
Nouveau calendrier
</Button>
</div>
<Button
onClick={() => {
@ -252,6 +504,7 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
location: event.location,
calendarId: event.calendarId,
originalEvent: event,
backgroundColor: cal.color,
}))
)}
locale={frLocale}
@ -268,7 +521,15 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
</Card>
</Tabs>
{/* Event creation/edit dialog */}
{/* Calendar dialog */}
<CalendarDialog
open={isCalendarModalOpen}
onClose={() => setIsCalendarModalOpen(false)}
onSave={handleCalendarSave}
initialData={selectedCalendar || undefined}
/>
{/* Event dialog */}
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
<DialogContent>
<DialogHeader>