424 lines
10 KiB
TypeScript
424 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import style from "./page.module.scss";
|
|
import moment, { Moment, unitOfTime } from "moment";
|
|
import { FcGraduationCap } from "react-icons/fc";
|
|
import { TbArrowNarrowLeft, TbArrowNarrowRight } from "react-icons/tb";
|
|
import { MdOutlineArrowDropUp } from "react-icons/md";
|
|
import Cookies from "universal-cookie";
|
|
import { motion } from "framer-motion";
|
|
import promos from "../constants/promos";
|
|
|
|
const cookies = new Cookies();
|
|
moment.locale("fr");
|
|
|
|
const Days = Object.freeze(["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]);
|
|
const Months: { [key: string]: string } = Object.freeze({
|
|
January: "Janvier",
|
|
February: "Février",
|
|
March: "Mars",
|
|
April: "Avril",
|
|
May: "Mai",
|
|
June: "Juin",
|
|
July: "Juillet",
|
|
August: "Août",
|
|
September: "Septembre",
|
|
October: "Octobre",
|
|
November: "Novembre",
|
|
December: "Décembre",
|
|
});
|
|
|
|
type Promo = {
|
|
name: string;
|
|
id: number;
|
|
promoId: string;
|
|
campus: 0 | 1;
|
|
};
|
|
|
|
type Week = {
|
|
start: Moment;
|
|
end: Moment;
|
|
count: number;
|
|
};
|
|
|
|
type RawPlanningEvent = {
|
|
title: string;
|
|
room: string;
|
|
teacher: string;
|
|
start: string;
|
|
end: string;
|
|
color: string;
|
|
};
|
|
|
|
type PlanningEvent = {
|
|
title: string;
|
|
room: string;
|
|
teacher: string;
|
|
start: Moment;
|
|
end: Moment;
|
|
color: string;
|
|
};
|
|
|
|
export async function getGroups(promo: Promo): Promise<string[]> {
|
|
const url = new URL("http://localhost:3000/api/groups");
|
|
url.searchParams.append("promo", promo.id.toString());
|
|
|
|
const res = await fetch(url.toString(), {
|
|
next: { revalidate: 86400 },
|
|
});
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export async function getPlanning(
|
|
promo: Promo,
|
|
group: string,
|
|
start: Moment,
|
|
end: Moment
|
|
): Promise<RawPlanningEvent[]> {
|
|
const url = new URL("http://localhost:3000/api/planning");
|
|
|
|
url.searchParams.append("start", start.toJSON());
|
|
url.searchParams.append("end", end.toJSON());
|
|
url.searchParams.append("grp", group);
|
|
url.searchParams.append("promo", promo.id.toString());
|
|
|
|
const res = await fetch(url.toString(), {
|
|
next: { revalidate: 0 },
|
|
});
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export default function Planning() {
|
|
const [group, setGroup] = useState(null as string | null);
|
|
const [promo, setPromo] = useState(null as Promo | null);
|
|
const [groups, setGroups] = useState([] as string[]);
|
|
const [groupsVisible, setGroupsVisible] = useState(false);
|
|
const [promosVisible, setPromosVisible] = useState(false);
|
|
const [mobile, setMobile] = useState(true);
|
|
|
|
const [week, setWeek] = useState({
|
|
start: moment().startOf("day"),
|
|
end: moment().endOf("day"),
|
|
count: 0,
|
|
});
|
|
|
|
const [days, setDays] = useState(
|
|
new Array(1).fill(null).map(() => []) as PlanningEvent[][]
|
|
);
|
|
|
|
async function fetchPlanning(
|
|
week: {
|
|
start: Moment;
|
|
end: Moment;
|
|
count: number;
|
|
},
|
|
group: string,
|
|
promo: Promo,
|
|
isMobile: boolean
|
|
) {
|
|
const planning = await getPlanning(promo, group, week.start, week.end);
|
|
const planningDays: PlanningEvent[][] = new Array(isMobile ? 1 : 5)
|
|
.fill(null)
|
|
.map(() => []);
|
|
|
|
planning.forEach((event) => {
|
|
const start = moment(event.start);
|
|
const end = moment(event.end);
|
|
const day = start.diff(week.start, "day") - 1;
|
|
|
|
planningDays[isMobile ? 0 : day].push({
|
|
title: event.title,
|
|
room: event.room,
|
|
teacher: event.teacher,
|
|
start: start,
|
|
end: end,
|
|
color: event.color,
|
|
});
|
|
});
|
|
|
|
setDays(planningDays);
|
|
}
|
|
|
|
function defineGroup(newGroup: string) {
|
|
cookies.set("group", newGroup);
|
|
setGroup(newGroup);
|
|
setGroupsVisible(false);
|
|
|
|
if (promo) fetchPlanning(week, newGroup, promo, mobile);
|
|
}
|
|
|
|
async function definePromo(newPromo: Promo) {
|
|
cookies.set("promo", newPromo);
|
|
setPromo(newPromo);
|
|
setPromosVisible(false);
|
|
|
|
const groups = await getGroups(newPromo);
|
|
const defaultGroup = cookies.get("group") ?? groups[0];
|
|
|
|
setGroups(groups);
|
|
setGroup(defaultGroup);
|
|
|
|
fetchPlanning(week, defaultGroup, newPromo, mobile);
|
|
}
|
|
|
|
function addWeek(addCount: number) {
|
|
const unit: unitOfTime.DurationConstructor = mobile ? "day" : "week";
|
|
|
|
if (addCount === 0) {
|
|
var count = 0;
|
|
var start = moment().startOf(unit);
|
|
var end = moment().endOf(unit);
|
|
} else {
|
|
var count = week.count + addCount;
|
|
var start = moment().startOf(unit).add(count, unit);
|
|
var end = moment().endOf(unit).add(count, unit);
|
|
}
|
|
|
|
setWeek({ count, start, end });
|
|
if (group && promo) {
|
|
fetchPlanning({ count, start, end }, group, promo, mobile);
|
|
}
|
|
}
|
|
|
|
function getDayStart(events: PlanningEvent[]): Moment {
|
|
return events.reduce((pre: any, val) => {
|
|
if (!pre) return val.start;
|
|
if (val.start.diff(pre, "minutes") < 0) return val.start;
|
|
return pre;
|
|
}, null);
|
|
}
|
|
|
|
function getDayEnd(events: PlanningEvent[]): Moment {
|
|
return events.reduce((pre: any, val) => {
|
|
if (!pre) return val.end;
|
|
if (val.end.diff(pre, "minutes") > 0) return val.end;
|
|
return pre;
|
|
}, null);
|
|
}
|
|
|
|
async function updatePlanning(week: Week, mobile: boolean) {
|
|
const defaultPromo = cookies.get("promo") ?? promos[0];
|
|
setPromo(defaultPromo);
|
|
|
|
const groups = await getGroups(defaultPromo);
|
|
const defaultGroup = cookies.get("group") ?? groups[0];
|
|
|
|
setGroups(groups);
|
|
setGroup(defaultGroup);
|
|
|
|
fetchPlanning(week, defaultGroup, defaultPromo, mobile);
|
|
}
|
|
|
|
useEffect(() => {
|
|
let isMobile = true;
|
|
let currentWeek = week;
|
|
|
|
function handleResize() {
|
|
isMobile = window.innerWidth < 768;
|
|
const unit: unitOfTime.DurationConstructor = isMobile ? "day" : "week";
|
|
|
|
currentWeek = {
|
|
start: moment().startOf(unit),
|
|
end: moment().endOf(unit),
|
|
count: 0,
|
|
};
|
|
|
|
setMobile(isMobile);
|
|
setWeek(currentWeek);
|
|
|
|
setDays(
|
|
new Array(isMobile ? 1 : 5)
|
|
.fill(null)
|
|
.map(() => []) as PlanningEvent[][]
|
|
);
|
|
}
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
handleResize();
|
|
|
|
updatePlanning(currentWeek, isMobile);
|
|
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<motion.section
|
|
initial={{ y: -50, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: -50, opacity: 0 }}
|
|
transition={{
|
|
ease: "easeInOut",
|
|
duration: 0.5,
|
|
}}
|
|
className={style.buttons}
|
|
>
|
|
<article className={style.selectionBar}>
|
|
<button
|
|
className={style.selected}
|
|
onClick={() => setPromosVisible(!promosVisible)}
|
|
>
|
|
<p>{promo?.name}</p>
|
|
<MdOutlineArrowDropUp
|
|
className={
|
|
promosVisible ? style.unfoldIconUp : style.unfoldIconDown
|
|
}
|
|
/>
|
|
</button>
|
|
<div
|
|
className={style.choices}
|
|
style={{
|
|
pointerEvents: promosVisible ? "all" : "none",
|
|
opacity: +promosVisible,
|
|
animation: promosVisible
|
|
? "showDown 200ms ease"
|
|
: "hideUp 200ms ease",
|
|
}}
|
|
>
|
|
{promos.map((p, i) => (
|
|
<button
|
|
className={style.choice}
|
|
onClick={() => definePromo(p)}
|
|
key={i}
|
|
>
|
|
{p.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</article>
|
|
{groups.length > 1 && (
|
|
<article className={style.selectionBar}>
|
|
<button
|
|
className={style.selected}
|
|
onClick={() => setGroupsVisible(!groupsVisible)}
|
|
>
|
|
<p>Groupe {group}</p>
|
|
<MdOutlineArrowDropUp
|
|
className={
|
|
groupsVisible ? style.unfoldIconUp : style.unfoldIconDown
|
|
}
|
|
/>
|
|
</button>
|
|
<div
|
|
className={style.choices}
|
|
style={{
|
|
pointerEvents: groupsVisible ? "all" : "none",
|
|
opacity: +groupsVisible,
|
|
animation: groupsVisible
|
|
? "showDown 200ms ease"
|
|
: "hideUp 200ms ease",
|
|
}}
|
|
>
|
|
{groups.map((g, i) => (
|
|
<button
|
|
className={style.choice}
|
|
onClick={() => defineGroup(g)}
|
|
key={i}
|
|
>
|
|
Groupe {g}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</article>
|
|
)}
|
|
<article className={style.directions}>
|
|
<button onClick={() => addWeek(-1)} className={style.directionButton}>
|
|
<TbArrowNarrowLeft />
|
|
<p className={style.textContent}>Précédent</p>
|
|
</button>
|
|
<button onClick={() => addWeek(0)} className={style.directionButton}>
|
|
<p className={style.textContent}>Aujourd'hui</p>
|
|
</button>
|
|
<button onClick={() => addWeek(1)} className={style.directionButton}>
|
|
<p className={style.textContent}>Suivant</p>
|
|
<TbArrowNarrowRight />
|
|
</button>
|
|
</article>
|
|
</motion.section>
|
|
<motion.section
|
|
initial={{ y: 50, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: 50, opacity: 0 }}
|
|
transition={{
|
|
ease: "easeInOut",
|
|
duration: 0.5,
|
|
}}
|
|
className={style.planning}
|
|
>
|
|
{days.map((day, index) => (
|
|
<article className={style.planningDay} key={index}>
|
|
<div className={style.title}>
|
|
{!mobile && <h3 className={style.content}>{Days[index]}</h3>}
|
|
<p className={style.date}>
|
|
{moment(week.start)
|
|
.add(index + (mobile ? 0 : 1), "days")
|
|
.format("DD MMMM")
|
|
.replace(/([a-z]+)/gi, (m) => {
|
|
return Months[m];
|
|
})}
|
|
</p>
|
|
<p className={style.duration}>
|
|
{getDayStart(day)?.format("HH:mm")} -{" "}
|
|
{getDayEnd(day)?.format("HH:mm")}
|
|
</p>
|
|
</div>
|
|
<div className={style.dayEvents}>
|
|
<div className={style.events}>
|
|
{day.map((event: PlanningEvent, i) => (
|
|
<motion.div
|
|
key={i}
|
|
initial={{ y: 10, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: 10, opacity: 0 }}
|
|
transition={{
|
|
ease: "easeInOut",
|
|
duration: 0.4,
|
|
}}
|
|
className={style.event}
|
|
data-room={event.room}
|
|
style={{
|
|
top: `${
|
|
(event.start.get("hours") +
|
|
event.start.get("minutes") / 60 -
|
|
8) *
|
|
75
|
|
}px`,
|
|
height: `${
|
|
(event.end.diff(event.start, "minutes") / 60) * 75
|
|
}px`,
|
|
borderLeftColor: event.color,
|
|
}}
|
|
>
|
|
<p className={style.interval}>
|
|
{event.start.format("HH:mm")} -{" "}
|
|
{event.end.format("HH:mm")}
|
|
</p>
|
|
<p className={style.title}>{event.title}</p>
|
|
<div className={style.teacher}>
|
|
<FcGraduationCap className={style.teacherIcon} />
|
|
<p className={style.teacherContent}>{event.teacher}</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
<div className={style.hourPlaceholders}>
|
|
{new Array(11).fill(null).map((_, j) => (
|
|
<div
|
|
key={j}
|
|
className={style.dayEvent}
|
|
data-start={8 + j}
|
|
data-end={9 + j}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</motion.section>
|
|
</React.Fragment>
|
|
);
|
|
}
|