calendar 8
This commit is contained in:
parent
f6b451a388
commit
d76af98aec
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
@ -9,11 +9,28 @@ import frLocale from "@fullcalendar/core/locales/fr";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 { Calendar, Event } from "@prisma/client";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 {
|
interface CalendarClientProps {
|
||||||
initialCalendars: (Calendar & { events: Event[] })[];
|
initialCalendars: (Calendar & { events: Event[] })[];
|
||||||
@ -30,6 +47,192 @@ interface EventFormData {
|
|||||||
calendarId?: string;
|
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) {
|
export function CalendarClient({ initialCalendars, userId }: CalendarClientProps) {
|
||||||
const [calendars, setCalendars] = useState(initialCalendars);
|
const [calendars, setCalendars] = useState(initialCalendars);
|
||||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
||||||
@ -37,7 +240,9 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
);
|
);
|
||||||
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
||||||
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
||||||
|
const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);
|
||||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||||
|
const [selectedCalendar, setSelectedCalendar] = useState<Calendar | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [eventForm, setEventForm] = useState<EventFormData>({
|
const [eventForm, setEventForm] = useState<EventFormData>({
|
||||||
@ -51,6 +256,41 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
|
|
||||||
const calendarRef = useRef<any>(null);
|
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 handleDateSelect = (selectInfo: any) => {
|
||||||
const startDate = new Date(selectInfo.start);
|
const startDate = new Date(selectInfo.start);
|
||||||
const endDate = new Date(selectInfo.end);
|
const endDate = new Date(selectInfo.end);
|
||||||
@ -177,7 +417,8 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</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 justify-between items-center gap-4 mb-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{calendars.map((calendar) => (
|
{calendars.map((calendar) => (
|
||||||
@ -185,10 +426,21 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
key={calendar.id}
|
key={calendar.id}
|
||||||
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
||||||
onClick={() => setSelectedCalendarId(calendar.id)}
|
onClick={() => setSelectedCalendarId(calendar.id)}
|
||||||
|
style={{ backgroundColor: calendar.id === selectedCalendarId ? calendar.color : undefined }}
|
||||||
>
|
>
|
||||||
{calendar.name}
|
{calendar.name}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCalendar(null);
|
||||||
|
setIsCalendarModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Nouveau calendrier
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -252,6 +504,7 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
location: event.location,
|
location: event.location,
|
||||||
calendarId: event.calendarId,
|
calendarId: event.calendarId,
|
||||||
originalEvent: event,
|
originalEvent: event,
|
||||||
|
backgroundColor: cal.color,
|
||||||
}))
|
}))
|
||||||
)}
|
)}
|
||||||
locale={frLocale}
|
locale={frLocale}
|
||||||
@ -268,7 +521,15 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
</Card>
|
</Card>
|
||||||
</Tabs>
|
</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}>
|
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user