Newer
Older
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}`);
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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);
}
}, []);
<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>
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
}
}