localization, context, react stuff

This commit is contained in:
2022-12-01 01:28:38 +01:00
parent a84a51018c
commit cb75e22811
32 changed files with 379 additions and 208 deletions

View File

@@ -25,7 +25,6 @@ class Account extends TemplateDocument {
protected function loadParameters() { protected function loadParameters() {
$settings = $this->getSettings(); $settings = $this->getSettings();
$templateName = $this->getTemplateName(); $templateName = $this->getTemplateName();
$language = $this->getContext()->getLanguage();
$this->parameters["view"] = ["success" => true]; $this->parameters["view"] = ["success" => true];
switch ($templateName) { switch ($templateName) {

View File

@@ -14,5 +14,6 @@ class Admin extends TemplateDocument {
$this->searchable = false; $this->searchable = false;
parent::__construct($router, $template, $params); parent::__construct($router, $template, $params);
$this->enableCSP(); $this->enableCSP();
$this->addCSPWhitelist("/js/admin-panel/");
} }
} }

View File

@@ -0,0 +1,11 @@
<?php
return [
"dashboard" => "Dashboard",
"visitor_statistics" => "Besucherstatistiken",
"user_groups" => "Benutzer & Gruppen",
"page_routes" => "Seiten & Routen",
"settings" => "Einstellungen",
"logs" => "Logs",
"help" => "Hilfe",
];

View File

@@ -9,4 +9,5 @@ return [
"submit" => "Absenden", "submit" => "Absenden",
"language" => "Sprache", "language" => "Sprache",
"loading" => "Laden", "loading" => "Laden",
"logout" => "Ausloggen",
]; ];

View File

@@ -0,0 +1,11 @@
<?php
return [
"dashboard" => "Dashboard",
"visitor_statistics" => "Visitor Statistics",
"user_groups" => "User & Groups",
"page_routes" => "Pages & Routes",
"settings" => "Settings",
"logs" => "Logs",
"help" => "Help",
];

View File

@@ -9,4 +9,5 @@ return [
"submit" => "Submit", "submit" => "Submit",
"language" => "Language", "language" => "Language",
"loading" => "Loading", "loading" => "Loading",
"logout" => "Logout",
]; ];

View File

