localization, context, react stuff

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

@ -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) {

@ -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/");
} }
} }

@ -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",
];

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

@ -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",
];

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

@ -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 %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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')),

@ -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>

@ -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>
}

@ -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>
} }
} }

@ -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>
) )
} }

@ -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)) {

@ -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"}>

@ -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>);

File diff suppressed because one or more lines are too long

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

@ -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 = () => {

@ -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() {
}
}*/

@ -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"
}, },

@ -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) {

@ -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) {

@ -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"