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 d1cab3a..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%); } @@ -199,6 +201,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) { @@ -377,12 +388,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/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-lock.json b/package-lock.json index 155139c..6c6a756 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.24.0", "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", @@ -1704,9 +1708,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.15.20", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz", - "integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" @@ -1953,10 +1957,20 @@ "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.17.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.0.tgz", - "integrity": "sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -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", @@ -5307,9 +5338,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.0.tgz", - "integrity": "sha512-ZPfWlcQQ1PsZonB/vqksOsBQV74z5osi/QcdoBCyKJXl/wOVjS1yRDmvkpMM52KJeLbiF2+djwVEnEgVCDdvtw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", "license": "MIT", "engines": { "node": ">= 16" @@ -5612,20 +5643,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7482,6 +7499,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", @@ -11457,7 +11485,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" }, @@ -11469,7 +11496,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" @@ -11483,13 +11509,26 @@ "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.24.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.0.tgz", - "integrity": "sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.0.tgz", + "integrity": "sha512-bziKjCcDbcxgWS9WlWFcQIVZ2vJHnCP6DGpQDT0l+0PFDasfJKgzf9CM22eTyhFsZkjk8ApCdKjJwKtzqH80jQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.17.0" + "@remix-run/router": "1.18.0" }, "engines": { "node": ">=14.0.0" @@ -11499,13 +11538,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.0.tgz", - "integrity": "sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.0.tgz", + "integrity": "sha512-BhcczgDWWgvGZxjDDGuGHrA8HrsSudilqTaRSBYLWDayvo1ClchNIDVt5rldqp6e7Dro5dEFx9Mzc+r292lN0w==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.17.0", - "react-router": "6.24.0" + "@remix-run/router": "1.18.0", + "react-router": "6.25.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 0e81109..c0f5442 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", @@ -33,6 +34,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 +79,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.24.0", "toobusy-js": "^0.5.1" }, diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 8f879d6..0a05fac 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,8 +1,30 @@ -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([]); + +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; + } +} + +function loginDefault(userInfo) { + if (!userInfo) { return false; } + + const date = new Date(); + const exp = userInfo.exp + return date.getTime() / 1000 <= exp; +} + const router = createBrowserRouter([ { path: "/", @@ -15,10 +37,13 @@ const router = createBrowserRouter([ ]); const App = () => { - + const [userInfo, setUserInfo] = useState(convertJwt()); + const [isLoggedIn, setLogin] = useState(loginDefault(userInfo)); return ( - + + + ); } diff --git a/src/client/components/LinearBuffer.tsx b/src/client/components/LinearBuffer.tsx new file mode 100644 index 0000000..3b6bc86 --- /dev/null +++ b/src/client/components/LinearBuffer.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import LinearProgress from '@mui/material/LinearProgress'; + +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(() => { + 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 date = new Date(); + const now = date.getTime(); + const progressCalcValue = ((now - msStart) / duration) * 100; + 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); + + }; + }); + + React.useEffect(() => { + const timer = setInterval(() => { + progressRef.current(); + }, 300); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/src/client/components/Map.tsx b/src/client/components/Map.tsx new file mode 100644 index 0000000..3eb782a --- /dev/null +++ b/src/client/components/Map.tsx @@ -0,0 +1,44 @@ +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, lon, zoom }: { lat: number, lon: number, zoom: number }) => { + const map = useMap(); + + useEffect(() => { + // Fly to that coordinates and set new zoom level + map.flyTo([lat, lon], zoom); + }, [lat, lon]); + return null; + +}; + +function Map({ entries }: { entries: Models.IEntry[] }) { + if (!entries?.length) { + return No Data to be displayed + } + const lastEntry = entries.at(-1); + + return ( + + + + + + {JSON.stringify(lastEntry, null, 2)} + + + + ) +} + +export default Map 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() { + {isLoading && } diff --git a/src/client/pages/Start.tsx b/src/client/pages/Start.tsx index 377955e..451508c 100644 --- a/src/client/pages/Start.tsx +++ b/src/client/pages/Start.tsx @@ -1,19 +1,154 @@ -import React from 'react' -import { Button, Typography } from '@mui/material'; +import React, { useEffect, useState, useContext, useRef } 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'; +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 [messageObj, setMessageObj] = useState({ isError: null, status: null, message: null }); + const [lastFetch, setLastFetch] = useState(); + const [nextFetch, setNextFetch] = useState(); + + const index = useRef(0); + const intervalID = useRef(); + + const fetchIntervalMs = 1000 * 55; + + const getData = async () => { + const token = localStorage.getItem("jwt"); + let response; + + if (!token) { + setLogin(false); + setMessageObj({ isError: true, status: "403", message: "No valid login" }) + return false; + } + + try { + const now = new Date().getTime(); + setLastFetch(now); + response = await axios({ + method: 'get', + url: "/read?index=" + index.current + "&noCache=" + now, + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const newEntries = response.data.entries; + if (newEntries.length) { + setEntries((prevEntries) => [prevEntries, ...newEntries]); + index.current += newEntries.length; + } + + 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 ( -
-

Hello, React!!

- Test Headline - - + <> +
+
+ {messageObj.isError && +
+ {messageObj.status} {messageObj.message} +
+ } + {!messageObj.isError && userInfo && +
+ {userInfo.user} Welcome back +
+ } + +
+ +
+
+
+
+
image1
+
image2
+
image3
+
+
+ {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/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 diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 78fb959..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 * 2 }); + const token = jwt.sign(payload, key, { expiresIn: 60 * 30 }); res.locals.token = token; return token; } diff --git a/src/testData/createTestData.test.ts b/src/testData/createTestData.test.ts new file mode 100644 index 0000000..9d1c789 --- /dev/null +++ b/src/testData/createTestData.test.ts @@ -0,0 +1,68 @@ +import axios, { AxiosError } from 'axios'; + + +async function callServer(timestamp = new Date().getTime(), query: string, expectStatus: number = 200, method: string = "HEAD") { + const url = new URL("http://localhost:80/write?"); + url.search = "?" + query; + const params = new URLSearchParams(url.search); + params.set("timestamp", timestamp.toString()); + url.search = params.toString(); + + + let response; + if (expectStatus == 200) { + if (method == "GET") { + response = await axios.get(url.toString()); + } else { + response = await axios.head(url.toString()); + } + expect(response.status).toBe(expectStatus); + } else { + try { + await axios.head(url.toString()); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(expectStatus); + } else { + console.error(axiosError); + } + } + } +} + + + + +describe('test Data', () => { + 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)); + +}); + + + + + + + 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 {