mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
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:
86
Front-End/src/components/Calendar/MonthView.js
Normal file
86
Front-End/src/components/Calendar/MonthView.js
Normal 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;
|
||||
111
Front-End/src/components/Calendar/PlanningView.js
Normal file
111
Front-End/src/components/Calendar/PlanningView.js
Normal 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;
|
||||
208
Front-End/src/components/Calendar/WeekView.js
Normal file
208
Front-End/src/components/Calendar/WeekView.js
Normal 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;
|
||||
52
Front-End/src/components/Calendar/YearView.js
Normal file
52
Front-End/src/components/Calendar/YearView.js
Normal 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;
|
||||
Reference in New Issue
Block a user