diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..5c0c19b --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,17 @@ +name: 🗑 Cleanup session files +# Hitting the API checks for inactive sessions and cleans up any orphaned files. +# This action runs on an hourly schedule to make sure this cleanup happens at least +# once an hour. + +on: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 * * * *' + +jobs: + cleanup: + name: 🗑 Cleanup + runs-on: ubuntu-latest + steps: + - name: Cleanup + run: curl https://seroviz.seroanalytics.org/api/ diff --git a/package-lock.json b/package-lock.json index 60ece76..c09c9a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "seroviz", "version": "0.1.0", + "license": "GPL-3.0", "dependencies": { "@testing-library/jest-dom": "^5.17.0", "@types/jest": "^27.5.2", @@ -37,6 +38,7 @@ "json-schema-to-typescript": "^15.0.0", "mime-types": "^2.1.35", "react-plotly.js": "^2.6.0", + "react-router-dom": "^7.0.2", "ts-jest": "^29.2.4", "ts-node": "^10.9.2" } @@ -5061,6 +5063,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.11", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", @@ -20077,6 +20085,55 @@ "react": ">= 16.3" } }, + "node_modules/react-router": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz", + "integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz", + "integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==", + "dev": true, + "dependencies": { + "react-router": "7.0.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -22435,6 +22492,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -24038,6 +24101,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "dev": true + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", diff --git a/package.json b/package.json index d824aa6..5bb5329 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "json-schema-to-typescript": "^15.0.0", "mime-types": "^2.1.35", "react-plotly.js": "^2.6.0", + "react-router-dom": "^7.0.2", "ts-jest": "^29.2.4", "ts-node": "^10.9.2" }, diff --git a/src/components/FAQ.tsx b/src/components/FAQ.tsx new file mode 100644 index 0000000..b00c6d8 --- /dev/null +++ b/src/components/FAQ.tsx @@ -0,0 +1,105 @@ +import React, {useReducer} from 'react'; +import {Col, Container, Row} from "react-bootstrap"; +import TopNav from "./TopNav"; +import usePersistedState from "../hooks/usePersistedState"; +import { + initialState, + RootContext, + RootDispatchContext +} from "../RootContext"; +import {rootReducer} from "../reducers/rootReducer"; + +export default function FAQ() { + + const [theme, setTheme] = usePersistedState("theme", "dark"); + const [state, dispatch] = useReducer(rootReducer, initialState); + + return + + void}> + + + +

FAQ

+
    +
  • + What format does my data need to be in? +
  • +

    Data should be a CSV containing an absolute or + relative time series of biomarker values, in + long-format. + Required columns + are biomarker and value, + plus a column containing a time variable, the + name of which is configurable but defaults + to day. Arbitrarily many extra columns + are allowed and will be made available to + disaggregate by.

    +
  • + What is the difference between "serological + surveillance" and "post-exposure" data? +
  • +

    + By "serological surveillance" we mean time + series + data where the time variable is based on + absolute + calendar date; for example, a study that + monitors a population level biomarker over time. + The time variable here might be day of study, or + calendar date.

    +

    + By "post-exposure" data we mean a relative time + series, + where biomarker levels are measured for each + individual + relative to a known exposure (vaccination or + infection). The time variable here would be time + since exposure. +

    +
  • + Where is my data kept? Is it secure? +
  • +

    SeroViz is deployed to Digital Ocean's App + Platform; access to the remote server is + limited to app maintainers and secured via 2fa. + You can read more about Digital + Ocean's infrastructure security here. +

    +

    Files are temporarily uploaded to the remote + server + under a unique session id, and will persist as + long as your keep your browser open. + When you close your browser all session cookies + will be deleted, and remote files associated + with inactive sessions are deleted every hour. +

    +

    Or, to force your session and all associated + files to be deleted immediately, you can click + the "End session" link in the top-right + corner. +

    +
  • + Who funded this project? Who maintains it? +
  • + This project was funded by the Wellcome Trust. It + was built + by Alex + Hill and is maintained by David + Hodgson. +
  • + How do I request new features? +
  • + Please raise a GitHub issue on the seroviz + repo. +
+ +
+
+
+
+} diff --git a/src/components/NoPage.tsx b/src/components/NoPage.tsx new file mode 100644 index 0000000..50499ce --- /dev/null +++ b/src/components/NoPage.tsx @@ -0,0 +1,26 @@ +import React, {useReducer} from 'react'; +import {Container} from "react-bootstrap"; +import TopNav from "./TopNav"; +import usePersistedState from "../hooks/usePersistedState"; +import { + initialState, + RootContext, + RootDispatchContext +} from "../RootContext"; +import {rootReducer} from "../reducers/rootReducer"; + +export default function NoPage() { + + const [theme, setTheme] = usePersistedState("theme", "dark"); + const [state, dispatch] = useReducer(rootReducer, initialState); + + return + + void}> + +

Page not found

+
+
+
+} diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index b800a9f..c76e916 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -39,7 +39,7 @@ export default function TopNav({