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