2024-01-26 21:10:25 +01:00

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>
);
}