calendar 2
This commit is contained in:
parent
2170594bc4
commit
24d3ae011c
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { redirect } from "next/navigation";
|
||||
@ -12,52 +14,58 @@ export const metadata = {
|
||||
export default async function CalendarPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
redirect("/signin");
|
||||
if (!session?.user) {
|
||||
redirect("/api/auth/signin");
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupérer tous les calendriers de l'utilisateur
|
||||
const userCalendars = await prisma.calendar.findMany({
|
||||
where: {
|
||||
// Get user's calendars
|
||||
const userCalendars = await prisma.calendar.findMany({
|
||||
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,
|
||||
},
|
||||
include: {
|
||||
events: true,
|
||||
},
|
||||
events: true
|
||||
}
|
||||
});
|
||||
|
||||
// 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>
|
||||
);
|
||||
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>
|
||||
<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";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
@ -10,132 +10,163 @@ 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 { useCalendarEvents } from "@/hooks/use-calendar-events";
|
||||
import { EventDialog } from "@/components/calendar/event-dialog";
|
||||
import { Calendar, Calendar as CalendarType } from "@prisma/client";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { CalendarDialog } from "./calendar-dialog";
|
||||
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";
|
||||
|
||||
interface CalendarClientProps {
|
||||
initialCalendars: CalendarType[];
|
||||
initialCalendars: (Calendar & { events: Event[] })[];
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export function CalendarClient({ initialCalendars }: CalendarClientProps) {
|
||||
const [isCalendarDialogOpen, setIsCalendarDialogOpen] = useState(false);
|
||||
const [calendars, setCalendars] = useState<CalendarType[]>(initialCalendars);
|
||||
interface EventFormData {
|
||||
title: string;
|
||||
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>(
|
||||
initialCalendars[0]?.id || ""
|
||||
);
|
||||
const [view, setView] = useState<
|
||||
"dayGridMonth" | "timeGridWeek" | "timeGridDay"
|
||||
>("dayGridMonth");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: new Date(),
|
||||
end: new Date(new Date().setMonth(new Date().getMonth() + 1)),
|
||||
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
||||
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [eventForm, setEventForm] = useState<EventFormData>({
|
||||
title: "",
|
||||
description: null,
|
||||
start: "",
|
||||
end: "",
|
||||
allDay: false,
|
||||
location: 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) => {
|
||||
setSelectedEvent({
|
||||
start: selectInfo.startStr,
|
||||
end: selectInfo.endStr,
|
||||
const startDate = new Date(selectInfo.start);
|
||||
const endDate = new Date(selectInfo.end);
|
||||
|
||||
setEventForm({
|
||||
title: "",
|
||||
description: null,
|
||||
start: startDate.toISOString().slice(0, 16),
|
||||
end: endDate.toISOString().slice(0, 16),
|
||||
allDay: selectInfo.allDay,
|
||||
location: null,
|
||||
calendarId: selectedCalendarId,
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
setIsEventModalOpen(true);
|
||||
};
|
||||
|
||||
// Gérer le clic sur un événement existant
|
||||
const handleEventClick = (clickInfo: any) => {
|
||||
setSelectedEvent({
|
||||
id: clickInfo.event.id,
|
||||
title: clickInfo.event.title,
|
||||
description: clickInfo.event.extendedProps.description,
|
||||
start: clickInfo.event.startStr,
|
||||
end: clickInfo.event.endStr,
|
||||
location: clickInfo.event.extendedProps.location,
|
||||
allDay: clickInfo.event.allDay,
|
||||
const event = clickInfo.event;
|
||||
const startDate = new Date(event.start);
|
||||
const endDate = new Date(event.end || event.start);
|
||||
|
||||
setSelectedEvent(event.extendedProps.originalEvent);
|
||||
setEventForm({
|
||||
title: event.title,
|
||||
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 handleEventSave = async (eventData: any) => {
|
||||
const handleEventSubmit = async () => {
|
||||
try {
|
||||
if (eventData.id) {
|
||||
await updateEvent(eventData);
|
||||
toast({
|
||||
title: "Événement mis à jour",
|
||||
description: "L'événement a été modifié avec succès.",
|
||||
});
|
||||
} else {
|
||||
await createEvent({
|
||||
...eventData,
|
||||
calendarId: selectedCalendarId,
|
||||
});
|
||||
toast({
|
||||
title: "Événement créé",
|
||||
description: "L'événement a été ajouté au calendrier.",
|
||||
});
|
||||
setLoading(true);
|
||||
const method = selectedEvent ? "PUT" : "POST";
|
||||
const url = selectedEvent
|
||||
? `/api/calendar?id=${selectedEvent.id}`
|
||||
: "/api/calendar";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...eventForm,
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la sauvegarde de l'événement:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible d'enregistrer l'événement.",
|
||||
variant: "destructive",
|
||||
|
||||
// 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);
|
||||
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 (eventId: string) => {
|
||||
const handleEventDelete = async () => {
|
||||
if (!selectedEvent) return;
|
||||
|
||||
try {
|
||||
await deleteEvent(eventId);
|
||||
toast({
|
||||
title: "Événement supprimé",
|
||||
description: "L'événement a été supprimé du calendrier.",
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/calendar?id=${selectedEvent.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
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) {
|
||||
console.error("Erreur lors de la suppression de l'événement:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de supprimer l'événement.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setError((error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Changer la vue du calendrier
|
||||
const handleViewChange = (
|
||||
newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay"
|
||||
) => {
|
||||
const handleViewChange = (newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay") => {
|
||||
setView(newView);
|
||||
if (calendarRef.current) {
|
||||
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 (
|
||||
<div className='space-y-4'>
|
||||
{/* Options et filtres du calendrier */}
|
||||
<div className='flex flex-wrap justify-between items-center gap-4 mb-4'>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<div className="space-y-4">
|
||||
{/* Calendar filters and options */}
|
||||
<div className="flex flex-wrap justify-between items-center gap-4 mb-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{calendars.map((calendar) => (
|
||||
<Button
|
||||
key={calendar.id}
|
||||
variant={
|
||||
calendar.id === selectedCalendarId ? "default" : "outline"
|
||||
}
|
||||
onClick={() => handleCalendarChange(calendar.id)}
|
||||
style={{
|
||||
backgroundColor:
|
||||
calendar.id === selectedCalendarId
|
||||
? calendar.color
|
||||
: undefined,
|
||||
borderColor: calendar.color,
|
||||
}}
|
||||
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
||||
onClick={() => setSelectedCalendarId(calendar.id)}
|
||||
>
|
||||
{calendar.name}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => setIsCalendarDialogOpen(true)}
|
||||
title='Créer un nouveau calendrier'
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sélecteur de vue */}
|
||||
<Tabs value={view} className='w-full'>
|
||||
<TabsList className='mb-4'>
|
||||
{/* View selector */}
|
||||
<Tabs value={view} className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger
|
||||
value='dayGridMonth'
|
||||
value="dayGridMonth"
|
||||
onClick={() => handleViewChange("dayGridMonth")}
|
||||
>
|
||||
Mois
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='timeGridWeek'
|
||||
value="timeGridWeek"
|
||||
onClick={() => handleViewChange("timeGridWeek")}
|
||||
>
|
||||
Semaine
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='timeGridDay'
|
||||
value="timeGridDay"
|
||||
onClick={() => handleViewChange("timeGridDay")}
|
||||
>
|
||||
Jour
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Affichage du calendrier */}
|
||||
<Card className='p-4'>
|
||||
{/* Calendar display */}
|
||||
<Card className="p-4">
|
||||
{error && (
|
||||
<div className='p-4 mb-4 text-red-500 bg-red-50 rounded-md'>
|
||||
Erreur: {error.message}
|
||||
<div className="p-4 mb-4 text-red-500 bg-red-50 rounded-md">
|
||||
Erreur: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !events.length ? (
|
||||
<div className='h-96 flex items-center justify-center'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-primary' />
|
||||
<span className='ml-2'>Chargement des événements...</span>
|
||||
{loading ? (
|
||||
<div className="h-96 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2">Chargement des événements...</span>
|
||||
</div>
|
||||
) : (
|
||||
<FullCalendar
|
||||
@ -274,51 +246,117 @@ export function CalendarClient({ initialCalendars }: CalendarClientProps) {
|
||||
center: "title",
|
||||
right: "",
|
||||
}}
|
||||
events={events.map((event) => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
allDay: event.isAllDay,
|
||||
extendedProps: {
|
||||
events={calendars.flatMap(cal =>
|
||||
cal.events.map(event => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
allDay: event.isAllDay,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
},
|
||||
}))}
|
||||
calendarId: event.calendarId,
|
||||
originalEvent: event,
|
||||
}))
|
||||
)}
|
||||
locale={frLocale}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
dayMaxEvents={true}
|
||||
weekends={true}
|
||||
locale={frLocale}
|
||||
select={handleDateSelect}
|
||||
eventClick={handleEventClick}
|
||||
datesSet={handleDatesSet}
|
||||
height='auto'
|
||||
height="auto"
|
||||
aspectRatio={1.8}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Tabs>
|
||||
|
||||
{isCalendarDialogOpen && (
|
||||
<CalendarDialog
|
||||
open={isCalendarDialogOpen}
|
||||
onClose={() => setIsCalendarDialogOpen(false)}
|
||||
onSave={handleCreateCalendar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Dialogue pour créer/modifier un événement */}
|
||||
{isDialogOpen && (
|
||||
<EventDialog
|
||||
open={isDialogOpen}
|
||||
event={selectedEvent}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
onSave={handleEventSave}
|
||||
onDelete={selectedEvent?.id ? handleEventDelete : undefined}
|
||||
calendars={calendars}
|
||||
/>
|
||||
)}
|
||||
{/* Event creation/edit dialog */}
|
||||
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedEvent ? "Modifier l'événement" : "Nouvel événement"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label>Titre</label>
|
||||
<Input
|
||||
value={eventForm.title}
|
||||
onChange={(e) =>
|
||||
setEventForm({ ...eventForm, title: e.target.value })
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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 Image from "next/image";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { CalendarNav } from "@/components/sidebar/calendar-nav";
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
@ -229,6 +230,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
<span>{item.title}</span>
|
||||
</Button>
|
||||
))}
|
||||
<CalendarNav />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</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