From 0ce3751d0855fc236d38d6f666ac86d94bbd4f94 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 9 Sep 2025 12:39:03 +0200 Subject: [PATCH] Create test design --- webui/index.html | 5 +- webui/package.json | 2 + webui/pnpm-lock.yaml | 16 ++ webui/src/App.jsx | 16 +- webui/src/common/components/Button/Button.jsx | 50 ++++++ webui/src/common/components/Button/index.js | 1 + .../src/common/components/Button/styles.sass | 84 ++++++++++ webui/src/common/components/Input/Input.jsx | 27 +++ webui/src/common/components/Input/index.js | 1 + webui/src/common/components/Input/styles.sass | 58 +++++++ webui/src/common/contexts/UserContext.jsx | 13 +- webui/src/common/layouts/Root.jsx | 42 ++++- webui/src/common/styles/main.sass | 157 ++++++++++++++++++ webui/src/pages/Login/Login.jsx | 58 ++++++- webui/src/pages/Login/styles.sass | 11 ++ 15 files changed, 521 insertions(+), 20 deletions(-) create mode 100644 webui/src/common/components/Button/Button.jsx create mode 100644 webui/src/common/components/Button/index.js create mode 100644 webui/src/common/components/Button/styles.sass create mode 100644 webui/src/common/components/Input/Input.jsx create mode 100644 webui/src/common/components/Input/index.js create mode 100644 webui/src/common/components/Input/styles.sass diff --git a/webui/index.html b/webui/index.html index 86fa92c..93946fc 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3,7 +3,10 @@ - Vite + React + Arkendro + + +
diff --git a/webui/package.json b/webui/package.json index 0f1631e..33c6c52 100644 --- a/webui/package.json +++ b/webui/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/plus-jakarta-sans": "^5.2.6", "@phosphor-icons/react": "^2.1.10", + "classnames": "^2.5.1", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", diff --git a/webui/pnpm-lock.yaml b/webui/pnpm-lock.yaml index a26bbd0..84af155 100644 --- a/webui/pnpm-lock.yaml +++ b/webui/pnpm-lock.yaml @@ -8,9 +8,15 @@ importers: .: dependencies: + '@fontsource/plus-jakarta-sans': + specifier: ^5.2.6 + version: 5.2.6 '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + classnames: + specifier: ^2.5.1 + version: 2.5.1 react: specifier: ^19.1.1 version: 19.1.1 @@ -334,6 +340,9 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource/plus-jakarta-sans@5.2.6': + resolution: {integrity: sha512-mvUiz1ta3bCVhP/DPmAOmuzhHQi6ddOo1GgaW58rpojr510Rx9BkqXqcnMhGEOMZZB3+84frvfFmw/jKCctHLw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -648,6 +657,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1490,6 +1502,8 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@fontsource/plus-jakarta-sans@5.2.6': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -1750,6 +1764,8 @@ snapshots: readdirp: 4.1.2 optional: true + classnames@2.5.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 diff --git a/webui/src/App.jsx b/webui/src/App.jsx index 6e62a88..60bb13f 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -1,18 +1,30 @@ import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"; +import { UserProvider } from '@/common/contexts/UserContext.jsx'; import "@/common/styles/main.sass"; import Root from "@/common/layouts/Root.jsx"; +import "@fontsource/plus-jakarta-sans/300.css"; +import "@fontsource/plus-jakarta-sans/400.css"; +import "@fontsource/plus-jakarta-sans/600.css"; +import "@fontsource/plus-jakarta-sans/700.css"; +import "@fontsource/plus-jakarta-sans/800.css"; + +const Placeholder = ({title}) =>

{title}

Content coming soon.

; + const App = () => { const router = createBrowserRouter([ { path: "/", element: , children: [ - { path: "/", element: }, + { path: "/", element: }, + { path: "/dashboard", element: }, + { path: "/servers", element: }, + { path: "/settings", element: }, ], }, ]); - return ; + return ; }; export default App; \ No newline at end of file diff --git a/webui/src/common/components/Button/Button.jsx b/webui/src/common/components/Button/Button.jsx new file mode 100644 index 0000000..bf7a0e5 --- /dev/null +++ b/webui/src/common/components/Button/Button.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import cn from "classnames"; +import "./styles.sass"; + +/* +Props: +- variant: primary | subtle | danger +- size: sm | md | lg +- full: boolean (full width) +- icon: ReactNode (left icon) +- iconRight: ReactNode (right icon) +- loading: boolean +*/ + +export const Button = ({ + as: Component = "button", + variant = "primary", + size = "md", + full = false, + icon, + iconRight, + loading = false, + disabled, + className, + children, + ...rest +}) => { + const isDisabled = disabled || loading; + return ( + + {loading && } + {icon && {icon}} + {children} + {iconRight && {iconRight}} + + ); +}; + +export default Button; diff --git a/webui/src/common/components/Button/index.js b/webui/src/common/components/Button/index.js new file mode 100644 index 0000000..5ccf887 --- /dev/null +++ b/webui/src/common/components/Button/index.js @@ -0,0 +1 @@ +export { Button as default } from "./Button.jsx"; \ No newline at end of file diff --git a/webui/src/common/components/Button/styles.sass b/webui/src/common/components/Button/styles.sass new file mode 100644 index 0000000..e99ea7c --- /dev/null +++ b/webui/src/common/components/Button/styles.sass @@ -0,0 +1,84 @@ +.btn + --c-bg: #ffffff + --c-bg-hover: #f2f5f8 + --c-bg-active: #e6ebf0 + --c-border: #d0d7de + --c-border-hover: #c2cbd3 + --c-text: #1f2429 + --c-accent: #0f62fe + --c-danger: #d93025 + position: relative + display: inline-flex + align-items: center + justify-content: center + gap: .5rem + font-family: inherit + font-weight: 500 + line-height: 1.2 + cursor: pointer + border: 1px solid var(--c-border) + background: var(--c-bg) + color: var(--c-text) + border-radius: 6px + transition: background .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease + user-select: none + text-decoration: none + &:hover:not(:disabled) + background: var(--c-bg-hover) + border-color: var(--c-border-hover) + &:active:not(:disabled) + background: var(--c-bg-active) + &:focus-visible + outline: 2px solid var(--c-accent) + outline-offset: 2px + &:disabled + opacity: .55 + cursor: not-allowed + &.btn--full + width: 100% + &.btn--sm + font-size: .75rem + padding: .4rem .7rem + &.btn--md + font-size: .85rem + padding: .6rem 1rem + &.btn--lg + font-size: 1rem + padding: .8rem 1.2rem + &.btn--primary + --c-bg: #0f62fe + --c-bg-hover: #0d55dd + --c-bg-active: #0b47b8 + --c-border: #0f62fe + --c-text: #ffffff + &.btn--subtle + --c-bg: #f3f6f9 + --c-bg-hover: #e8edf2 + --c-bg-active: #dfe5eb + --c-border: #e1e6eb + &.btn--danger + --c-bg: #d93025 + --c-bg-hover: #c22b21 + --c-bg-active: #a9241b + --c-border: #d93025 + --c-text: #ffffff + +.btn__icon + display: inline-flex + align-items: center + &--left + margin-right: .25rem + &--right + margin-left: .25rem + +.btn__spinner + width: 14px + height: 14px + border: 2px solid rgba(0,0,0,.15) + border-top-color: var(--c-text) + border-radius: 50% + animation: spin .7s linear infinite + +@keyframes spin + to + transform: rotate(360deg) diff --git a/webui/src/common/components/Input/Input.jsx b/webui/src/common/components/Input/Input.jsx new file mode 100644 index 0000000..0e2ef30 --- /dev/null +++ b/webui/src/common/components/Input/Input.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import cn from "classnames"; +import "./styles.sass"; + +/* Minimal input wrapper with label, error text, and optional icon */ +export const Input = ({ + label, + error, + icon, + className, + containerClassName, + type = "text", + ...rest +}) => { + return ( +
+ {label && } +
+ {icon && {icon}} + +
+ {error &&
{error}
} +
+ ); +}; + +export default Input; diff --git a/webui/src/common/components/Input/index.js b/webui/src/common/components/Input/index.js new file mode 100644 index 0000000..8a20e7b --- /dev/null +++ b/webui/src/common/components/Input/index.js @@ -0,0 +1 @@ +export { Input as default } from "./Input.jsx"; \ No newline at end of file diff --git a/webui/src/common/components/Input/styles.sass b/webui/src/common/components/Input/styles.sass new file mode 100644 index 0000000..e446ebe --- /dev/null +++ b/webui/src/common/components/Input/styles.sass @@ -0,0 +1,58 @@ +.field + display: flex + flex-direction: column + gap: .35rem + font-size: .8rem + font-weight: 500 + color: #374048 + +.field__label + letter-spacing: .5px + +.field__control + position: relative + display: flex + align-items: center + background: #ffffff + border: 1px solid #d0d7de + border-radius: 6px + padding: .55rem .7rem + transition: border-color .15s ease, background .15s ease, box-shadow .15s ease + &.has-icon .field__input + padding-left: 1.6rem + &.has-error + border-color: #d93025 + box-shadow: 0 0 0 1px #d93025 + &:focus-within + border-color: #0f62fe + box-shadow: 0 0 0 1px #0f62fe20 + +.field__icon + position: absolute + left: .55rem + top: 50% + transform: translateY(-50%) + display: inline-flex + font-size: 1rem + color: #6b7781 + pointer-events: none + +.field__input + appearance: none + outline: none + background: transparent + border: 0 + color: #1f2429 + font: inherit + width: 100% + line-height: 1.2 + &::placeholder + color: #a0abb4 + &:focus + outline: none + +.field__error + font-size: .65rem + font-weight: 600 + color: #d93025 + letter-spacing: .5px diff --git a/webui/src/common/contexts/UserContext.jsx b/webui/src/common/contexts/UserContext.jsx index 2f9fa6b..9fd293c 100644 --- a/webui/src/common/contexts/UserContext.jsx +++ b/webui/src/common/contexts/UserContext.jsx @@ -7,7 +7,7 @@ export const UserContext = createContext({}); export const UserProvider = ({ children }) => { const [sessionToken, setSessionToken] = useState(localStorage.getItem("sessionToken")); - const [firstTimeSetup, setFirstTimeSetup] = useState(false); + const [isSetupCompleted, setIsSetupCompleted] = useState(false); const [user, setUser] = useState(null); const updateSessionToken = (sessionToken) => { @@ -19,7 +19,7 @@ export const UserProvider = ({ children }) => { const checkFirstTimeSetup = async () => { try { const response = await getRequest("setup/status"); - setFirstTimeSetup(response?.first_user_exists); + setIsSetupCompleted(response?.first_user_exists); } catch (error) { console.error(error); } @@ -38,8 +38,11 @@ export const UserProvider = ({ children }) => { }; const logout = async () => { - await postRequest("auth/logout", { token: sessionToken }); - + try { + await postRequest("auth/logout", { token: sessionToken }); + } catch (e) { + // ignore + } window.location.reload(); }; @@ -60,7 +63,7 @@ export const UserProvider = ({ children }) => { }, []); return ( - + {user == null && } {user !== null && children} diff --git a/webui/src/common/layouts/Root.jsx b/webui/src/common/layouts/Root.jsx index 29da720..60f068b 100644 --- a/webui/src/common/layouts/Root.jsx +++ b/webui/src/common/layouts/Root.jsx @@ -1,7 +1,35 @@ -export default () => { - return ( - <> - Root - - ) -} \ No newline at end of file +import React, { useContext } from 'react'; +import { NavLink, Outlet } from 'react-router-dom'; +import { HouseIcon, GearSixIcon, SquaresFourIcon } from '@phosphor-icons/react'; + +const navItems = [ + { to: '/dashboard', label: 'Dashboard', icon: }, + { to: '/servers', label: 'Servers', icon: }, + { to: '/settings', label: 'Settings', icon: }, +]; + +const Root = () => { + return ( +
+ +
+
+