@@ -7,7 +7,7 @@
{% block body %} {% block body %}
<noscript>You need Javascript enabled to run this app</noscript> <noscript>You need Javascript enabled to run this app</noscript>
<div class="wrapper" type="module" id="admin-panel"></div> <div type="module" id="admin-panel"></div>
<script src="/js/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script> <script src="/js/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script>
<link rel="stylesheet" href="/js/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link> <link rel="stylesheet" href="/js/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link>
{% endblock %} {% endblock %}

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
!function(){function e(e,t,n,r){Object.defineProperty(e,t,{get:n,set:r,enumerable:!0,configurable:!0})}var t=("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).parcelRequireeec4;t.register("aNQgM",(function(n,r){var o;o=n.exports,Object.defineProperty(o,"__esModule",{value:!0,configurable:!0}),e(n.exports,"default",(function(){return c}));var a=t("lBpE3");t("6cds3");var s=t("1dF7x"),i=t("2eEzJ");function c(e){for(var t=new Date,n=(0,i.default)(t),r=["#ff4444","#ffbb33","#00C851","#33b5e5"];r.length<n;)r=r.concat(r);var o=new Array(n).fill(0);Array.from(Array(n),(function(e,t){return t+1})),format(t,"MMMM");return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("div",{className:"content-header",children:(0,a.jsx)("div",{className:"container-fluid",children:(0,a.jsxs)("div",{className:"row mb-2",children:[(0,a.jsx)("div",{className:"col-sm-6",children:(0,a.jsx)("h1",{className:"m-0 text-dark",children:"Dashboard"})}),(0,a.jsx)("div",{className:"col-sm-6",children:(0,a.jsxs)("ol",{className:"breadcrumb float-sm-right",children:[(0,a.jsx)("li",{className:"breadcrumb-item",children:(0,a.jsx)(s.Link,{to:"/admin/dashboard",children:"Home"})}),(0,a.jsx)("li",{className:"breadcrumb-item active",children:"Dashboard"})]})})]})})}),(0,a.jsx)("section",{className:"content"})]})}})),t.register("2eEzJ",(function(n,r){e(n.exports,"default",(function(){return s}));var o=t("18SRp"),a=t("3QBsJ");function s(e){(0,a.default)(1,arguments);var t=(0,o.default)(e),n=t.getFullYear(),r=t.getMonth(),s=new Date(0);return s.setFullYear(n,r+1,0),s.setHours(0,0,0,0),s.getDate()}})),t.register("18SRp",(function(n,r){e(n.exports,"default",(function(){return s}));var o=t("3QBsJ");function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function s(e){(0,o.default)(1,arguments);var t=Object.prototype.toString.call(e);return e instanceof Date||"object"===a(e)&&"[object Date]"===t?new Date(e.getTime()):"number"==typeof e||"[object Number]"===t?new Date(e):("string"!=typeof e&&"[object String]"!==t||"undefined"==typeof console||(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments"),console.warn((new Error).stack)),new Date(NaN))}})),t.register("3QBsJ",(function(t,n){function r(e,t){if(t.length<e)throw new TypeError(e+" argument"+(e>1?"s":"")+" required, but only "+t.length+" present")}e(t.exports,"default",(function(){return r}))}))}();
//# sourceMappingURL=overview.01501b20.js.map

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -4,10 +4,22 @@ const {
override, override,
removeModuleScopePlugin, removeModuleScopePlugin,
babelInclude, babelInclude,
addWebpackModuleRule,
} = require('customize-cra'); } = require('customize-cra');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const addMiniCssExtractPlugin = config => {
config.plugins.push(new MiniCssExtractPlugin());
return config;
}
module.exports = override( module.exports = override(
removeModuleScopePlugin(), removeModuleScopePlugin(),
addMiniCssExtractPlugin,
addWebpackModuleRule({
test: /\.css$/,
use: [ MiniCssExtractPlugin.loader, 'css-loader' ]
}),
babelInclude([ babelInclude([
path.resolve(path.join(__dirname, 'src')), path.resolve(path.join(__dirname, 'src')),
fs.realpathSync(path.join(__dirname, '../shared')), fs.realpathSync(path.join(__dirname, '../shared')),

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/fontawesome.min.css"> <link rel="stylesheet" href="/css/fontawesome.min.css">
</head> </head>
<body> <body>
<div id="admin-panel"></div> <div id="admin-panel"></div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,79 @@
import React, {lazy, Suspense, useCallback, useState} from "react";
import {BrowserRouter, Route, Routes} from "react-router-dom";
import Header from "./elements/header";
import Sidebar from "./elements/sidebar";
import Dialog from "./elements/dialog";
import Footer from "./elements/footer";
import {useContext, useEffect} from "react";
import {LocaleContext} from "shared/locale";
// css
import './res/adminlte.min.css';
// views
const Overview = lazy(() => import('./views/overview'));
export default function AdminDashboard(props) {
const api = props.api;
const info = props.info;
const [dialog, setDialog] = useState({show: false});
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
setDialog({ show: true, message: message, title: title, options: options, onOption: onOption });
}, []);
const hideDialog = useCallback(() => {
setDialog({show: false});
}, []);
useEffect(() => {
requestModules(api, ["general", "admin"], currentLocale).then(data => {
if (!data.success) {
alert(data.msg);
}
});
}, [currentLocale]);
const controlObj = {
...props,
showDialog: showDialog,
hideDialog: hideDialog
};
return <BrowserRouter>
<Header {...controlObj} />
<Sidebar {...controlObj} />
<div className={"wrapper"}>
<div className={"content-wrapper p-2"}>
<section className={"content"}>
<Suspense fallback={<div>{L("general.loading")}... </div>}>
<Routes>
<Route path={"/admin/dashboard"} element={<Overview {...controlObj} />}/>
</Routes>
</Suspense>
{/*<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/user/edit/:userId"} render={(props) => {
let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} />
}}/>
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route>*/}
<Dialog {...dialog}/>
</section>
</div>
</div>
<Footer info={info} />
</BrowserRouter>
}

View File

@@ -1,102 +1,61 @@
import React from 'react'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import './res/adminlte.min.css';
import './res/index.css';
import API from "shared/api"; import API from "shared/api";
import Icon from "shared/elements/icon"; import Icon from "shared/elements/icon";
import {BrowserRouter, Routes} from "react-router-dom";
import Dialog from "./elements/dialog";
import Footer from "./elements/footer";
import Header from "./elements/header";
import Sidebar from "./elements/sidebar";
import LoginForm from "./views/login"; import LoginForm from "./views/login";
import {Alert} from "@material-ui/lab"; import {Alert} from "@material-ui/lab";
import {Button} from "@material-ui/core"; import {Button} from "@material-ui/core";
import { LocaleContext } from "shared/locale"; import { LocaleContext } from "shared/locale";
import AdminDashboard from "./AdminDashboard";
const L = (key) => { export default function App() {
return "<nope>";
}
export default class AdminDashboard extends React.Component { const api = useMemo(() => new API(), []);
const [user, setUser] = useState(null);
const [loaded, setLoaded] = useState(false);
const [info, setInfo] = useState({});
const [error, setError] = useState(null);
const {translate: L} = useContext(LocaleContext);
static contextType = LocaleContext; const fetchUser = useCallback(() => {
api.fetchUser().then(data => {
constructor(props) {
super(props);
this.api = new API();
this.state = {
loaded: false,
dialog: { onClose: () => this.hideDialog() },
info: { },
error: null
};
}
onUpdate() {
}
showDialog(message, title, options=["Close"], onOption = null) {
const props = { show: true, message: message, title: title, options: options, onOption: onOption };
this.setState({ ...this.state, dialog: { ...this.state.dialog, ...props } });
}
hideDialog() {
this.setState({ ...this.state, dialog: { ...this.state.dialog, show: false } });
}
onInit() {
// return;
this.setState({ ...this.state, loaded: false, error: null });
this.api.getLanguageEntries("general").then(data => {
if (data.success) { if (data.success) {
this.api.info().then(data => { setUser(data.user || null);
setLoaded(true);
} else {
setError(data.msg);
}
});
}, [api]);
const onInit = useCallback((force = false) => {
if (loaded && !force) {
return;
}
setError(false);
setLoaded(false);
api.getLanguageEntries("general").then(data => {
if (data.success) {
api.info().then(data => {
if (data.success) { if (data.success) {
this.setState({...this.state, info: data.info }) setInfo(data.info);
this.api.fetchUser().then(data => { fetchUser();
if (data.success) {
setInterval(this.onUpdate.bind(this), 60*1000);
this.setState({...this.state, loaded: true});
} else {
this.setState({ ...this.state, error: data.msg })
}
});
} else { } else {
this.setState({ ...this.state, error: data.msg }) setError(data.msg);
} }
}); });
} else { } else {
this.setState({ ...this.state, error: data.msg }) setError(data.msg);
} }
}); });
} }, [api, loaded, fetchUser]);
componentDidMount() { useEffect(() => {
this.onInit(); onInit();
} }, []);
onLogin(username, password, rememberMe, callback) { /*
this.setState({ ...this.state, error: "" }); const onTotp2FA = useCallback((code, callback) => {
return this.api.login(username, password, rememberMe).then((res) => {
if (res.success) {
this.api.fetchUser().then(() => {
this.setState({ ...this.state, user: res });
callback(res);
})
} else {
callback(res);
}
});
}
onLogout(callback) {
this.api.logout().then(() => {
this.api.loggedIn = false;
this.setState({ ...this.state, user: { } })
if (callback) callback();
});
}
onTotp2FA(code, callback) {
this.setState({ ...this.state, error: "" }); this.setState({ ...this.state, error: "" });
return this.api.verifyTotp2FA(code).then((res) => { return this.api.verifyTotp2FA(code).then((res) => {
if (res.success) { if (res.success) {
@@ -108,7 +67,7 @@ export default class AdminDashboard extends React.Component {
callback(res); callback(res);
} }
}); });
} }, [api]);
onKey2FA(credentialID, clientDataJson, authData, signature, callback) { onKey2FA(credentialID, clientDataJson, authData, signature, callback) {
this.setState({ ...this.state, error: "" }); this.setState({ ...this.state, error: "" });
@@ -124,62 +83,24 @@ export default class AdminDashboard extends React.Component {
}); });
} }
render() { */
if (!this.state.loaded) { console.log(loaded, user, api.loggedIn);
if (this.state.error) {
return <Alert severity={"error"} title={L("general.error_occurred")}> if (!loaded) {
<div>{this.state.error}</div> if (error) {
<Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}> return <Alert severity={"error"} title={L("general.error_occurred")}>
Retry <div>{error}</div>
</Button> <Button type={"button"} variant={"outlined"} onClick={() => onInit(true)}>
</Alert> Retry
} else { </Button>
return <b>{L("general.loading")} <Icon icon={"spinner"}/></b> </Alert>
} } else {
return <b>{L("general.loading")} <Icon icon={"spinner"}/></b>
} }
} else if (!user || !api.loggedIn) {
this.controlObj = { return <LoginForm api={api} info={info} onLogin={fetchUser} />
showDialog: this.showDialog.bind(this), } else {
api: this.api, return <AdminDashboard api={api} info={info} />
info: this.state.info,
onLogout: this.onLogout.bind(this),
onLogin: this.onLogin.bind(this),
onTotp2FA: this.onTotp2FA.bind(this),
onKey2FA: this.onKey2FA.bind(this),
};
if (!this.api.loggedIn) {
return <LoginForm {...this.controlObj}/>
}
return <BrowserRouter>
<Header {...this.controlObj} />
<Sidebar {...this.controlObj} />
<div className={"content-wrapper p-2"}>
<section className={"content"}>
<Routes>
{/*<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/user/edit/:userId"} render={(props) => {
let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} />
}}/>
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route>*/}
</Routes>
<Dialog {...this.state.dialog}/>
</section>
</div>
<Footer info={this.state.info} />
</BrowserRouter>
} }
} }

View File

@@ -4,8 +4,8 @@ export default function Footer(props) {
return ( return (
<footer className={"main-footer"}> <footer className={"main-footer"}>
Theme: <strong>Copyright © 2014-2019 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>&nbsp; Theme: <strong>Copyright © 2014-2021 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.2.0</strong>&nbsp;
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version} Framework: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version}
</footer> </footer>
) )
} }

View File

@@ -29,7 +29,12 @@ export default function LanguageSelection(props) {
let flags = []; let flags = [];
if (languages === null) { if (languages === null) {
api.getLanguages().then((res) => { api.getLanguages().then((res) => {
setLanguages(res.languages); if (res.success) {
setLanguages(res.languages);
} else {
setLanguages({});
alert(res.msg);
}
}); });
} else { } else {
for (const language of Object.values(languages)) { for (const language of Object.values(languages)) {

View File

@@ -1,55 +1,51 @@
import React from 'react'; import React, {useCallback, useContext} from 'react';
import {Link, NavLink} from "react-router-dom"; import {Link, NavLink} from "react-router-dom";
import Icon from "shared/elements/icon"; import Icon from "shared/elements/icon";
import {LocaleContext} from "shared/locale";
export default function Sidebar(props) { export default function Sidebar(props) {
let parent = { const api = props.api;
showDialog: props.showDialog || function() {}, const showDialog = props.showDialog;
api: props.api, const {translate: L} = useContext(LocaleContext);
};
function onLogout() { const onLogout = useCallback(() => {
parent.api.logout().then(obj => { api.logout().then(obj => {
if (obj.success) { if (obj.success) {
document.location = "/admin"; document.location = "/admin";
} else { } else {
parent.showDialog("Error logging out: " + obj.msg, "Error logging out"); showDialog("Error logging out: " + obj.msg, "Error logging out");
} }
}); });
} }, [api, showDialog]);
const menuItems = { const menuItems = {
"dashboard": { "dashboard": {
"name": "Dashboard", "name": "admin.dashboard",
"icon": "tachometer-alt" "icon": "tachometer-alt"
}, },
"visitors": { "visitors": {
"name": "Visitor Statistics", "name": "admin.visitor_statistics",
"icon": "chart-bar", "icon": "chart-bar",
}, },
"users": { "users": {
"name": "Users & Groups", "name": "admin.user_groups",
"icon": "users" "icon": "users"
}, },
"pages": { "pages": {
"name": "Pages & Routes", "name": "admin.page_routes",
"icon": "copy", "icon": "copy",
}, },
"settings": { "settings": {
"name": "Settings", "name": "admin.settings",
"icon": "tools" "icon": "tools"
}, },
"logs": { "logs": {
"name": "Logs & Notifications", "name": "admin.logs",
"icon": "file-medical-alt" "icon": "file-medical-alt"
}, },
"contact": {
"name": "Contact Requests",
"icon": "comments"
},
"help": { "help": {
"name": "Help", "name": "admin.help",
"icon": "question-circle" "icon": "question-circle"
}, },
}; };
@@ -61,8 +57,8 @@ export default function Sidebar(props) {
li.push( li.push(
<li key={id} className={"nav-item"}> <li key={id} className={"nav-item"}>
<NavLink to={"/admin/" + id} className={"nav-link"} activeClassName={"active"}> <NavLink to={"/admin/" + id} className={"nav-link"}>
<Icon icon={obj.icon} className={"nav-icon"} /><p>{obj.name}{badge}</p> <Icon icon={obj.icon} className={"nav-icon"} /><p>{L(obj.name)}{badge}</p>
</NavLink> </NavLink>
</li> </li>
); );
@@ -71,7 +67,7 @@ export default function Sidebar(props) {
li.push(<li className={"nav-item"} key={"logout"}> li.push(<li className={"nav-item"} key={"logout"}>
<a href={"#"} onClick={() => onLogout()} className={"nav-link"}> <a href={"#"} onClick={() => onLogout()} className={"nav-link"}>
<Icon icon={"arrow-left"} className={"nav-icon"} /> <Icon icon={"arrow-left"} className={"nav-icon"} />
<p>Logout</p> <p>{L("general.logout")}</p>
</a> </a>
</li>); </li>);
@@ -96,7 +92,7 @@ export default function Sidebar(props) {
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}> <div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
<div className="user-panel mt-3 pb-3 mb-3 d-flex"> <div className="user-panel mt-3 pb-3 mb-3 d-flex">
<div className="info"> <div className="info">
<a href="#" className="d-block">Logged in as: {parent.api.user.name}</a> <a href="#" className="d-block">Logged in as: {api.user.name}</a>
</div> </div>
</div> </div>
<nav className={"mt-2"}> <nav className={"mt-2"}>

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import AdminDashboard from "./App"; import App from "./App";
import {LocaleProvider} from "shared/locale"; import {LocaleProvider} from "shared/locale";
const root = createRoot(document.getElementById('admin-panel')); const root = createRoot(document.getElementById('admin-panel'));
root.render(<LocaleProvider> root.render(<LocaleProvider>
<AdminDashboard /> <App />
</LocaleProvider>); </LocaleProvider>);

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
.page-link { color: #222629; }
.page-link:hover { color: black; }
.ReactCollapse--collapse {
transition: height 500ms;
}

View File

@@ -12,7 +12,6 @@ import {
import {makeStyles} from '@material-ui/core/styles'; import {makeStyles} from '@material-ui/core/styles';
import {Alert} from '@material-ui/lab'; import {Alert} from '@material-ui/lab';
import React, {useCallback, useContext, useEffect, useState} from "react"; import React, {useCallback, useContext, useEffect, useState} from "react";
import {Navigate} from "react-router-dom";
import ReplayIcon from '@material-ui/icons/Replay'; import ReplayIcon from '@material-ui/icons/Replay';
import LanguageSelection from "../elements/language-selection"; import LanguageSelection from "../elements/language-selection";
import {decodeText, encodeText, getParameter, removeParameter} from "shared/util"; import {decodeText, encodeText, getParameter, removeParameter} from "shared/util";
@@ -85,10 +84,6 @@ export default function LoginForm(props) {
const {translate: L, currentLocale, requestModules} = useContext(LocaleContext); const {translate: L, currentLocale, requestModules} = useContext(LocaleContext);
const getNextUrl = () => {
return getParameter("next") || "/admin";
}
const onUpdateLocale = useCallback(() => { const onUpdateLocale = useCallback(() => {
requestModules(api, ["general", "account"], currentLocale).then(data => { requestModules(api, ["general", "account"], currentLocale).then(data => {
setLoaded(true); setLoaded(true);
@@ -107,17 +102,19 @@ export default function LoginForm(props) {
setError(""); setError("");
setLoggingIn(true); setLoggingIn(true);
removeParameter("success"); removeParameter("success");
props.onLogin(username, password, rememberMe, (res) => { api.login(username, password, rememberMe).then((res) => {
set2FAState(0); set2FAState(0);
setLoggingIn(false); setLoggingIn(false);
setPassword(""); setPassword("");
if (!res.success) { if (!res.success) {
setEmailConfirmed(res.emailConfirmed); setEmailConfirmed(res.emailConfirmed);
setError(res.msg); setError(res.msg);
} else {
props.onLogin();
} }
}); });
} }
}, [isLoggingIn, password, props, rememberMe, username]); }, [api, isLoggingIn, password, props, rememberMe, username]);
const onSubmit2FA = useCallback(() => { const onSubmit2FA = useCallback(() => {
setLoggingIn(true); setLoggingIn(true);
@@ -168,14 +165,7 @@ export default function LoginForm(props) {
set2FAError(e.toString()); set2FAError(e.toString());
}); });
} }
}, [api.loggedIn, api.user, tfaState, props]) }, [api.loggedIn, api.user, tfaState, props]);
if (api.loggedIn) {
if (!api.user["2fa"] || !api.user["2fa"].confirmed || api.user["2fa"].authenticated) {
// Redirect by default takes only path names
return <Navigate to={getNextUrl()}/>
}
}
const createForm = () => { const createForm = () => {

View File

@@ -0,0 +1,119 @@
import * as React from "react";
import {Link} from "react-router-dom";
import {format, getDaysInMonth} from "date-fns";
export default function Overview(props) {
const today = new Date();
const numDays = getDaysInMonth(today);
let colors = [ '#ff4444', '#ffbb33', '#00C851', '#33b5e5' ];
while (colors.length < numDays) {
colors = colors.concat(colors);
}
let data = new Array(numDays).fill(0);
let visitorCount = 0;
/*
for (let date in this.state.visitors) {
if (this.state.visitors.hasOwnProperty(date)) {
let day = parseInt(date.split("/")[2]) - 1;
if (day >= 0 && day < numDays) {
let count = parseInt(this.state.visitors[date]);
data[day] = count;
visitorCount += count;
}
}
}
*/
let labels = Array.from(Array(numDays), (_, i) => i + 1);
let chartOptions = {};
let chartData = {
labels: labels,
datasets: [{
label: 'Unique Visitors ' + format(today, "MMMM"),
borderWidth: 1,
data: data,
backgroundColor: colors,
}]
};
/*
let loadAvg = this.state.server.load_avg;
if (Array.isArray(this.state.server.load_avg)) {
loadAvg = this.state.server.load_avg.join(" ");
}
*/
return <>
<div className={"content-header"}>
<div className={"container-fluid"}>
<div className={"row mb-2"}>
<div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>Dashboard</h1>
</div>
<div className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active">Dashboard</li>
</ol>
</div>
</div>
</div>
</div>
<section className={"content"}>
</section>
</>
}
/*
export default class Overview extends React.Component {
constructor(props) {
super(props);
this.state = {
chartVisible : true,
statusVisible : true,
userCount: 0,
notificationCount: 0,
visitorsTotal: 0,
visitors: { },
server: { load_avg: ["Unknown"] },
errors: []
}
}
removeError(i) {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
}
}
componentDidMount() {
this.parent.api.getStats().then((res) => {
if(!res.success) {
let errors = this.state.errors.slice();
errors.push({ message: res.msg, title: "Error fetching Stats" });
this.setState({ ...this.state, errors: errors });
} else {
this.setState({
...this.state,
userCount: res.userCount,
pageCount: res.pageCount,
visitors: res.visitors,
visitorsTotal: res.visitorsTotal,
server: res.server
});
}
});
}
render() {
}
}*/

View File

View File

@@ -36,9 +36,13 @@
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"chart.js": "^4.0.1",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"mini-css-extract-plugin": "^2.7.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.0.1",
"react-collapse": "^5.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.4.3" "react-router-dom": "^6.4.3"
}, },

View File

@@ -53,7 +53,12 @@ export default class API {
} }
async logout() { async logout() {
return this.apiCall("user/logout"); const res = await this.apiCall("user/logout");
if (res.success) {
this.loggedIn = false;
}
return res;
} }
async getUser(id) { async getUser(id) {

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {createContext, useCallback, useState} from "react"; import {createContext, useCallback, useState} from "react";
const LocaleContext = React.createContext(null); const LocaleContext = createContext(null);
function LocaleProvider(props) { function LocaleProvider(props) {

View File

@@ -3649,6 +3649,11 @@ char-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e"
integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==
chart.js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.0.1.tgz#93d5d50ac222a5b3b6ac7488e82e1553ac031592"
integrity sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA==
check-types@^11.1.1: check-types@^11.1.1:
version "11.2.2" version "11.2.2"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4" resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4"
@@ -6885,7 +6890,7 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-css-extract-plugin@^2.4.5: mini-css-extract-plugin@^2.4.5, mini-css-extract-plugin@^2.7.1:
version "2.7.1" version "2.7.1"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz#ec924df783cff32ce6183fceb653028f70128643" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz#ec924df783cff32ce6183fceb653028f70128643"
integrity sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw== integrity sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw==
@@ -8165,6 +8170,16 @@ react-app-rewired@^2.2.1:
dependencies: dependencies:
semver "^5.6.0" semver "^5.6.0"
react-chartjs-2@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz#7ef7dd57441903e8d34e1d06a1aead1095d0bca8"
integrity sha512-u38C9OxynlNCBp+79grgXRs7DSJ9w8FuQ5/HO5FbYBbri8HSZW+9SWgjVshLkbXBfXnMGWakbHEtvN0nL2UG7Q==
react-collapse@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-collapse/-/react-collapse-5.1.1.tgz#a2fa08ef13f372141b02e6a7d49ef72427bcbc2b"
integrity sha512-k6cd7csF1o9LBhQ4AGBIdxB60SUEUMQDAnL2z1YvYNr9KoKr+nDkhN6FK7uGaBd/rYrYfrMpzpmJEIeHRYogBw==
react-dev-utils@^12.0.1: react-dev-utils@^12.0.1:
version "12.0.1" version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"