import React, {useEffect, useRef, useState} from 'react'; import PanelWrapper from "../../meta/PanelWrapper"; import PanelTitle from "../../meta/PanelTitle"; import PanelContent from "../../meta/PanelContent"; import {CanteenAPIResponse} from "./types/canteenAPI"; import {Leaf, Plant, Bone, Record, Fish, ForkKnife} from "@phosphor-icons/react"; type Dish = { name: string, details: string, typeIcons: React.FC<any>[] } type MensaPanelDefinition = { canteenId: number, closingTime: { hours: number, minutes: number } } const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => { const menus = useRef<Dish[]>([]); const specials = useRef<Dish[]>([]); const [relativeDay, setRelativeDay] = useState<string>("heute"); const [groupName, setGroupName] = useState<string>("Menüs") const cycle = useRef<number>(0); const [dishes, setDishes] = useState<Dish[]>([]); useEffect(() => { const update = async () => { try { // Determine day to fetch for const now = new Date(); let fetchFor: string; if ( now.getHours() > props.definition.closingTime.hours || ( now.getHours() === props.definition.closingTime.hours && now.getMinutes() > props.definition.closingTime.minutes ) ) { // After closing, fetch for next day setRelativeDay("morgen"); const tomorrow = new Date(now.setTime(now.getTime() + 24 * 60 * 60 * 1000)); fetchFor = toYYYYMMDD(tomorrow); } else { // otherwise, fetch for today setRelativeDay("heute"); fetchFor = toYYYYMMDD(now); } // Request the API const request = await fetch(`/canteen-menu/v3/canteens/${props.definition.canteenId}/${fetchFor}`); if (request.status !== 200) { menus.current = []; specials.current = []; return; } const data = await request.json() as CanteenAPIResponse; console.log(data); const old_menus_count = menus.current.length; const old_specials_count = menus.current.length; // ToDo: This needs to be cleaned up! menus.current = data .filter(d => d.counter !== "Beilagen") .filter(d => d.counter !== "Aktionsteller") .sort((a, b) => { return a.position - b.position }) .map(d => ({ name: (d.title.de .split(" | ") .at(0) ?? "Name nicht bekannt") .replace(" nach Wahl", ""), details: d.title.de .split(" | ") .slice(1, -1) .join(", ") .replace(" nach Wahl", ""), typeIcons: d.type.map(typeToIcon).filter(i => i !== null) as unknown as React.FC<any>[] })) specials.current = data .filter(d => d.counter === "Aktionsteller") .sort((a, b) => { return a.position - b.position }) .map(d => ({ name: (d.title.de .split(" | ") .at(0) ?? "Name nicht bekannt") .replace(" nach Wahl", ""), details: d.title.de .split(" | ") .slice(1, -1) .join(", ") .replace(" nach Wahl", ""), typeIcons: d.type.map(typeToIcon).filter(i => i !== null) as unknown as React.FC<any>[] })) // If the count of menus and specials changed, reset the cycler if (menus.current.length !== old_menus_count || specials.current.length !== old_specials_count) { setDishes(menus.current); cycle.current = 0; setGroupName("Menüs") } } catch (e) { console.warn("MensaPlan not showing data because", e); menus.current = []; specials.current = []; } } update(); const interval = setInterval(update, 1 * 60 * 60 * 1000); return () => { clearInterval(interval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const update = async () => { switch (cycle.current % 2) { case 0: setGroupName("Menüs"); setDishes(menus.current); break; case 1: setGroupName("Aktionsteller"); setDishes(specials.current); break; } cycle.current = (cycle.current + 1) % 2; } update(); const interval = setInterval(update, 20 * 1000); return () => { clearInterval(interval); } }, []); return ( <PanelWrapper> <PanelTitle title={"Mensaplan für " + relativeDay} info={groupName} /> <PanelContent> <div className={"flex flex-col gap-4"}> {dishes.map(dish => ( <div key={dish.name} className={"flex flex-row items-center gap-4"}> {/* Fixme: This shifts the name out of line if there is more than one */} <div className={"flex flex-row gap-2 mr-2"}> {dish.typeIcons.map((Icon, index) => ( <Icon key={Icon.name + index} size={32} className={"text-zinc-400"}/> ))} </div> <h3 className={"text-xl leading-tight"}> {dish.name} </h3> <p className={"text-sm text-zinc-300 leading-tight"}> {dish.details} </p> </div> ))} </div> {dishes.length === 0 && ( <div className={"h-full w-full flex justify-center items-center"}> <div className={"mb-10 flex flex-col items-center"}> <ForkKnife size={48} className={"mb-3"}/> <p className={"text-center text-zinc-400"}> Es werden keine Gerichte in dieser Kategorie angeboten </p> </div> </div> )} </PanelContent> </PanelWrapper> ); }; export default MensaplanPanel; function toYYYYMMDD(input: Date): string { return `${input.getFullYear().toString().padStart(4, "20")}-${(input.getMonth() + 1).toString().padStart(2, "0")}-${input.getDate().toString().padStart(2, "0")}` } function typeToIcon(type: string): React.FC | null { switch (type) { case "N": // Vegan return Plant case "V": // Vegetarisch return Leaf case "G": // Geflügel return Bone case "K": // Kosher // ToDo: Work out proper icon return Record case "S": // Schwein return Bone case "R": // Rind (geraten) return Bone case "W": // Wild return Bone case "F": // Fisch (geraten) return Fish default: return null } }