calendar 2
This commit is contained in:
parent
2170594bc4
commit
24d3ae011c
@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { getServerSession } from "next-auth/next";
|
import { getServerSession } from "next-auth/next";
|
||||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@ -12,52 +14,58 @@ export const metadata = {
|
|||||||
export default async function CalendarPage() {
|
export default async function CalendarPage() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
if (!session) {
|
if (!session?.user) {
|
||||||
redirect("/signin");
|
redirect("/api/auth/signin");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Get user's calendars
|
||||||
// Récupérer tous les calendriers de l'utilisateur
|
const userCalendars = await prisma.calendar.findMany({
|
||||||
const userCalendars = await prisma.calendar.findMany({
|
where: {
|
||||||
where: {
|
userId: session.user.username || session.user.email,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
events: {
|
||||||
|
orderBy: {
|
||||||
|
start: 'asc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create default calendar if none exists
|
||||||
|
let calendars = userCalendars;
|
||||||
|
if (calendars.length === 0) {
|
||||||
|
const defaultCalendar = await prisma.calendar.create({
|
||||||
|
data: {
|
||||||
|
name: "Calendrier principal",
|
||||||
|
color: "#0082c9",
|
||||||
|
description: "Calendrier par défaut",
|
||||||
userId: session.user.username || session.user.email,
|
userId: session.user.username || session.user.email,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
events: true,
|
events: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
calendars = [defaultCalendar];
|
||||||
// Si aucun calendrier n'existe, en créer un par défaut
|
|
||||||
let calendars = userCalendars;
|
|
||||||
if (calendars.length === 0) {
|
|
||||||
const defaultCalendar = await prisma.calendar.create({
|
|
||||||
data: {
|
|
||||||
name: "Calendrier principal",
|
|
||||||
color: "#0082c9",
|
|
||||||
description: "Calendrier par défaut",
|
|
||||||
userId: session.user.username || session.user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
calendars = [defaultCalendar];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='container mx-auto py-8'>
|
|
||||||
<div className='mb-6'>
|
|
||||||
<h1 className='text-3xl font-bold'>Calendrier</h1>
|
|
||||||
<p className='text-muted-foreground'>
|
|
||||||
Gérez vos rendez-vous et événements
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<CalendarClient initialCalendars={calendars} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Database Error:", error);
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<p>Unable to load calendar data. Please try again later.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container mx-auto py-8'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-3xl font-bold'>Calendrier</h1>
|
||||||
|
<p className='text-muted-foreground'>
|
||||||
|
Gérez vos rendez-vous et événements
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<CalendarClient
|
||||||
|
initialCalendars={calendars}
|
||||||
|
userId={session.user.username || session.user.email}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useRef } 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";
|
||||||
@ -10,132 +10,163 @@ 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 } from "lucide-react";
|
||||||
import { useCalendarEvents } from "@/hooks/use-calendar-events";
|
import { Calendar, Event } from "@prisma/client";
|
||||||
import { EventDialog } from "@/components/calendar/event-dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Calendar, Calendar as CalendarType } from "@prisma/client";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { CalendarDialog } from "./calendar-dialog";
|
|
||||||
|
|
||||||
interface CalendarClientProps {
|
interface CalendarClientProps {
|
||||||
initialCalendars: CalendarType[];
|
initialCalendars: (Calendar & { events: Event[] })[];
|
||||||
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CalendarClient({ initialCalendars }: CalendarClientProps) {
|
interface EventFormData {
|
||||||
const [isCalendarDialogOpen, setIsCalendarDialogOpen] = useState(false);
|
title: string;
|
||||||
const [calendars, setCalendars] = useState<CalendarType[]>(initialCalendars);
|
description: string | null;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
allDay: boolean;
|
||||||
|
location: string | null;
|
||||||
|
calendarId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CalendarClient({ initialCalendars, userId }: CalendarClientProps) {
|
||||||
|
const [calendars, setCalendars] = useState(initialCalendars);
|
||||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
||||||
initialCalendars[0]?.id || ""
|
initialCalendars[0]?.id || ""
|
||||||
);
|
);
|
||||||
const [view, setView] = useState<
|
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
||||||
"dayGridMonth" | "timeGridWeek" | "timeGridDay"
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
||||||
>("dayGridMonth");
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [dateRange, setDateRange] = useState({
|
const [eventForm, setEventForm] = useState<EventFormData>({
|
||||||
start: new Date(),
|
title: "",
|
||||||
end: new Date(new Date().setMonth(new Date().getMonth() + 1)),
|
description: null,
|
||||||
|
start: "",
|
||||||
|
end: "",
|
||||||
|
allDay: false,
|
||||||
|
location: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarRef = useRef<any>(null);
|
const calendarRef = useRef<any>(null);
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const {
|
|
||||||
events,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh,
|
|
||||||
createEvent,
|
|
||||||
updateEvent,
|
|
||||||
deleteEvent,
|
|
||||||
} = useCalendarEvents(selectedCalendarId, dateRange.start, dateRange.end);
|
|
||||||
|
|
||||||
// Mettre à jour la plage de dates lorsque la vue change
|
|
||||||
const handleDatesSet = (arg: any) => {
|
|
||||||
setDateRange({
|
|
||||||
start: arg.start,
|
|
||||||
end: arg.end,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gérer la sélection d'une plage de dates pour créer un événement
|
|
||||||
const handleDateSelect = (selectInfo: any) => {
|
const handleDateSelect = (selectInfo: any) => {
|
||||||
setSelectedEvent({
|
const startDate = new Date(selectInfo.start);
|
||||||
start: selectInfo.startStr,
|
const endDate = new Date(selectInfo.end);
|
||||||
end: selectInfo.endStr,
|
|
||||||
|
setEventForm({
|
||||||
|
title: "",
|
||||||
|
description: null,
|
||||||
|
start: startDate.toISOString().slice(0, 16),
|
||||||
|
end: endDate.toISOString().slice(0, 16),
|
||||||
allDay: selectInfo.allDay,
|
allDay: selectInfo.allDay,
|
||||||
|
location: null,
|
||||||
|
calendarId: selectedCalendarId,
|
||||||
});
|
});
|
||||||
setIsDialogOpen(true);
|
setIsEventModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gérer le clic sur un événement existant
|
|
||||||
const handleEventClick = (clickInfo: any) => {
|
const handleEventClick = (clickInfo: any) => {
|
||||||
setSelectedEvent({
|
const event = clickInfo.event;
|
||||||
id: clickInfo.event.id,
|
const startDate = new Date(event.start);
|
||||||
title: clickInfo.event.title,
|
const endDate = new Date(event.end || event.start);
|
||||||
description: clickInfo.event.extendedProps.description,
|
|
||||||
start: clickInfo.event.startStr,
|
setSelectedEvent(event.extendedProps.originalEvent);
|
||||||
end: clickInfo.event.endStr,
|
setEventForm({
|
||||||
location: clickInfo.event.extendedProps.location,
|
title: event.title,
|
||||||
allDay: clickInfo.event.allDay,
|
description: event.extendedProps.description,
|
||||||
|
start: startDate.toISOString().slice(0, 16),
|
||||||
|
end: endDate.toISOString().slice(0, 16),
|
||||||
|
allDay: event.isAllDay,
|
||||||
|
location: event.extendedProps.location,
|
||||||
|
calendarId: event.extendedProps.calendarId,
|
||||||
});
|
});
|
||||||
setIsDialogOpen(true);
|
setIsEventModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gérer la création ou mise à jour d'un événement
|
const handleEventSubmit = async () => {
|
||||||
const handleEventSave = async (eventData: any) => {
|
|
||||||
try {
|
try {
|
||||||
if (eventData.id) {
|
setLoading(true);
|
||||||
await updateEvent(eventData);
|
const method = selectedEvent ? "PUT" : "POST";
|
||||||
toast({
|
const url = selectedEvent
|
||||||
title: "Événement mis à jour",
|
? `/api/calendar?id=${selectedEvent.id}`
|
||||||
description: "L'événement a été modifié avec succès.",
|
: "/api/calendar";
|
||||||
});
|
|
||||||
} else {
|
const response = await fetch(url, {
|
||||||
await createEvent({
|
method,
|
||||||
...eventData,
|
headers: {
|
||||||
calendarId: selectedCalendarId,
|
"Content-Type": "application/json",
|
||||||
});
|
},
|
||||||
toast({
|
body: JSON.stringify({
|
||||||
title: "Événement créé",
|
...eventForm,
|
||||||
description: "L'événement a été ajouté au calendrier.",
|
id: selectedEvent?.id,
|
||||||
});
|
start: new Date(eventForm.start),
|
||||||
|
end: new Date(eventForm.end),
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la sauvegarde de l'événement");
|
||||||
}
|
}
|
||||||
setIsDialogOpen(false);
|
|
||||||
refresh();
|
// Refresh calendar data
|
||||||
} catch (error) {
|
const eventsResponse = await fetch("/api/calendar");
|
||||||
console.error("Erreur lors de la sauvegarde de l'événement:", error);
|
const updatedEvents = await eventsResponse.json();
|
||||||
toast({
|
setCalendars(calendars.map(cal => ({
|
||||||
title: "Erreur",
|
...cal,
|
||||||
description: "Impossible d'enregistrer l'événement.",
|
events: updatedEvents.filter((event: Event) => event.calendarId === cal.id)
|
||||||
variant: "destructive",
|
})));
|
||||||
|
|
||||||
|
setIsEventModalOpen(false);
|
||||||
|
setSelectedEvent(null);
|
||||||
|
setEventForm({
|
||||||
|
title: "",
|
||||||
|
description: null,
|
||||||
|
start: "",
|
||||||
|
end: "",
|
||||||
|
allDay: false,
|
||||||
|
location: null,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setError((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gérer la suppression d'un événement
|
const handleEventDelete = async () => {
|
||||||
const handleEventDelete = async (eventId: string) => {
|
if (!selectedEvent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteEvent(eventId);
|
setLoading(true);
|
||||||
toast({
|
const response = await fetch(`/api/calendar?id=${selectedEvent.id}`, {
|
||||||
title: "Événement supprimé",
|
method: "DELETE",
|
||||||
description: "L'événement a été supprimé du calendrier.",
|
|
||||||
});
|
});
|
||||||
setIsDialogOpen(false);
|
|
||||||
refresh();
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la suppression de l'événement");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh calendar data
|
||||||
|
const eventsResponse = await fetch("/api/calendar");
|
||||||
|
const updatedEvents = await eventsResponse.json();
|
||||||
|
setCalendars(calendars.map(cal => ({
|
||||||
|
...cal,
|
||||||
|
events: updatedEvents.filter((event: Event) => event.calendarId === cal.id)
|
||||||
|
})));
|
||||||
|
|
||||||
|
setIsEventModalOpen(false);
|
||||||
|
setSelectedEvent(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression de l'événement:", error);
|
setError((error as Error).message);
|
||||||
toast({
|
} finally {
|
||||||
title: "Erreur",
|
setLoading(false);
|
||||||
description: "Impossible de supprimer l'événement.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Changer la vue du calendrier
|
const handleViewChange = (newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay") => {
|
||||||
const handleViewChange = (
|
|
||||||
newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay"
|
|
||||||
) => {
|
|
||||||
setView(newView);
|
setView(newView);
|
||||||
if (calendarRef.current) {
|
if (calendarRef.current) {
|
||||||
const calendarApi = calendarRef.current.getApi();
|
const calendarApi = calendarRef.current.getApi();
|
||||||
@ -143,126 +174,67 @@ export function CalendarClient({ initialCalendars }: CalendarClientProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Changer le calendrier sélectionné
|
|
||||||
const handleCalendarChange = (calendarId: string) => {
|
|
||||||
setSelectedCalendarId(calendarId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour créer un nouveau calendrier
|
|
||||||
const handleCreateCalendar = async (calendarData: Partial<Calendar>) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/calendars", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(calendarData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Erreur ${response.status}: ${await response.text()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCalendar = await response.json();
|
|
||||||
|
|
||||||
// Mettre à jour la liste des calendriers
|
|
||||||
setCalendars([...calendars, newCalendar]);
|
|
||||||
|
|
||||||
// Sélectionner automatiquement le nouveau calendrier
|
|
||||||
setSelectedCalendarId(newCalendar.id);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Calendrier créé",
|
|
||||||
description: `Le calendrier "${newCalendar.name}" a été créé avec succès.`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la création du calendrier:", error);
|
|
||||||
toast({
|
|
||||||
title: "Erreur",
|
|
||||||
description: "Impossible de créer le calendrier.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className="space-y-4">
|
||||||
{/* Options et filtres du calendrier */}
|
{/* Calendar filters and options */}
|
||||||
<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) => (
|
||||||
<Button
|
<Button
|
||||||
key={calendar.id}
|
key={calendar.id}
|
||||||
variant={
|
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
||||||
calendar.id === selectedCalendarId ? "default" : "outline"
|
onClick={() => setSelectedCalendarId(calendar.id)}
|
||||||
}
|
|
||||||
onClick={() => handleCalendarChange(calendar.id)}
|
|
||||||
style={{
|
|
||||||
backgroundColor:
|
|
||||||
calendar.id === selectedCalendarId
|
|
||||||
? calendar.color
|
|
||||||
: undefined,
|
|
||||||
borderColor: calendar.color,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{calendar.name}
|
{calendar.name}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
size='icon'
|
|
||||||
onClick={() => setIsCalendarDialogOpen(true)}
|
|
||||||
title='Créer un nouveau calendrier'
|
|
||||||
>
|
|
||||||
<Plus className='h-4 w-4' />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
setIsDialogOpen(true);
|
setIsEventModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className='mr-2 h-4 w-4' />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Nouvel événement
|
Nouvel événement
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sélecteur de vue */}
|
{/* View selector */}
|
||||||
<Tabs value={view} className='w-full'>
|
<Tabs value={view} className="w-full">
|
||||||
<TabsList className='mb-4'>
|
<TabsList className="mb-4">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value='dayGridMonth'
|
value="dayGridMonth"
|
||||||
onClick={() => handleViewChange("dayGridMonth")}
|
onClick={() => handleViewChange("dayGridMonth")}
|
||||||
>
|
>
|
||||||
Mois
|
Mois
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value='timeGridWeek'
|
value="timeGridWeek"
|
||||||
onClick={() => handleViewChange("timeGridWeek")}
|
onClick={() => handleViewChange("timeGridWeek")}
|
||||||
>
|
>
|
||||||
Semaine
|
Semaine
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value='timeGridDay'
|
value="timeGridDay"
|
||||||
onClick={() => handleViewChange("timeGridDay")}
|
onClick={() => handleViewChange("timeGridDay")}
|
||||||
>
|
>
|
||||||
Jour
|
Jour
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Affichage du calendrier */}
|
{/* Calendar display */}
|
||||||
<Card className='p-4'>
|
<Card className="p-4">
|
||||||
{error && (
|
{error && (
|
||||||
<div className='p-4 mb-4 text-red-500 bg-red-50 rounded-md'>
|
<div className="p-4 mb-4 text-red-500 bg-red-50 rounded-md">
|
||||||
Erreur: {error.message}
|
Erreur: {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && !events.length ? (
|
{loading ? (
|
||||||
<div className='h-96 flex items-center justify-center'>
|
<div className="h-96 flex items-center justify-center">
|
||||||
<Loader2 className='h-8 w-8 animate-spin text-primary' />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
<span className='ml-2'>Chargement des événements...</span>
|
<span className="ml-2">Chargement des événements...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FullCalendar
|
<FullCalendar
|
||||||
@ -274,51 +246,117 @@ export function CalendarClient({ initialCalendars }: CalendarClientProps) {
|
|||||||
center: "title",
|
center: "title",
|
||||||
right: "",
|
right: "",
|
||||||
}}
|
}}
|
||||||
events={events.map((event) => ({
|
events={calendars.flatMap(cal =>
|
||||||
id: event.id,
|
cal.events.map(event => ({
|
||||||
title: event.title,
|
id: event.id,
|
||||||
start: event.start,
|
title: event.title,
|
||||||
end: event.end,
|
start: event.start,
|
||||||
allDay: event.isAllDay,
|
end: event.end,
|
||||||
extendedProps: {
|
allDay: event.isAllDay,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
},
|
calendarId: event.calendarId,
|
||||||
}))}
|
originalEvent: event,
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
locale={frLocale}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
selectMirror={true}
|
selectMirror={true}
|
||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
weekends={true}
|
weekends={true}
|
||||||
locale={frLocale}
|
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
eventClick={handleEventClick}
|
eventClick={handleEventClick}
|
||||||
datesSet={handleDatesSet}
|
height="auto"
|
||||||
height='auto'
|
|
||||||
aspectRatio={1.8}
|
aspectRatio={1.8}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{isCalendarDialogOpen && (
|
{/* Event creation/edit dialog */}
|
||||||
<CalendarDialog
|
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
|
||||||
open={isCalendarDialogOpen}
|
<DialogContent>
|
||||||
onClose={() => setIsCalendarDialogOpen(false)}
|
<DialogHeader>
|
||||||
onSave={handleCreateCalendar}
|
<DialogTitle>
|
||||||
/>
|
{selectedEvent ? "Modifier l'événement" : "Nouvel événement"}
|
||||||
)}
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
{/* Dialogue pour créer/modifier un événement */}
|
<div className="space-y-4 py-4">
|
||||||
{isDialogOpen && (
|
<div className="space-y-2">
|
||||||
<EventDialog
|
<label>Titre</label>
|
||||||
open={isDialogOpen}
|
<Input
|
||||||
event={selectedEvent}
|
value={eventForm.title}
|
||||||
onClose={() => setIsDialogOpen(false)}
|
onChange={(e) =>
|
||||||
onSave={handleEventSave}
|
setEventForm({ ...eventForm, title: e.target.value })
|
||||||
onDelete={selectedEvent?.id ? handleEventDelete : undefined}
|
}
|
||||||
calendars={calendars}
|
/>
|
||||||
/>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
|
<label>Description</label>
|
||||||
|
<Textarea
|
||||||
|
value={eventForm.description || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, description: e.target.value || null })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label>Début</label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={eventForm.start}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, start: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label>Fin</label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={eventForm.end}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, end: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label>Lieu</label>
|
||||||
|
<Input
|
||||||
|
value={eventForm.location || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, location: e.target.value || null })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex justify-between">
|
||||||
|
{selectedEvent && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleEventDelete}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsEventModalOpen(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleEventSubmit} disabled={loading}>
|
||||||
|
{selectedEvent ? "Modifier" : "Créer"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
173
components/calendar/calendar-widget.tsx
Normal file
173
components/calendar/calendar-widget.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { format, isToday, isTomorrow, addDays } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
import { CalendarIcon, ClockIcon, ChevronRight } from "lucide-react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
type Event = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
isAllDay: boolean;
|
||||||
|
calendarId: string;
|
||||||
|
calendarName?: string;
|
||||||
|
calendarColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CalendarWidget() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
const fetchUpcomingEvents = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const calendarsRes = await fetch("/api/calendars");
|
||||||
|
if (!calendarsRes.ok) {
|
||||||
|
throw new Error("Impossible de charger les calendriers");
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendars = await calendarsRes.json();
|
||||||
|
if (calendars.length === 0) {
|
||||||
|
setEvents([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const nextWeek = addDays(now, 7);
|
||||||
|
|
||||||
|
const allEventsPromises = calendars.map(async (calendar: any) => {
|
||||||
|
const eventsRes = await fetch(
|
||||||
|
`/api/calendars/${calendar.id}/events?start=${now.toISOString()}&end=${nextWeek.toISOString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!eventsRes.ok) {
|
||||||
|
console.warn(
|
||||||
|
`Impossible de charger les événements du calendrier ${calendar.id}`
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await eventsRes.json();
|
||||||
|
return events.map((event: any) => ({
|
||||||
|
...event,
|
||||||
|
calendarName: calendar.name,
|
||||||
|
calendarColor: calendar.color,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const allEventsArrays = await Promise.all(allEventsPromises);
|
||||||
|
const allEvents = allEventsArrays.flat();
|
||||||
|
const sortedEvents = allEvents.sort(
|
||||||
|
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
setEvents(sortedEvents.slice(0, 5));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors du chargement des événements:", err);
|
||||||
|
setError("Impossible de charger les événements à venir");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUpcomingEvents();
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const formatEventDate = (date: string, isAllDay: boolean) => {
|
||||||
|
const eventDate = new Date(date);
|
||||||
|
let dateString = "";
|
||||||
|
|
||||||
|
if (isToday(eventDate)) {
|
||||||
|
dateString = "Aujourd'hui";
|
||||||
|
} else if (isTomorrow(eventDate)) {
|
||||||
|
dateString = "Demain";
|
||||||
|
} else {
|
||||||
|
dateString = format(eventDate, "EEEE d MMMM", { locale: fr });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllDay) {
|
||||||
|
dateString += ` · ${format(eventDate, "HH:mm", { locale: fr })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateString;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className='transition-transform duration-500 ease-in-out transform hover:scale-105'>
|
||||||
|
<CardHeader className='flex flex-row items-center justify-between pb-2'>
|
||||||
|
<CardTitle className='text-lg font-medium'>
|
||||||
|
Événements à venir
|
||||||
|
</CardTitle>
|
||||||
|
<Link href='/calendar' passHref>
|
||||||
|
<Button variant='ghost' size='sm' className='h-8 w-8 p-0'>
|
||||||
|
<ChevronRight className='h-4 w-4' />
|
||||||
|
<span className='sr-only'>Voir le calendrier</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='pb-3'>
|
||||||
|
{loading ? (
|
||||||
|
<div className='flex items-center justify-center py-4'>
|
||||||
|
<div className='h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent' />
|
||||||
|
<span className='ml-2 text-sm text-muted-foreground'>
|
||||||
|
Chargement...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<p className='text-sm text-red-500'>{error}</p>
|
||||||
|
) : events.length === 0 ? (
|
||||||
|
<p className='text-sm text-muted-foreground py-2'>
|
||||||
|
Aucun événement à venir cette semaine
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{events.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className='flex items-start space-x-3 rounded-md border border-muted p-2'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='h-3 w-3 flex-shrink-0 rounded-full mt-1'
|
||||||
|
style={{ backgroundColor: event.calendarColor || "#0082c9" }}
|
||||||
|
/>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<h5
|
||||||
|
className='text-sm font-medium truncate'
|
||||||
|
title={event.title}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</h5>
|
||||||
|
<div className='flex items-center text-xs text-muted-foreground mt-1'>
|
||||||
|
<CalendarIcon className='h-3 w-3 mr-1' />
|
||||||
|
<span>{formatEventDate(event.start, event.isAllDay)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Link href='/calendar' passHref>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
className='w-full transition-all ease-in-out duration-500 bg-muted text-black hover:text-white hover:bg-primary'
|
||||||
|
>
|
||||||
|
Voir tous les événements
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
components/navbar.tsx
Normal file
15
components/navbar.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CalendarButton } from "@/components/navbar/calendar-button";
|
||||||
|
|
||||||
|
// ... existing imports ...
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
return (
|
||||||
|
<header className="...">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* ... existing buttons ... */}
|
||||||
|
<CalendarButton />
|
||||||
|
{/* ... other buttons ... */}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
components/navbar/calendar-button.tsx
Normal file
32
components/navbar/calendar-button.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
export function CalendarButton() {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link href="/calendar">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9"
|
||||||
|
aria-label="Ouvrir le calendrier"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Calendrier</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ import { useRouter, usePathname } from "next/navigation";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { CalendarNav } from "@/components/sidebar/calendar-nav";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -229,6 +230,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
<CalendarNav />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
components/sidebar/calendar-nav.tsx
Normal file
24
components/sidebar/calendar-nav.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function CalendarNav() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isActive = pathname === "/calendar";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href="/calendar"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50",
|
||||||
|
isActive && "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
<span>Calendrier</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user