From ef81273ecb8ad5bfe24d552ad9e28bbd3bdec1ca Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 26 Jun 2024 17:00:01 +0200 Subject: [PATCH 01/14] [Task] #77 1st draft layout --- src/client/css/start.css | 56 +++++++++++++++++++++++++++----------- src/client/pages/Start.tsx | 15 ++++------ 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/client/css/start.css b/src/client/css/start.css index a5548f6..233801d 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -5,30 +5,54 @@ } .start { + /* theming */ --text: color-mix(in oklch, var(--neutral) 50%, black); - [data-mui-color-scheme="dark"] & { --text: var(--main); } + color: var(--text); - min-height: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - color: var(--text); + /* grid layout */ + height: 100%; + display: grid; + grid-template-columns: 1fr 40vmin; + grid-template-rows: 3rem 1fr 1fr 1fr 3rem; + + +} + +.grid-item { + background-color: aqua; + + &.info { + background-color: bisque; + + } + + &.map { + grid-column: 1; + grid-row: 2 / span 3; + background-color: darkkhaki; + } + + &.status { + grid-column: 2; + grid-row: 1 / span 2; + background-color: gold; + } + + &.image { + grid-column: 2; + background-color: moccasin; + } - .headline { - margin-inline: auto; - padding-block: max(1em, 10dvh); - text-align: center; - font-size: clamp(4rem, 5dvmax, 10rem); - flex-basis: 100%; + &.image+.image { + background-color: lightgoldenrodyellow; } - a { - display: block; - font-size: 2rem; + &.subinfo { + grid-column: 1 / -1; + background-color: peachpuff; } } \ No newline at end of file diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 377955e..9a518ce 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,18 +1,15 @@ import React from 'react' -import { Button, Typography } from '@mui/material'; import "../css/start.css"; function Start() { return (
-

Hello, React!!

