🎉 Init project from scratch
This commit is contained in:
5
pkg/view/.gitignore
vendored
Normal file
5
pkg/view/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
28
pkg/view/README.md
Normal file
28
pkg/view/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
6
pkg/view/embed.go
Normal file
6
pkg/view/embed.go
Normal file
@ -0,0 +1,6 @@
|
||||
package view
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var FS embed.FS
|
13
pkg/view/index.html
Normal file
13
pkg/view/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Goatpass</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
26
pkg/view/package.json
Normal file
26
pkg/view/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@hydrogen/interactive-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.10.10",
|
||||
"solid-js": "^1.8.7",
|
||||
"universal-cookie": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.17",
|
||||
"daisyui": "^4.6.0",
|
||||
"postcss": "^8.4.33",
|
||||
"solid-devtools": "^0.29.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-solid": "^2.8.0"
|
||||
}
|
||||
}
|
6
pkg/view/postcss.config.js
Normal file
6
pkg/view/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
21
pkg/view/public/favicon.svg
Normal file
21
pkg/view/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
5
pkg/view/src/.prettierrc
Normal file
5
pkg/view/src/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false
|
||||
}
|
197
pkg/view/src/assets/fonts/fonts.css
Normal file
197
pkg/view/src/assets/fonts/fonts.css
Normal file
@ -0,0 +1,197 @@
|
||||
:root {
|
||||
--bs-body-font-family: "IBM Plex Serif", "Noto Serif SC", sans-serif !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: var(--bs-body-font-family);
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-100 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url("./ibm-plex-serif-v19-latin-100.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-100italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url("./ibm-plex-serif-v19-latin-100italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-200 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./ibm-plex-serif-v19-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-200italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url("./ibm-plex-serif-v19-latin-200italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-300 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./ibm-plex-serif-v19-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-300italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url("./ibm-plex-serif-v19-latin-300italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./ibm-plex-serif-v19-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./ibm-plex-serif-v19-latin-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-500 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./ibm-plex-serif-v19-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-500italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url("./ibm-plex-serif-v19-latin-500italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-600 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./ibm-plex-serif-v19-latin-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-600italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url("./ibm-plex-serif-v19-latin-600italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-700 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./ibm-plex-serif-v19-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-700italic - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "IBM Plex Serif";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./ibm-plex-serif-v19-latin-700italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-200 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-300 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-regular - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-500 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-600 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-700 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* noto-serif-sc-900 - chinese-simplified */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Noto Serif SC";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url("./noto-serif-sc-v22-chinese-simplified-900.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-regular.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-regular.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-200.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-200.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-300.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-300.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-500.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-500.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-600.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-600.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-700.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-700.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-900.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-900.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-regular.woff2
Executable file
BIN
pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-regular.woff2
Executable file
Binary file not shown.
8
pkg/view/src/index.css
Normal file
8
pkg/view/src/index.css
Normal file
@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
32
pkg/view/src/index.tsx
Normal file
32
pkg/view/src/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import "solid-devtools";
|
||||
|
||||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import "./assets/fonts/fonts.css";
|
||||
import { lazy } from "solid-js";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
|
||||
import RootLayout from "./layouts/RootLayout.tsx";
|
||||
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
||||
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
render(() => (
|
||||
<WellKnownProvider>
|
||||
<UserinfoProvider>
|
||||
<Router root={RootLayout}>
|
||||
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
|
||||
<Route path="/security" component={lazy(() => import("./pages/security.tsx"))} />
|
||||
<Route path="/personalise" component={lazy(() => import("./pages/personalise.tsx"))} />
|
||||
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
|
||||
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
|
||||
<Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} />
|
||||
<Route path="/auth/oauth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
|
||||
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
|
||||
</Router>
|
||||
</UserinfoProvider>
|
||||
</WellKnownProvider>
|
||||
), root!);
|
46
pkg/view/src/layouts/RootLayout.tsx
Normal file
46
pkg/view/src/layouts/RootLayout.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import Navbar from "./shared/Navbar.tsx";
|
||||
import { readProfiles, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { readWellKnown } from "../stores/wellKnown.tsx";
|
||||
import { BeforeLeaveEventArgs, useBeforeLeave, useLocation, useNavigate } from "@solidjs/router";
|
||||
|
||||
export default function RootLayout(props: any) {
|
||||
const [ready, setReady] = createSignal(false);
|
||||
|
||||
Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true));
|
||||
|
||||
const navigate = useNavigate();
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
createEffect(() => {
|
||||
if (ready()) {
|
||||
keepGate(location.pathname);
|
||||
}
|
||||
}, [ready, userinfo]);
|
||||
|
||||
function keepGate(path: string, e?: BeforeLeaveEventArgs) {
|
||||
const whitelist = ["/auth/login", "/auth/register", "/users/me/confirm"];
|
||||
|
||||
if (!userinfo?.isLoggedIn && !whitelist.includes(path)) {
|
||||
if (!e?.defaultPrevented) e?.preventDefault();
|
||||
navigate(`/auth/login?redirect_uri=${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
useBeforeLeave((e: BeforeLeaveEventArgs) => keepGate(e.to.toString().split("?")[0], e));
|
||||
|
||||
return (
|
||||
<Show when={ready()} fallback={
|
||||
<div class="h-screen w-screen flex justify-center items-center">
|
||||
<div>
|
||||
<span class="loading loading-lg loading-infinity"></span>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<Navbar />
|
||||
<main class="h-[calc(100vh-68px)] mt-[68px]">{props.children}</main>
|
||||
</Show>
|
||||
);
|
||||
}
|
118
pkg/view/src/layouts/shared/Navbar.tsx
Normal file
118
pkg/view/src/layouts/shared/Navbar.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { For, Match, Show, Switch } from "solid-js";
|
||||
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useWellKnown } from "../../stores/wellKnown.tsx";
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
const nav: MenuItem[] = [
|
||||
{
|
||||
label: "You", children: [
|
||||
{ label: "Dashboard", href: "/" },
|
||||
{ label: "Security", href: "/security" },
|
||||
{ label: "Personalise", href: "/personalise" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const wellKnown = useWellKnown();
|
||||
const userinfo = useUserinfo();
|
||||
const navigate = useNavigate();
|
||||
|
||||
function logout() {
|
||||
clearUserinfo();
|
||||
navigate("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="navbar bg-base-100 shadow-md px-5 z-10 fixed top-0">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
||||
>
|
||||
<For each={nav}>
|
||||
{(item) => (
|
||||
<li>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
<Show when={item.children}>
|
||||
<ul class="p-2">
|
||||
<For each={item.children}>
|
||||
{(item) =>
|
||||
<li>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</li>
|
||||
}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl">
|
||||
{wellKnown?.name ?? "Goatpass"}
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<For each={nav}>
|
||||
{(item) => (
|
||||
<li>
|
||||
<Show when={item.children} fallback={<a href={item.href}>{item.label}</a>}>
|
||||
<details>
|
||||
<summary>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</summary>
|
||||
<ul class="p-2">
|
||||
<For each={item.children}>
|
||||
{(item) =>
|
||||
<li>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</li>
|
||||
}
|
||||
</For>
|
||||
</ul>
|
||||
</details>
|
||||
</Show>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end pe-5">
|
||||
<Switch>
|
||||
<Match when={userinfo?.isLoggedIn}>
|
||||
<button type="button" class="btn btn-sm btn-ghost" onClick={() => logout()}>Logout</button>
|
||||
</Match>
|
||||
<Match when={!userinfo?.isLoggedIn}>
|
||||
<a href="/auth/login" class="btn btn-sm btn-primary">Login</a>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
108
pkg/view/src/pages/dashboard.tsx
Normal file
108
pkg/view/src/pages/dashboard.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
function getGreeting() {
|
||||
const currentHour = new Date().getHours();
|
||||
|
||||
if (currentHour >= 0 && currentHour < 12) {
|
||||
return "Good morning! Wishing you a day filled with joy and success. ☀️";
|
||||
} else if (currentHour >= 12 && currentHour < 18) {
|
||||
return "Afternoon! Hope you have a productive and joyful afternoon! ☀️";
|
||||
} else {
|
||||
return "Good evening! Wishing you a relaxing and pleasant evening. 🌙";
|
||||
}
|
||||
}
|
||||
|
||||
async function readNotification(item: any) {
|
||||
const res = await fetch(`/api/notifications/${item.id}/read`, {
|
||||
method: "PUT",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
await readProfiles();
|
||||
setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="max-w-[720px] mx-auto px-5 pt-12">
|
||||
<div id="greeting" class="px-5">
|
||||
<h1 class="text-2xl font-bold">{userinfo?.displayName}</h1>
|
||||
<p>{getGreeting()}</p>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={!userinfo?.meta?.confirmed_at}>
|
||||
<div role="alert" class="alert alert-warning mt-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<span>Your account isn't confirmed yet. Please check your inbox and confirm your account.</span> <br />
|
||||
<span>Otherwise your account will be deactivate after 48 hours.</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error mt-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-xl mt-5">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Notifications</h2>
|
||||
<div class="bg-base-200 mt-3 mx-[-32px]">
|
||||
<Show when={userinfo?.meta?.notifications?.length <= 0}>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-[32px]">You're done! There are no notifications unread for you.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Show>
|
||||
<Show when={userinfo?.meta?.notifications?.length > 0}>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<For each={userinfo?.meta?.notifications}>
|
||||
{item =>
|
||||
<tr>
|
||||
<td class="px-[32px]">
|
||||
<h2 class="font-bold">{item.subject}</h2>
|
||||
<p>{item.content}</p>
|
||||
<div class="flex gap-2">
|
||||
<Show when={item.is_important}>
|
||||
<span class="font-bold">Important</span>
|
||||
</Show>
|
||||
<a class="link" onClick={() => readNotification(item)}>Mark as read</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
95
pkg/view/src/stores/userinfo.tsx
Normal file
95
pkg/view/src/stores/userinfo.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import Cookie from "universal-cookie";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
export interface Userinfo {
|
||||
isLoggedIn: boolean,
|
||||
displayName: string,
|
||||
profiles: any,
|
||||
meta: any
|
||||
}
|
||||
|
||||
const UserinfoContext = createContext<Userinfo>();
|
||||
|
||||
const defaultUserinfo: Userinfo = {
|
||||
isLoggedIn: false,
|
||||
displayName: "Citizen",
|
||||
profiles: null,
|
||||
meta: null
|
||||
};
|
||||
|
||||
const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo));
|
||||
|
||||
export function getAtk(): string {
|
||||
return new Cookie().get("access_token");
|
||||
}
|
||||
|
||||
export async function refreshAtk() {
|
||||
const rtk = new Cookie().get("refresh_token");
|
||||
|
||||
const res = await fetch("/api/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
refresh_token: rtk,
|
||||
grant_type: "refresh_token"
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
console.error(await res.text())
|
||||
} else {
|
||||
const data = await res.json();
|
||||
new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined });
|
||||
new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
function checkLoggedIn(): boolean {
|
||||
return new Cookie().get("access_token");
|
||||
}
|
||||
|
||||
export async function readProfiles(recovering = true) {
|
||||
if (!checkLoggedIn()) return;
|
||||
|
||||
const res = await fetch("/api/users/me", {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (recovering) {
|
||||
// Auto retry after refresh access token
|
||||
await refreshAtk();
|
||||
return await readProfiles(false);
|
||||
} else {
|
||||
clearUserinfo();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
setUserinfo({
|
||||
isLoggedIn: true,
|
||||
displayName: data["nick"],
|
||||
profiles: null,
|
||||
meta: data
|
||||
});
|
||||
}
|
||||
|
||||
export function clearUserinfo() {
|
||||
new Cookie().remove("access_token", { path: "/", maxAge: undefined });
|
||||
new Cookie().remove("refresh_token", { path: "/", maxAge: undefined });
|
||||
setUserinfo(defaultUserinfo);
|
||||
}
|
||||
|
||||
export function UserinfoProvider(props: any) {
|
||||
return (
|
||||
<UserinfoContext.Provider value={userinfo}>
|
||||
{props.children}
|
||||
</UserinfoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUserinfo() {
|
||||
return useContext(UserinfoContext);
|
||||
}
|
23
pkg/view/src/stores/wellKnown.tsx
Normal file
23
pkg/view/src/stores/wellKnown.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
const WellKnownContext = createContext<any>();
|
||||
|
||||
const [wellKnown, setWellKnown] = createStore<any>(null);
|
||||
|
||||
export async function readWellKnown() {
|
||||
const res = await fetch("/.well-known")
|
||||
setWellKnown(await res.json())
|
||||
}
|
||||
|
||||
export function WellKnownProvider(props: any) {
|
||||
return (
|
||||
<WellKnownContext.Provider value={wellKnown}>
|
||||
{props.children}
|
||||
</WellKnownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWellKnown() {
|
||||
return useContext(WellKnownContext);
|
||||
}
|
1
pkg/view/src/vite-env.d.ts
vendored
Normal file
1
pkg/view/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
44
pkg/view/tailwind.config.js
Normal file
44
pkg/view/tailwind.config.js
Normal file
@ -0,0 +1,44 @@
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}"
|
||||
],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
...require("daisyui/src/theming/themes")["light"],
|
||||
primary: "#4750a3",
|
||||
secondary: "#93c5fd",
|
||||
accent: "#0f766e",
|
||||
info: "#67e8f9",
|
||||
success: "#15803d",
|
||||
warning: "#f97316",
|
||||
error: "#dc2626",
|
||||
"--rounded-box": "0",
|
||||
"--rounded-btn": "0",
|
||||
"--rounded-badge": "0",
|
||||
"--tab-radius": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
dark: {
|
||||
...require("daisyui/src/theming/themes")["dark"],
|
||||
primary: "#4750a3",
|
||||
secondary: "#93c5fd",
|
||||
accent: "#0f766e",
|
||||
info: "#67e8f9",
|
||||
success: "#15803d",
|
||||
warning: "#f97316",
|
||||
error: "#dc2626",
|
||||
"--rounded-box": "0",
|
||||
"--rounded-btn": "0",
|
||||
"--rounded-badge": "0",
|
||||
"--tab-radius": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [require("daisyui")]
|
||||
};
|
||||
|
26
pkg/view/tsconfig.json
Normal file
26
pkg/view/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "ES2015", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
pkg/view/tsconfig.node.json
Normal file
10
pkg/view/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
13
pkg/view/vite.config.ts
Normal file
13
pkg/view/vite.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
import devtools from "solid-devtools/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [devtools({ autoname: true }), solid()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://localhost:8444",
|
||||
"/.well-known": "http://localhost:8444"
|
||||
}
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user