mirror of
https://github.com/usatiuk/dhfs.git
synced 2025-10-28 20:47:49 +01:00
webui skeleton
This commit is contained in:
3
webui/.dockerignore
Normal file
3
webui/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.parcel-cache
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
28
webui/.eslintrc.json
Normal file
28
webui/.eslintrc.json
Normal 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
12
webui/.gitignore
vendored
Normal 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
9
webui/.parcelrc
Normal 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
5
webui/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
6825
webui/package-lock.json
generated
Normal file
6825
webui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
webui/package.json
Normal file
36
webui/package.json
Normal 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
5
webui/src/App.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#appRoot {
|
||||||
|
font-family: sans-serif;
|
||||||
|
max-width: 50rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
42
webui/src/App.tsx
Normal file
42
webui/src/App.tsx
Normal 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
73
webui/src/Home.scss
Normal 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
29
webui/src/Home.tsx
Normal 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
5
webui/src/PeerState.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@import "./common";
|
||||||
|
|
||||||
|
#PeerState {
|
||||||
|
@include home-view;
|
||||||
|
}
|
||||||
5
webui/src/PeerState.tsx
Normal file
5
webui/src/PeerState.tsx
Normal 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
35
webui/src/api/dto.ts
Normal 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
90
webui/src/api/utils.ts
Normal 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
9
webui/src/common.scss
Normal 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
11
webui/src/index.html
Normal 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
13
webui/src/index.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
8
webui/src/index.tsx
Normal file
8
webui/src/index.tsx
Normal 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
22
webui/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user