calendar 8
This commit is contained in:
parent
f6b451a388
commit
d76af98aec
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user