Skip to content
Snippets Groups Projects
Commit 2606b0d2 authored by Niklas Schrötler's avatar Niklas Schrötler
Browse files

MensaPanel: Implemented first draft

parent 77c04b1c
No related branches found
No related tags found
No related merge requests found
...@@ -61,12 +61,19 @@ ...@@ -61,12 +61,19 @@
} }
}, },
{ {
"type": "placeholder", "type": "mensaplan",
"position": { "position": {
"x": 9, "x": 9,
"y": 7, "y": 7,
"w": 16, "w": 16,
"h": 6 "h": 6
},
"config": {
"canteenId": 341,
"closingTime": {
"hours": 14,
"minutes": 15
}
} }
} }
] ]
......
import React from 'react'; import React from 'react';
const PanelTitle = (props: {title: string}) => { const PanelTitle = (props: {title: string, info?: string}) => {
return ( return (
<div className={"px-6 py-4 text-zinc-400"}> <div className={"px-6 py-4 text-zinc-400 flex flex-row justify-between"}>
<h2>{props.title}</h2> <h2>{props.title}</h2>
{props.info && (
<p>{props.info}</p>
)}
</div> </div>
); );
}; };
......
import React from 'react'; 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, Warning, Record} 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 () => {
// 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(`https://infoscreen.oh14.de/canteen-menu/v3/canteens/${props.definition.canteenId}/${fetchFor}`);
if(!request.ok) {
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")
}
}
update();
const interval = setInterval(update, 1 * 60 * 60 * 1000);
return () => {
clearInterval(interval);
}
}, []);
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);
}
}, []);
const MensaplanPanel = () => {
return ( return (
<div> <PanelWrapper>
Mensaplan <PanelTitle title={"Mensaplan für " + relativeDay} info={groupName} />
</div> <PanelContent>
<div className={"flex flex-col gap-4"}>
{dishes.map(dish => (
<div 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 => (
<Icon 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"}>
<Warning size={48} className={"mb-3"}/>
<p className={"text-center text-zinc-400"}>
Aktuell sind keine Mensaplan-Daten verfügbar.
</p>
</div>
</div>
)}
</PanelContent>
</PanelWrapper>
); );
}; };
export default MensaplanPanel; export default MensaplanPanel;
function toYYYYMMDD(input: Date): string {
return `${input.getFullYear()}-${input.getMonth() + 1}-${input.getDate()}`
}
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
default:
return null
}
}
export type CanteenAPIResponse = CanteenAPIDish[];
export type CanteenAPIDish = {
additives: string[],
category: string,
counter: string,
dispoId: string,
position: number,
price: {
student: string,
staff: string,
guest: string
},
title: {
de: string,
en: string
},
type: string[]
}
...@@ -8,6 +8,7 @@ import FahrplanPanel from "./Fahrplan/FahrplanPanel"; ...@@ -8,6 +8,7 @@ import FahrplanPanel from "./Fahrplan/FahrplanPanel";
import {PanelDefinition} from "../types/LayoutConfig"; import {PanelDefinition} from "../types/LayoutConfig";
import PanelWrapper from "../meta/PanelWrapper"; import PanelWrapper from "../meta/PanelWrapper";
import BildPanel from "./Bild/BildPanel"; import BildPanel from "./Bild/BildPanel";
import MensaplanPanel from "./Mensaplan/MensaplanPanel";
/* /*
* First, please claim a unique id for your panel here. Convention is that it is all lowercase, in snake-case to be * First, please claim a unique id for your panel here. Convention is that it is all lowercase, in snake-case to be
...@@ -24,6 +25,7 @@ export type PanelTypes = "fahrplan" | "bild"; ...@@ -24,6 +25,7 @@ export type PanelTypes = "fahrplan" | "bild";
export const PanelRenderers: {[panelType: string]: React.FC<any & {definition: PanelDefinition<any>}>} = { export const PanelRenderers: {[panelType: string]: React.FC<any & {definition: PanelDefinition<any>}>} = {
"fahrplan": FahrplanPanel, "fahrplan": FahrplanPanel,
"bild": BildPanel, "bild": BildPanel,
"mensaplan": MensaplanPanel,
"placeholder": () => ( "placeholder": () => (
<PanelWrapper className={"flex flex-col items-center justify-center text-zinc-400"}> <PanelWrapper className={"flex flex-col items-center justify-center text-zinc-400"}>
Dieses Panel wird noch entwickelt Dieses Panel wird noch entwickelt
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment