Initial setup for new frontend.
This commit is contained in:
parent
7671bfd4d3
commit
59f7897d8e
33 changed files with 2594 additions and 12 deletions
15
frontend/.eslintrc.cjs
Normal file
15
frontend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript/recommended",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
env: {
|
||||
"vue/setup-compiler-macros": true,
|
||||
},
|
||||
};
|
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
52
frontend/README.md
Normal file
52
frontend/README.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
yarn run test:unit
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
yarn run lint
|
||||
```
|
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
17
frontend/index.html
Normal file
17
frontend/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
36
frontend/package.json
Normal file
36
frontend/package.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "ffffng-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"test:unit": "vitest --environment jsdom",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^2.0.14",
|
||||
"vue": "^3.2.34",
|
||||
"vue-router": "^4.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.3",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^17.0.34",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/test-utils": "^2.0.0-rc.21",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-plugin-vue": "^9.0.1",
|
||||
"jsdom": "^19.0.0",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.51.0",
|
||||
"typescript": "~4.6.4",
|
||||
"vite": "^2.9.9",
|
||||
"vitest": "^0.12.6",
|
||||
"vue-tsc": "^0.34.15"
|
||||
}
|
||||
}
|
BIN
frontend/public/apple-touch-icon.png
Normal file
BIN
frontend/public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
frontend/public/icon-192.png
Normal file
BIN
frontend/public/icon-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/icon-512.png
Normal file
BIN
frontend/public/icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
197
frontend/public/icon.svg
Normal file
197
frontend/public/icon.svg
Normal file
|
@ -0,0 +1,197 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="341.21152"
|
||||
height="341.21152"
|
||||
id="svg3447"
|
||||
version="1.1"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/home/baldo/projects/ffhh/ffffng/frontend/public/apple-touch-icon.png"
|
||||
inkscape:export-xdpi="50.643074"
|
||||
inkscape:export-ydpi="50.643074"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs3449">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4434">
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
ry="1.8153608"
|
||||
rx="1.2299931"
|
||||
y="8491.9297"
|
||||
x="1914.3979"
|
||||
height="261.76361"
|
||||
width="261.76361"
|
||||
id="rect4436"
|
||||
style="opacity:0.576613;fill:#ff0000;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4590">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4592"
|
||||
d="m -4623.8797,7360.2562 -32.3968,-15.9515 21.309,-23.1111 z m -53.9986,-39.0626 21.3091,23.1067 -32.3947,15.9559 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4594">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4596"
|
||||
d="m -4611.873,7366.1514 -44.3484,-21.8363 29.1702,-31.6371 z m -73.9194,-53.4734 29.1702,31.6312 -44.3455,21.8422 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4598">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4600"
|
||||
d="m -4588.8535,7377.4531 -67.2619,-33.1184 44.2416,-47.9831 z m -112.1113,-81.1015 44.2416,47.9741 -67.2575,33.1274 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4434-6">
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
ry="1.8153608"
|
||||
rx="1.2299931"
|
||||
y="8491.9297"
|
||||
x="1914.3979"
|
||||
height="261.76361"
|
||||
width="261.76361"
|
||||
id="rect4436-4"
|
||||
style="opacity:0.576613;fill:#ff0000;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4590-9">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4592-5"
|
||||
d="m -4623.8797,7360.2562 -32.3968,-15.9515 21.309,-23.1111 z m -53.9986,-39.0626 21.3091,23.1067 -32.3947,15.9559 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4594-4">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4596-2"
|
||||
d="m -4611.873,7366.1514 -44.3484,-21.8363 29.1702,-31.6371 z m -73.9194,-53.4734 29.1702,31.6312 -44.3455,21.8422 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4598-4">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4600-8"
|
||||
d="m -4588.8535,7377.4531 -67.2619,-33.1184 44.2416,-47.9831 z m -112.1113,-81.1015 44.2416,47.9741 -67.2575,33.1274 z"
|
||||
style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.8529412"
|
||||
inkscape:cx="267.95238"
|
||||
inkscape:cy="168.65079"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g3580"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="2558"
|
||||
inkscape:window-height="1408"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="true" />
|
||||
<metadata
|
||||
id="metadata3452">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-60.513146,-24.984767)">
|
||||
<g
|
||||
id="g3580"
|
||||
transform="translate(2.688147,-2.688134)">
|
||||
<g
|
||||
id="path4355">
|
||||
<path
|
||||
style="color:#000000;fill:#333333;stroke-width:22.2988;stroke-linecap:square;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 57.824999,198.27868 c 0,94.22296 76.382801,170.60575 170.605761,170.60575 50.69154,0 96.21945,-22.10819 127.46763,-57.20847 26.8344,-30.1424 43.13814,-69.86587 43.13814,-113.39728 0,-94.22297 -76.3828,-170.605779 -170.60577,-170.605779 -94.22296,0 -170.605761,76.382809 -170.605761,170.605779 z"
|
||||
id="path1157" />
|
||||
<path
|
||||
style="color:#000000;fill:#e5287a;stroke-linecap:square;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 228.43164,30.966797 c -92.28211,0 -167.312499,75.030383 -167.312499,167.312503 0,92.28211 75.030389,167.31054 167.312499,167.31054 49.64738,0 94.37002,-21.69128 125.00391,-56.10156 26.30688,-29.54986 42.30664,-68.57425 42.30664,-111.20898 0,-92.28212 -75.02843,-167.312503 -167.31055,-167.312503 z m 0,20.527344 c 81.18863,0 146.7832,65.596529 146.7832,146.785159 0,37.50949 -14.0143,71.61624 -37.11132,97.56054 -26.89608,30.21168 -65.99275,49.22461 -109.67188,49.22461 -81.18862,0 -146.785156,-65.59654 -146.785156,-146.78515 0,-81.18863 65.596546,-146.785159 146.785156,-146.785159 z"
|
||||
id="path1154" />
|
||||
</g>
|
||||
<path
|
||||
style="display:inline;fill:none;stroke:#ededed;stroke-width:9.63968;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 96.1688,198.27867 c 0,73.04626 59.21569,132.26196 132.26196,132.26196 73.04627,0 132.26196,-59.2157 132.26196,-132.26196 0,-73.04627 -59.21569,-132.261977 -132.26196,-132.261977 -73.04627,0 -132.26196,59.215707 -132.26196,132.261977 z"
|
||||
id="path4357"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="color:#000000;overflow:visible;fill:#fdbc41;stop-color:#000000"
|
||||
id="path1810"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="8"
|
||||
sodipodi:cx="-173.53415"
|
||||
sodipodi:cy="-46.257851"
|
||||
sodipodi:r1="101.43466"
|
||||
sodipodi:r2="76.075996"
|
||||
sodipodi:arg1="0.78539816"
|
||||
sodipodi:arg2="1.1780972"
|
||||
inkscape:rounded="0.55"
|
||||
inkscape:randomized="0"
|
||||
d="m -101.80901,25.467286 c -16.58168,16.581684 -20.9471,-10.414024 -42.61211,-1.440082 -21.66502,8.973943 -5.66298,31.149606 -29.11303,31.149607 -23.45004,10e-7 -7.44801,-22.175662 -29.11302,-31.149604 -21.66502,-8.973944 -26.03043,18.021764 -42.61212,1.44008 -16.58168,-16.5816823 10.41403,-20.9470983 1.44009,-42.612111 -8.97395,-21.665016 -31.14961,-5.662982 -31.14961,-29.113026 0,-23.450041 22.17566,-7.448008 31.1496,-29.113021 8.97395,-21.665016 -18.02176,-26.030429 -1.44008,-42.612119 16.58169,-16.58168 20.9471,10.41403 42.61211,1.44008 21.66502,-8.97394 5.66299,-31.1496 29.11303,-31.1496 23.45004,0 7.44801,22.17566 29.11302,31.1496 21.66502,8.97395 26.03043,-18.02176 42.61212,-1.44008 16.581681,16.58168 -10.41403,20.9471 -1.44008,42.612113 8.973939,21.665015 31.149602,5.662982 31.149603,29.113026 10e-7,23.450041 -22.175663,7.448008 -31.149603,29.113021 -8.97395,21.6650153 18.021763,26.0304305 1.44008,42.612116 z"
|
||||
transform="matrix(0.81688462,0,0,0.81688462,370.18813,236.06599)" />
|
||||
<circle
|
||||
style="color:#000000;overflow:visible;fill:#333333;stroke-width:0.844574;stop-color:#000000"
|
||||
id="path953"
|
||||
cx="228.43076"
|
||||
cy="198.27866"
|
||||
r="41.555809"
|
||||
inkscape:export-filename="/home/baldo/projects/ffhh/ffffng/frontend/public/icon-512.png"
|
||||
inkscape:export-xdpi="591.40002"
|
||||
inkscape:export-ydpi="591.40002" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
6
frontend/public/manifest.webmanifest
Normal file
6
frontend/public/manifest.webmanifest
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"icons": [
|
||||
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
||||
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
|
||||
]
|
||||
}
|
17
frontend/src/App.vue
Normal file
17
frontend/src/App.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import {RouterLink, RouterView} from "vue-router";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import PageFooter from "@/components/PageFooter.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader />
|
||||
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
|
||||
<PageFooter />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
19
frontend/src/components/PageFooter.vue
Normal file
19
frontend/src/components/PageFooter.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { useVersionStore } from "@/stores/version";
|
||||
|
||||
const version = useVersionStore();
|
||||
|
||||
function refresh(): void {
|
||||
version.refresh();
|
||||
}
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer>
|
||||
{{ version.getVersion }}
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
15
frontend/src/components/PageHeader.vue
Normal file
15
frontend/src/components/PageHeader.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink> |
|
||||
<RouterLink to="/admin">Admin</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
12
frontend/src/main.ts
Normal file
12
frontend/src/main.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
21
frontend/src/router/index.ts
Normal file
21
frontend/src/router/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: AdminDashboardView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
29
frontend/src/stores/statistics.ts
Normal file
29
frontend/src/stores/statistics.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { isStatistics, type Statistics } from "@/types";
|
||||
import { internalApi } from "@/utils/Api";
|
||||
|
||||
interface StatisticsStoreState {
|
||||
statistics: Statistics | null;
|
||||
}
|
||||
|
||||
export const useStatisticsStore = defineStore({
|
||||
id: "statistics",
|
||||
state(): StatisticsStoreState {
|
||||
return {
|
||||
statistics: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getStatistics(state: StatisticsStoreState): Statistics | null {
|
||||
return state.statistics;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async refresh(): Promise<void> {
|
||||
this.statistics = await internalApi.get<Statistics>(
|
||||
"statistics",
|
||||
isStatistics
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
38
frontend/src/stores/version.ts
Normal file
38
frontend/src/stores/version.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { isObject, isVersion, type Version } from "@/types";
|
||||
import { api } from "@/utils/Api";
|
||||
|
||||
interface VersionResponse {
|
||||
version: Version;
|
||||
}
|
||||
|
||||
function isVersionResponse(arg: unknown): arg is VersionResponse {
|
||||
return isObject(arg) && isVersion((arg as VersionResponse).version);
|
||||
}
|
||||
|
||||
interface VersionStoreState {
|
||||
version: Version | null;
|
||||
}
|
||||
|
||||
export const useVersionStore = defineStore({
|
||||
id: "version",
|
||||
state(): VersionStoreState {
|
||||
return {
|
||||
version: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getVersion(state: VersionStoreState): Version | null {
|
||||
return state.version;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async refresh(): Promise<void> {
|
||||
const response = await api.get<VersionResponse>(
|
||||
"version",
|
||||
isVersionResponse
|
||||
);
|
||||
this.version = response.version;
|
||||
},
|
||||
},
|
||||
});
|
1
frontend/src/types/index.ts
Normal file
1
frontend/src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./shared";
|
1
frontend/src/types/shared.ts
Symbolic link
1
frontend/src/types/shared.ts
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../server/types/shared.ts
|
37
frontend/src/utils/Api.ts
Normal file
37
frontend/src/utils/Api.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
class Api {
|
||||
private baseURL: string = import.meta.env.BASE_URL;
|
||||
private apiPrefix = "api/";
|
||||
|
||||
constructor(apiPrefix?: string) {
|
||||
if (apiPrefix) {
|
||||
this.apiPrefix = apiPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
private toURL(path: string): string {
|
||||
return this.baseURL + this.apiPrefix + path;
|
||||
}
|
||||
|
||||
async get<T>(path: string, isT: (arg: unknown) => arg is T): Promise<T> {
|
||||
const url = this.toURL(path);
|
||||
const result = await fetch(url);
|
||||
const json = await result.json();
|
||||
|
||||
if (!isT(json)) {
|
||||
console.log(json);
|
||||
throw new Error(`API get result has wrong type. ${url} => ${json}`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new Api();
|
||||
|
||||
class InternalApi extends Api {
|
||||
constructor() {
|
||||
super("internal/api/");
|
||||
}
|
||||
}
|
||||
|
||||
export const internalApi = new InternalApi();
|
30
frontend/src/views/AdminDashboardView.vue
Normal file
30
frontend/src/views/AdminDashboardView.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { useStatisticsStore } from "@/stores/statistics";
|
||||
|
||||
const statistics = useStatisticsStore();
|
||||
|
||||
function refresh(): void {
|
||||
statistics.refresh();
|
||||
}
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div v-if="statistics.getStatistics">
|
||||
<h1>Nodes</h1>
|
||||
|
||||
<div>
|
||||
Registered: {{ statistics.getStatistics.nodes.registered }}<br />
|
||||
With VPN-key: {{ statistics.getStatistics.nodes.withVPN }}<br />
|
||||
With coordinates: {{ statistics.getStatistics.nodes.withCoords }}<br />
|
||||
Monitoring active: {{ statistics.getStatistics.nodes.monitoring.active }}<br />
|
||||
Monitoring pending: {{ statistics.getStatistics.nodes.monitoring.pending }}
|
||||
</div>
|
||||
<button @click="refresh()">Refresh</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
8
frontend/src/views/HomeView.vue
Normal file
8
frontend/src/views/HomeView.vue
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Home
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.vite-config.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
8
frontend/tsconfig.vite-config.json
Normal file
8
frontend/tsconfig.vite-config.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node", "vitest"]
|
||||
}
|
||||
}
|
8
frontend/tsconfig.vitest.json
Normal file
8
frontend/tsconfig.vitest.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
24
frontend/vite.config.ts
Normal file
24
frontend/vite.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { fileURLToPath, URL } from "url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/": {
|
||||
target: "http://localhost:8080",
|
||||
},
|
||||
"/internal/api/": {
|
||||
target: "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
1907
frontend/yarn.lock
Normal file
1907
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,5 @@
|
|||
import {ArrayField, Field, RawJsonField} from "sparkson"
|
||||
|
||||
export type Version = string;
|
||||
|
||||
// TODO: Replace string types by more specific types like URL, Password, etc.
|
||||
|
||||
export class LoggingConfig {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./config";
|
||||
export * from "./logger";
|
||||
export * from "./shared";
|
||||
|
||||
// TODO: Token type.
|
||||
export type Token = string;
|
||||
|
@ -48,16 +49,6 @@ export type NodeSecrets = {
|
|||
monitoringToken?: MonitoringToken,
|
||||
};
|
||||
|
||||
export type NodeStatistics = {
|
||||
registered: number,
|
||||
withVPN: number,
|
||||
withCoords: number,
|
||||
monitoring: {
|
||||
active: number,
|
||||
pending: number
|
||||
}
|
||||
};
|
||||
|
||||
export type MailId = string;
|
||||
export type MailData = any;
|
||||
export type MailType = string;
|
||||
|
|
45
server/types/shared.ts
Normal file
45
server/types/shared.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Types shared with the client.
|
||||
|
||||
export function isObject(arg: unknown): arg is object {
|
||||
return arg !== null && typeof arg === "object";
|
||||
}
|
||||
|
||||
export type Version = string;
|
||||
|
||||
export function isVersion(arg: unknown): arg is Version {
|
||||
// Should be good enough for now.
|
||||
return typeof arg === "string";
|
||||
}
|
||||
|
||||
export type NodeStatistics = {
|
||||
registered: number;
|
||||
withVPN: number;
|
||||
withCoords: number;
|
||||
monitoring: {
|
||||
active: number;
|
||||
pending: number;
|
||||
};
|
||||
};
|
||||
|
||||
export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
|
||||
if (!isObject(arg)) {
|
||||
return false;
|
||||
}
|
||||
const stats = arg as NodeStatistics;
|
||||
return (
|
||||
typeof stats.registered === "number" &&
|
||||
typeof stats.withVPN === "number" &&
|
||||
typeof stats.withCoords === "number" &&
|
||||
typeof stats.monitoring === "object" &&
|
||||
typeof stats.monitoring.active === "number" &&
|
||||
typeof stats.monitoring.pending === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
nodes: NodeStatistics;
|
||||
}
|
||||
|
||||
export function isStatistics(arg: unknown): arg is Statistics {
|
||||
return isObject(arg) && isNodeStatistics((arg as Statistics).nodes);
|
||||
}
|
|
@ -11,6 +11,7 @@ stdenv.mkDerivation rec {
|
|||
nasm
|
||||
nodejs-16_x
|
||||
rsync
|
||||
sass
|
||||
sqlite
|
||||
yarn
|
||||
zlib
|
||||
|
|
Loading…
Reference in a new issue