webui: Add i18n
17
webui/i18next-parser.config.js
Normal 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',
|
||||
],
|
||||
}
|
|
@ -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"
|
||||
|
|
BIN
webui/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
webui/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
webui/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
webui/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 549 B |
BIN
webui/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 15 KiB |
|
@ -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>
|
||||
|
|
25
webui/public/locales/de/auth.json
Normal 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"
|
||||
}
|
12
webui/public/locales/de/panel.json
Normal 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"
|
||||
}
|
||||
}
|
3
webui/public/locales/de/translation.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"test": "Tst2"
|
||||
}
|
25
webui/public/locales/en/auth.json
Normal 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"
|
||||
}
|
12
webui/public/locales/en/panel.json
Normal 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"
|
||||
}
|
||||
}
|
3
webui/public/locales/en/translation.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"test": "Test"
|
||||
}
|
29
webui/public/logo.svg
Normal 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 |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}>
|
||||
<React.Suspense fallback={<h1>Loading...</h1>}>
|
||||
<BrowserRouter>
|
||||
<App/>
|
||||
</BrowserRouter>
|
||||
</React.Suspense>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|