Test

+
+ +
+
+ ); +}; + +export default Root; \ No newline at end of file diff --git a/webui/src/common/styles/main.sass b/webui/src/common/styles/main.sass index e69de29..1fa8afe 100644 --- a/webui/src/common/styles/main.sass +++ b/webui/src/common/styles/main.sass @@ -0,0 +1,157 @@ +:root + --bg: #f7f9fb + --bg-alt: #ffffff + --bg-elev: #f0f3f6 + --border: #dfe3e8 + --border-strong: #c7ced6 + --text: #1f2429 + --text-dim: #5c6a78 + --accent: #0f62fe + --danger: #d93025 + --radius-sm: 4px + --radius: 6px + --radius-lg: 10px + --sidebar-width: 240px + +*, *::before, *::after + box-sizing: border-box + +html, body, #root + height: 100% + +body + margin: 0 + font-family: "Plus Jakarta Sans", sans-serif + background: var(--bg) + color: var(--text) + -webkit-font-smoothing: antialiased + font-size: 14px + +h1, h2, h3, h4, h5, h6, p + margin: 0 + +ul + margin: 0 + padding: 0 + list-style: none + +a + color: var(--accent) + text-decoration: none + +.layout + display: flex + min-height: 100vh + +.sidebar + width: var(--sidebar-width) + background: var(--bg-alt) + border-right: 1px solid var(--border) + display: flex + flex-direction: column + padding: 1rem .9rem + gap: 1.2rem + +.sidebar__brand + font-size: .95rem + font-weight: 600 + letter-spacing: .5px + display: flex + align-items: center + gap: .55rem + +.sidebar__nav + display: flex + flex-direction: column + gap: .2rem + +.nav-item + display: flex + align-items: center + gap: .65rem + padding: .6rem .7rem + color: var(--text-dim) + font-size: .75rem + font-weight: 500 + letter-spacing: .5px + border-radius: var(--radius-sm) + transition: background .15s ease, color .15s ease + &.active, &:hover + background: var(--bg-elev) + color: var(--text) + +.nav-item__icon + font-size: 1.1rem + display: inline-flex + color: inherit + +.main + flex: 1 + display: flex + flex-direction: column + min-width: 0 + +.topbar + height: 54px + display: flex + align-items: center + padding: 0 1.1rem + border-bottom: 1px solid var(--border) + background: var(--bg-alt) + gap: 1rem + +.grow + flex: 1 + +.content + flex: 1 + padding: 1.4rem 1.3rem 2rem + display: flex + flex-direction: column + gap: 1.2rem + + +/* Auth page */ +.auth-wrapper + min-height: 100vh + display: flex + align-items: center + justify-content: center + padding: 2rem 1rem + +.auth-box + width: 100% + max-width: 380px + background: var(--bg-alt) + border: 1px solid var(--border) + border-radius: var(--radius-lg) + padding: 1.8rem 1.6rem 1.7rem + display: flex + flex-direction: column + gap: 1.2rem + box-shadow: 0 4px 14px -4px rgba(31,36,41,.08), 0 2px 4px -1px rgba(31,36,41,.06) + +.auth-title + font-size: 1.1rem + font-weight: 600 + letter-spacing: .5px + +.form + display: flex + flex-direction: column + gap: .95rem + +.sep + height: 1px + background: var(--border) + margin: .25rem 0 + +.text-center + text-align: center + +.small + font-size: .65rem + color: var(--text-dim) + +.inline + display: inline \ No newline at end of file diff --git a/webui/src/pages/Login/Login.jsx b/webui/src/pages/Login/Login.jsx index 87d316e..542c2af 100644 --- a/webui/src/pages/Login/Login.jsx +++ b/webui/src/pages/Login/Login.jsx @@ -1,6 +1,54 @@ -import "./styles.sass"; +import React, { useContext, useState } from 'react'; +import './styles.sass'; +import Input from '@/common/components/Input'; +import Button from '@/common/components/Button'; +import { UserContext } from '@/common/contexts/UserContext.jsx'; +import { request } from '@/common/utils/RequestUtil.js'; +import { LockIcon, UserIcon } from '@phosphor-icons/react'; + export const Login = () => { - return ( -
Login Page
- ) -} \ No newline at end of file + const { updateSessionToken, isSetupCompleted } = useContext(UserContext); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [mode, setMode] = useState(isSetupCompleted ? 'login' : 'setup'); + + React.useEffect(()=>{ + setMode(isSetupCompleted ? 'login' : 'setup'); + }, [isSetupCompleted]); + + const submit = async (e) => { + e.preventDefault(); + setError(null); + setLoading(true); + try { + if (mode === 'login') { + const data = await request('/auth/login','POST',{username, password}); + updateSessionToken(data.token); + } else { // setup + await request('/setup/init','POST',{username, password}); + const data = await request('/auth/login','POST',{username, password}); + updateSessionToken(data.token); + } + } catch (err) { + setError(err.error || err.message || 'Error'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
{mode === 'login' ? 'Sign in' : 'Initial Setup'}
+
+ setUsername(e.target.value)} icon={} autoFocus required /> + setPassword(e.target.value)} icon={} required /> + {error &&
{error}
} + + +
+
+ ); +}; \ No newline at end of file diff --git a/webui/src/pages/Login/styles.sass b/webui/src/pages/Login/styles.sass index e69de29..6f090d7 100644 --- a/webui/src/pages/Login/styles.sass +++ b/webui/src/pages/Login/styles.sass @@ -0,0 +1,11 @@ +.auth-box + box-shadow: 0 0 0 1px rgba(255,255,255,.02), 0 4px 16px -6px rgba(0,0,0,.6) + animation: fadeIn .3s ease + +@keyframes fadeIn + from + opacity: 0 + transform: translateY(4px) + to + opacity: 1 + transform: translateY(0) \ No newline at end of file