- Test Headline - - - - - - +
info
+
map
+
status
+
image1
+
image2
+
subinfo
) } From 87970eb8810721dc257d6b338f7cfa90abfebfae Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 28 Jun 2024 13:16:13 +0200 Subject: [PATCH 02/14] [Change] #70 update token expire date --- src/scripts/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 78fb959..12106fd 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -82,7 +82,7 @@ export function createJWT(req: Request, res: Response) { date: dateString, user: req.body.user }; - const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); + const token = jwt.sign(payload, key, { expiresIn: 60 * 25 }); res.locals.token = token; return token; } From 8c548bda2907dec763f99121e867482d8d31a83d Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 28 Jun 2024 13:17:03 +0200 Subject: [PATCH 03/14] [Temp] #77, log out data on valid request, temp: error handling and display --- src/client/pages/Start.tsx | 34 ++++++++++++++++++++++++++++++++-- src/client/tsconfig.json | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 9a518ce..7c4f6c2 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,10 +1,40 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import "../css/start.css"; +import axios from 'axios'; function Start() { + const [entries, setEntries] = useState([]); + + useEffect(() => { + const token = sessionStorage.getItem("jwt"); + let response; + + const getData = async () => { + try { + response = await axios({ + method: 'get', + url: "/read?index=0", + headers: { + 'Authorization': `Bearer ${token}`, + } + }); + setEntries(response.data.entries); + } catch (error) { + console.log(error) + } + }; + + getData(); + console.log(response); + + return () => { + console.log("cleanup") + }; + + }, []); return (
-
info
+
info: {JSON.stringify(entries)}
map
status
image1
diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 04b7ddc..7777c58 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -6,5 +6,5 @@ "jsx": "react", "esModuleInterop": true }, - "include": ["**/*.tsx", "**/*.ts", "types.d.ts"] + "include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts"] } \ No newline at end of file From 561f923c55fc6589ecde0f1f1bcae21f18b68086 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Sun, 30 Jun 2024 22:19:19 +0200 Subject: [PATCH 04/14] [Temp] --- src/client/components/App.tsx | 10 +++++++--- src/client/pages/Login.tsx | 4 ++-- src/client/pages/Start.tsx | 14 ++++++++------ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8f879d6..8bab3b5 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { createContext, useState } from 'react'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Start from '../pages/Start'; import Login from '../pages/Login'; +export const LoginContext = createContext(true); + const router = createBrowserRouter([ { path: "/", @@ -15,10 +17,12 @@ const router = createBrowserRouter([ ]); const App = () => { - + const [isLoggedIn, setLogin] = useState(true); return ( - + + + ); } diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 56506ba..eb22d83 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -23,7 +23,7 @@ function Login() { const [isLoading, setLoading] = React.useState(false); const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); - const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; //&& formInfo.token; + const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; // TODO check token and tests && formInfo.token; function updateField(name: string, value: string) { const hasError = validateField(name, value, false); @@ -75,7 +75,7 @@ function Login() { headers: { "content-type": "application/x-www-form-urlencoded" } }) const token = response.data.token; - sessionStorage.setItem("jwt", token); + sessionStorage.setItem("jwt", token); // TODO check expire date setMessageObj({isError: false, status: , message: "Success!" }) } catch (error) { setMessageObj({isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 7c4f6c2..f0116f1 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useContext } from 'react' import "../css/start.css"; import axios from 'axios'; +import { LoginContext } from "../components/App"; function Start() { + const isLoggedIn = useContext(LoginContext); const [entries, setEntries] = useState([]); useEffect(() => { @@ -23,18 +25,18 @@ function Start() { console.log(error) } }; - - getData(); + + getData(); console.log(response); - + return () => { console.log("cleanup") }; - + }, []); return (
-
info: {JSON.stringify(entries)}
+
loggedIn: {isLoggedIn ? "yes" : "no" } info: {JSON.stringify(entries)}
map
status
image1
From 369e654441ebd9db6ebc3ca19e2d26302af37021 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 1 Jul 2024 23:47:36 +0200 Subject: [PATCH 05/14] [Task] #77, login Button functionality, default state --- src/client/components/App.tsx | 22 +++++++++++++++++++--- src/client/css/start.css | 7 ++----- src/client/pages/Login.tsx | 24 ++++++++++++++++-------- src/client/pages/Start.tsx | 22 ++++++++++++++++++++-- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8bab3b5..3e6cfd5 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -3,7 +3,23 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Start from '../pages/Start'; import Login from '../pages/Login'; -export const LoginContext = createContext(true); +export const LoginContext = createContext([]); + +function loginDefault() { + const token = sessionStorage?.jwt; + let exp; + + if (token) { + try { + exp = JSON.parse(window.atob(token.split('.')[1])).exp; + const date = new Date(); + return date.getTime() / 1000 <= exp; + } catch (error) { + console.error("Unable to parse JWT Data, for login default state"); + return false; + } + } +} const router = createBrowserRouter([ { @@ -17,10 +33,10 @@ const router = createBrowserRouter([ ]); const App = () => { - const [isLoggedIn, setLogin] = useState(true); + const [isLoggedIn, setLogin] = useState(loginDefault()); return ( - + ); diff --git a/src/client/css/start.css b/src/client/css/start.css index 233801d..8198196 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -17,16 +17,13 @@ height: 100%; display: grid; grid-template-columns: 1fr 40vmin; - grid-template-rows: 3rem 1fr 1fr 1fr 3rem; - - + grid-template-rows: minmax(3em, auto) 1fr 1fr 1fr minmax(3em, auto); } .grid-item { - background-color: aqua; &.info { - background-color: bisque; + } diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index eb22d83..556aa7b 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,12 +1,16 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { TextField, Button, InputAdornment, CircularProgress } from '@mui/material'; import { AccountCircle, Lock, HighlightOff, Login as LoginIcon, Check } from '@mui/icons-material'; import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; import qs from 'qs'; +import { LoginContext } from '../components/App'; +import { useNavigate } from 'react-router-dom'; function Login() { + const navigate = useNavigate(); + const [isLoggedIn, setLogin] = useContext(LoginContext); const [formInfo, updateFormInfo] = useState({ user: { isError: false, @@ -44,7 +48,7 @@ function Login() { async function submit(e) { e.preventDefault(); setLoading(true); - setMessageObj({ isError: null, status: null, message: null }) + setMessageObj({ isError: null, status: null, message: null }); let token = null; // get csrf token @@ -52,18 +56,18 @@ function Login() { token = await axios({ method: "post", url: "/login/csrf", - headers: { + headers: { "content-type": "application/x-www-form-urlencoded", "x-requested-with": "XMLHttpRequest" } }) updateFormInfo({ ...formInfo, token: token.data }); } catch (error) { - setMessageObj({isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } - if (!token) {setLoading(false); return; } // skip when the first request has an error + if (!token) { setLoading(false); return; } // skip when the first request has an error // collect data and convert to urlencoded string then send const bodyFormData = { "user": formInfo.user.value, "password": formInfo.password.value, csrfToken: token.data }; @@ -76,9 +80,13 @@ function Login() { }) const token = response.data.token; sessionStorage.setItem("jwt", token); // TODO check expire date - setMessageObj({isError: false, status: , message: "Success!" }) + setLogin(true); + setTimeout(() => { navigate("/") }, 300); + + + setMessageObj({ isError: false, status: , message: "Success!" }) } catch (error) { - setMessageObj({isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); } finally { setLoading(false); // Reset loading after request is complete @@ -153,7 +161,7 @@ function Login() { {errorObj.status ? (

{errorObj.status} - {errorObj.message.split('\n').map((line:string, index:string) => ( + {errorObj.message.split('\n').map((line: string, index: string) => ( {line}
diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index f0116f1..996fa5a 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -2,9 +2,12 @@ import React, { useEffect, useState, useContext } from 'react' import "../css/start.css"; import axios from 'axios'; import { LoginContext } from "../components/App"; +import { HighlightOff, Check } from '@mui/icons-material'; +import { Button } from '@mui/material'; + function Start() { - const isLoggedIn = useContext(LoginContext); + const [isLoggedIn] = useContext(LoginContext); const [entries, setEntries] = useState([]); useEffect(() => { @@ -34,9 +37,24 @@ function Start() { }; }, []); + return (

-
loggedIn: {isLoggedIn ? "yes" : "no" } info: {JSON.stringify(entries)}
+
+ + + + {isLoggedIn ? "yes" : "no"} info: {JSON.stringify(entries)}
map
status
image1
From e57e9ba2f7b6ad2a3453a48d67f70c4d134ca9ff Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 2 Jul 2024 14:04:48 +0200 Subject: [PATCH 06/14] [Task] #77, removed outdated comments --- src/client/components/App.tsx | 3 ++- src/client/pages/Login.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 3e6cfd5..089f166 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -18,7 +18,8 @@ function loginDefault() { console.error("Unable to parse JWT Data, for login default state"); return false; } - } + } + return false; } const router = createBrowserRouter([ diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 556aa7b..cf77510 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -27,7 +27,7 @@ function Login() { const [isLoading, setLoading] = React.useState(false); const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); - const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; // TODO check token and tests && formInfo.token; + const isFormValid = formInfo.user.value && !formInfo.user.isError && formInfo.password.value && !formInfo.password.isError; function updateField(name: string, value: string) { const hasError = validateField(name, value, false); @@ -79,7 +79,7 @@ function Login() { headers: { "content-type": "application/x-www-form-urlencoded" } }) const token = response.data.token; - sessionStorage.setItem("jwt", token); // TODO check expire date + sessionStorage.setItem("jwt", token); setLogin(true); setTimeout(() => { navigate("/") }, 300); From 3c01fed439be41a66807ce810c5f035ecea3d14b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 3 Jul 2024 00:34:05 +0200 Subject: [PATCH 07/14] [Task] #77, introduced linearBuffer Bar for login --- httpdocs/css/base.css | 12 ++-- src/client/components/LinearBuffer.tsx | 41 +++++++++++++ src/client/css/start.css | 79 ++++++++++++++++++-------- src/client/pages/Login.tsx | 24 ++++++-- src/client/pages/Start.tsx | 31 +++++++--- 5 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 src/client/components/LinearBuffer.tsx diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index d1cab3a..1b8335b 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -377,12 +377,12 @@ Neutral: #131211 /* --mui-palette-FilledInput-bg:rgba(0, 0, 0, 0.06); --mui-palette-FilledInput-hoverBg:rgba(0, 0, 0, 0.09); --mui-palette-FilledInput-disabledBg:rgba(0, 0, 0, 0.12); */ - --mui-palette-LinearProgress-primaryBg: color-mix(in oklch, var(--mui-palette-primary-main) 85%, transparent); - --mui-palette-LinearProgress-secondaryBg: color-mix(in oklch, var(--mui-palette-secondary-main) 85%, transparent); - --mui-palette-LinearProgress-errorBg: color-mix(in oklch, var(--mui-error-primary-main) 85%, transparent); - --mui-palette-LinearProgress-infoBg: color-mix(in oklch, var(--mui-palette-info-main) 85%, transparent); - --mui-palette-LinearProgress-successBg: color-mix(in oklch, var(--mui-palette-success-main) 85%, transparent); - --mui-palette-LinearProgress-warningBg: color-mix(in oklch, var(--mui-palette-warning-main) 85%, transparent); + --mui-palette-LinearProgress-primaryBg: color-mix(in oklch, var(--mui-palette-primary-main) 50%, transparent); + --mui-palette-LinearProgress-secondaryBg: color-mix(in oklch, var(--mui-palette-secondary-main) 50%, transparent); + --mui-palette-LinearProgress-errorBg: color-mix(in oklch, var(--mui-error-primary-main) 50%, transparent); + --mui-palette-LinearProgress-infoBg: color-mix(in oklch, var(--mui-palette-info-main) 50%, transparent); + --mui-palette-LinearProgress-successBg: color-mix(in oklch, var(--mui-palette-success-main) 50%, transparent); + --mui-palette-LinearProgress-warningBg: color-mix(in oklch, var(--mui-palette-warning-main) 50%, transparent); --mui-palette-Skeleton-bg: rgba(var(--mui-palette-text-primaryChannel) / 0.11); --mui-palette-Slider-primaryTrack: var(--mui-palette-primary-main); --mui-palette-Slider-secondaryTrack: var(--mui-palette-secondary-main); diff --git a/src/client/components/LinearBuffer.tsx b/src/client/components/LinearBuffer.tsx new file mode 100644 index 0000000..59e5316 --- /dev/null +++ b/src/client/components/LinearBuffer.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import LinearProgress from '@mui/material/LinearProgress'; + +export default function LinearBuffer({ msStart, msFinish }: { msStart: number, msFinish: number }) { + const [progress, setProgress] = React.useState(0); + const [buffer, setBuffer] = React.useState(10); + + const progressRef = React.useRef(() => { }); + React.useEffect(() => { + progressRef.current = () => { + const duration = msFinish - msStart; // duration based on input props + const secondPhase = duration == 1000; + const date = new Date(); + const now = date.getTime(); + + const bufferValue = secondPhase ? 100 : 90; + const progressCalcValue = ((now - msStart) / duration) * 100; + const progressValue = secondPhase ? 100 : Math.min(progressCalcValue, bufferValue); + + setProgress(progressValue); + setBuffer(bufferValue); + }; + }); + + React.useEffect(() => { + const timer = setInterval(() => { + progressRef.current(); + }, 300); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + + ); +} \ No newline at end of file diff --git a/src/client/css/start.css b/src/client/css/start.css index 8198196..f3b10ff 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -7,49 +7,80 @@ .start { /* theming */ --text: color-mix(in oklch, var(--neutral) 50%, black); + [data-mui-color-scheme="dark"] & { --text: var(--main); } + color: var(--text); /* grid layout */ height: 100%; display: grid; - grid-template-columns: 1fr 40vmin; + grid-template-columns: 1fr 40vmin; grid-template-rows: minmax(3em, auto) 1fr 1fr 1fr minmax(3em, auto); -} -.grid-item { + .grid-item { - &.info { - + &.info { + display: flex; + width: 100%; + justify-content: space-between; + padding: 0.7em 2em; - } + } - &.map { - grid-column: 1; - grid-row: 2 / span 3; - background-color: darkkhaki; - } + &.map { + grid-column: 1; + grid-row: 2 / span 3; + background-color: darkkhaki; + } - &.status { - grid-column: 2; - grid-row: 1 / span 2; - background-color: gold; - } + &.status { + grid-column: 2; + grid-row: 1 / span 2; + background-color: gold; + } + + &.image { + grid-column: 2; + background-color: moccasin; + } + + &.image+.image { + background-color: lightgoldenrodyellow; + } - &.image { - grid-column: 2; - background-color: moccasin; + &.subinfo { + grid-column: 1 / -1; + background-color: peachpuff; + } } - &.image+.image { - background-color: lightgoldenrodyellow; + .error { + display: inline-flex; + flex-wrap: wrap; + align-content: center; + justify-content: center; + font-size: 1.3em; + + .statusCode { + width: 100%; + text-align: center; + } } - &.subinfo { - grid-column: 1 / -1; - background-color: peachpuff; + .loginButton { + color: var(--bg); + margin-left: auto; + cursor: pointer; + + &.loginButton--loggedIn { + svg { + position: relative; + top: -0.1em; right: 0.1em; + } + } } } \ No newline at end of file diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index cf77510..a6da38e 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -7,8 +7,11 @@ import axios from 'axios'; import qs from 'qs'; import { LoginContext } from '../components/App'; import { useNavigate } from 'react-router-dom'; +import LinearBuffer from '../components/LinearBuffer'; function Login() { + const [finish, setFinish] = useState(1); + const [start, setStart] = useState(1); const navigate = useNavigate(); const [isLoggedIn, setLogin] = useContext(LoginContext); const [formInfo, updateFormInfo] = useState({ @@ -47,6 +50,11 @@ function Login() { async function submit(e) { e.preventDefault(); + const date = new Date(); + setStart(date.getTime()); + const milliseconds = 9 * 1000; // Estimated bcrypt Time + setFinish(new Date(date.getTime() + milliseconds).getTime()); + setLoading(true); setMessageObj({ isError: null, status: null, message: null }); @@ -63,8 +71,8 @@ function Login() { }) updateFormInfo({ ...formInfo, token: token.data }); } catch (error) { - setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) } if (!token) { setLoading(false); return; } // skip when the first request has an error @@ -81,14 +89,19 @@ function Login() { const token = response.data.token; sessionStorage.setItem("jwt", token); setLogin(true); - setTimeout(() => { navigate("/") }, 300); + setMessageObj({ isError: false, status: , message: "Success!" }) + // update linearBar for delay until redirect + const date = new Date(); + setStart(date.getTime()); + setFinish(new Date(date.getTime() + 1000).getTime()); + + // redirect back to main page + setTimeout(() => { setLoading(false); navigate("/") }, 1000); - setMessageObj({ isError: false, status: , message: "Success!" }) } catch (error) { - setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) console.log(error); - } finally { + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }) setLoading(false); // Reset loading after request is complete } } @@ -180,6 +193,7 @@ function Login() { Login
+ {isLoading && }
diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 996fa5a..cf9812f 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -5,27 +5,35 @@ import { LoginContext } from "../components/App"; import { HighlightOff, Check } from '@mui/icons-material'; import { Button } from '@mui/material'; - function Start() { - const [isLoggedIn] = useContext(LoginContext); + const [isLoggedIn, setLogin] = useContext(LoginContext); const [entries, setEntries] = useState([]); + const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); + useEffect(() => { const token = sessionStorage.getItem("jwt"); let response; const getData = async () => { + if (!token) { + setLogin(false); + setMessageObj({ isError: true, status: "403", message: "No token / logged out" }) + return false; + } + try { response = await axios({ method: 'get', url: "/read?index=0", headers: { - 'Authorization': `Bearer ${token}`, + 'Authorization': `Bearer ${token}` } }); setEntries(response.data.entries); + setMessageObj({ isError: null, status: null, message: null }); } catch (error) { - console.log(error) + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); } }; @@ -41,20 +49,25 @@ function Start() { return (
+ {errorObj.isError && +
+ {errorObj.status} {errorObj.message} +
+ } +
- - {isLoggedIn ? "yes" : "no"} info: {JSON.stringify(entries)}
map
status
image1
From f9ca19ddeb77024b54bbe722f6ccb048aaad19f1 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Wed, 3 Jul 2024 17:29:59 +0200 Subject: [PATCH 08/14] [Task] #77, added modeSwticher to start page --- src/client/components/ModeSwitcher.tsx | 2 +- src/client/css/login.css | 5 ++++- src/client/css/modeSwticher.module.css | 6 ++---- src/client/css/start.css | 24 ++++++++++++++++++++---- src/client/pages/Login.tsx | 8 +++++--- src/client/pages/Start.tsx | 13 +++++++++---- 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/client/components/ModeSwitcher.tsx b/src/client/components/ModeSwitcher.tsx index a9ca0d7..cf4bb31 100644 --- a/src/client/components/ModeSwitcher.tsx +++ b/src/client/components/ModeSwitcher.tsx @@ -16,7 +16,7 @@ function ModeSwitcher() {
map
+
status
-
image1
-
image2
+
+
image1
+
image2
+
+
subinfo
) From 4b48089adcabf34866bd8909472cd1126d58c078 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 4 Jul 2024 15:46:05 +0200 Subject: [PATCH 09/14] [Task] #77, display last entry on map demo --- package-lock.json | 57 +++++++++++++++++++++++++++++++++-- package.json | 4 +++ src/client/components/Map.tsx | 30 ++++++++++++++++++ src/client/css/map.module.css | 3 ++ src/client/pages/Start.tsx | 3 +- types.d.ts | 2 +- 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/client/components/Map.tsx create mode 100644 src/client/css/map.module.css diff --git a/package-lock.json b/package-lock.json index 2accda0..8cddd0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,9 +25,12 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.23.1", "toobusy-js": "^0.5.1" }, @@ -40,6 +43,7 @@ "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", + "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", "@types/react": "^18.2.74", "@types/react-dom": "^18.2.24", @@ -1953,6 +1957,16 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -2144,6 +2158,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2223,6 +2244,16 @@ "@types/node": "*" } }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -7479,6 +7510,17 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-defaulticon-compatibility": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz", + "integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==", + "license": "BSD-2-Clause" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11454,7 +11496,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -11466,7 +11507,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11480,6 +11520,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-router": { "version": "6.23.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", diff --git a/package.json b/package.json index 4a2c0a2..f40ba28 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", + "@types/leaflet": "^1.9.12", "@types/node": "^20.11.30", "@types/react": "^18.2.74", "@types/react-dom": "^18.2.24", @@ -77,9 +78,12 @@ "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "module-alias": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.23.1", "toobusy-js": "^0.5.1" }, diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx new file mode 100644 index 0000000..53b33bd --- /dev/null +++ b/src/client/components/Map.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' +import 'leaflet/dist/leaflet.css'; +import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package +import L from 'leaflet'; +import 'leaflet-defaulticon-compatibility'; +import * as css from "../css/map.module.css"; + +function Map({ entries }: { entries: Models.IEntry[] }) { + if(!entries?.length) { + return ( "No Data to be displayed" ); + } + const lastEntry = entries.at(-1); + + return ( + + + + + {JSON.stringify(lastEntry)} + + + + ) +} + +export default Map diff --git a/src/client/css/map.module.css b/src/client/css/map.module.css new file mode 100644 index 0000000..59c6bb8 --- /dev/null +++ b/src/client/css/map.module.css @@ -0,0 +1,3 @@ +.mapContainer { + height: 100%; +} \ No newline at end of file diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 5d1f6c3..f904114 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -5,6 +5,7 @@ import { LoginContext } from "../components/App"; import { HighlightOff, Check } from '@mui/icons-material'; import { Button } from '@mui/material'; import ModeSwitcher from '../components/ModeSwitcher'; +import Map from '../components/Map'; function Start() { const [isLoggedIn, setLogin] = useContext(LoginContext); @@ -69,7 +70,7 @@ function Start() { -
map
+
status
diff --git a/types.d.ts b/types.d.ts index 3cde1ce..0bab5de 100644 --- a/types.d.ts +++ b/types.d.ts @@ -30,7 +30,7 @@ namespace File { namespace Models { interface IEntries { - entries: Models.IEntry[] + entries: IEntry[] } interface IEntry { From d3a5bc756fb492226b46f75805aebde91e22b7af Mon Sep 17 00:00:00 2001 From: Type-Style Date: Fri, 5 Jul 2024 15:53:44 +0200 Subject: [PATCH 10/14] [Task] #77, enhance login, show pastUser if availabe, show user on mainpage --- .vscode/settings.json | 4 ++- httpdocs/css/base.css | 9 +++++ src/client/components/App.tsx | 34 ++++++++++--------- src/client/components/Map.tsx | 2 +- src/client/css/login.css | 4 +++ src/client/css/start.css | 62 ++++++++++++++++++++++++++++++----- src/client/pages/Login.tsx | 12 ++++--- src/client/pages/Start.tsx | 19 +++++++---- 8 files changed, 109 insertions(+), 37 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3311857..8d2c763 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,7 @@ "workbench.editor.enablePreview": false, "editor.rename.enablePreview": false, "typescript.tsdk": "node_modules\\typescript\\lib", - "html.format.wrapLineLength": 150 + "html.format.wrapLineLength": 150, + "javascript.preferences.quoteStyle": "double", + "typescript.preferences.quoteStyle": "double" } \ No newline at end of file diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 1b8335b..0b23b43 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -199,6 +199,15 @@ Neutral: #131211 font-weight: calc(800 + var(--baseFontWeightModifier)); } +.fade { animation: fade 1s 1s forwards; } +.fadeIn { animation: reverse fade 1s forwards; } +@keyframes fade { + to { + font-size: 0; + opacity: 0; + } +} + @media screen and (prefers-reduced-motion: reduce), (update: slow) { diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 089f166..0a05fac 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -5,21 +5,24 @@ import Login from '../pages/Login'; export const LoginContext = createContext([]); -function loginDefault() { - const token = sessionStorage?.jwt; - let exp; - - if (token) { - try { - exp = JSON.parse(window.atob(token.split('.')[1])).exp; - const date = new Date(); - return date.getTime() / 1000 <= exp; - } catch (error) { - console.error("Unable to parse JWT Data, for login default state"); +export function convertJwt() { + const token = localStorage?.jwt; + if (!token) { return false } + try { + const { user, exp } = JSON.parse(window.atob(token.split('.')[1])); + return { user, exp }; + } catch (error) { + console.error("Unable to parse JWT Data"); return false; - } } - return false; +} + +function loginDefault(userInfo) { + if (!userInfo) { return false; } + + const date = new Date(); + const exp = userInfo.exp + return date.getTime() / 1000 <= exp; } const router = createBrowserRouter([ @@ -34,10 +37,11 @@ const router = createBrowserRouter([ ]); const App = () => { - const [isLoggedIn, setLogin] = useState(loginDefault()); + const [userInfo, setUserInfo] = useState(convertJwt()); + const [isLoggedIn, setLogin] = useState(loginDefault(userInfo)); return ( - + ); diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 53b33bd..5bf0973 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -2,7 +2,7 @@ import React from 'react' import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package -import L from 'leaflet'; +// import L from 'leaflet'; import 'leaflet-defaulticon-compatibility'; import * as css from "../css/map.module.css"; diff --git a/src/client/css/login.css b/src/client/css/login.css index 57fb8c4..8f03d2a 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -58,6 +58,10 @@ padding-block: 1em; text-align: center; font-size: 3.5rem; + + &.sub { + font-size: 2.1em; + } } form { diff --git a/src/client/css/start.css b/src/client/css/start.css index 948ab3c..33ea08f 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -28,7 +28,10 @@ display: flex; width: 100%; justify-content: space-between; - padding: 0.7em 2em; + padding: 0.5em 0.8em; + @media (min-width: 30em) { + padding: 0.7em 2em; + } } @@ -60,8 +63,8 @@ display: grid; } - image { - background-color: moccasin; + image { + background-color: moccasin; } .image+.image { @@ -74,14 +77,24 @@ } } - .error { + .message { display: inline-flex; flex-wrap: wrap; align-content: center; justify-content: center; - font-size: 1.3em; + font-size: 0.9em; + + @media (min-width: 30em) { + font-size: 1.3em; + } + + &.center { + margin-left: auto; + } - .statusCode { + .title { + font-size: 1.1em; + @media (min-width: 30em) { font-size: inherit; } width: 100%; text-align: center; } @@ -91,12 +104,43 @@ color: var(--bg); margin-left: auto; cursor: pointer; - + white-space: nowrap; + + font-size: 0; + padding: 0; + + @media (min-width: 30em) { + /* reset to MUI default */ + font-size: 1.3rem; + padding: 8px 22px; + } + + .MuiButton-icon { + font-size: 1.3rem; + @media (min-width: 30em) { + font-size: inherit; + } + } + .MuiButton-startIcon { + margin-left: 6px; + @media (min-width: 30em) { + margin-left: 0px; + } + } + .MuiButton-endIcon { + margin-left: 0; + @media (min-width: 30em) { + margin-left: 4px; + } + } + + &.loginButton--loggedIn { svg { position: relative; - top: -0.1em; right: 0.1em; + top: -0.1em; + right: 0.1em; } - } + } } } \ No newline at end of file diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index b0763c9..60040e4 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -5,7 +5,7 @@ import "../css/login.css"; import ModeSwitcher from '../components/ModeSwitcher'; import axios from 'axios'; import qs from 'qs'; -import { LoginContext } from '../components/App'; +import { LoginContext, convertJwt } from '../components/App'; import { useNavigate } from 'react-router-dom'; import LinearBuffer from '../components/LinearBuffer'; @@ -13,12 +13,12 @@ function Login() { const [finish, setFinish] = useState(1); const [start, setStart] = useState(1); const navigate = useNavigate(); - const [isLoggedIn, setLogin] = useContext(LoginContext); + const [isLoggedIn, setLogin, userInfo, setUserInfo] = useContext(LoginContext); const [formInfo, updateFormInfo] = useState({ user: { isError: false, message: "Minimum 2", - value: "" + value: userInfo?.user || "" }, password: { isError: false, @@ -87,7 +87,7 @@ function Login() { headers: { "content-type": "application/x-www-form-urlencoded" } }) const token = response.data.token; - sessionStorage.setItem("jwt", token); + localStorage.setItem("jwt", token); setLogin(true); setMessageObj({ isError: false, status: , message: "Success!" }) @@ -95,6 +95,7 @@ function Login() { const date = new Date(); setStart(date.getTime()); setFinish(new Date(date.getTime() + 1000).getTime()); + setUserInfo(convertJwt()); // redirect back to main page setTimeout(() => { setLoading(false); navigate("/") }, 1000); @@ -115,6 +116,9 @@ function Login() {

Login Page

+ {isLoggedIn && +

You are logged in

+ }
([]); const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); useEffect(() => { - const token = sessionStorage.getItem("jwt"); + const token = localStorage.getItem("jwt"); let response; const getData = async () => { @@ -52,15 +52,20 @@ function Start() {
{errorObj.isError && -
- {errorObj.status} {errorObj.message} +
+ {errorObj.status} {errorObj.message} +
+ } + {!errorObj.isError && userInfo && +
+ {userInfo.user} Welcome back
}
-
-
+
+
status
image1
From eb606728f812946a74b77f7cb2aec3c6e7290a7c Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Jul 2024 16:25:23 +0200 Subject: [PATCH 11/14] [!!!Task] #77 first draft of functionality --- httpdocs/css/base.css | 4 +- src/client/components/LinearBuffer.tsx | 27 ++-- src/client/components/Map.tsx | 6 +- src/client/components/Status.tsx | 16 +++ src/client/css/start.css | 79 +++++++++-- src/client/css/status.module.css | 0 src/client/pages/Login.tsx | 3 +- src/client/pages/Start.tsx | 188 +++++++++++++++++-------- src/scripts/token.ts | 8 +- 9 files changed, 243 insertions(+), 88 deletions(-) create mode 100644 src/client/components/Status.tsx create mode 100644 src/client/css/status.module.css diff --git a/httpdocs/css/base.css b/httpdocs/css/base.css index 0b23b43..fd4fe7f 100644 --- a/httpdocs/css/base.css +++ b/httpdocs/css/base.css @@ -135,6 +135,8 @@ Neutral: #131211 font-weight: calc(400 + var(--baseFontWeightModifier)); + accent-color: var(--main); + /* dark theme, initial state (prefers mq) by react */ &[data-mui-color-scheme="dark"] { --main: oklch(75% 0.1738 64.55); @@ -166,7 +168,7 @@ Neutral: #131211 width:1px; } -.cut { +.cut, .cut-after::after { --cut: 2em; clip-path: polygon(0% var(--cut), var(--cut) 0%, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%); } diff --git a/src/client/components/LinearBuffer.tsx b/src/client/components/LinearBuffer.tsx index 59e5316..3b6bc86 100644 --- a/src/client/components/LinearBuffer.tsx +++ b/src/client/components/LinearBuffer.tsx @@ -1,25 +1,32 @@ import * as React from 'react'; -import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; -export default function LinearBuffer({ msStart, msFinish }: { msStart: number, msFinish: number }) { +export default function LinearBuffer({ msStart, msFinish, variant = "buffer" }: { msStart: number, msFinish: number, variant?: "buffer" | "determinate" }) { const [progress, setProgress] = React.useState(0); const [buffer, setBuffer] = React.useState(10); const progressRef = React.useRef(() => { }); React.useEffect(() => { - progressRef.current = () => { + if (!msStart || !msFinish) { + console.log("LinearProgress did not recieve correct data") + } + progressRef.current = () => { + let progressValue; const duration = msFinish - msStart; // duration based on input props - const secondPhase = duration == 1000; const date = new Date(); const now = date.getTime(); - - const bufferValue = secondPhase ? 100 : 90; const progressCalcValue = ((now - msStart) / duration) * 100; - const progressValue = secondPhase ? 100 : Math.min(progressCalcValue, bufferValue); + progressValue = progressCalcValue; + if (variant == "buffer") { + const secondPhase = duration == 1000; + const bufferValue = secondPhase ? 100 : 90; + progressValue = secondPhase ? 100 : Math.min(progressCalcValue, bufferValue); + + setBuffer(bufferValue); + } setProgress(progressValue); - setBuffer(bufferValue); + }; }); @@ -34,8 +41,6 @@ export default function LinearBuffer({ msStart, msFinish }: { msStart: number, m }, []); return ( - - - + ); } \ No newline at end of file diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 5bf0973..6e29961 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -7,8 +7,8 @@ import 'leaflet-defaulticon-compatibility'; import * as css from "../css/map.module.css"; function Map({ entries }: { entries: Models.IEntry[] }) { - if(!entries?.length) { - return ( "No Data to be displayed" ); + if (!entries?.length) { + return No Data to be displayed } const lastEntry = entries.at(-1); @@ -20,7 +20,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { /> - {JSON.stringify(lastEntry)} + {JSON.stringify(lastEntry, null, 2)} diff --git a/src/client/components/Status.tsx b/src/client/components/Status.tsx new file mode 100644 index 0000000..54e5bcd --- /dev/null +++ b/src/client/components/Status.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import * as css from "../css/status.module.css"; + +function Map({ entries }: { entries: Models.IEntry[] }) { + if(!entries?.length) { + return No Data to be displayed + } + //const lastEntry = entries.at(-1); + + + return ( + Status! + ) +} + +export default Map diff --git a/src/client/css/start.css b/src/client/css/start.css index 33ea08f..b197310 100644 --- a/src/client/css/start.css +++ b/src/client/css/start.css @@ -32,7 +32,6 @@ @media (min-width: 30em) { padding: 0.7em 2em; } - } &.theme { @@ -41,19 +40,40 @@ display: grid; align-items: center; justify-items: center; - background-color: var(--bg); } &.map { grid-column: 1; grid-row: 2 / span 3; - background-color: darkkhaki; + margin-right: 1em; + + background-color: color-mix(in oklab, transparent 50%, var(--main)); } &.status { grid-column: 2; grid-row: 2; - background-color: gold; + margin: 1em 1em 1em 0; + position: relative; + z-index: 0; + + &::after { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + + background-color: var(--semiBg); + + --shadowColor: var(--text); + filter: url(#rough-light); + box-shadow: 0 0 0.2em var(--shadowColor); + } + [data-mui-color-scheme="dark"] &::after { + --shadowColor: var(--main); + filter: url(#rough-light) drop-shadow(0 3px 5px var(--shadowColor)); + } + } &.images { @@ -61,19 +81,46 @@ grid-row: 3 / span 2; display: grid; + overflow: auto; } - image { + .image { + display: inline-block; + aspect-ratio: 16/9; background-color: moccasin; } .image+.image { background-color: lightgoldenrodyellow; } + .image+.image+.image { + background-color: antiquewhite; + } &.subinfo { - grid-column: 1 / -1; - background-color: peachpuff; + grid-column: 1; + padding: 0.5em 0.8em; + @media (min-width: 30em) { + padding: 0.7em 2em; + } + + .MuiLinearProgress-root { + margin: -0.5em 0 1em -0.8em; + @media (min-width: 30em) { + margin: -0.7em -1em 1em -2em; + } + } + + .info { + display: inline-block; + padding-inline: 1em; + border-right: 0.1rem solid; + + &:last-child, &.noDivider { + border: none; + padding-right: 0; + } + } } } @@ -92,6 +139,10 @@ margin-left: auto; } + &.error { + color: var(--alert); + } + .title { font-size: 1.1em; @media (min-width: 30em) { font-size: inherit; } @@ -101,7 +152,8 @@ } .loginButton { - color: var(--bg); + color: white; + [data-mui-color-scheme="dark"] & { color: black;} margin-left: auto; cursor: pointer; white-space: nowrap; @@ -143,4 +195,15 @@ } } } +} + +.noData { + display: block; + font-size: 1.4em; + text-align: center; + width: min-content; + margin: 2.5em auto; + padding: 2.5em; + color: var(--alert); + background-color: var(--semiBg); } \ No newline at end of file diff --git a/src/client/css/status.module.css b/src/client/css/status.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 60040e4..00a8ee5 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -129,11 +129,11 @@ function Login() { error={formInfo.user.isError} helperText={formInfo.user.isError ? formInfo.user.message : false} required + autoFocus={!userInfo?.user} InputProps={{ classes: { root: "cut", }, - autoFocus: true, name: "user", startAdornment: ( @@ -158,6 +158,7 @@ function Login() { required error={formInfo.password.isError} helperText={formInfo.password.isError ? formInfo.password.message : false} + autoFocus={!!userInfo?.user} InputProps={{ classes: { root: "cut", diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 6290998..451508c 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useContext } from 'react' +import React, { useEffect, useState, useContext, useRef } from 'react' import "../css/start.css"; import axios from 'axios'; import { LoginContext } from "../components/App"; @@ -6,85 +6,149 @@ import { HighlightOff, Check } from '@mui/icons-material'; import { Button } from '@mui/material'; import ModeSwitcher from '../components/ModeSwitcher'; import Map from '../components/Map'; +import Status from '../components/Status'; +import LinearBuffer from "../components/LinearBuffer"; + +function timeAgo(timestamp: number): string { + if (!Number.isInteger(timestamp)) { + return ""; + } + const now = Date.now(); + const diffInSeconds = Math.floor((now - timestamp) / 1000); + + const seconds = diffInSeconds; + const minutes = Math.floor(diffInSeconds / 60); + const hours = Math.floor(diffInSeconds / 3600); + const days = Math.floor(diffInSeconds / 86400); + const weeks = Math.floor(diffInSeconds / 604800); + const months = Math.floor(diffInSeconds / 2592000); + const years = Math.floor(diffInSeconds / 31536000); + + if (seconds < 8) { return "Instant"; } + else if (seconds < 30) { return "Just now" } + else if (seconds < 60) { return "a moment ago" } + else if (minutes < 60) { return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; } + else if (hours < 24) { return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; } + else if (days < 7) { return `${days} ${days === 1 ? "day" : "days"} ago`; } + else if (weeks < 4) { return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; } + else if (months < 12) { return `${months} ${months === 1 ? "month" : "months"} ago`; } + else { return `${years} ${years === 1 ? "year" : "years"} ago`; } +} function Start() { const [isLoggedIn, setLogin, userInfo] = useContext(LoginContext); const [entries, setEntries] = useState([]); - const [errorObj, setMessageObj] = React.useState({ isError: null, status: null, message: null }); + const [messageObj, setMessageObj] = useState({ isError: null, status: null, message: null }); + const [lastFetch, setLastFetch] = useState(); + const [nextFetch, setNextFetch] = useState(); + const index = useRef(0); + const intervalID = useRef(); - useEffect(() => { + const fetchIntervalMs = 1000 * 55; + + const getData = async () => { const token = localStorage.getItem("jwt"); let response; - const getData = async () => { - if (!token) { - setLogin(false); - setMessageObj({ isError: true, status: "403", message: "No token / logged out" }) - return false; - } + if (!token) { + setLogin(false); + setMessageObj({ isError: true, status: "403", message: "No valid login" }) + return false; + } - try { - response = await axios({ - method: 'get', - url: "/read?index=0", - headers: { - 'Authorization': `Bearer ${token}` - } - }); - setEntries(response.data.entries); - setMessageObj({ isError: null, status: null, message: null }); - } catch (error) { - setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); - } - }; + try { + const now = new Date().getTime(); + setLastFetch(now); + response = await axios({ + method: 'get', + url: "/read?index=" + index.current + "&noCache=" + now, + headers: { + 'Authorization': `Bearer ${token}` + } + }); - getData(); - console.log(response); + const newEntries = response.data.entries; + if (newEntries.length) { + setEntries((prevEntries) => [prevEntries, ...newEntries]); + index.current += newEntries.length; + } - return () => { - console.log("cleanup") - }; + setMessageObj({ isError: null, status: null, message: null }); + setNextFetch(new Date().getTime() + fetchIntervalMs); + } catch (error) { + clearInterval(intervalID.current); intervalID.current = null; + console.info("cleared Interval"); + setNextFetch(null); + if (error.response.status == 403) { setLogin(false) } + setMessageObj({ isError: true, status: error.response.data.status || error.response.status, message: error.response.data.message || error.message }); + } + }; + useEffect(() => { + if (isLoggedIn) { + getData(); + intervalID.current = setInterval(getData, fetchIntervalMs); // capture interval ID as return from setInterval and pass to state + return () => { console.log("cleanup"); clearInterval(intervalID.current); intervalID.current = null; }; + } else if (userInfo) { // no valid login but userInfo + setMessageObj({ isError: true, status: "403", message: "Login expired" }) + } }, []); return ( -
-
- {errorObj.isError && -
- {errorObj.status} {errorObj.message} -
- } - {!errorObj.isError && userInfo && -
- {userInfo.user} Welcome back -
- } - -
+ <> +
+
+ {messageObj.isError && +
+ {messageObj.status} {messageObj.message} +
+ } + {!messageObj.isError && userInfo && +
+ {userInfo.user} Welcome back +
+ } + +
-
-
-
status
-
-
image1
-
image2
-
+
+
+
+
+
image1
+
image2
+
image3
+
-
subinfo
-
+
+ {isLoggedIn && intervalID && + + } + {isLoggedIn && intervalID && entries?.length > 0 && + <> + GPS: + {entries.at(-1).lat} / {entries.at(-1).lon} + {timeAgo(entries.at(-1).time.created)} + + } +
+
+ + + + ) } diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 12106fd..9a012f2 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -59,7 +59,11 @@ export function validateJWT(req: Request) { } catch (err) { let message = "could not verify"; if (err instanceof Error) { - message = `${err.name} - ${err.message}`; + if (err.name == "TokenExpiredError") { + message = "Login expired"; + } else { + message = `${err.name} - ${err.message}`; + } } return { success: false, status: 403, message: message }; @@ -82,7 +86,7 @@ export function createJWT(req: Request, res: Response) { date: dateString, user: req.body.user }; - const token = jwt.sign(payload, key, { expiresIn: 60 * 25 }); + const token = jwt.sign(payload, key, { expiresIn: 60 * 30 }); res.locals.token = token; return token; } From d8e6eec47cdbb629f2e4bbe6df96c2009233bd83 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 15 Jul 2024 16:36:58 +0200 Subject: [PATCH 12/14] [Task] #77 move map to new location --- src/client/components/Map.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 6e29961..19a0ea7 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -1,11 +1,24 @@ -import React from 'react' -import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' +import React, { useEffect } from 'react' +import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package // import L from 'leaflet'; import 'leaflet-defaulticon-compatibility'; import * as css from "../css/map.module.css"; + +// Used to recenter the map to new coordinates +const MapRecenter= ({ lat, lng, zoomLevel }) => { + const map = useMap(); + + useEffect(() => { + // Fly to that coordinates and set new zoom level + map.flyTo([lat, lng], zoomLevel ); + }, [lat, lng]); + return null; + +}; + function Map({ entries }: { entries: Models.IEntry[] }) { if (!entries?.length) { return No Data to be displayed @@ -14,6 +27,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { return ( + Date: Tue, 16 Jul 2024 20:13:42 +0200 Subject: [PATCH 13/14] [Task] #77 create testData --- jest.config.js | 2 +- jest.testData.config.js | 10 +++++ package.json | 1 + src/client/components/Map.tsx | 16 +++---- src/testData/createTestData.test.ts | 68 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 jest.testData.config.js create mode 100644 src/testData/createTestData.test.ts diff --git a/jest.config.js b/jest.config.js index 0a57f54..5cada10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - modulePathIgnorePatterns: ['/dist/'], + modulePathIgnorePatterns: ['/dist/', '/src/testData/'], moduleNameMapper: { '^@src/(.*)$': '/src/$1', }, diff --git a/jest.testData.config.js b/jest.testData.config.js new file mode 100644 index 0000000..f8e72a3 --- /dev/null +++ b/jest.testData.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + }, + testMatch: ['/src/testData/createTestData.test.ts'], + bail: true +}; \ No newline at end of file diff --git a/package.json b/package.json index f40ba28..a74c13c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "lint:client": "eslint httpdocs/js/ --fix", "lint:react": "eslint src/client/ --fix", "test": "jest", + "test:data": "jest --config jest.testData.config.js", "test:app": "jest src/tests/app.test.ts", "test:login": "jest src/tests/login.test.ts", "test:unit": "jest src/tests/unit.test.ts", diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx index 19a0ea7..3eb782a 100644 --- a/src/client/components/Map.tsx +++ b/src/client/components/Map.tsx @@ -8,14 +8,14 @@ import * as css from "../css/map.module.css"; // Used to recenter the map to new coordinates -const MapRecenter= ({ lat, lng, zoomLevel }) => { - const map = useMap(); +const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => { + const map = useMap(); - useEffect(() => { - // Fly to that coordinates and set new zoom level - map.flyTo([lat, lng], zoomLevel ); - }, [lat, lng]); - return null; + useEffect(() => { + // Fly to that coordinates and set new zoom level + map.flyTo([lat, lon], zoom); + }, [lat, lon]); + return null; }; @@ -27,7 +27,7 @@ function Map({ entries }: { entries: Models.IEntry[] }) { return ( - + { + const entries = 5; + const start = { lat: 52.51625, lon: 13.37661 }; + const end = { lat: 52.50960, lon: 13.27457 }; + const diff = {lat: end.lat - start.lat, lon: end.lon - start.lon}; + // eslint-disable-next-line jest/expect-expect + it('create ' + entries + 'entries', () => { + return new Promise(done => { + + for (let i = 0; i <= entries; i++) { + const lat = start.lat + (diff.lat / entries * i); + const lon = start.lon + (diff.lon / entries * i); + setTimeout(async () => { + console.log("call server " + i); + await callServer(undefined, `user=xx&lat=${lat}&lon=${lon}×tamp=R3Pl4C3&hdop=${Math.floor(Math.random() * 15) + 1}&altitude=${i+1}&speed=${46 + i}&heading=${262 + Math.floor(Math.random() * 20) - 10}&key=test`, 200, "GET"); + }, 1000 * 30 * i); + } + + setTimeout(async () => { + done(); + }, 1000 * 30 * entries); + }) + }, 1000 * 30 * (entries + 1)); + +}); + + + + + + + From 0261d8a79eaaa682a214b96f1d94b205e1c4ad53 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 16 Jul 2024 20:32:12 +0200 Subject: [PATCH 14/14] [Fix] #77 codeFactor complains --- src/client/css/login.css | 28 ++++++++++++-------------- src/client/css/modeSwticher.module.css | 3 +-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/client/css/login.css b/src/client/css/login.css index 8f03d2a..624f753 100644 --- a/src/client/css/login.css +++ b/src/client/css/login.css @@ -69,22 +69,7 @@ flex-flow: nowrap column; gap: 2em; margin-bottom: 10vh; - } - - input { - font-size: 2.2rem; - } - - label { - font-size: 1.8rem; - margin-bottom: 0.5em; - - &.Mui-focused { - color: var(--text); - } - } - form { .MuiSvgIcon-root { font-size: 2.7rem; max-height: 100%; @@ -100,6 +85,19 @@ } } + input { + font-size: 2.2rem; + } + + label { + font-size: 1.8rem; + margin-bottom: 0.5em; + + &.Mui-focused { + color: var(--text); + } + } + .subWrapper { display: flex; width: 100%; diff --git a/src/client/css/modeSwticher.module.css b/src/client/css/modeSwticher.module.css index 1d9f750..88f91fa 100644 --- a/src/client/css/modeSwticher.module.css +++ b/src/client/css/modeSwticher.module.css @@ -6,7 +6,7 @@ visibility: hidden; font-size: 0; - &>span { + & > span { visibility: visible; margin: 0; } @@ -29,5 +29,4 @@ font-size: 1rem; visibility: visible; } - } \ No newline at end of file