webui skeleton

This commit is contained in:
2024-06-30 17:05:59 +02:00
parent a7a9770c9e
commit 91184eafc0
20 changed files with 7265 additions and 0 deletions

3
webui/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.parcel-cache
dist
node_modules

28
webui/.eslintrc.json Normal file
View File

@@ -0,0 +1,28 @@
{
"root": true,
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/jsx-runtime",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"react"
],
"rules": {
"@typescript-eslint/no-floating-promises": [
"error"
]
}
}

12
webui/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.idea/
node_modules/
build/
tmp/
temp/
dist/
.env
.cache
.directory
.history
.parcel-cache
frontend-reports/

9
webui/.parcelrc Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
},
"validators": {
"*.{ts,tsx}": ["@parcel/validator-typescript"]
}
}

5
webui/.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"trailingComma": "all",
"tabWidth": 4,
"endOfLine": "auto"
}

6825
webui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
webui/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "dhfs-webui",
"version": "0.0.1",
"private": true,
"source": "src/index.html",
"scripts": {
"start": "parcel --public-url /webui",
"build": "parcel build --public-url /webui"
},
"browserslist": "> 0.5%, last 2 versions, not dead",
"dependencies": {
"jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@parcel/transformer-sass": "^2.12.0",
"@parcel/transformer-typescript-tsc": "^2.12.0",
"@parcel/validator-typescript": "^2.12.0",
"@types/eslint": "^8.56.10",
"@types/eslint-config-prettier": "^6.11.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"eslint": "^8",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.34.3",
"parcel": "^2.12.0",
"prettier": "^3.3.2",
"process": "^0.11.10",
"typescript": "^5.5.2"
}
}

5
webui/src/App.scss Normal file
View File

@@ -0,0 +1,5 @@
#appRoot {
font-family: sans-serif;
max-width: 50rem;
margin: 0 auto;
}

42
webui/src/App.tsx Normal file
View File

@@ -0,0 +1,42 @@
import React from "react";
import {
createBrowserRouter,
redirect,
RouterProvider,
} from "react-router-dom";
import "./App.scss";
import { Home } from "./Home";
import { PeerState } from "./PeerState";
const router = createBrowserRouter(
[
{
path: "/",
loader: async () => {
return redirect("/home");
// if (getToken() == null) {
// return redirect("/login");
// } else {
// return redirect("/home");
// }
},
},
{
path: "/home",
element: <Home />,
children: [{ path: "peers", element: <PeerState /> }],
},
],
{ basename: "/webui" },
);
export function App() {
return (
<React.StrictMode>
<div id={"appRoot"}>
<RouterProvider router={router} />
</div>
</React.StrictMode>
);
}

73
webui/src/Home.scss Normal file
View File

@@ -0,0 +1,73 @@
@import "./common";
#Home {
display: flex;
flex-direction: row;
min-height: 100vh;
flex: auto;
#HomeSidebar {
min-width: 15rem;
max-width: 15rem;
min-height: 100vh;
border-right: solid gray 1px;
border-bottom-right-radius: 7px;
padding: 2rem 1rem;
position: fixed;
top: 0;
box-shadow: 1px 0px 0px 0px rgba(0, 0, 0, 0.2), 2px 0px 0px 0px rgba(0, 0, 0, 0.1), 3px 0px 0px 0px rgba(0, 0, 0, 0.05);
#SidebarUserInfo {
display: flex;
flex-direction: column;
button {
background: none;
border: none;
padding: 0;
text-decoration: none;
cursor: pointer;
color: inherit;
font-size: inherit;
}
}
* {
margin: 0.5rem 0;
}
#SidebarNav {
a {
color: black;
text-decoration: none;
min-width: 100%;
height: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
padding-left: 1rem;
font-size: 1.15rem;
@include border-shadow;
&.active {
background-color: #EFEFEF;
}
&.pending {
background-color: #EEEEEE;
}
}
}
}
#HomeContent {
width: 100%;
margin: 0 0 0 15rem;
}
}

29
webui/src/Home.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { NavLink, Outlet } from "react-router-dom";
import "./Home.scss";
export function Home() {
const activePendingClassName = ({
isActive,
isPending,
}: {
isActive: boolean;
isPending: boolean;
}) => (isActive ? "active" : isPending ? "pending" : "");
return (
<div id="Home">
<div id="HomeSidebar">
<div id="SidebarUserInfo">DHFS</div>
<div id="SidebarNav">
<NavLink to={"peers"} className={activePendingClassName}>
Peers
</NavLink>{" "}
</div>
</div>
<div id="HomeContent">
<Outlet />
</div>
</div>
);
}

