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() {
$settings = $this->getSettings();
$templateName = $this->getTemplateName();
$language = $this->getContext()->getLanguage();
$this->parameters["view"] = ["success" => true];
switch ($templateName) {

@ -14,5 +14,6 @@ class Admin extends TemplateDocument {
$this->searchable = false;
parent::__construct($router, $template, $params);
$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",
"language" => "Sprache",
"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",
"language" => "Language",
"loading" => "Loading",
"logout" => "Logout",
];

@ -7,7 +7,7 @@
{% block body %}
<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>
<link rel="stylesheet" href="/js/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link>
{% 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,
removeModuleScopePlugin,
babelInclude,
addWebpackModuleRule,
} = require('customize-cra');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const addMiniCssExtractPlugin = config => {
config.plugins.push(new MiniCssExtractPlugin());
return config;
}
module.exports = override(
removeModuleScopePlugin(),
addMiniCssExtractPlugin,
addWebpackModuleRule({
test: /\.css$/,
use: [ MiniCssExtractPlugin.loader, 'css-loader' ]
}),
babelInclude([
path.resolve(path.join(__dirname, 'src')),
fs.realpathSync(path.join(__dirname, '../shared')),

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

@ -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 './res/adminlte.min.css';
import './res/index.css';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import API from "shared/api";
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 {Alert} from "@material-ui/lab";
import {Button} from "@material-ui/core";
import { LocaleContext } from "shared/locale";
import AdminDashboard from "./AdminDashboard";
const L = (key) => {
return "<nope>";
}
export default function App() {
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;
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 => {
const fetchUser = useCallback(() => {
api.fetchUser().then(data => {
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) {
this.setState({...this.state, info: data.info })
this.api.fetchUser().then(data => {
api.info().then(data => {
if (data.success) {
setInterval(this.onUpdate.bind(this), 60*1000);
this.setState({...this.state, loaded: true});
setInfo(data.info);
fetchUser();
} else {
this.setState({ ...this.state, error: data.msg })
setError(data.msg);
}
});
} else {
this.setState({ ...this.state, error: data.msg })
setError(data.msg);
}
});
} else {
this.setState({ ...this.state, error: data.msg })
}
});
}
}, [api, loaded, fetchUser]);
componentDidMount() {
this.onInit();
}
useEffect(() => {
onInit();
}, []);
onLogin(username, password, rememberMe, callback) {
this.setState({ ...this.state, error: "" });
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) {
/*
const onTotp2FA = useCallback((code, callback) => {
this.setState({ ...this.state, error: "" });
return this.api.verifyTotp2FA(code).then((res) => {
if (res.success) {
@ -108,7 +67,7 @@ export default class AdminDashboard extends React.Component {
callback(res);
}
});
}
}, [api]);
onKey2FA(credentialID, clientDataJson, authData, signature, callback) {
this.setState({ ...this.state, error: "" });
@ -124,62 +83,24 @@ export default class AdminDashboard extends React.Component {
});
}
render() {
*/
if (!this.state.loaded) {
if (this.state.error) {
console.log(loaded, user, api.loggedIn);
if (!loaded) {
if (error) {
return <Alert severity={"error"} title={L("general.error_occurred")}>
<div>{this.state.error}</div>
<Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}>
<div>{error}</div>
<Button type={"button"} variant={"outlined"} onClick={() => onInit(true)}>
Retry
</Button>
</Alert>
} else {
return <b>{L("general.loading")} <Icon icon={"spinner"}/></b>
}
}
this.controlObj = {
showDialog: this.showDialog.bind(this),
api: this.api,
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>
} else if (!user || !api.loggedIn) {
return <LoginForm api={api} info={info} onLogin={fetchUser} />
} else {
return <AdminDashboard api={api} info={info} />
}
}

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

@ -29,7 +29,12 @@ export default function LanguageSelection(props) {
let flags = [];
if (languages === null) {
api.getLanguages().then((res) => {
if (res.success) {
setLanguages(res.languages);
} else {
setLanguages({});
alert(res.msg);
}
});
} else {
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 Icon from "shared/elements/icon";
import {LocaleContext} from "shared/locale";
export default function Sidebar(props) {
let parent = {
showDialog: props.showDialog || function() {},
api: props.api,
};
const api = props.api;
const showDialog = props.showDialog;
const {translate: L} = useContext(LocaleContext);
function onLogout() {
parent.api.logout().then(obj => {
const onLogout = useCallback(() => {
api.logout().then(obj => {
if (obj.success) {
document.location = "/admin";
} else {
parent.showDialog("Error logging out: " + obj.msg, "Error logging out");
showDialog("Error logging out: " + obj.msg, "Error logging out");
}
});
}
}, [api, showDialog]);
const menuItems = {
"dashboard": {
"name": "Dashboard",
"name": "admin.dashboard",
"icon": "tachometer-alt"
},
"visitors": {
"name": "Visitor Statistics",
"name": "admin.visitor_statistics",
"icon": "chart-bar",
},
"users": {
"name": "Users & Groups",
"name": "admin.user_groups",
"icon": "users"
},
"pages": {
"name": "Pages & Routes",
"name": "admin.page_routes",
"icon": "copy",
},
"settings": {
"name": "Settings",
"name": "admin.settings",
"icon": "tools"
},
"logs": {
"name": "Logs & Notifications",
"name": "admin.logs",
"icon": "file-medical-alt"
},
"contact": {
"name": "Contact Requests",
"icon": "comments"
},
"help": {
"name": "Help",
"name": "admin.help",
"icon": "question-circle"
},
};
@ -61,8 +57,8 @@ export default function Sidebar(props) {
li.push(
<li key={id} className={"nav-item"}>
<NavLink to={"/admin/" + id} className={"nav-link"} activeClassName={"active"}>
<Icon icon={obj.icon} className={"nav-icon"} /><p>{obj.name}{badge}</p>
<NavLink to={"/admin/" + id} className={"nav-link"}>
<Icon icon={obj.icon} className={"nav-icon"} /><p>{L(obj.name)}{badge}</p>
</NavLink>
</li>
);
@ -71,7 +67,7 @@ export default function Sidebar(props) {
li.push(<li className={"nav-item"} key={"logout"}>
<a href={"#"} onClick={() => onLogout()} className={"nav-link"}>
<Icon icon={"arrow-left"} className={"nav-icon"} />
<p>Logout</p>
<p>{L("general.logout")}</p>
</a>
</li>);
@ -96,7 +92,7 @@ export default function Sidebar(props) {
<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="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>
<nav className={"mt-2"}>

@ -1,9 +1,9 @@
import React from "react";
import {createRoot} from "react-dom/client";
import AdminDashboard from "./App";
import App from "./App";
import {LocaleProvider} from "shared/locale";
const root = createRoot(document.getElementById('admin-panel'));
root.render(<LocaleProvider>
<AdminDashboard />
<App />
</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 {Alert} from '@material-ui/lab';
import React, {useCallback, useContext, useEffect, useState} from "react";
import {Navigate} from "react-router-dom";
import ReplayIcon from '@material-ui/icons/Replay';
import LanguageSelection from "../elements/language-selection";
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 getNextUrl = () => {
return getParameter("next") || "/admin";
}
const onUpdateLocale = useCallback(() => {
requestModules(api, ["general", "account"], currentLocale).then(data => {
setLoaded(true);
@ -107,17 +102,19 @@ export default function LoginForm(props) {
setError("");
setLoggingIn(true);
removeParameter("success");
props.onLogin(username, password, rememberMe, (res) => {
api.login(username, password, rememberMe).then((res) => {
set2FAState(0);
setLoggingIn(false);
setPassword("");
if (!res.success) {
setEmailConfirmed(res.emailConfirmed);
setError(res.msg);
} else {
props.onLogin();
}
});
}
}, [isLoggingIn, password, props, rememberMe, username]);
}, [api, isLoggingIn, password, props, rememberMe, username]);
const onSubmit2FA = useCallback(() => {
setLoggingIn(true);
@ -168,14 +165,7 @@ export default function LoginForm(props) {
set2FAError(e.toString());
});
}
}, [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()}/>
}
}
}, [api.loggedIn, api.user, tfaState, props]);
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/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"chart.js": "^4.0.1",
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"mini-css-extract-plugin": "^2.7.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.0.1",
"react-collapse": "^5.1.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.3"
},

@ -53,7 +53,12 @@ export default class API {
}
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) {

@ -1,7 +1,7 @@
import React from 'react';
import {createContext, useCallback, useState} from "react";
const LocaleContext = React.createContext(null);
const LocaleContext = createContext(null);
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"
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:
version "11.2.2"
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"
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"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz#ec924df783cff32ce6183fceb653028f70128643"
integrity sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw==
@ -8165,6 +8170,16 @@ react-app-rewired@^2.2.1:
dependencies:
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:
version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"