From 7994d700ea1a893193e9e1995f7ca1c8422c84ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Schr=C3=B6tler?= <niklas@allround.digital> Date: Thu, 10 Aug 2023 20:25:19 +0200 Subject: [PATCH] Implemented a base of components --- package-lock.json | 90 +++++++++++++++++++ package.json | 2 + src/App.tsx | 65 +++++++++++--- src/layout/HSplit.tsx | 2 +- src/layout/LayoutElement.tsx | 2 +- src/layout/VSplit.tsx | 2 +- src/meta/PanelContent.tsx | 11 +++ src/meta/PanelTitle.tsx | 11 +++ src/meta/PanelWrapper.tsx | 2 +- src/meta/ScreenBar.tsx | 38 ++++++++ src/meta/ScreenWrapper.tsx | 15 ++++ src/panels/Fahrplan/Fahrplan.tsx | 37 -------- src/panels/Fahrplan/FahrplanPanel.tsx | 46 ++++++++++ .../Fahrplan/components/PlanElement.tsx | 37 +++----- .../Fahrplan/components/ProgressIndicator.tsx | 49 ++++++++++ 15 files changed, 333 insertions(+), 76 deletions(-) create mode 100644 src/meta/PanelContent.tsx create mode 100644 src/meta/PanelTitle.tsx create mode 100644 src/meta/ScreenBar.tsx create mode 100644 src/meta/ScreenWrapper.tsx delete mode 100644 src/panels/Fahrplan/Fahrplan.tsx create mode 100644 src/panels/Fahrplan/FahrplanPanel.tsx create mode 100644 src/panels/Fahrplan/components/ProgressIndicator.tsx diff --git a/package-lock.json b/package-lock.json index 602f605..4cac2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@types/node": "^16.18.36", "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", + "framer-motion": "^10.12.22", + "javascript-time-ago": "^2.5.9", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", @@ -2295,6 +2297,21 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -8256,6 +8273,29 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/framer-motion": { + "version": "10.12.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.12.22.tgz", + "integrity": "sha512-bBGYPOxvxcfzS7/py9MEqDucmXBkVl2g42HNlXXPieSTSGGkr8L7+MilCnrU6uX3HrNk/tcB++1SkWE8BosHFw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9604,6 +9644,14 @@ "node": ">=8" } }, + "node_modules/javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "dependencies": { + "relative-time-format": "^1.1.6" + } + }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -14530,6 +14578,11 @@ "node": ">= 0.10" } }, + "node_modules/relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -18723,6 +18776,21 @@ "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "requires": {} }, + "@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "requires": { + "@emotion/memoize": "0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -23101,6 +23169,15 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" }, + "framer-motion": { + "version": "10.12.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.12.22.tgz", + "integrity": "sha512-bBGYPOxvxcfzS7/py9MEqDucmXBkVl2g42HNlXXPieSTSGGkr8L7+MilCnrU6uX3HrNk/tcB++1SkWE8BosHFw==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "tslib": "^2.4.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -24034,6 +24111,14 @@ } } }, + "javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "requires": { + "relative-time-format": "^1.1.6" + } + }, "jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -27446,6 +27531,11 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" }, + "relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", diff --git a/package.json b/package.json index dcb616e..e6d4c3e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "@types/node": "^16.18.36", "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", + "framer-motion": "^10.12.22", + "javascript-time-ago": "^2.5.9", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", diff --git a/src/App.tsx b/src/App.tsx index d5b0aac..a40e644 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,63 @@ import React from 'react'; import './App.css'; -import LayoutElement from "./layout/LayoutElement"; -import useLayout from "./hooks/useLayout"; +import VSplit from "./layout/VSplit"; +import HSplit from "./layout/HSplit"; +import ScreenWrapper from "./meta/ScreenWrapper"; +import PanelWrapper from "./meta/PanelWrapper"; +import PanelTitle from "./meta/PanelTitle"; +import PanelContent from "./meta/PanelContent"; +import FahrplanPanel from "./panels/Fahrplan/FahrplanPanel"; function App() { - const layout = useLayout(); + // const layout = useLayout(); - if(!layout) { - return ( - <div> - Loading... - </div> - ) - } + // if(!layout) { + // return ( + // <div> + // Loading... + // </div> + // ) + // } return ( - <div className={"overflow-hidden w-screen h-screen bg-zinc-900 text-white"} style={{fontFamily: "Be Vietnam Pro"}}> - <LayoutElement config={layout} /> + <div className={"overflow-hidden w-screen h-screen bg-zinc-950 text-white"} style={{fontFamily: "Inter"}}> + {/*<LayoutElement config={layout} />*/} + <ScreenWrapper> + <VSplit left={( + <HSplit top={( + <FahrplanPanel /> + )} bottom={( + <PanelWrapper> + <PanelTitle title={"Als nächstes im CZI"} /> + <PanelContent> + <p>Next up</p> + </PanelContent> + </PanelWrapper> + )} split={.9}/> + )} right={( + <HSplit top={( + <PanelWrapper> + <p>News</p> + </PanelWrapper> + )} bottom={( + <HSplit top={( + <PanelWrapper> + <PanelTitle title={"Mensa Öffnungszeiten"} /> + <PanelContent> + <p>Mensa Öffnungszeiten</p> + </PanelContent> + </PanelWrapper> + )} bottom={( + <PanelWrapper> + <PanelTitle title={"Mensaplan Hauptmensa"} /> + <PanelContent> + <p>Mensa Plan</p> + </PanelContent> + </PanelWrapper> + )} split={.4}/> + )} split={.9}/> + )} split={.35}/> + </ScreenWrapper> </div> ); } diff --git a/src/layout/HSplit.tsx b/src/layout/HSplit.tsx index 9bb0d3e..8a3cd2f 100644 --- a/src/layout/HSplit.tsx +++ b/src/layout/HSplit.tsx @@ -12,7 +12,7 @@ const HSplit = (props: { } return ( - <div className={"flex flex-col w-full h-full"}> + <div className={"flex flex-col w-full h-full gap-6"}> <div style={{height: `${split * 100}%`}}> {props.top} </div> diff --git a/src/layout/LayoutElement.tsx b/src/layout/LayoutElement.tsx index cbfea13..8bc4765 100644 --- a/src/layout/LayoutElement.tsx +++ b/src/layout/LayoutElement.tsx @@ -3,7 +3,7 @@ import {Layout} from "../types/LayoutConfig"; import config from "tailwindcss/defaultConfig"; const LayoutElement = (props: {config: Layout}) => { - switch (config.type) + // switch (config.type)s }; export default LayoutElement; diff --git a/src/layout/VSplit.tsx b/src/layout/VSplit.tsx index aacb3e4..af94e6b 100644 --- a/src/layout/VSplit.tsx +++ b/src/layout/VSplit.tsx @@ -12,7 +12,7 @@ const VSplit = (props: { } return ( - <div className={"flex flex-row w-full h-full"}> + <div className={"flex flex-row w-full h-full gap-6"}> <div style={{width: `${split * 100}%`}}> {props.left} </div> diff --git a/src/meta/PanelContent.tsx b/src/meta/PanelContent.tsx new file mode 100644 index 0000000..d16cda1 --- /dev/null +++ b/src/meta/PanelContent.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const PanelContent = (props: {padding?: boolean, children: any}) => { + return ( + <div className={"flex-1 " + ((props.padding ?? true) ? "px-5 pb-4" : "")}> + {props.children} + </div> + ); +}; + +export default PanelContent; diff --git a/src/meta/PanelTitle.tsx b/src/meta/PanelTitle.tsx new file mode 100644 index 0000000..bdb6268 --- /dev/null +++ b/src/meta/PanelTitle.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const PanelTitle = (props: {title: string}) => { + return ( + <div className={"px-5 py-4 text-zinc-400"}> + <h2>{props.title}</h2> + </div> + ); +}; + +export default PanelTitle; diff --git a/src/meta/PanelWrapper.tsx b/src/meta/PanelWrapper.tsx index 47453e7..3173a13 100644 --- a/src/meta/PanelWrapper.tsx +++ b/src/meta/PanelWrapper.tsx @@ -3,7 +3,7 @@ import React from 'react'; const PanelWrapper = (props: {children: any, className?: string}) => { // ToDo: The className thing is not pretty. Re-do return ( - <div className={`w-full h-full ${props.className ?? ""}`}> + <div className={`w-full h-full bg-zinc-900 rounded-2xl overflow-hidden flex flex-col ${props.className ?? ""}`}> {props.children} </div> ); diff --git a/src/meta/ScreenBar.tsx b/src/meta/ScreenBar.tsx new file mode 100644 index 0000000..a2d5fbe --- /dev/null +++ b/src/meta/ScreenBar.tsx @@ -0,0 +1,38 @@ +import React, {useEffect, useState} from 'react'; + +const MONTH_NAMES = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]; +const WEEKDAY_NAMES = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; + +const ScreenBar = () => { + const [time, setTime] = useState<string>(""); + const [date, setDate] = useState<string>("") + + useEffect(() => { + let update = () => { + const d = new Date(); + + const hh = d.getHours().toString().padStart(2, "0"); + const mm = d.getMinutes().toString().padStart(2, "0"); + + const yyyy = d.getFullYear().toString(); + const monthname = MONTH_NAMES[d.getMonth()]; + const dd = d.getDate() + const weekday = WEEKDAY_NAMES[d.getDay()]; + + setTime(`${hh}:${mm}`); + setDate(`${weekday}, ${dd}. ${monthname} ${yyyy}`); + } + + setInterval(update, 1000); + update(); + }, []) + + return ( + <div className={"flex flex-row justify-between px-8 py-6"}> + <p>{date}</p> + <p>{time}</p> + </div> + ); +}; + +export default ScreenBar; diff --git a/src/meta/ScreenWrapper.tsx b/src/meta/ScreenWrapper.tsx new file mode 100644 index 0000000..137ca10 --- /dev/null +++ b/src/meta/ScreenWrapper.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ScreenBar from "./ScreenBar"; + +const ScreenWrapper = (props: {children: any}) => { + return ( + <div className={"h-full w-full flex flex-col"} style={{backgroundImage: "/logo512.png"}}> + <ScreenBar /> + <div className={"flex-1 px-6 pb-6"}> + {props.children} + </div> + </div> + ); +}; + +export default ScreenWrapper; diff --git a/src/panels/Fahrplan/Fahrplan.tsx b/src/panels/Fahrplan/Fahrplan.tsx deleted file mode 100644 index 90e41d8..0000000 --- a/src/panels/Fahrplan/Fahrplan.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PanelWrapper from "../../meta/PanelWrapper"; -import PlanElement from "./components/PlanElement"; - -const Fahrplan = () => { - return ( - <PanelWrapper className={"bg-blue-900"}> - <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ - { - name: "Essen", - time: "10:00", - delay: 20 - }, - { - name: "Nicht Essen", - time: "10:00", - delay: 20 - } - ]} /> - - <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ - { - name: "Essen", - time: "10:00", - delay: 20 - }, - { - name: "Nicht Essen", - time: "10:00", - delay: 20 - } - ]} /> - </PanelWrapper> - ); -}; - -export default Fahrplan; diff --git a/src/panels/Fahrplan/FahrplanPanel.tsx b/src/panels/Fahrplan/FahrplanPanel.tsx new file mode 100644 index 0000000..64c18fd --- /dev/null +++ b/src/panels/Fahrplan/FahrplanPanel.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PanelWrapper from "../../meta/PanelWrapper"; +import PlanElement from "./components/PlanElement"; +import PanelTitle from "../../meta/PanelTitle"; +import PanelContent from "../../meta/PanelContent"; + +const FahrplanPanel = () => { + return ( + <PanelWrapper> + <PanelTitle title={"ÖPNV Monitor"}/> + <PanelContent> + <div className={"flex flex-col gap-3"}> + <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ + { + name: "Essen", + time: "10:00", + delay: 20 + }, + { + name: "Nicht Essen", + time: "10:00", + delay: 20 + } + ]}/> + + <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ + { + name: "Essen", + time: "10:00", + delay: 20 + }, + { + name: "Nicht Essen", + time: "10:00", + delay: 20 + } + ]}/> + </div> + </PanelContent> + </PanelWrapper> + + + ); +}; + +export default FahrplanPanel; diff --git a/src/panels/Fahrplan/components/PlanElement.tsx b/src/panels/Fahrplan/components/PlanElement.tsx index d785f13..99a7885 100644 --- a/src/panels/Fahrplan/components/PlanElement.tsx +++ b/src/panels/Fahrplan/components/PlanElement.tsx @@ -1,4 +1,6 @@ +import { motion } from 'framer-motion'; import React from 'react'; +import ProgressIndicator from "./ProgressIndicator"; const PlanElement = (props: { trainIdentifier: string, @@ -10,32 +12,21 @@ const PlanElement = (props: { }[] }) => { return ( - <div className={"grid grid-cols-2"}> - <div className={"bg-white text-blue-900 px-4 py-0.5"}> - {props.trainIdentifier} + <div> + <div className={"flex flex-row gap-4 items-center font-semibold"}> + <div className={"flex justify-center items-center bg-green-700 text-lg h-8 w-16 leading-none"}> + {props.trainIdentifier} + </div> + <h3 className={"text-xl"}> + {props.trainHeading} + </h3> </div> - <div className={"px-2 py-0.5 w-full"}> - {props.trainHeading} - </div> - - {props.stops.map(stop => ( - <> - <div> - {stop.time} - - {(stop.delay && stop.delay > 0) && ( - <span> - + {stop.delay} - </span> - )} - </div> - <div> - {stop.name} - </div> - </> - ))} + <motion.ol role="list" className="overflow-hidden" aria-hidden="true"> + <ProgressIndicator first={true} id={"a"} name={"Meitnerweg"} arrival={new Date("Mon Jul 17 2023 23:56:56 GMT+0200 (Central European Summer Time)")}/> + <ProgressIndicator id={"b"} name={"Dortmund Universität S"} arrival={new Date("Mon Jul 17 2023 23:57:07 GMT+0200 (Central European Summer Time)")}/> + </motion.ol> </div> ); }; diff --git a/src/panels/Fahrplan/components/ProgressIndicator.tsx b/src/panels/Fahrplan/components/ProgressIndicator.tsx new file mode 100644 index 0000000..9de47d1 --- /dev/null +++ b/src/panels/Fahrplan/components/ProgressIndicator.tsx @@ -0,0 +1,49 @@ +import TimeAgo from 'javascript-time-ago' +import de from 'javascript-time-ago/locale/de' +import {useEffect, useState} from "react"; + +export default function ProgressIndicator(props: {first?: boolean, id: string, name: string, arrival: Date}) { + const [timeUntil, setTimeUntil] = useState<string>(""); + + useEffect(() => { + // Setup TimeAgo + TimeAgo.addDefaultLocale(de); + const timeAgo = new TimeAgo('de-DE') + + const update = () => { + setTimeUntil(timeAgo.format(new Date(), {future: true})); + } + + setInterval(update, 1000); + update(); + }, []); + + return ( + <li className={`relative ${!(props.first ?? false) ? "-mt-3.5" : "-mt-1"} text-sm text-zinc-300`} key={props.id}> + <div className={"flex flex-row gap-4"}> + <div className={"w-16 flex flex-row justify-center"}> + <div className="w-[3px] h-6 -mb-5 bg-zinc-400" aria-hidden="true" /> + </div> + <div className={"flex-1"}></div> + </div> + + <div className="relative flex flex-row items-center gap-4"> + <div className={"w-16 flex flex-row justify-center"}> + <span className="h-9 flex items-center" aria-hidden="true"> + <span className="relative z-10 w-[18px] h-[18px] flex items-center justify-center bg-zinc-400 rounded-full"> + <span className="h-[12px] w-[12px] bg-zinc-900 rounded-full" /> + </span> + </span> + </div> + + <div className={"flex-1"}> + {props.name} + </div> + + <div> + {timeUntil} + </div> + </div> + </li> + ) +} -- GitLab