chore: Initial Commit

feat: Gestion des inscriptions [#1]
feat(frontend): Création des vues pour le paramétrage de l'école [#2]
feat: Gestion du login [#6]
fix: Correction lors de la migration des modèle [#8]
feat: Révision du menu principal [#9]
feat: Ajout d'un footer [#10]
feat: Création des dockers compose pour les environnements de
développement et de production [#12]
doc(ci): Mise en place de Husky et d'un suivi de version automatique [#14]
This commit is contained in:
Luc SORIGNET
2024-11-18 10:02:58 +01:00
committed by N3WT DE COMPET
commit af0cd1c840
228 changed files with 22694 additions and 0 deletions

View File

@ -0,0 +1,86 @@
import React from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getEventsForDate } from '@/utils/events';
const MonthView = ({ onDateClick, onEventClick }) => {
const { currentDate, setViewType, setCurrentDate, events } = usePlanning();
// Obtenir tous les jours du mois actuel
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const startDate = startOfWeek(monthStart, { locale: fr, weekStartsOn: 1 });
const endDate = endOfWeek(monthEnd, { locale: fr, weekStartsOn: 1 });
const days = eachDayOfInterval({ start: startDate, end: endDate });
const handleDayClick = (day) => {
setCurrentDate(day); // Met à jour la date courante
setViewType('week'); // Change la vue en mode semaine
};
const renderDay = (day) => {
const isCurrentMonth = isSameMonth(day, currentDate);
const dayEvents = getEventsForDate(day, events);
const isCurrentDay = isToday(day);
return (
<div
key={day.toString()}
className={`p-2 overflow-y-auto relative flex flex-col
${!isCurrentMonth ? 'bg-gray-100 text-gray-400' : ''}
${isCurrentDay ? 'bg-emerald-50' : ''}
hover:bg-gray-100 cursor-pointer border-b border-r`}
onClick={() => handleDayClick(day)}
>
<div className="flex justify-between items-center mb-1">
<span className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
${!isCurrentMonth ? 'text-gray-400' : ''}`}
>
{format(day, 'd')}
</span>
</div>
<div className="space-y-1 flex-1">
{dayEvents.map((event, index) => (
<div
key={event.id}
className="text-xs p-1 rounded truncate cursor-pointer"
style={{
backgroundColor: `${event.color}15`,
color: event.color,
borderLeft: `2px solid ${event.color}`
}}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
{event.title}
</div>
))}
</div>
</div>
);
};
return (
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white">
{/* En-tête des jours de la semaine */}
<div className="grid grid-cols-7 border-b">
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500">
{day}
</div>
))}
</div>
{/* Grille des jours */}
<div className="flex-1 grid grid-cols-7 grid-rows-[repeat(6,1fr)]">
{days.map((day) => renderDay(day))}
</div>
</div>
);
};
export default MonthView;

View File

@ -0,0 +1,111 @@
import React from 'react';
import { format, isSameDay, eachDayOfInterval } from 'date-fns';
import { fr } from 'date-fns/locale';
const PlanningView = ({ events, onEventClick }) => {
// Fonction pour diviser un événement en jours
const splitEventByDays = (event) => {
const start = new Date(event.start);
const end = new Date(event.end);
// Si même jour, retourner l'événement tel quel
if (isSameDay(start, end)) {
return [event];
}
// Sinon, créer une entrée pour chaque jour
const days = eachDayOfInterval({ start, end });
return days.map(day => ({
...event,
displayDate: day,
isMultiDay: true
}));
};
// Aplatir tous les événements en incluant les événements sur plusieurs jours
const flattenedEvents = events
.flatMap(splitEventByDays)
.sort((a, b) => new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start));
return (
<div className="bg-white h-full overflow-auto">
<table className="w-full border-collapse">
<thead className="bg-gray-50 sticky top-0 z-10">
<tr>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Date
</th>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Horaires
</th>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Événement
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{flattenedEvents.map((event, index) => {
const start = new Date(event.displayDate || event.start);
const end = new Date(event.end);
const isMultiDay = event.isMultiDay;
return (
<tr
key={`${event.id}-${index}`}
className="hover:bg-gray-50 cursor-pointer"
onClick={() => onEventClick(event)}
>
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
<div className="flex items-center gap-1">
<span className="font-extrabold">{format(start, 'd')}</span>
<span className="font-semibold">{format(start, 'MMM', { locale: fr }).toLowerCase()}</span>
<span className="font-semibold">{format(start, 'EEE', { locale: fr })}</span>
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: event.color }}
/>
{isMultiDay
? (isSameDay(start, new Date(event.start))
? "À partir de "
: isSameDay(start, end)
? "Jusqu'à "
: "Toute la journée")
: ""
}
{format(new Date(event.start), 'HH:mm')}
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
</div>
</td>
<td className="py-3 px-4">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900">
{event.title}
</div>
{event.description && (
<div className="text-sm text-gray-500">
{event.description}
</div>
)}
{event.location && (
<div className="text-sm text-gray-500">
{event.location}
</div>
)}
</div>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default PlanningView;

View File

@ -0,0 +1,208 @@
import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format, startOfWeek, addDays, differenceInMinutes, isSameDay } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getWeekEvents } from '@/utils/events';
import { isToday } from 'date-fns';
const WeekView = ({ onDateClick, onEventClick, events }) => {
const { currentDate } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date());
const scrollContainerRef = useRef(null); // Ajouter cette référence
// Déplacer ces déclarations avant leur utilisation
const timeSlots = Array.from({ length: 24 }, (_, i) => i);
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
// Maintenant on peut utiliser weekDays
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
// Mettre à jour la position de la ligne toutes les minutes
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(new Date());
}, 60000);
return () => clearInterval(interval);
}, []);
// Modifier l'useEffect pour l'auto-scroll
useEffect(() => {
if (scrollContainerRef.current && isCurrentWeek) {
const currentHour = new Date().getHours();
const scrollPosition = currentHour * 80;
// Ajout d'un délai pour laisser le temps au DOM de se mettre à jour
setTimeout(() => {
scrollContainerRef.current.scrollTop = scrollPosition - 200;
}, 0);
}
}, [currentDate, isCurrentWeek]); // Ajout de currentDate dans les dépendances
// Calculer la position de la ligne de temps
const getCurrentTimePosition = () => {
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
return `${(hours + minutes / 60) * 5}rem`;
};
// Utiliser les événements déjà filtrés passés en props
const weekEventsMap = weekDays.reduce((acc, day) => {
acc[format(day, 'yyyy-MM-dd')] = getWeekEvents(day, events);
return acc;
}, {});
const isWeekend = (date) => {
const day = date.getDay();
return day === 0 || day === 6;
};
const findOverlappingEvents = (event, dayEvents) => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
return dayEvents.filter(otherEvent => {
if (otherEvent.id === event.id) return false;
const otherStart = new Date(otherEvent.start);
const otherEnd = new Date(otherEvent.end);
return !(otherEnd <= eventStart || otherStart >= eventEnd);
});
};
const calculateEventStyle = (event, dayEvents) => {
const start = new Date(event.start);
const end = new Date(event.end);
const startMinutes = (start.getMinutes() / 60) * 5;
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
// Trouver les événements qui se chevauchent
const overlappingEvents = findOverlappingEvents(event, dayEvents);
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
const totalOverlapping = overlappingEvents.length + 1;
// Calculer la largeur et la position horizontale
const width = `calc((100% / ${totalOverlapping}) - 4px)`;
const left = `calc((100% / ${totalOverlapping}) * ${eventIndex})`;
return {
height: `${duration}rem`,
position: 'absolute',
width,
left,
backgroundColor: `${event.color}15`,
borderLeft: `3px solid ${event.color}`,
borderRadius: '0.25rem',
zIndex: 1,
transform: `translateY(${startMinutes}rem)`
};
};
const renderEventInCell = (event, dayEvents) => {
const eventStyle = calculateEventStyle(event, dayEvents);
return (
<div
key={event.id}
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
style={eventStyle}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
<div className="p-1">
<div className="font-semibold text-xs truncate" style={{ color: event.color }}>
{event.title}
</div>
<div className="text-xs" style={{ color: event.color, opacity: 0.75 }}>
{format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
</div>
{event.location && (
<div className="text-xs truncate" style={{ color: event.color, opacity: 0.75 }}>
{event.location}
</div>
)}
</div>
</div>
);
};
return (
<div className="flex flex-col h-full overflow-hidden">
{/* En-tête des jours */}
<div className="grid gap-[1px] bg-gray-100 pr-[17px]" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
<div className="bg-white h-14"></div>
{weekDays.map((day) => (
<div
key={day}
className={`p-2 text-center border-b
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
>
<div className="text-xs font-medium text-gray-500">
{format(day, 'EEEE', { locale: fr })}
</div>
<div className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}>
{format(day, 'd', { locale: fr })}
</div>
</div>
))}
</div>
{/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
{/* Ligne de temps actuelle */}
{isCurrentWeek && (
<div
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
style={{
top: getCurrentTimePosition(),
}}
>
<div
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
/>
</div>
)}
<div className="grid gap-[1px] bg-gray-100" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
{timeSlots.map(hour => (
<React.Fragment key={hour}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hour.toString().padStart(2, '0')}:00`}
</div>
{weekDays.map((day) => {
const dayKey = format(day, 'yyyy-MM-dd');
const dayEvents = weekEventsMap[dayKey] || [];
return (
<div
key={`${hour}-${day}`}
className={`h-20 relative border-b border-gray-100
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
onClick={() => {
const date = new Date(day);
date.setHours(hour);
onDateClick(date);
}}
>
<div className="flex gap-1"> {/* Ajout de gap-1 */}
{dayEvents.filter(event => {
const eventStart = new Date(event.start);
return eventStart.getHours() === hour;
}).map(event => renderEventInCell(event, dayEvents))}
</div>
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
</div>
);
};
export default WeekView;

View File

@ -0,0 +1,52 @@
import React from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getMonthEventCount } from '@/utils/events';
import { isSameMonth } from 'date-fns';
const MonthCard = ({ month, eventCount, onClick }) => (
<div
className={`bg-white p-4 rounded shadow hover:shadow-lg cursor-pointer
${isSameMonth(month, new Date()) ? 'ring-2 ring-emerald-500' : ''}`}
onClick={onClick}
>
<h3 className="font-medium text-center mb-2">
{format(month, 'MMMM', { locale: fr })}
</h3>
<div className="text-center text-sm">
<span className="inline-flex items-center justify-center bg-emerald-100 text-emerald-800 px-2 py-1 rounded-full">
{eventCount} événements
</span>
</div>
</div>
);
const YearView = ({ onDateClick }) => {
const { currentDate, events, setViewType, setCurrentDate } = usePlanning();
const months = Array.from(
{ length: 12 },
(_, i) => new Date(currentDate.getFullYear(), i, 1)
);
const handleMonthClick = (month) => {
setCurrentDate(month);
setViewType('month');
};
return (
<div className="grid grid-cols-4 gap-4 p-4">
{months.map(month => (
<MonthCard
key={month.getTime()}
month={month}
eventCount={getMonthEventCount(month, events)}
onClick={() => handleMonthClick(month)}
/>
))}
</div>
);
};
export default YearView;