diff --git a/package-lock.json b/package-lock.json index 602f60531ec03423366a89e81885dbd189bb4796..4cac2cff65af6bef3603c3a3833d0994e993320d 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 dcb616ee8f90b965dfcced10c4db5663f643ebd1..e6d4c3e50917104144d178067b097723aebf3919 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 d5b0aac3515c3cd93ab35b73f6d817151575419b..a40e6444d05bf9b3713e83a8f5524dc6dfb0503f 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 9bb0d3eeae300d2eb5945c05b3958e9fa82a98db..8a3cd2f8d2944fcd7b16b074dfd6eec653df0748 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 cbfea13f8ed0e1b545241a05d557404529223437..8bc4765d84ef7efc22ebb9ac1926bcb61d60cc0d 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 aacb3e474fa117f50f2aaf7f46bd798a436665d7..af94e6bed41acfbe252960429b7f335603f6510b 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 0000000000000000000000000000000000000000..d16cda1defc09835a7d3ed1c8967bd3983ba4cf7 --- /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 0000000000000000000000000000000000000000..bdb6268e82ed4fb1c29ac8ebceab4afbb175e1b1 --- /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 47453e7c38efce14b1d843eb1fdafe87a3e589cb..3173a13113764aa3dbc4f6dd532ccbfbef77cb4e 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 0000000000000000000000000000000000000000..a2d5fbece5db7fbe693d0ab9b38592f41a29cb48 --- /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 0000000000000000000000000000000000000000..137ca10ffd8d082e7a5dcb1e722ba6ad89da6333 --- /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 90e41d8c2f5bee92809a5d365138786c61ba7bfa..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..64c18fd9813c8dfb3c938c081c6a0b2ed8651290 --- /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 d785f13210248e5a4fe5d4bb217c9055929ecd98..99a78856d1aa7a89a27ff72d6c4159ee35610044 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 0000000000000000000000000000000000000000..9de47d18ab81d78f73f10c92964825413ec8a7da --- /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> + ) +}