Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • fix/sitzungstermin
  • main
  • marquee
  • master
  • punctuation-workaround
5 results

Target

Select target project
  • julianbohnenkaemper/infoscreen
  • acul/infoscreen-new
  • tudo-fsinfo/infoscreen/infoscreen
  • alexr/infoscreen
  • fabianvanrissenbeck/infoscreen
  • evysgarden/infoscreen
  • falk-pages/infoscreen
  • smmokerg/infoscreen
  • smmivog2/infoscreen-fussball
  • smmivog2/infoscreen-update-preisliste
10 results
Select Git revision
  • 1-issue-czi-wtf
  • master
  • update-deps
3 results
Show changes
Showing
with 412 additions and 150 deletions
File deleted
File deleted
File deleted
File deleted
File deleted
File deleted
File deleted
File deleted
File deleted
......@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="https://static.fachschaften.org/icon/tight/color/fsorg-icon-color-tight.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
......@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Infoscreen</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
......
......@@ -17,21 +17,38 @@
cursor: none;
}
/* inter-100 - latin_latin-ext */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter';
font-style: normal;
font-weight: 100;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
.progress-bar {
justify-content: center;
border-radius: 10px;
padding: 0 5px;
display: flex;
height: 10px;
width: 100%;;
}
.progress-bar-fill {
border-radius: 10px;
height: 10px;
width: 0;
animation-timing-function: linear;
}
.progress-bar-animate {
animation-name: progress-bar-animation;
}
@keyframes progress-bar-animation {
0% { width: 0; }
100% { width: 100%; }
}
/* inter-200 - latin_latin-ext */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter';
font-style: normal;
font-weight: 200;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-300 - latin_latin-ext */
@font-face {
......@@ -39,7 +56,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-regular - latin_latin-ext */
@font-face {
......@@ -47,7 +64,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-500 - latin_latin-ext */
@font-face {
......@@ -55,7 +72,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-600 - latin_latin-ext */
@font-face {
......@@ -63,7 +80,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-700 - latin_latin-ext */
@font-face {
......@@ -71,7 +88,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-800 - latin_latin-ext */
@font-face {
......@@ -79,13 +96,5 @@
font-family: 'Inter';
font-style: normal;
font-weight: 800;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-900 - latin_latin-ext */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter';
font-style: normal;
font-weight: 900;
src: url('./../public/fonts/inter/inter-v13-latin_latin-ext-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
src: url('./../public/fonts/inter/inter-v13-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
import React from 'react';
import React from "react";
const PanelTitle = (props: {title: string, info?: string}) => {
return (
<div className={"px-6 py-4 text-zinc-400 flex flex-row justify-between"}>
<h2>{props.title}</h2>
{props.info && (
<p>{props.info}</p>
)}
</div>
);
const PanelTitle = (props: { title: string; info?: string }) => {
return (
<div
className={
"px-6 pt-4 pb-2 text-zinc-400 flex flex-row justify-between"
}
>
<h2>{props.title}</h2>
{props.info && <p>{props.info}</p>}
</div>
);
};
export default PanelTitle;
const ProgressBar = (props: { duration: number, idName: string, callbackFunction: () => void }) => {
const runCallback = () => {
props.callbackFunction();
const el = document.getElementById(props.idName);
if (el) {
el.classList.remove('progress-bar-animate');
void el.offsetWidth;
el.classList.add('progress-bar-animate');
}
}
return (
<div className="progress-bar">
<div
id={props.idName}
className={'progress-bar-fill progress-bar-animate bg-zinc-700'}
style={{ animationDuration: `${props.duration}ms` }}
onAnimationEnd={() => runCallback()}
/>
</div>
);
};
export default ProgressBar;
......@@ -23,7 +23,8 @@ type Route = {
name: string,
arrival: Date,
delay?: number,
countdown: number
countdown: number,
cancelled: boolean
}[],
countdown: number
}
......@@ -70,7 +71,8 @@ const FahrplanPanel = (props: {definition: FahrplanPanelDefinition}) => {
name: departure.internal.stop,
arrival,
delay,
countdown: parseInt(departure.countdown)
countdown: parseInt(departure.countdown),
cancelled: departure.is_cancelled === 1
}
],
countdown: parseInt(departure.countdown)
......@@ -81,7 +83,8 @@ const FahrplanPanel = (props: {definition: FahrplanPanelDefinition}) => {
name: departure.internal.stop,
arrival,
delay: stringToDelay(departure.delay),
countdown: parseInt(departure.countdown)
countdown: parseInt(departure.countdown),
cancelled: departure.is_cancelled === 1
})
newRoutes[existing_ind].stops = newRoutes[existing_ind].stops
......@@ -170,8 +173,6 @@ async function getStopData(stop: string): Promise<StationResponse> {
}
}))
console.log(data);
return data as StationResponse;
}
......
......@@ -10,7 +10,8 @@ const PlanElement = (props: {
stops: {
name: string,
arrival: Date,
delay?: number
delay?: number,
cancelled: boolean
}[]
}) => {
const [shown, setShown] = useState<boolean>(true);
......@@ -87,6 +88,7 @@ const PlanElement = (props: {
name={deDortmund(stop.name)}
arrival={stop.arrival}
delay={stop.delay}
cancelled={stop.cancelled}
/>
))
}
......
......@@ -5,7 +5,8 @@ type ProgressIndicatorProps = {
id: string,
name: string,
arrival: Date,
delay?: number
delay?: number,
cancelled: boolean
}
export default function ProgressIndicator(props: Readonly<ProgressIndicatorProps>) {
......@@ -18,7 +19,10 @@ export default function ProgressIndicator(props: Readonly<ProgressIndicatorProps
<div className={"flex-1"}></div>
</div>
<div className="relative flex flex-row items-center gap-4">
<div className={classNames("relative flex flex-row items-center gap-4", props.cancelled ? "text-red-400" : "")}
style={{
textDecoration: props.cancelled ? "line-through" : "inherit"
}}>
<div className={"w-20 flex flex-row justify-center"}>
<span className="h-[2.6rem] flex items-center" aria-hidden="true">
<span className="relative z-10 w-[1.5rem] h-[1.5rem] flex items-center justify-center bg-zinc-400 rounded-full">
......@@ -31,7 +35,7 @@ export default function ProgressIndicator(props: Readonly<ProgressIndicatorProps
{props.name}
</div>
<div className={classNames("tabular-nums", props.delay ? "text-red-400" : "text-zinc-300")}>
<div className={classNames("tabular-nums", (props.delay || props.cancelled) ? "text-red-400" : "text-zinc-300")}>
{props.delay ? (
<span className={"text-red-400"}>
(+{props.delay})&nbsp;&nbsp;
......
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from "react";
import PanelWrapper from "../../meta/PanelWrapper";
import PanelContent from "../../meta/PanelContent";
import PanelTitle from '../../meta/PanelTitle'
import PanelTitle from "../../meta/PanelTitle";
import QRCode from "react-qr-code";
import { Clock, MapPin } from '@phosphor-icons/react'
import { Clock, MapPin } from "@phosphor-icons/react";
export type GremiumPanelDefinition = {
gremien: [Gremium]
}
gremien: [Gremium];
};
type Gremium = {
name: string,
description: string,
link: string,
time: string,
location: string
}
name: string;
description: string;
link: string;
time: string;
location: string;
};
const GremiumPanel = (props: {definition: GremiumPanelDefinition}) => {
const [gremium, setGremium] = useState<Gremium>(props.definition.gremien[0]);
const cycle = useRef<number>(0);
const GremiumPanel = (props: { definition: GremiumPanelDefinition }) => {
const [gremium, setGremium] = useState<Gremium>(props.definition.gremien[0]);
const cycle = useRef<number>(0);
useEffect(() => {
const update = async () => {
setGremium(props.definition.gremien[cycle.current++ % props.definition.gremien.length]);
console.log(gremium);
console.log(cycle.current);
}
useEffect(() => {
const update = async () => {
setGremium(
props.definition.gremien[
cycle.current++ % props.definition.gremien.length
],
);
};
update();
const interval = setInterval(update, 20 * 1000);
update();
const interval = setInterval(update, 20 * 1000);
return () => {
clearInterval(interval);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return () => {
clearInterval(interval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<PanelWrapper>
<PanelTitle title={"Termine"}/>
<PanelContent>
<div className={"flex flex-row"}>
<div className={"flex-1"}>
<h2 className={"text-xl font-semibold mt-2"}>{gremium.description}</h2>
<div className={"flex flex-row gap-4"}>
<p className={"text-sm text-gray-400"}>
<Clock size={20} className={"inline mb-1.5 mr-1"}/>
{gremium.time}
</p>
<p className={"text-sm text-gray-400"}>
<MapPin size={20} className={"inline mb-1.5 mr-1"}/>
{gremium.location}
</p>
</div>
</div>
<div className={""}>
<QRCode
value={gremium.link}
className={"h-24 w-24"}
fgColor={"#ffffff"}
bgColor={"#18181b"}
/>
</div>
</div>
</PanelContent>
</PanelWrapper>
);
return (
<PanelWrapper>
<PanelTitle title={"Termine"} />
<PanelContent>
<div className={"flex flex-row"}>
<div className={"flex-1"}>
<h2 className={"text-xl font-semibold mt-2"}>
{gremium.description}
</h2>
<div className={"flex flex-row gap-4"}>
<p className={"text-sm text-gray-400"}>
<Clock size={20} className={"inline mb-1.5 mr-1"} />
{gremium.time}
</p>
<p className={"text-sm text-gray-400"}>
<MapPin size={20} className={"inline mb-1.5 mr-1"} />
{gremium.location}
</p>
</div>
</div>
<div className={""}>
<QRCode
value={gremium.link}
className={"h-24 w-24"}
fgColor={"#ffffff"}
bgColor={"#18181b"}
/>
</div>
</div>
</PanelContent>
</PanelWrapper>
);
};
export default GremiumPanel;
import { useEffect, useState } from "react";
import PanelWrapper from "../../meta/PanelWrapper";
import PanelTitle from "../../meta/PanelTitle";
import PanelContent from "../../meta/PanelContent";
type MensaJetztAPIResponse = {
day: string,
date: string,
attendance: Attendee[],
}
type Attendee = {
"name": string,
"name_modifiers": string,
"time": string,
"canteen": string,
"color": string,
}
type Attendance = { [time: string]: Attendee[] }
const MensaJetztPanel = () => {
const [marqueeContent, setMarqueeContent] = useState<string>("Niemand :(");
useEffect(() => {
const update = async () => {
try {
const request = await fetch(`https://mensa.jetzt/api/entries/`);
if (request.status !== 200) {
setMarqueeContent("API Error :(");
console.error("mensa.jetzt API returned error code");
return;
}
const data = await request.json() as MensaJetztAPIResponse;
let marquee = Object.entries(
data.attendance
.sort((a, b) => a.time.localeCompare(b.time))
.reduce((acc, curr) => {
if (acc[curr.time]) {
acc[curr.time].push(curr);
} else {
acc[curr.time] = [curr];
}
return acc;
}, {} as Attendance)
).map(([time, attendees]) => `${time} - ${attendees.map(a => a.name).join(", ")}`)
.join(" | ");
if (marquee) {
marquee += " |";
} else {
marquee = "Niemand :(";
}
setMarqueeContent(marquee.trim());
}
catch (e) {
console.warn("mensa.jetzt not showing data because", e);
setMarqueeContent("Niemand :(");
}
}
update();
const interval = setInterval(update, 5 * 60 * 1000);
return () => {
clearInterval(interval);
}
}, []);
return (
<PanelWrapper className={"relative"}>
<PanelTitle title={"Mensa.jetzt"}/>
<PanelContent>
{marqueeContent.includes("Uhr") ? (
<div className={"flex overflow-hidden whitespace-nowrap"}>
<div className={"animate-marquee"}>
{marqueeContent}
</div>
<div className={"animate-marquee ml-1"}>
{marqueeContent}
</div>
</div>
) : (
<div className={"text-center"}>{marqueeContent}</div>
)}
</PanelContent>
</PanelWrapper>
);
};
export default MensaJetztPanel;
......@@ -4,11 +4,17 @@ 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";
import ProgressBar from "../../meta/ProgressBar";
type Dish = {
name: string,
details: string,
typeIcons: React.FC<any>[]
typeIcons: React.FC<any>[],
price: {
"student": string,
"staff": string,
"guest": string
},
}
type MensaPanelDefinition = {
......@@ -61,8 +67,6 @@ const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => {
const data = await request.json() as CanteenAPIResponse;
console.log(data);
const old_menus_count = menus.current.length;
const old_specials_count = menus.current.length;
......@@ -77,13 +81,16 @@ const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => {
name: (d.title.de
.split(" | ")
.at(0) ?? "Name nicht bekannt")
.replace(" nach Wahl", ""),
.replace(" nach Wahl", "")
.replaceAll(/\(.*\)/g, ""),
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>[]
.replace(" nach Wahl", "")
.replaceAll(/\(.*\)/g, ""),
typeIcons: d.type.map(typeToIcon).filter(i => i !== null) as unknown as React.FC<any>[],
price: d.price
}))
specials.current = data
......@@ -95,13 +102,16 @@ const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => {
name: (d.title.de
.split(" | ")
.at(0) ?? "Name nicht bekannt")
.replace(" nach Wahl", ""),
.replace(" nach Wahl", "")
.replaceAll(/\(.*\)/g, ""),
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>[]
.replace(" nach Wahl", "")
.replaceAll(/\(.*\)/g, ""),
typeIcons: d.type.map(typeToIcon).filter(i => i !== null) as unknown as React.FC<any>[],
price: d.price
}))
// If the count of menus and specials changed, reset the cycler
......@@ -127,56 +137,59 @@ const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => {
// 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;
const updateGroup = () => {
cycle.current = (cycle.current + 1) % 2;
switch (cycle.current) {
case 0:
setGroupName("Menüs");
setDishes(menus.current);
break;
case 1:
setGroupName("Aktionsteller");
setDishes(specials.current);
break;
default:
console.error("You fucked up bad!");
break;
}
}
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"}>
<div className={"grid gap-y-1 gap-x-1.5"} style={{gridTemplateColumns: "1fr 10fr 1fr"}}>
{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>
<>
{/* Fixme: This shifts the name out of line if there is more than one */}
<div className={"flex flex-row gap-1.5"}>
{dish.typeIcons.map((Icon) => (
<Icon key={Icon.name} size={32} className={"text-zinc-400"}/>
))}
</div>
<h3 className={"text-xl leading-tight"}>
{dish.name}
</h3>
<p className={"text-sm text-zinc-400 leading-tight text-right"}>
{dish.price.student}
</p>
<div>
</div>
<p className={"text-sm text-zinc-400 leading-tight col-span-2 mb-4"}>
{dish.details}
</p>
</>
))}
</div>
{dishes.length === 0 && (
<div className={"h-full w-full flex justify-center items-center"}>
{
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"}>
......@@ -186,6 +199,7 @@ const MensaplanPanel = (props: {definition: MensaPanelDefinition}) => {
</div>
)}
</PanelContent>
<ProgressBar duration={20000} idName={'mena-progress'} callbackFunction={updateGroup} />
</PanelWrapper>
);
};
......
import { useEffect, useState } from "react";
import PanelWrapper from "../../meta/PanelWrapper";
import PanelTitle from "../../meta/PanelTitle";
import PanelContent from "../../meta/PanelContent";
import StationElement from "./components/StationElement";
type NextbikeAPIResponse = {
countries: {
cities: {
places: {
number: number,
bikes_available_to_rent: number
}[]
}[]
}[]
}
type NextbikePanelDefinition = {
city: string;
station_ids: number[];
station_names: string[];
}
type NextbikeStation = {
id: number,
name: string,
available: number
}
const NextbikePanel = (props: {definition: NextbikePanelDefinition}) => {
const [stations, setStations] = useState<NextbikeStation[]>([]);
useEffect(() => {
const update = async () => {
try {
const request = await fetch(`https://api.nextbike.net/maps/nextbike-live.json?city=${props.definition.city}`);
if (request.status !== 200) {
return;
}
const data = await request.json() as NextbikeAPIResponse;
const all_stations = data.countries[0].cities[0].places;
const stations = all_stations.filter((station: any) => {
return props.definition.station_ids.includes(station.number);
});
// sort as in definition
stations.sort((a: any, b: any) => {
return props.definition.station_ids.indexOf(a.number) - props.definition.station_ids.indexOf(b.number);
});
const available_bikes = stations.map((station: any) => {
return {
id: station.number,
name: props.definition.station_names[props.definition.station_ids.indexOf(station.number)],
available: station.bikes_available_to_rent
}
});
setStations(available_bikes);
}
catch (e) {
console.warn("NextbikePanel not showing data because", e);
}
}
update();
const interval = setInterval(update, 2 * 60 * 1000);
return () => {
clearInterval(interval);
}
}, [props]);
return (
<PanelWrapper>
<PanelTitle title={"Nextbike Monitor"}/>
<PanelContent className={"flex flex-col"}>
{stations.length > 0 ? (
<div className={"flex-1 flex flex-col gap-3"}>
{stations.map((station) => (
<StationElement
key={station.id}
stationId={station.id}
stationName={station.name}
availableBikes={station.available}
/>
))}
</div>
) : (
<div className={"flex-1 flex justify-center items-center"}>
<div className={"mb-10 flex flex-col items-center"}>
<p className={"max-w-xs text-center text-zinc-400"}>
Aktuell sind keine Daten verfügbar.
</p>
</div>
</div>
)}
</PanelContent>
</PanelWrapper>
)
}
export default NextbikePanel;