webui: Add i18n

This commit is contained in:
Kevin Kandlbinder 2022-03-06 01:31:02 +01:00
parent ac5faca859
commit 9e05c54d81
30 changed files with 1098 additions and 112 deletions

View file

@ -0,0 +1,17 @@
module.exports = {
createOldCatalogs: true,
lexers: {
js: ['JsxLexer'],
default: ['JavascriptLexer'],
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
},
skipDefaultValues: (locale, ns) => {return locale !== "en"},
locales: ['en', 'de'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
input: [
'src/**/*.tsx',
'src/*.tsx',
],
}

View file

@ -12,11 +12,18 @@
"@types/node": "^12.20.46",
"@types/react": "^16.14.23",
"@types/react-dom": "^16.9.14",
"@types/react-helmet": "^6.1.5",
"@types/react-redux": "^7.1.22",
"axios": "^0.26.0",
"i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"i18next-parser": "^5.4.0",
"lucide-react": "^0.17.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-i18next": "^11.15.5",
"react-redux": "^7.2.6",
"react-router-dom": "6",
"react-scripts": "5.0.0",
@ -27,7 +34,8 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"extract-translations": "i18next --fail-on-warnings"
},
"eslintConfig": {
"extends": "react-app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -7,37 +7,14 @@
<meta name="theme-color" content="#000000"/>
<meta
name="description"
content="Web site created using create-react-app"
content="Protector of your Matrix-Harvest!"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Redux App</title>
<title>Matrix-Veles WebUI</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -0,0 +1,25 @@
{
"login": {
"htmlTitle": "Einloggen bei Veles",
"title": "Einloggen",
"logging_in": "Logge ein...",
"login": "Einloggen",
"register_instead": "Ich habe noch keinen Account"
},
"username": "Benutzername",
"password": "Passwort",
"register": {
"register": "Registrieren",
"htmlTitle": "Registrieren bei Veles",
"title": "Registrieren",
"login_instead": "Ich habe schon einen Account"
},
"matrix_handle": "Matrix-Name",
"selector": {
"question": "Kennen wir uns?",
"login": "Jup, ich log mich ein",
"register": "Nee, ich registrier' mich"
},
"help": "Hilfe",
"source": "Quellcode"
}

View file

@ -0,0 +1,12 @@
{
"jump_to_content": "Zum Inhalt springen",
"jump_to_navigation": "Zur Navigation springen",
"documentation": "Dokumentation",
"navigation": {
"dashboard": "Dashboard",
"rooms": "Meine Räume",
"hash_checker": "Hash-Prüfer",
"hash_lists": "Listen",
"hash_entries": "Einträge"
}
}

View file

@ -0,0 +1,3 @@
{
"test": "Tst2"
}

View file

@ -0,0 +1,25 @@
{
"login": {
"htmlTitle": "Login to Veles",
"title": "Login",
"logging_in": "Logging in...",
"login": "Login",
"register_instead": "I don't have an account"
},
"username": "Username",
"password": "Password",
"register": {
"register": "Register",
"htmlTitle": "Register with Veles",
"title": "Register",
"login_instead": "I already have an account"
},
"matrix_handle": "Matrix-Handle",
"selector": {
"question": "Do we know each other?",
"login": "Yeah, let me log in",
"register": "Nah, I'll sign up"
},
"help": "Help",
"source": "Source Code"
}

View file

@ -0,0 +1,12 @@
{
"jump_to_content": "Jump to Content",
"jump_to_navigation": "Jump to Navigation",
"documentation": "Documentation",
"navigation": {
"dashboard": "Dashboard",
"rooms": "My Rooms",
"hash_checker": "Hash-Checker",
"hash_lists": "Lists",
"hash_entries": "Entries"
}
}

View file

@ -0,0 +1,3 @@
{
"test": "Test"
}

29
webui/public/logo.svg Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="120" height="120" version="1.1" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient43644">
<stop stop-color="#007300" offset="0"/>
<stop stop-color="#007300" stop-opacity=".0072181" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient1301" x1="49.693" x2="69.839" y1="25.512" y2="25.512" gradientTransform="translate(4,8)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
<linearGradient id="linearGradient1303" x1="49.777" x2="70.07" y1="81.546" y2="81.546" gradientTransform="translate(0,6)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
<linearGradient id="linearGradient31669" x1="91.453" x2="72.905" y1="48.722" y2="48.722" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
</defs>
<g transform="translate(-3.6704 12.24)" fill="url(#linearGradient31669)" style="shape-inside:url(#rect8261);white-space:pre" aria-label="101 011 110 100">
<path d="m74.758 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 2.1413q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m108.69 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m79.928 27.475q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m91.725 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m108.69 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m74.758 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m91.725 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m113.86 52.808q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m74.758 93.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 78.141q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m113.86 78.141q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
</g>
<path d="m4.0226 8.0649 30.026 50.894h59.933l30.009-50.864" fill="none" stroke="url(#linearGradient1301)" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.048"/>
<path d="m36.324 67.382h47.484l-23.793 40.329z" fill="none" stroke="url(#linearGradient1303)" stroke-linejoin="round" stroke-width="4.048"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,19 +1,29 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Veles",
"name": "Matrix-Veles",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"sizes": "16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}

View file

@ -8,12 +8,16 @@ import {useAppDispatch} from "./app/hooks";
import broadcastChannel from "./app/broadcastChannel";
import {logOut, receiveAuthUpdate} from "./features/auth/authSlice";
import PanelLayout from "./layouts/PanelLayout";
import {Trans, useTranslation} from "react-i18next";
function App() {
const dispatch = useAppDispatch()
// This needs to be here to prevent a weird bug
useTranslation()
broadcastChannel.on("message", (ev) => {
if(ev.action == "updateAuth") {
if(ev.action === "updateAuth") {
dispatch(receiveAuthUpdate(ev))
}
})
@ -25,10 +29,11 @@ function App() {
<Route path={"register"} element={<RegisterView/>} />
</Route>
<Route path={"/"} element={<PanelLayout/>}>
<Route path={""} element={<RequireAuth><h1>hi</h1> <button onClick={() => {
<Route path={""} element={<RequireAuth><h1><Trans i18nKey={"test"}>Test</Trans></h1> <button onClick={() => {
dispatch(logOut())
}
}>Log out</button></RequireAuth>} />
<Route path={"rooms"} element={<RequireAuth><h1>rooms</h1></RequireAuth>} />
<Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>} />
<Route path={"hashing/entries"} element={<RequireAuth><h1>entries</h1></RequireAuth>} />
</Route>

View file

@ -1,16 +1,18 @@
import React, {useRef, useState} from "react";
import React, {useState} from "react";
import styles from "./AuthViews.module.scss";
import {ReactComponent as Logo} from "../../logo.svg";
import {Link, useLocation, useNavigate} from "react-router-dom";
import {Link, useLocation} from "react-router-dom";
import {axiosDefault} from "../../context/axios";
import {AxiosError} from "axios";
import {useAppDispatch, useAppSelector} from "../../app/hooks";
import {logIn, selectAuth} from "../../features/auth/authSlice";
import {useAppDispatch} from "../../app/hooks";
import {logIn} from "../../features/auth/authSlice";
import {Key} from "lucide-react";
import {AuthLocationState} from "../../layouts/AuthLayout";
import {Helmet} from "react-helmet";
import {Trans, useTranslation} from "react-i18next";
const LoginView = () => {
const [username, setUsername] = useState("");
@ -25,6 +27,8 @@ const LoginView = () => {
const locationState = location.state as AuthLocationState
const {t} = useTranslation()
const onSubmit = async () => {
setLoading(true)
setError("")
@ -42,7 +46,7 @@ const LoginView = () => {
if((e as AxiosError).isAxiosError) {
const axErr = e as AxiosError
setError("Server returned error: "+axErr.response?.data.error)
setError(": "+axErr.response?.data.error)
} else {
setError("An unknown error occurred.")
}
@ -53,23 +57,27 @@ const LoginView = () => {
return <>
<Logo width={64} height={64} />
<h1>Login</h1>
<Helmet>
<title>{t("auth:login.htmlTitle", "Login to Veles")}</title>
</Helmet>
<h1><Trans i18nKey={"auth:login.title"}>Login</Trans></h1>
{ loading && <div className={styles.loader}>
<Key/>
<span>Logging in...</span>
<span><Trans i18nKey={"auth:login.logging_in"}>Logging in...</Trans></span>
</div>}
{ !loading && <>
{error !== "" && <span className={styles.error}>{error}</span>}
<form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={"Username"} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={"Password"} type={"password"} />
<button onClick={() => onSubmit()}>Login</button>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={t("auth:username", "Username")} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={t("auth:password", "Password")} type={"password"} />
<button onClick={() => onSubmit()}><Trans i18nKey={"auth:login.login"}>Login</Trans></button>
</form>
<Link to={"/auth/register"} className={styles.mindChangedLink} aria-label={"Register"} state={locationState}>I don't have an account</Link>
<Link to={"/auth/register"} className={styles.mindChangedLink} aria-label={t("auth:register.register", "Register")} state={locationState}><Trans i18nKey={"auth:login.register_instead"}>I don't have an account</Trans></Link>
</>}
</>
}

View file

@ -1,12 +1,13 @@
import React, {useRef, useState} from "react";
import React, {useState} from "react";
import styles from "./AuthViews.module.scss";
import {ReactComponent as Logo} from "../../logo.svg";
import {Link, useLocation, useNavigate} from "react-router-dom";
import {Link, useLocation} from "react-router-dom";
import {AuthLocationState} from "../../layouts/AuthLayout";
import {useAppDispatch, useAppSelector} from "../../app/hooks";
import {selectAuth} from "../../features/auth/authSlice";
import {useAppDispatch} from "../../app/hooks";
import {Helmet} from "react-helmet";
import {Trans, useTranslation} from "react-i18next";
const RegisterView = () => {
const [username, setUsername] = useState("");
@ -19,22 +20,28 @@ const RegisterView = () => {
const dispatch = useAppDispatch()
const {t} = useTranslation()
const onSubmit = () => {
console.log(username, password)
console.log(username, password, matrix)
}
return <>
<Logo width={64} height={64} />
<h1>Register</h1>
<Helmet>
<title>{t("auth:register.htmlTitle", "Register with Veles")}</title>
</Helmet>
<h1><Trans i18nKey={"auth:register.title"}>Register</Trans></h1>
<form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={"Username"} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={"Password"} type={"password"} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setMatrix(ev.target.value)} value={matrix} placeholder={"Matrix-Handle (@user:matrix.org)"} autoCapitalize={"no"} autoCorrect={"no"} />
<button onClick={() => onSubmit()}>Register</button>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={t("auth:username", "Username")} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={t("auth:password", "Password")} type={"password"} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setMatrix(ev.target.value)} value={matrix} placeholder={t("auth:matrix_handle", "Matrix-Handle")+" (@user:matrix.org)"} autoCapitalize={"no"} autoCorrect={"no"} />
<button onClick={() => onSubmit()}><Trans i18nKey={"auth:register.register"}>Register</Trans></button>
</form>
<Link to={"/auth/login"} className={styles.mindChangedLink} aria-label={"Register"} state={locationState}>I already have an account</Link>
<Link to={"/auth/login"} className={styles.mindChangedLink} aria-label={t("auth:login.login", "Login")} state={locationState}><Trans i18nKey={"auth:register.login_instead"}>I already have an account</Trans></Link>
</>
}

View file

@ -1,4 +1,4 @@
import React, {ReactNode} from "react";
import React from "react";
import {useAppSelector} from "../../app/hooks";
import {selectAuth} from "./authSlice";
import {useLocation} from "react-router-dom";

29
webui/src/i18n.ts Normal file
View file

@ -0,0 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
ns: ["translation", "auth", "panel"],
supportedLngs: ["en", "de"],
fallbackLng: "en",
interpolation: {
escapeValue: false
},
detection: {
lookupCookie: "velesLng",
lookupQuerystring: "lang",
lookupLocalStorage: "velesLng",
lookupSessionStorage: "velesLng"
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
}
})
export default i18n

View file

@ -26,6 +26,7 @@ html, body, #root {
--veles-layout-padding: 20px;
--veles-layout-padding-slim: 10px;
--veles-layout-padding-wide: 40px;
--veles-layout-border-radius: 10px;
}

View file

@ -7,12 +7,16 @@ import {Provider} from 'react-redux';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from "react-router-dom";
import "./i18n";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
<React.Suspense fallback={<h1>Loading...</h1>}>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.Suspense>
</Provider>
</React.StrictMode>,
document.getElementById('root')

View file

@ -7,6 +7,7 @@ import {UserPlus, User} from "lucide-react";
import {ReactComponent as Logo} from "../logo.svg";
import {useAppSelector} from "../app/hooks";
import {selectAuth} from "../features/auth/authSlice";
import {Trans} from "react-i18next";
export type AuthLocationState = {
location?: Location
@ -23,13 +24,13 @@ const AuthLayout = () => {
useEffect(() => {
if(authState.status == "logged_in") {
if(authState.status === "logged_in") {
if(locationState && locationState.location) {
navigate(locationState.location, {replace: true})
}
navigate("/", {replace: true})
}
}, [authState])
}, [authState, locationState, navigate])
return <div className={styles.auth}>
<div className={styles.background}/>
@ -38,21 +39,21 @@ const AuthLayout = () => {
{outlet || <>
<Logo width={64} height={64} />
<h1>Matrix-Veles</h1>
<h2>Do we know each other?</h2>
<h2><Trans i18nKey={"auth:selector.question"}>Do we know each other?</Trans></h2>
<div className={styles.splitChoice}>
<Link to={"./login"} state={locationState}>
<User/>
<span>Yeah, let me log in</span>
<span><Trans i18nKey={"auth:selector.login"}>Yeah, let me log in</Trans></span>
</Link>
<Link to={"./register"} state={locationState}>
<UserPlus/>
<span>Nah, I'll sign up</span>
<span><Trans i18nKey={"auth:selector.register"}>Nah, I'll sign up</Trans></span>
</Link>
</div>
</>}
</div>
<footer>Veles WebUI | <a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}>Help</a> | <a href={"https://github.com/Unkn0wnCat/matrix-veles"} target={"_blank"} rel={"noreferrer"}>Source Code</a></footer>
<footer>Veles WebUI | <a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><Trans i18nKey={"auth:help"}>Help</Trans></a> | <a href={"https://github.com/Unkn0wnCat/matrix-veles"} target={"_blank"} rel={"noreferrer"}><Trans i18nKey={"auth:source"}>Source Code</Trans></a></footer>
</div>
</div>
}

View file

@ -24,8 +24,17 @@
transition: color .25s;
background: transparent;
color: inherit;
font: inherit;
border: none;
font-weight: 600;
cursor: pointer;
height: 65px;
&:hover, &.active, &:focus {
color: var(--veles-color-accent);
}
@ -46,10 +55,12 @@
display: flex;
flex-direction: column;
//overflow: hidden;
.topBar {
display: flex;
border-bottom: thin solid var(--veles-color-border);
height: 66px;
a {
@include panelTopBarLink;
@ -74,11 +85,55 @@
display: flex;
flex-direction: column;
border-right: thin solid var(--veles-color-border);
overflow-x: auto;
height: calc(100vh - 66px);
width: 250px;
a {
>* {
flex-shrink: 0;
}
.dropdown {
height: 65px;
overflow: hidden;
transition: height .25s;
> button > svg {
transition: transform .25s;
}
> a {
display: none;
padding-left: var(--veles-layout-padding-wide)
}
&.expanded {
height: unset;
> a {
display: flex;
}
> button > svg {
transform: rotate(90deg);
}
}
}
a, button {
@include panelTopBarLink;
width: 100%;
padding: var(--veles-layout-padding);
}
button {
&:focus {
color: white;
}
&:active, &:hover {
color: var(--veles-color-accent);
}
}
}
> main {

View file

@ -1,27 +1,35 @@
import React from "react";
import React, {useState} from "react";
import {Link, NavLink, useOutlet} from "react-router-dom";
import {Home, List, ClipboardList, ExternalLink} from "lucide-react";
import {Home, List, ClipboardList, ExternalLink, ChevronRight, MessageSquare} from "lucide-react";
import {ReactComponent as Logo} from "../logo.svg";
import styles from "./PanelLayout.module.scss";
import {Trans} from "react-i18next";
const PanelLayout = () => {
const outlet = useOutlet();
const [hashingExpanded, setHashingExpanded] = useState(false)
return <div className={styles.panel}>
<a href={"#main"} className={styles.skipToContent}>Jump to Content</a>
<a href={"#navigation"} className={styles.skipToContent}>Jump to Navigation</a>
<a href={"#main"} className={styles.skipToContent}><Trans i18nKey={"panel:jump_to_content"}>Jump to Content</Trans></a>
<a href={"#navigation"} className={styles.skipToContent}><Trans i18nKey={"panel:jump_to_navigation"}>Jump to Navigation</Trans></a>
<div className={styles.topBar}>
<Link to={"/"} className={styles.logo}><Logo/> <span>Matrix-Veles</span></Link>
<a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><ExternalLink/> <span>Documentation</span></a>
<a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><ExternalLink/> <span><Trans i18nKey={"panel:documentation"}>Documentation</Trans></span></a>
</div>
<div className={styles.content}>
<nav id={"navigation"}>
<NavLink to={"/"}><Home/><span>Dashboard</span></NavLink>
<NavLink to={"/hashing/lists"}><List/><span>Lists</span></NavLink>
<NavLink to={"/hashing/entries"}><ClipboardList/><span>Entries</span></NavLink>
<NavLink to={"/"}><Home/><span><Trans i18nKey={"panel:navigation.dashboard"}>Dashboard</Trans></span></NavLink>
<NavLink to={"/rooms"}><MessageSquare/><span><Trans i18nKey={"panel:navigation.rooms"}>My Rooms</Trans></span></NavLink>
<div className={styles.dropdown + (hashingExpanded?" "+styles.expanded:"")}>
<button onClick={() => setHashingExpanded(!hashingExpanded)}>
<ChevronRight/> <span><Trans i18nKey={"panel:navigation.hash_checker"}>Hash-Checker</Trans></span>
</button>
<NavLink to={"/hashing/lists"}><List/><span><Trans i18nKey={"panel:navigation.hash_lists"}>Lists</Trans></span></NavLink>
<NavLink to={"/hashing/entries"}><ClipboardList/><span><Trans i18nKey={"panel:navigation.hash_entries"}>Entries</Trans></span></NavLink>
</div>
</nav>
<main id={"main"}>
{outlet}

File diff suppressed because it is too large Load diff