5
webui/src/PeerState.scss Normal file
View File

@@ -0,0 +1,5 @@
@import "./common";
#PeerState {
@include home-view;
}

5
webui/src/PeerState.tsx Normal file
View File

@@ -0,0 +1,5 @@
import "./PeerState.scss";
export function PeerState() {
return <div id={"PeerState"}>peerstate</div>;
}

35
webui/src/api/dto.ts Normal file
View File

@@ -0,0 +1,35 @@
import { z } from "zod";
export const ErrorTo = z.object({
errors: z.array(z.string()),
code: z.number(),
});
export type TErrorTo = z.infer<typeof ErrorTo>;
export function isError(value: unknown): value is TErrorTo {
return ErrorTo.safeParse(value).success;
}
function CreateAPIResponse<T extends z.ZodTypeAny>(obj: T) {
return z.union([ErrorTo, obj]);
}
export const NoContentTo = z.object({});
export type TNoContentTo = z.infer<typeof NoContentTo>;
export const NoContentToResp = CreateAPIResponse(NoContentTo);
export type TNoContentToResp = z.infer<typeof NoContentToResp>;
export const TokenRequestTo = z.object({
username: z.string(),
password: z.string(),
});
export type TTokenRequestTo = z.infer<typeof TokenRequestTo>;
export const TokenTo = z.object({
token: z.string(),
});
export type TTokenTo = z.infer<typeof TokenTo>;
export const TokenToResp = CreateAPIResponse(TokenTo);
export type TTokenToResp = z.infer<typeof TokenToResp>;

90
webui/src/api/utils.ts Normal file
View File

@@ -0,0 +1,90 @@
import { jwtDecode } from "jwt-decode";
import { isError } from "./dto";
declare const process: {
env: {
NODE_ENV: string;
};
};
const apiRoot: string =
process.env.NODE_ENV == "production" ? "" : "http://localhost:8080";
let token: string | null;
export function setToken(_token: string): void {
token = _token;
localStorage.setItem("jwt_token", token);
}
export function getToken(): string | null {
if (!token && localStorage.getItem("jwt_token") != null) {
token = localStorage.getItem("jwt_token");
}
return token;
}
export function getTokenUserUuid(): string | null {
const token = getToken();
if (!token) return null;
return jwtDecode(token).sub ?? null;
}
export function deleteToken(): void {
token = null;
localStorage.removeItem("jwt_token");
}
export async function fetchJSON<T, P extends { parse: (arg: string) => T }>(
path: string,
method: string,
parser: P,
body?: string | Record<string, unknown> | File,
headers?: Record<string, string>,
): Promise<T> {
const reqBody = () =>
body instanceof File
? (() => {
const fd = new FormData();
fd.append("file", body);
return fd;
})()
: JSON.stringify(body);
const reqHeaders = () =>
body instanceof File
? headers
: { ...headers, "Content-Type": "application/json" };
const response = await fetch(apiRoot + path, {
method,
headers: reqHeaders(),
body: reqBody(),
});
const json = await response.json().catch(() => {
return {};
});
const parsed = parser.parse(json);
if (isError(parsed)) {
alert(parsed.errors.join(", "));
}
return parsed;
}
export async function fetchJSONAuth<T, P extends { parse: (arg: string) => T }>(
path: string,
method: string,
parser: P,
body?: string | Record<string, unknown> | File,
headers?: Record<string, unknown>,
): Promise<T> {
if (token) {
return fetchJSON(path, method, parser, body, {
...headers,
Authorization: `Bearer ${token}`,
});
} else {
throw new Error("Not logged in");
}
}

9
webui/src/common.scss Normal file
View File

@@ -0,0 +1,9 @@
@mixin border-shadow {
border-radius: 7px;
border: 1px solid #E0E0E0;
box-shadow: 1px 1px 0px 0px rgba(0, 0, 0, 0.05), 2px 2px 0px 0px rgba(0, 0, 0, 0.025);
}
@mixin home-view {
padding: 1rem;
}

11
webui/src/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DHFS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="index.tsx"></script>
</body>
</html>

13
webui/src/index.scss Normal file
View File

@@ -0,0 +1,13 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
min-height: 100vh;
}

8
webui/src/index.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./index.scss";
const container = document.getElementById("app")!;
const root = createRoot(container);
root.render(<App />);

22
webui/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": [
"ES2023",
"dom"
],
"jsx": "react-jsx",
"target": "es2015",
"moduleResolution": "Node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"isolatedModules": true
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
}