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