removed old admin panel

This commit is contained in:
Roman 2024-05-04 11:18:44 +02:00
parent e7283310f0
commit ae5210ec57
34 changed files with 0 additions and 43823 deletions

@ -1,3 +0,0 @@
{
"presets": ["react"],
}

@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -1 +0,0 @@
DENY FROM ALL

@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/adminPanel.iml" filepath="$PROJECT_DIR$/.idea/adminPanel.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

File diff suppressed because it is too large Load Diff

@ -1,49 +0,0 @@
{
"name": "admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"chart.js": "^2.9.3",
"draft-js": "^0.11.6",
"draftjs-to-html": "^0.9.1",
"html-to-draftjs": "latest",
"moment": "^2.26.0",
"rc-color-picker": "^1.2.6",
"react": "^16.13.1",
"react-chartjs-2": "^2.9.0",
"react-collapse": "^5.0.1",
"react-datepicker": "^3.0.0",
"react-dom": "^16.13.1",
"react-draft-wysiwyg": "^1.14.5",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"react-select": "^3.1.0",
"react-tooltip": "^4.2.7",
"sanitize-html": "^1.27.0"
},
"scripts": {
"build": "webpack --mode production && mv dist/main.js ../js/admin.min.js",
"debug": "react-scripts start"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"@babel/preset-react": "^7.10.1",
"babel-loader": "^8.1.0",
"babel-polyfill": "^6.26.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
}
}

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

@ -1,25 +0,0 @@
import Icon from "./icon";
import React from "react";
export default function Alert(props) {
const onClose = props.onClose || null;
const title = props.title || "Untitled Alert";
const message = props.message || "Alert message";
const type = props.type || "danger";
let icon = "ban";
if (type === "warning") {
icon = "exclamation-triangle";
} else if(type === "success") {
icon = "check";
}
return (
<div className={"alert alert-" + type + " alert-dismissible"}>
{onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null}
<h5><Icon icon={icon} className={"icon"} /> {title}</h5>
{message}
</div>
)
}

@ -1,45 +0,0 @@
import React from "react";
export default function Dialog(props) {
const show = props.show;
const classes = "modal fade" + (show ? " show" : "");
const style = { paddingRight: "12px", display: (show ? "block" : "none") };
const onClose = props.onClose || function() { };
const onOption = props.onOption || function() { };
const options = props.options || ["Close"];
let buttons = [];
for (let name of options) {
let type = "default";
if (name === "Yes") type = "warning";
else if(name === "No") type = "danger";
buttons.push(
<button type="button" key={"button-" + name} className={"btn btn-" + type} data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}>
{name}
</button>
)
}
return (
<div className={classes} id="modal-default" style={style} aria-modal="true" onClick={() => onClose()}>
<div className="modal-dialog" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">{props.title}</h4>
<button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={() => onClose()}>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<p>{props.message}</p>
</div>
<div className="modal-footer">
{ buttons }
</div>
</div>
</div>
</div>
);
}

@ -1,11 +0,0 @@
import React from "react";
export default function Footer() {
return (
<footer className={"main-footer"}>
Theme: <strong>Copyright © 2014-2019 <a href={"http://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> 1.2.6
</footer>
)
}

@ -1,101 +0,0 @@
import * as React from "react";
import Icon from "./icon";
import {useState} from "react";
import {getPeriodString} from "../global";
import {Link} from "react-router-dom";
export default function Header(props) {
const parent = {
notifications: props.notifications || [ ],
};
const [dropdownVisible, showDropdown] = useState(false);
const mailIcon = <Icon icon={"envelope"} type={"fas"} />;
let notificationCount = parent.notifications.length;
let notificationText = "No new notifications";
if(notificationCount === 1) {
notificationText = "1 new notification";
} else if(notificationCount > 1) {
notificationText = notificationCount + " new notification";
}
let notificationItems = [];
for (let i = 0; i < parent.notifications.length; i++) {
const notification = parent.notifications[i];
const id = notification.id;
const createdAt = getPeriodString(notification["created_at"]);
notificationItems.push(
<Link to={"/admin/logs?notification=" + id} className={"dropdown-item"} key={"notification-" + id}>
{mailIcon}
<span className={"ml-2"}>{notification.title}</span>
<span className={"float-right text-muted text-sm"}>{createdAt}</span>
</Link>);
}
function onToggleSidebar() {
let classes = document.body.classList;
if (classes.contains("sidebar-collapse")) {
classes.remove("sidebar-collapse");
classes.add("sidebar-open");
} else {
classes.add("sidebar-collapse");
classes.remove("sidebar-add");
}
}
return (
<nav className={"main-header navbar navbar-expand navbar-white navbar-light"}>
{/*Left navbar links */}
<ul className={"navbar-nav"}>
<li className={"nav-item"}>
<a href={"#"} className={"nav-link"} role={"button"} onClick={onToggleSidebar}>
<Icon icon={"bars"}/>
</a>
</li>
<li className={"nav-item d-none d-sm-inline-block"}>
<Link to={"/admin/dashboard"} className={"nav-link"}>
Home
</Link>
</li>
</ul>
{/* SEARCH FORM */}
<form className={"form-inline ml-3"}>
<div className={"input-group input-group-sm"}>
<input className={"form-control form-control-navbar"} type={"search"} placeholder={"Search"} aria-label={"Search"} />
<div className={"input-group-append"}>
<button className={"btn btn-navbar"} type={"submit"}>
<Icon icon={"search"}/>
</button>
</div>
</div>
</form>
{/* Right navbar links */}
<ul className={"navbar-nav ml-auto"}>
{/* Notifications Dropdown Menu */}
<li className={"nav-item dropdown"} onClick={() => showDropdown(!dropdownVisible)}>
<a href={"#"} className={"nav-link"} data-toggle={"dropdown"}>
<Icon icon={"bell"} type={"far"} />
<span className={"badge badge-warning navbar-badge"} style={{display: (notificationCount > 0 ? "block" : "none")}}>
{notificationCount}
</span>
</a>
<div className={"dropdown-menu dropdown-menu-lg dropdown-menu-right " + (dropdownVisible ? " show" : "")}>
<span className={"dropdown-item dropdown-header"}>
{notificationText}
</span>
{notificationItems}
<div className={"dropdown-divider"} />
<Link to={"/admin/logs"} className={"dropdown-item dropdown-footer"}>See All Notifications</Link>
</div>
</li>
</ul>
</nav>
)
}

@ -1,24 +0,0 @@
import * as React from "react";
export default function Icon(props) {
let classes = props.className || [];
classes = Array.isArray(classes) ? classes : classes.toString().split(" ");
let type = props.type || "fas";
let icon = props.icon;
classes.push(type);
classes.push("fa-" + icon);
if (icon === "spinner" || icon === "circle-notch") {
classes.push("fa-spin");
}
let newProps = {...props, className: classes.join(" ") };
delete newProps["type"];
delete newProps["icon"];
return (
<i {...newProps} />
);
}

@ -1,130 +0,0 @@
import React from 'react';
import Icon from "./icon";
import {Link, NavLink} from "react-router-dom";
export default function Sidebar(props) {
let parent = {
showDialog: props.showDialog || function() {},
api: props.api,
notifications: props.notifications || [ ],
contactRequests: props.contactRequests || [ ]
};
function onLogout() {
parent.api.logout().then(obj => {
if (obj.success) {
document.location = "/admin";
} else {
parent.showDialog("Error logging out: " + obj.msg, "Error logging out");
}
});
}
const menuItems = {
"dashboard": {
"name": "Dashboard",
"icon": "tachometer-alt"
},
"visitors": {
"name": "Visitor Statistics",
"icon": "chart-bar",
},
"users": {
"name": "Users & Groups",
"icon": "users"
},
"pages": {
"name": "Pages & Routes",
"icon": "copy",
},
"settings": {
"name": "Settings",
"icon": "tools"
},
"logs": {
"name": "Logs & Notifications",
"icon": "file-medical-alt"
},
"contact": {
"name": "Contact Requests",
"icon": "comments"
},
"help": {
"name": "Help",
"icon": "question-circle"
},
};
let numNotifications = parent.notifications.length;
if (numNotifications > 0) {
if (numNotifications > 9) numNotifications = "9+";
menuItems["logs"]["badge"] = { type: "warning", value: numNotifications };
}
console.log("sidebar", parent.contactRequests);
let numUnreadContactMessages = 0;
for (const contactRequest of parent.contactRequests) {
numUnreadContactMessages += contactRequest.unread;
}
if (numUnreadContactMessages > 0) {
menuItems["contact"]["badge"] = { type: "info", value: numUnreadContactMessages };
}
let li = [];
for (let id in menuItems) {
let obj = menuItems[id];
const badge = (obj.badge ? <span className={"right badge badge-" + obj.badge.type}>{obj.badge.value}</span> : <></>);
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>
</li>
);
}
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>
</a>
</li>);
return (
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
<span className={"brand-text font-weight-light ml-2"}>WebBase</span>
</Link>
<div className={"sidebar os-host os-theme-light os-host-overflow os-host-overflow-y os-host-resize-disabled os-host-scrollbar-horizontal-hidden os-host-transition"}>
{/* IDK what this is */}
<div className={"os-resize-observer-host"}>
<div className={"os-resize-observer observed"} style={{left: "0px", right: "auto"}}/>
</div>
<div className={"os-size-auto-observer"} style={{height: "calc(100% + 1px)", float: "left"}}>
<div className={"os-resize-observer observed"}/>
</div>
<div className={"os-content-glue"} style={{margin: "0px -8px"}}/>
<div className={"os-padding"}>
<div className={"os-viewport os-viewport-native-scrollbars-invisible"} style={{right: "0px", bottom: "0px"}}>
<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>
</div>
</div>
<nav className={"mt-2"}>
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
{li}
</ul>
</nav>
</div>
</div>
</div>
</div>
</aside>
)
}

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

File diff suppressed because one or more lines are too long

@ -1,132 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './include/adminlte.min.css';
import './include/index.css';
import API from './api.js';
import Header from './elements/header.js';
import Sidebar from './elements/sidebar.js';
import UserOverview from './views/users.js';
import Overview from './views/overview.js'
import CreateUser from "./views/adduser";
import Icon from "./elements/icon";
import Dialog from "./elements/dialog";
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import View404 from "./views/404";
import Logs from "./views/logs";
import PageOverview from "./views/pages";
import HelpPage from "./views/help";
import Footer from "./elements/footer";
import EditUser from "./views/edituser";
import CreateGroup from "./views/addgroup";
import Settings from "./views/settings";
import PermissionSettings from "./views/permissions";
import Visitors from "./views/visitors";
import ContactRequestOverview from "./views/contact";
class AdminDashboard extends React.Component {
constructor(props) {
super(props);
this.api = new API();
this.state = {
loaded: false,
dialog: { onClose: () => this.hideDialog() },
notifications: [ ],
contactRequests: [ ]
};
}
onUpdate() {
this.fetchNotifications();
this.fetchContactRequests();
}
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 } });
}
fetchNotifications() {
this.api.getNotifications().then((res) => {
if (!res.success) {
this.showDialog("Error fetching notifications: " + res.msg, "Error fetching notifications");
} else {
this.setState({...this.state, notifications: res.notifications });
}
});
}
fetchContactRequests() {
this.api.fetchContactRequests().then((res) => {
if (!res.success) {
this.showDialog("Error fetching contact requests: " + res.msg, "Error fetching contact requests");
} else {
this.setState({...this.state, contactRequests: res.contactRequests });
}
});
}
componentDidMount() {
this.api.fetchUser().then(Success => {
if (!Success) {
document.location = "/admin";
} else {
this.fetchNotifications();
this.fetchContactRequests();
setInterval(this.onUpdate.bind(this), 60*1000);
this.setState({...this.state, loaded: true});
}
});
}
render() {
if (!this.state.loaded) {
return <b>Loading <Icon icon={"spinner"} /></b>
}
this.controlObj = {
showDialog: this.showDialog.bind(this),
fetchNotifications: this.fetchNotifications.bind(this),
api: this.api
};
return <Router>
<Header {...this.controlObj} notifications={this.state.notifications} />
<Sidebar {...this.controlObj} notifications={this.state.notifications} contactRequests={this.state.contactRequests}/>
<div className={"content-wrapper p-2"}>
<section className={"content"}>
<Switch>
<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>
</Switch>
<Dialog {...this.state.dialog}/>
</section>
</div>
<Footer />
</Router>
}
}
ReactDOM.render(
<AdminDashboard />,
document.getElementById('root')
);

@ -1,37 +0,0 @@
import * as React from "react";
import {Link, useLocation, useHistory} from "react-router-dom";
import Icon from "../elements/icon";
export default function View404() {
const location = useLocation();
const history = useHistory();
if (location.pathname === "/admin" || location.pathname === "/admin/") {
history.push("/admin/dashboard");
}
return (
<div className={"error-page"}>
<h2 className={"headline text-warning"}>404</h2>
<div className={"error-content"}>
<h3>
<Icon icon={"exclamation-triangle"} className={"text-warning"}/> Oops! Page not found.
</h3>
<p>
We could not find the page you were looking for.
Meanwhile, you may <Link to={"/admin/dashboard"}>return to dashboard</Link> or try using the search form.
</p>
<form className={"search-form"} onSubmit={(e) => e.preventDefault()}>
<div className={"input-group"}>
<input type={"text"} name={"search"} className={"form-control"} placeholder={"Search"} />
<div className={"input-group-append"}>
<button type="submit" name="submit" className={"btn btn-warning"}>
<Icon icon={"search"}/>
</button>
</div>
</div>
</form>
</div>
</div>
)
}

@ -1,133 +0,0 @@
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
import * as React from "react";
import Icon from "../elements/icon";
import ReactTooltip from "react-tooltip";
import 'rc-color-picker/assets/index.css';
import ColorPicker from 'rc-color-picker';
export default class CreateGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
alerts: [],
isSubmitting: false,
name: "",
color: "#0F0"
};
this.parent = {
api: props.api,
};
}
removeAlert(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
render() {
let alerts = [];
for (let i = 0; i < this.state.alerts.length; i++) {
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
}
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">Create a new group</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"><Link to={"/admin/users"}>Users</Link></li>
<li className="breadcrumb-item active">Add User</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"row"}>
<div className={"col-lg-6 pl-5 pr-5"}>
{alerts}
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
<div className={"form-group"}>
<label htmlFor={"name"}>Group Name</label>
<input type={"text"} className={"form-control"} placeholder={"Name"}
name={"name"} id={"name"} maxLength={32} value={this.state.name}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"color"}>Color</label>
<div className="input-group-prepend">
<span className="input-group-text" style={{ padding: "0.35rem 0.4rem 0 0.4rem" }}>
<ColorPicker
color={this.state.color}
alpha={100}
name={"color"}
onChange={this.onChangeColor.bind(this)}
placement="topLeft">
<span className="rc-color-picker-trigger"/>
</ColorPicker>
</span>
<input type={"text"} className={"form-control float-right"} readOnly={true}
value={this.state.color}/>
</div>
</div>
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
{this.state.isSubmitting
?
<button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading&nbsp;<Icon
icon={"circle-notch"}/></button>
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
}
</form>
</div>
</div>
</div>
<ReactTooltip/>
</>;
}
onChangeColor(e) {
this.setState({...this.state, color: e.color});
}
onChangeInput(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({...this.state, [name]: value});
}
submitForm(e) {
e.preventDefault();
const name = this.state.name;
const color = this.state.color;
this.setState({...this.state, isSubmitting: true});
this.parent.api.createGroup(name, color).then((res) => {
let alerts = this.state.alerts.slice();
if (res.success) {
alerts.push({message: "Group was successfully created", title: "Success!", type: "success"});
this.setState({...this.state, name: "", color: "", alerts: alerts, isSubmitting: false});
} else {
alerts.push({message: res.msg, title: "Error creating Group", type: "danger"});
this.setState({...this.state, alerts: alerts, isSubmitting: false});
}
});
}
}

@ -1,200 +0,0 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Alert from "../elements/alert";
import Icon from "../elements/icon";
import ReactTooltip from 'react-tooltip'
import {Collapse} from "react-collapse/lib/Collapse";
export default class CreateUser extends React.Component {
constructor(props) {
super(props);
this.parent = {
showDialog: props.showDialog || function () { },
api: props.api,
};
this.state = {
errors: [],
sendInvite: true,
username: "",
email: "",
password: "",
confirmPassword: "",
isSubmitting: false
}
}
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});
}
}
render() {
let errors = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
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">Create a new user</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"><Link to={"/admin/users"}>Users</Link></li>
<li className="breadcrumb-item active">Add User</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"row"}>
<div className={"col-lg-6 pl-5 pr-5"}>
{errors}
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
<div className={"form-group"}>
<label htmlFor={"username"}>Username</label>
<input type={"text"} className={"form-control"} placeholder={"Enter username"}
name={"username"} id={"username"} maxLength={32} value={this.state.username}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"email"}>E-Mail</label>
<input type={"email"} className={"form-control"} placeholder={"E-Mail address"}
id={"email"} name={"email"} maxLength={64} value={this.state.email}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
onChange={() => this.onCheckboxChange()}
id={"sendInvite"} name={"sendInvite"} defaultChecked={this.state.sendInvite}/>
<label className={"form-check-label"} htmlFor={"sendInvite"}>
Send Invitation
<Icon icon={"question-circle"} className={"ml-2"} style={{"color": "#0069d9"}}
data-tip={"The user will receive an invitation token via email and can choose the password on his own."}
data-type={"info"} data-place={"right"} data-effect={"solid"}/>
</label>
</div>
<Collapse isOpened={!this.state.sendInvite}>
<div className={"mt-2"}>
<div className={"form-group"}>
<label htmlFor={"password"}>Password</label>
<input type={"password"} className={"form-control"} placeholder={"Password"}
id={"password"} name={"password"} value={this.state.password}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"confirmPassword"}>Confirm Password</label>
<input type={"password"} className={"form-control"} placeholder={"Confirm Password"}
id={"confirmPassword"} name={"confirmPassword"} value={this.state.confirmPassword}
onChange={this.onChangeInput.bind(this)}/>
</div>
</div>
</Collapse>
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
{ this.state.isSubmitting
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading&nbsp;<Icon icon={"circle-notch"} /></button>
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
}
</form>
</div>
</div>
</div>
<ReactTooltip/>
</>;
}
submitForm(e) {
e.preventDefault();
if (this.state.isSubmitting) {
return;
}
const requiredFields = (this.state.sendInvite ?
["username", "email"] :
["username", "password", "confirmPassword"]);
let missingFields = [];
for (const field of requiredFields) {
if (!this.state[field]) {
missingFields.push(field);
}
}
if (missingFields.length > 0) {
let errors = this.state.errors.slice();
errors.push({title: "Missing input", message: "The following fields are missing: " + missingFields.join(", "), type: "warning"});
this.setState({ ...this.state, errors: errors });
return;
}
this.setState({ ...this.state, isSubmitting: true });
const username = this.state.username;
const email = this.state.email || "";
const password = this.state.password;
const confirmPassword = this.state.confirmPassword;
if (this.state.sendInvite) {
this.parent.api.inviteUser(username, email).then((res) => {
let errors = this.state.errors.slice();
if (!res.success) {
errors.push({ title: "Error inviting User", message: res.msg, type: "danger" });
this.setState({ ...this.state, errors: errors, isSubmitting: false });
} else {
errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" });
this.setState({ ...this.state, errors: errors, username: "", email: "", isSubmitting: false });
}
});
} else {
if (this.state.password !== this.state.confirmPassword) {
let errors = this.state.errors.slice();
errors.push({ title: "Error creating User", message: "The given passwords do not match", type: "danger" });
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
return;
}
this.parent.api.createUser(username, email, password, confirmPassword).then((res) => {
let errors = this.state.errors.slice();
if (!res.success) {
errors.push({ title: "Error creating User", message: res.msg, type: "danger" });
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
} else {
errors.push({ title: "Success", message: "The user was successfully created.", type: "success" });
this.setState({ ...this.state, errors: errors, username: "", email: "", password: "", confirmPassword: "", isSubmitting: false });
}
});
}
}
onCheckboxChange() {
this.setState({
...this.state,
sendInvite: !this.state.sendInvite,
});
}
onChangeInput(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({ ...this.state, [name]: value });
}
}

@ -1,63 +0,0 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Alert from "../elements/alert";
import ReactTooltip from "react-tooltip";
import {useState} from "react";
export default function ContactRequestOverview(props) {
let parent = {
api: props.api,
contactRequests: props.contactRequests || [ ]
};
let [errors, setErrors] = useState([]);
function removeError(i) {
if (i >= 0 && i < errors.length) {
let newErrors = errors.slice();
newErrors.splice(i, 1);
setErrors(newErrors);
}
}
console.log("contact site", parent.contactRequests);
let errorElements = [];
for (let i = 0; i < errors.length; i++) {
errorElements.push(<Alert key={"error-" + i} onClose={() => removeError(i)} {...errors[i]}/>)
}
let chats = [];
for (let i = 0; i < parent.contactRequests.length; i++) {
const req = parent.contactRequests[i];
chats.push(<div key={"contact-request-" + i}>
From: { req.from_name } - { req.from_email }
Unread messages: { req.unread }
</div>
);
}
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">Contact Requests</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">Contact</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
{errorElements}
{chats}
</div>
<ReactTooltip />
</>;
}

@ -1,322 +0,0 @@
import * as React from "react";
import Icon from "../elements/icon";
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
import "../include/select2.min.css";
export default class EditUser extends React.Component {
constructor(props) {
super(props);
this.parent = {
api: props.api,
showDialog: props.showDialog,
};
this.state = {
user: {},
alerts: [],
fetchError: null,
loaded: false,
isSaving: false,
isDeleting: false,
groups: { },
searchString: "",
searchActive: false
};
this.searchBox = React.createRef();
}
removeAlert(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
componentDidMount() {
this.parent.api.getUser(this.props.match.params["userId"]).then((res) => {
if (res.success) {
this.setState({ ...this.state, user: {
name: res.user.name,
email: res.user.email || "",
groups: res.user.groups,
password: "",
confirmed: res.user.confirmed
}
});
this.parent.api.fetchGroups(1, 50).then((res) => {
if (res.success) {
this.setState({ ...this.state, groups: res.groups, loaded: true });
} else {
this.setState({ ...this.state, fetchError: res.msg, loaded: true });
}
});
} else {
this.setState({ ...this.state, fetchError: res.msg, loaded: true });
}
});
}
onChangeInput(event) {
const target = event.target;
let value = target.value;
const name = target.name;
if (target.type === "checkbox") {
value = !!target.checked;
}
if (name === "search") {
this.setState({...this.state, searchString: value});
} else {
this.setState({ ...this.state, user: { ...this.state.user, [name]: value } });
}
}
onToggleSearch(e) {
e.stopPropagation();
this.setState({ ...this.state, searchActive: !this.state.searchActive });
this.searchBox.current.focus();
}
onSubmitForm(event) {
event.preventDefault();
event.stopPropagation();
const id = this.props.match.params["userId"];
const username = this.state.user["name"];
const email = this.state.user["email"];
let password = this.state.user["password"].length > 0 ? this.state.user["password"] : null;
let groups = Object.keys(this.state.user.groups);
let confirmed = this.state.user["confirmed"];
this.setState({ ...this.state, isSaving: true});
this.parent.api.editUser(id, username, email, password, groups, confirmed).then((res) => {
let alerts = this.state.alerts.slice();
if (res.success) {
alerts.push({ title: "Success", message: "User was successfully updated.", type: "success" });
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
} else {
alerts.push({ title: "Error updating user", message: res.msg, type: "danger" });
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
}
});
}
onDeleteUser(event) {
event.preventDefault();
event.stopPropagation();
const id = this.props.match.params["userId"];
this.parent.showDialog("Are you sure you want to delete this user permanently?", "Delete User?", ["Yes", "No"], (btn) => {
if (btn === "Yes") {
this.parent.api.deleteUser(id).then((res) => {
if (res.success) {
this.props.history.push("/admin/users");
} else {
let alerts = this.state.alerts.slice();
alerts.push({ title: "Error deleting user", message: res.msg, type: "danger" });
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
}
})
}
});
}
onRemoveGroup(event, groupId) {
event.stopPropagation();
if (this.state.user.groups.hasOwnProperty(groupId)) {
let groups = { ...this.state.user.groups };
delete groups[groupId];
this.setState({ ...this.state, user: { ...this.state.user, groups: groups }});
}
}
onAddGroup(event, groupId) {
event.stopPropagation();
if (!this.state.user.groups.hasOwnProperty(groupId)) {
let groups = { ...this.state.user.groups, [groupId]: { ...this.state.groups[groupId] } };
this.setState({ ...this.state, user: { ...this.state.user, groups: groups }, searchActive: false, searchString: "" });
}
}
render() {
if (!this.state.loaded) {
return <h2 className={"text-center"}>
Loading<br/>
<Icon icon={"spinner"} className={"mt-3 text-muted fa-2x"}/>
</h2>
}
let alerts = [];
let form = null;
if(this.state.fetchError) {
alerts.push(
<Alert key={"error-fetch"} title={"Error fetching data"} type={"danger"} message={
<div>{this.state.fetchError}<br/>You can meanwhile return to the&nbsp;
<Link to={"/admin/users"}>user overview</Link>
</div>
}/>
)
} else {
for (let i = 0; i < this.state.alerts.length; i++) {
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
}
let possibleOptions = [];
let renderedOptions = [];
for (let groupId in this.state.groups) {
if (this.state.groups.hasOwnProperty(groupId)) {
let groupName = this.state.groups[groupId].name;
let groupColor = this.state.groups[groupId].color;
if (this.state.user.groups.hasOwnProperty(groupId)) {
renderedOptions.push(
<li className={"select2-selection__choice"} key={"group-" + groupId} title={groupName} style={{backgroundColor: groupColor}}>
<span className="select2-selection__choice__remove" role="presentation"
onClick={(e) => this.onRemoveGroup(e, groupId)}>
×
</span>
{groupName}
</li>
);
} else {
if (this.state.searchString.length === 0 || groupName.toLowerCase().includes(this.state.searchString.toLowerCase())) {
possibleOptions.push(
<li className={"select2-results__option"} role={"option"} key={"group-" + groupId} aria-selected={false}
onClick={(e) => this.onAddGroup(e, groupId)}>
{groupName}
</li>
);
}
}
}
}
let searchWidth = "100%";
let placeholder = "Select Groups";
let searchVisible = (this.state.searchString.length > 0 || this.state.searchActive) ? "block" : "none";
if (renderedOptions.length > 0) {
searchWidth = (0.75 + this.state.searchString.length * 0.75) + "em";
placeholder = "";
}
if (this.state.searchString.length > 0 && possibleOptions.length === 0) {
possibleOptions.push(
<li className={"select2-results__option"} role={"option"} key={"group-notfound"} aria-selected={true}>
Group not found
</li>
);
}
form = <form role={"form"} onSubmit={(e) => e.preventDefault()}>
<div className={"form-group"}>
<label htmlFor={"username"}>Username</label>
<input type={"text"} className={"form-control"} placeholder={"Enter username"}
name={"username"} id={"username"} maxLength={32} value={this.state.user.name}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"email"}>E-Mail</label>
<input type={"email"} className={"form-control"} placeholder={"E-Mail address"}
id={"email"} name={"email"} maxLength={64} value={this.state.user.email}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"password"}>Password</label>
<input type={"password"} className={"form-control"} placeholder={"(unchanged)"}
id={"password"} name={"password"} value={this.state.user.password}
onChange={this.onChangeInput.bind(this)}/>
</div>
<div className={"form-group position-relative"}>
<label>Groups</label>
<span className={"select2 select2-container select2-container--default select2-container--below"}
dir={"ltr"} style={{width: "100%"}} >
<span className="selection">
<span className={"select2-selection select2-selection--multiple"} role={"combobox"} aria-haspopup={"true"}
aria-expanded={false} aria-disabled={false} onClick={this.onToggleSearch.bind(this)}>
<ul className={"select2-selection__rendered"}>
{renderedOptions}
<li className={"select2-search select2-search--inline"}>
<input className={"select2-search__field"} type={"search"} tabIndex={0}
autoComplete={"off"} autoCorrect={"off"} autoCapitalize={"none"} spellCheck={false}
role={"searchbox"} aria-autocomplete={"list"} placeholder={placeholder}
name={"search"} style={{width: searchWidth}} value={this.state.searchString}
onChange={this.onChangeInput.bind(this)} ref={this.searchBox} />
</li>
</ul>
</span>
</span>
<span className="dropdown-wrapper" aria-hidden="true"/>
</span>
<span className={"select2-container select2-container--default select2-container--open"}
style={{position: "absolute", bottom: 0, left: 0, width: "100%", display: searchVisible}}>
<span className={"select2-dropdown select2-dropdown--below"} dir={"ltr"}>
<span className={"select2-results"}>
<ul className={"select2-results__options"} role={"listbox"}
aria-multiselectable={true} aria-expanded={true} aria-hidden={false}>
{possibleOptions}
</ul>
</span>
</span>
</span>
</div>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
onChange={this.onChangeInput.bind(this)}
id={"confirmed"} name={"confirmed"} checked={this.state.user.confirmed}/>
<label className={"form-check-label"} htmlFor={"confirmed"}>
Confirmed
</label>
</div>
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
{ this.state.isSaving
? <button type={"submit"} className={"btn btn-primary mt-2 mr-2"} disabled>Saving&nbsp;<Icon icon={"circle-notch"} /></button>
: <button type={"submit"} className={"btn btn-primary mt-2 mr-2"} onClick={this.onSubmitForm.bind(this)}>Save</button>
}
{ this.state.isDeleting
? <button type={"submit"} className={"btn btn-danger mt-2"} disabled>Deleting&nbsp;<Icon icon={"circle-notch"} /></button>
: <button type={"submit"} className={"btn btn-danger mt-2"} onClick={this.onDeleteUser.bind(this)}>Delete</button>
}
</form>
}
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">Edit User</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"><Link to={"/admin/users"}>Users</Link></li>
<li className="breadcrumb-item active">Add User</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"row"}>
<div className={"col-lg-6 pl-5 pr-5"}>
{alerts}
{form}
</div>
</div>
</div>
</>
}
}

@ -1,147 +0,0 @@
import React, {useState} from "react";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import {Collapse} from "react-collapse";
export default function HelpPage() {
const [firstStepsCollapsed, collapseFirstSteps] = useState(false);
const [faqCollapsed, collapseFaq] = useState(false);
const [aboutCollapsed, collapseAbout] = useState(false);
return (
<>
<section className={"content-header"}>
<div className={"container-fluid"}>
<div className={"row mb-2"}>
<div className={"col-sm-6"}>
<h1>WebBase Help & Information</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"}>Help</li>
</ol>
</div>
</div>
</div>
</section>
<section className="content">
<div className={"container-fluid"}>
<div className={"row"}>
<div className="col-12 col-md-8 col-lg-4">
<p>
WebBase is a php framework to simplify user management, pages and routing.
It can easily be modified and extended by writing document classes and the database
can be accessed through the available abstracted scheme. It also includes
a REST API with access control, parameter type checking and more.
</p>
</div>
</div>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
<div className={"card"}>
<div className="card-header">
<h3 className="card-title">
<Icon icon={"walking"} className={"mr-1"}/>
First Steps
</h3>
<div className={"card-tools"}>
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
collapseFirstSteps(!firstStepsCollapsed);
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={!firstStepsCollapsed}>
<div className="card-body">
<ol>
<li>Customize <Link to={"/admin/settings"}>website settings</Link></li>
<li>Manage users and groups on <Link to={"/admin/users"}>this page</Link></li>
<li><Link to={"/admin/pages"}>Create routes</Link> for your website</li>
<li>For dynamic pages:
<ol>
<li>Create a document class in <b>/Core/Documents</b> according to the other classes</li>
<li>Create a view class in <b>/Core/Views</b> for every view you have</li>
</ol>
</li>
<li>For static pages:
<ul>
<li>Create html files in <b>/static</b></li>
</ul>
</li>
</ol>
</div>
</Collapse>
</div>
</div>
<div className={"col-12 col-lg-6"}>
<div className={"card"}>
<div className="card-header">
<h3 className="card-title">
<Icon icon={"question-circle"} className={"mr-1"}/>
FAQ
</h3>
<div className={"card-tools"}>
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
collapseFaq(!faqCollapsed);
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={!faqCollapsed}>
<div className="card-body">
Nobody asked questions so far
</div>
</Collapse>
</div>
</div>
<div className={"col-12 col-lg-6"}>
<div className={"card"}>
<div className="card-header">
<h3 className="card-title">
<Icon icon={"address-card"} className={"mr-1"}/>
About
</h3>
<div className={"card-tools"}>
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
collapseAbout(!aboutCollapsed);
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={!aboutCollapsed}>
<div className="card-body">
<b>Project Lead & Main Developer</b>
<ul className={"list-unstyled"}>
<li><small><Icon icon={"address-card"} className={"mr-1"}/>Roman Hergenreder</small></li>
<li>
<small><Icon icon={"globe"} className={"mr-1"}/>
<a href={"https://romanh.de/"} target={"_blank"} rel={"noopener"}>
https://romanh.de/
</a>
</small>
</li>
<li><small><Icon icon={"envelope"} className={"mr-1"}/><a href={"mailto:webmaster@romanh.de"}>webmaster@romanh.de</a></small></li>
</ul>
<b className={"mt-2"}>Backend Developer</b>
<ul className={"list-unstyled"}>
<li><small><Icon icon={"address-card"} className={"mr-1"}/>Leon Krause</small></li>
</ul>
</div>
</Collapse>
</div>
</div>
</div>
</div>
</section>
</>
)
}

@ -1,133 +0,0 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import moment from 'moment';
export default class Logs extends React.Component {
constructor(props) {
super(props);
this.state = {
alerts: [],
notifications: []
};
this.parent = {
api : props.api,
fetchNotifications: props.fetchNotifications,
}
}
removeError(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
componentDidMount() {
this.parent.api.getNotifications(false).then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
this.setState({ ...this.state, alerts: alerts });
} else {
this.setState({ ...this.state, notifications: res.notifications });
}
this.parent.api.markNotificationsSeen().then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
this.setState({ ...this.state, alerts: alerts });
}
this.parent.fetchNotifications();
});
});
}
render() {
const colors = ["red", "green", "blue", "purple", "maroon"];
let dates = { };
for (let notification of this.state.notifications) {
let day = moment(notification["created_at"]).format('ll');
if (!dates.hasOwnProperty(day)) {
dates[day] = [];
}
let icon = "bell";
if (notification.type === "message") {
icon = "envelope";
} else if(notification.type === "warning") {
icon = "exclamation-triangle";
}
dates[day].push({ ...notification, icon: icon, timestamp: notification["created_at"] });
}
let elements = [];
for (let date in dates) {
let color = colors[Math.floor(Math.random() * colors.length)];
elements.push(
<div className={"time-label"} key={"time-label-" + date}>
<span className={"bg-" + color}>{date}</span>
</div>
);
for (let event of dates[date]) {
let timeString = moment(event.timestamp).fromNow();
elements.push(
<div key={"time-entry-" + event.id}>
<Icon icon={event.icon} className={"bg-" + color}/>
<div className="timeline-item">
<span className="time"><Icon icon={"clock"}/> {timeString}</span>
<h3 className="timeline-header">{event.title}</h3>
<div className="timeline-body">{event.message}</div>
</div>
</div>
);
}
}
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">Logs & Notifications</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">Logs</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"container-fluid"}>
<div className={"row"}>
<div className={"col-lg-8 col-12"}>
<div className="timeline">
<div className={"time-label"}>
<span className={"bg-blue"}>Today</span>
</div>
{elements}
<div>
<Icon icon={"clock"} className={"bg-gray"}/>
</div>
</div>
</div>
</div>
</div>
</div>
</>;
}
}

@ -1,247 +0,0 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import { Bar } from 'react-chartjs-2';
import {Collapse} from 'react-collapse';
import moment from 'moment';
import Alert from "../elements/alert";
import humanReadableSize from "../global";
export default class Overview extends React.Component {
constructor(props) {
super(props);
this.parent = {
showDialog: props.showDialog,
api: props.api,
};
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() {
const numDays = moment().daysInMonth();
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 ' + moment().format("MMMM"),
borderWidth: 1,
data: data,
backgroundColor: colors,
}]
};
let errors = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
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"}>
<div className={"container-fluid"}>
{errors}
<div className={"row"}>
<div className={"col-lg-3 col-6"}>
<div className="small-box bg-info">
<div className={"inner"}>
<h3>{this.state.userCount}</h3>
<p>Users registered</p>
</div>
<div className="icon">
<Icon icon={"users"} />
</div>
<Link to={"/admin/users"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-success"}>
<div className={"inner"}>
<h3>{this.state.pageCount}</h3>
<p>Routes & Pages</p>
</div>
<div className="icon">
<Icon icon={"copy"} />
</div>
<Link to={"/admin/pages"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-warning"}>
<div className={"inner"}>
<h3>{this.props.notifications.length}</h3>
<p>new Notifications</p>
</div>
<div className={"icon"}>
<Icon icon={"bell"} />
</div>
<Link to={"/admin/logs"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-danger"}>
<div className={"inner"}>
<h3>{this.state.visitorsTotal}</h3>
<p>Unique Visitors</p>
</div>
<div className="icon">
<Icon icon={"chart-line"} />
</div>
<Link to={"/admin/visitors"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 col-12">
<div className="card card-info">
<div className="card-header">
<h3 className="card-title">Unique Visitors this month</h3>
<div className="card-tools">
<button type="button" className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
this.setState({ ...this.state, chartVisible: !this.state.chartVisible });
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={this.state.chartVisible}>
<div className="card-body">
<div className="chart">
<Bar data={chartData} options={chartOptions} />
</div>
</div>
</Collapse>
</div>
</div>
<div className="col-lg-6 col-12">
<div className="card card-warning">
<div className="card-header">
<h3 className="card-title">Server Status</h3>
<div className="card-tools">
<button type="button" className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
this.setState({ ...this.state, statusVisible: !this.state.statusVisible });
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={this.state.statusVisible}>
<div className="card-body">
<ul className={"list-unstyled"}>
<li><b>Version</b>: {this.state.server.version}</li>
<li><b>Server</b>: {this.state.server.server}</li>
<li><b>Memory Usage</b>: {humanReadableSize(this.state.server["memory_usage"])}</li>
<li><b>Load Average</b>: { loadAvg }</li>
<li><b>Database</b>: { this.state.server["database"] }</li>
<li><b>Mail</b>: { this.state.server["mail"] === true
? <span>
OK
<Icon icon={"check-circle"} className={"ml-2 text-success"}/>
</span>
: <span>
<Link to={"/admin/settings"}>Not configured</Link>
<Icon icon={"times-circle"} className={"ml-2 text-danger"}/>
</span>}
</li>
<li>
<b>Google reCaptcha</b>: { this.state.server["reCaptcha"] === true
? <span>
OK
<Icon icon={"check-circle"} className={"ml-2 text-success"}/>
</span>
: <span>
<Link to={"/admin/settings"}>Not configured</Link>
<Icon icon={"times-circle"} className={"ml-2 text-danger"}/>
</span>}
</li>
</ul>
</div>
</Collapse>
</div>
</div>
</div>
</section>
</>
}
}

@ -1,291 +0,0 @@
import * as React from "react";
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import ReactTooltip from "react-tooltip";
import Select from 'react-select';
export default class PageOverview extends React.Component {
constructor(props) {
super(props);
this.parent = {
api: props.api
};
this.state = {
routes: [],
errors: [],
isResetting: false,
isSaving: false
};
this.optionMap = {
"redirect_temporary": "Redirect Temporary",
"redirect_permanently": "Redirect Permanently",
"static": "Serve Static",
"dynamic": "Load Dynamic",
};
this.options = [];
for (let key in this.optionMap) {
this.options.push(this.buildOption(key));
}
}
buildOption(key) {
if (typeof key === 'object' && key.hasOwnProperty("key") && key.hasOwnProperty("label")) {
return key;
} else if (typeof key === 'string') {
return { value: key, label: this.optionMap[key] };
} else {
return this.options[key];
}
}
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.fetchRoutes()
}
render() {
let errors = [];
let rows = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
const inputStyle = { fontFamily: "Courier", paddingTop: "14px" };
for (let i = 0; i < this.state.routes.length; i++) {
let route = this.state.routes[i];
rows.push(
<tr key={"route-" + i}>
<td className={"align-middle"}>
<input type={"text"} maxLength={128} style={inputStyle} className={"form-control"}
value={route.request} onChange={(e) => this.changeRequest(i, e)} />
</td>
<td className={"text-center"}>
<Select options={this.options} value={this.buildOption(route.action)} onChange={(selectedOption) => this.changeAction(i, selectedOption)} />
</td>
<td className={"align-middle"}>
<input type={"text"} maxLength={128} style={inputStyle} className={"form-control"}
value={route.target} onChange={(e) => this.changeTarget(i, e)} />
</td>
<td className={"align-middle"}>
<input type={"text"} maxLength={64} style={inputStyle} className={"form-control"}
value={route.extra} onChange={(e) => this.changeExtra(i, e)} />
</td>
<td className={"text-center"}>
<input
type={"checkbox"}
checked={route.active === 1}
onChange={(e) => this.changeActive(i, e)} />
</td>
<td className={"text-center"}>
<input
type={"checkbox"}
checked={route.exact === 1}
onChange={(e) => this.changeExact(i, e)} />
</td>
<td>
<ReactTooltip id={"delete-" + i} />
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
data-tip={"Click to delete this route"}
data-type={"warning"} data-place={"right"}
data-for={"delete-" + i} data-effect={"solid"}
onClick={() => this.removeRoute(i)}/>
</td>
</tr>
);
}
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">Routes & Pages</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">Pages</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
{errors}
<div className={"content-fluid"}>
<div className={"row"}>
<div className={"col-lg-10 col-12"}>
<table className={"table"}>
<thead className={"thead-dark"}>
<tr>
<th>
Request&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"The request, the user is making. Can also be interpreted as a regular expression."}
data-type={"info"} data-place={"bottom"}/>
</th>
<th style={{minWidth: "200px"}}>
Action&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"The action to be taken"}
data-type={"info"} data-place={"bottom"}/>
</th>
<th>
Target&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"Any URL if action is redirect or static. Path to a class inheriting from Document, " +
"if dynamic is chosen"}
data-type={"info"} data-place={"bottom"}/>
</th>
<th>
Extra&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"If action is dynamic, a view name can be entered here, otherwise leave empty."}
data-type={"info"} data-place={"bottom"}/>
</th>
<th className={"text-center"}>
Active&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"True, if the route is currently active."}
data-type={"info"} data-place={"bottom"}/>
</th>
<th className={"text-center"}>
Exact&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"True, if the URL must match exactly."}
data-type={"info"} data-place={"bottom"}/>
</th>
<th/>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>
<button className={"btn btn-info"} onClick={() => this.onAddRoute()} disabled={this.state.isResetting || this.state.isSaving}>
<Icon icon={"plus"}/>&nbsp;Add new Route
</button>
<button className={"btn btn-secondary ml-2"} onClick={() => this.onResetRoutes()} disabled={this.state.isResetting || this.state.isSaving}>
{ this.state.isResetting ? <span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset" }
</button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSaveRoutes()} disabled={this.state.isResetting || this.state.isSaving}>
{ this.state.isSaving ? <span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save" }
</button>
</div>
</div>
</div>
</div>
</div>
<ReactTooltip data-effect={"solid"}/>
</>
}
onResetRoutes() {
this.setState({ ...this.state, isResetting: true });
this.fetchRoutes();
}
onSaveRoutes() {
this.setState({ ...this.state, isSaving: true });
let routes = [];
for (let i = 0; i < this.state.routes.length; i++) {
let route = this.state.routes[i];
routes.push({
request: route.request,
action: typeof route.action === 'object' ? route.action.value : route.action,
target: route.target,
extra: route.extra ?? "",
active: route.active === 1,
exact: route.exact === 1,
});
}
this.parent.api.saveRoutes(routes).then((res) => {
if (res.success) {
this.setState({...this.state, isSaving: false});
} else {
let errors = this.state.errors.slice();
errors.push({ title: "Error saving routes", message: res.msg });
this.setState({...this.state, errors: errors, isSaving: false});
}
});
}
changeRoute(index, key, value) {
if (index < 0 || index >= this.state.routes.length)
return;
let routes = this.state.routes.slice();
routes[index][key] = value;
this.setState({ ...this.state, routes: routes });
}
removeRoute(index) {
if (index < 0 || index >= this.state.routes.length)
return;
let routes = this.state.routes.slice();
routes.splice(index, 1);
this.setState({ ...this.state, routes: routes });
}
onAddRoute() {
let routes = this.state.routes.slice();
routes.push({ request: "", action: "dynamic", target: "", extra: "", active: 1, exact: 1 });
this.setState({ ...this.state, routes: routes });
}
changeAction(index, selectedOption) {
this.changeRoute(index, "action", selectedOption);
}
changeActive(index, e) {
this.changeRoute(index, "active", e.target.checked ? 1 : 0);
}
changeExact(index, e) {
this.changeRoute(index, "exact", e.target.checked ? 1 : 0);
}
changeRequest(index, e) {
this.changeRoute(index, "request", e.target.value);
}
changeTarget(index, e) {
this.changeRoute(index, "target", e.target.value);
}
changeExtra(index, e) {
this.changeRoute(index, "extra", e.target.value);
}
fetchRoutes() {
this.parent.api.getRoutes().then((res) => {
if (res.success) {
this.setState({...this.state, routes: res.routes, isResetting: false});
ReactTooltip.rebuild();
} else {
let errors = this.state.errors.slice();
errors.push({ title: "Error fetching routes", message: res.msg });
this.setState({...this.state, errors: errors, isResetting: false});
}
});
}
}

@ -1,262 +0,0 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import Alert from "../elements/alert";
import ReactTooltip from "react-tooltip";
export default class PermissionSettings extends React.Component {
constructor(props) {
super(props);
this.state = {
alerts: [],
permissions: [],
groups: {},
isSaving: false,
isResetting: false
};
this.parent = {
api: props.api
}
}
componentDidMount() {
this.fetchPermissions()
}
fetchPermissions() {
this.parent.api.fetchPermissions().then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching permissions" });
this.setState({...this.state, alerts: alerts, isResetting: false});
} else {
this.setState({...this.state, groups: res.groups, permissions: res.permissions, isResetting: false});
}
});
}
removeAlert(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
onChangeMethod(e, index) {
if (index < 0 || index >= this.state.permissions.length) {
return;
}
let value = e.target.value;
let newPermissions = this.state.permissions.slice();
newPermissions[index].method = value;
this.setState({ ...this.state, permissions: newPermissions })
}
render() {
let alerts = [];
for (let i = 0; i < this.state.alerts.length; i++) {
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
}
let th = [];
th.push(<th key={"th-method"}>Method</th>);
th.push(<th key={"th-everyone"} className={"text-center"}>Everyone</th>);
for (let groupId in this.state.groups) {
if (this.state.groups.hasOwnProperty(groupId)) {
let groupName = this.state.groups[groupId].name;
let groupColor = this.state.groups[groupId].color;
th.push(
<th key={"th-" + groupId} className={"text-center"}>
<span key={"group-" + groupId} className={"badge text-white"} style={{backgroundColor: groupColor}}>
{groupName}
</span>
</th>
);
}
}
let tr = [];
for (let i = 0; i < this.state.permissions.length; i++) {
let permission = this.state.permissions[i];
let td = [];
if (permission.description) {
td.push(
<td>
<ReactTooltip id={"tooltip-" + i} />
{ permission.method }
<Icon icon={"info-circle"} className={"text-info float-right"}
data-tip={permission.description} data-place={"right"} data-type={"info"}
data-effect={"solid"} data-for={"tooltip-" + i} />
</td>
);
} else {
td.push(
<td>
<ReactTooltip id={"tooltip-" + i} />
<input type={"text"} maxLength={32} value={this.state.permissions[i].method}
onChange={(e) => this.onChangeMethod(e, i)} />
<Icon icon={"trash"} className={"text-danger float-right"}
data-tip={"Delete"} data-place={"right"} data-type={"error"}
data-effect={"solid"} data-for={"tooltip-" + i}
onClick={() => this.onDeletePermission(i)} style={{cursor: "pointer"}} />
</td>
);
}
td.push(
<td key={"td-everyone"} className={"text-center"}>
<input type={"checkbox"} checked={this.state.permissions[i].groups.length === 0}
onChange={(e) => this.onChangePermission(e, i)}/>
</td>
);
for (let groupId in this.state.groups) {
if (this.state.groups.hasOwnProperty(groupId)) {
groupId = parseInt(groupId);
td.push(
<td key={"td-" + groupId} className={"text-center"}>
<input type={"checkbox"} checked={this.state.permissions[i].groups.includes(groupId)}
onChange={(e) => this.onChangePermission(e, i, groupId)}/>
</td>
);
}
}
tr.push(<tr key={"permission-" + i}>{td}</tr>);
}
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">API Access Control</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"><Link to={"/admin/users"}>Users</Link></li>
<li className="breadcrumb-item active">Permissions</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"row"}>
<div className={"col-lg-6 pl-5 pr-5"}>
{alerts}
<form onSubmit={(e) => e.preventDefault()}>
<table className={"table table-bordered table-hover dataTable dtr-inline"}>
<thead>
<tr role={"row"}>
{th}
</tr>
</thead>
<tbody>
{tr}
</tbody>
</table>
<div className={"mt-2"}>
<Link to={"/admin/users"} className={"btn btn-primary"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
<button className={"btn btn-info ml-2"} onClick={() => this.onAddPermission()} disabled={this.state.isResetting || this.state.isSaving}>
<Icon icon={"plus"}/>&nbsp;Add new Permission
</button>
<button className={"btn btn-secondary ml-2"} onClick={() => this.onResetPermissions()} disabled={this.state.isResetting || this.state.isSaving}>
{ this.state.isResetting ? <span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset" }
</button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSavePermissions()} disabled={this.state.isResetting || this.state.isSaving}>
{ this.state.isSaving ? <span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save" }
</button>
</div>
</form>
</div>
</div>
</div>
</>;
}
onAddPermission() {
let newPermissions = this.state.permissions.slice();
newPermissions.push({ method: "", groups: [], description: null });
this.setState({ ...this.state, permissions: newPermissions })
}
onResetPermissions() {
this.setState({ ...this.state, isResetting: true });
this.fetchPermissions();
}
onSavePermissions() {
this.setState({ ...this.state, isSaving: true });
let permissions = [];
for (let i = 0; i < this.state.permissions.length; i++) {
let permission = this.state.permissions[i];
permissions.push({ method: permission.method, groups: permission.groups });
}
this.parent.api.savePermissions(permissions).then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error saving permissions" });
this.setState({...this.state, alerts: alerts, isSaving: false});
} else {
this.setState({...this.state, isSaving: false});
}
});
}
onDeletePermission(index) {
if (index < 0 || index >= this.state.permissions.length) {
return;
}
let newPermissions = this.state.permissions.slice();
newPermissions.splice(index, 1);
this.setState({ ...this.state, permissions: newPermissions })
}
onChangePermission(event, index, group = null) {
if (index < 0 || index >= this.state.permissions.length) {
return;
}
let isChecked = event.target.checked;
let newPermissions = this.state.permissions.slice();
if (group === null) {
if (isChecked) {
newPermissions[index].groups = [];
} else {
return;
}
} else {
if (isChecked && !newPermissions[index].groups.includes(group)) {
newPermissions[index].groups.push(group);
} else if(!isChecked) {
let indexOf = newPermissions[index].groups.indexOf(group);
if (indexOf !== -1) {
newPermissions[index].groups.splice(indexOf, 1);
} else {
return;
}
} else {
return;
}
}
this.setState({ ...this.state, permissions: newPermissions })
}
};

@ -1,619 +0,0 @@
import React from "react";
import {Link} from "react-router-dom";
import Alert from "../elements/alert";
import {Collapse} from "react-collapse/lib/Collapse";
import Icon from "../elements/icon";
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import ReactTooltip from "react-tooltip";
export default class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
errors: [],
settings: {},
general: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
keys: ["site_name", "base_url", "user_registration_enabled"]
},
mail: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
isSending: false,
test_email: "",
unsavedMailSettings: false,
keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
},
recaptcha: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
keys: ["recaptcha_enabled", "recaptcha_public_key", "recaptcha_private_key"]
},
uncategorised: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
settings: []
},
};
this.parent = {
api: props.api,
showDialog: props.showDialog
};
this.hiddenKeys = [
"recaptcha_private_key",
"mail_password"
];
}
isDefaultKey(key) {
key = key.trim();
return this.state.general.keys.includes(key)
|| this.state.mail.keys.includes(key)
|| this.state.recaptcha.keys.includes(key)
|| this.hiddenKeys.includes(key);
}
getUncategorisedValues(res) {
let uncategorised = [];
for(let key in res.settings) {
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
uncategorised.push({key: key, value: res.settings[key]});
}
}
return uncategorised;
}
onDeleteUncategorisedProp(index) {
if (index < 0 || index >= this.state.uncategorised.settings.length) {
return;
}
let props = this.state.uncategorised.settings.slice();
props.splice(index, 1);
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
}
onChangeUncategorisedValue(event, index, isKey) {
if (index < 0 || index >= this.state.uncategorised.settings.length) {
return;
}
let props = this.state.uncategorised.settings.slice();
if (isKey) {
props[index].key = event.target.value;
} else {
props[index].value = event.target.value;
}
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
}
onAddUncategorisedProperty() {
let props = this.state.uncategorised.settings.slice();
props.push({key: "", value: ""});
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
}
componentDidMount() {
this.parent.api.getSettings().then((res) => {
if (res.success) {
let newState = {
...this.state,
settings: res.settings,
uncategorised: { ...this.state.uncategorised, settings: this.getUncategorisedValues(res) }
};
this.setState(newState);
} else {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, errors: errors });
}
});
}
removeError(i, category = null) {
if (category) {
if (i >= 0 && i < this.state[category].alerts.length) {
let alerts = this.state[category].alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, [category]: {...this.state[category], alerts: alerts}});
}
} else {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
}
}
}
toggleCollapse(category) {
this.setState({...this.state, [category]: {...this.state[category], isOpen: !this.state[category].isOpen}});
}
createCard(category, color, icon, title, content) {
let alerts = [];
for (let i = 0; i < this.state[category].alerts.length; i++) {
alerts.push(<Alert key={"alert-" + i}
onClose={() => this.removeError(i, category)} {...this.state[category].alerts[i]}/>)
}
return <div className={"card card-" + color} key={"card-" + category}>
<div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse(category)}>
<h4 className={"card-title"}>
<Icon className={"mr-2"} icon={icon} type={icon==="google"?"fab":"fas"} />
{title}
</h4>
<div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}>
<Icon icon={this.state[category].isOpen ? "angle-up" : "angle-down"}/>
</span>
</div>
</div>
<Collapse isOpened={this.state[category].isOpen}>
<div className={"card-body"}>
<div className={"row"}>
<div className={"col-12 col-lg-6"}>
{alerts}
{content}
<div>
<button className={"btn btn-secondary"} onClick={() => this.onReset(category)}
disabled={this.state[category].isResetting || this.state[category].isSaving}>
{this.state[category].isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSave(category)}
disabled={this.state[category].isResetting || this.state[category].isSaving}>
{this.state[category].isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button>
</div>
</div>
</div>
</div>
</Collapse>
</div>
}
createGeneralForm() {
return <>
<div className={"form-group"}>
<label htmlFor={"site_name"}>Site Name</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["site_name"] ?? ""}
placeholder={"Enter a title"} name={"site_name"} id={"site_name"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"base_url"}>Base URL</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["base_url"] ?? ""}
placeholder={"Enter a url"} name={"base_url"} id={"base_url"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"user_registration_enabled"}>User Registration</label>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"user_registration_enabled"}
id={"user_registration_enabled"}
checked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"}
htmlFor={"user_registration_enabled"}>
Allow anyone to register an account
</label>
</div>
</div>
</>
}
createMailForm() {
return <>
<div className={"form-group mt-2"}>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"mail_enabled"} id={"mail_enabled"}
checked={(this.state.settings["mail_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"} htmlFor={"mail_enabled"}>
Enable E-Mail service
</label>
</div>
</div>
<hr className={"m-3"}/>
<label htmlFor={"mail_username"}>Username</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"hashtag"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_username"] ?? ""}
placeholder={"Enter a username"} name={"mail_username"}
id={"mail_username"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"mail_password"} className={"mt-2"}>Password</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"key"}/>
</span>
</div>
<input type={"password"} className={"form-control"}
value={this.state.settings["mail_password"] ?? ""}
placeholder={"(unchanged)"} name={"mail_password"}
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"mail_from"} className={"mt-2"}>Sender Email Address</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.settings["mail_from"] ?? ""}
placeholder={"Enter a email address"} name={"mail_from"}
id={"mail_from"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"row"}>
<div className={"col-6"}>
<label htmlFor={"mail_host"} className={"mt-2"}>SMTP Host</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_host"] ?? ""}
placeholder={"e.g. smtp.example.com"} name={"mail_host"}
id={"mail_host"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
<div className={"col-6"}>
<label htmlFor={"mail_port"} className={"mt-2"}>SMTP Port</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"number"} className={"form-control"}
value={parseInt(this.state.settings["mail_port"] ?? "25")}
placeholder={"smtp port"} name={"mail_port"}
id={"mail_port"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
</div>
<div className={"mt-3"}>
<label htmlFor={"mail_from"} className={"mt-2"}>Send Test E-Mail</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.mail.test_email}
placeholder={"Enter a email address"}
onChange={(e) => this.setState({
...this.state,
mail: {...this.state.mail, test_email: e.target.value},
})}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"form-group form-inline mt-3"}>
<button className={"btn btn-info col-2"}
onClick={() => this.onSendTestMail()}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1" || this.state.mail.isSending}>
{this.state.mail.isSending ?
<span>Sending&nbsp;<Icon icon={"circle-notch"}/></span> : "Send Mail"}
</button>
<div className={"col-10"}>
{this.state.mail.unsavedMailSettings ?
<span className={"text-red"}>You need to save your mail settings first.</span> : null}
</div>
</div>
</div>
</>
}
getRecaptchaForm() {
return <>
<div className={"form-group mt-2"}>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"recaptcha_enabled"} id={"recaptcha_enabled"}
checked={(this.state.settings["recaptcha_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"} htmlFor={"recaptcha_enabled"}>
Enable Google's reCaptcha
<span className={"ml-2"}>
(<a href={"https://www.google.com/recaptcha/intro/v3.html"} target={"_blank"} rel={"noopener noreferrer"}>
More Info
<sup><small><Icon icon={"external-link-alt"} className={"ml-1"}/></small></sup>
</a>)
</span>
</label>
</div>
</div>
<hr className={"m-2"}/>
<label htmlFor={"recaptcha_public_key"} className={"mt-2"}>reCaptcha Site Key</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"unlock"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["recaptcha_public_key"] ?? ""}
placeholder={"Enter site key"} name={"recaptcha_public_key"}
id={"recaptcha_public_key"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"recaptcha_private_key"} className={"mt-2"}>reCaptcha Secret Key</label>
<div className={"input-group mb-3"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"lock"}/>
</span>
</div>
<input type={"password"} className={"form-control"}
value={this.state.settings["recaptcha_private_key"] ?? ""}
placeholder={"(unchanged)"} name={"recaptcha_private_key"}
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
</div>
</>
}
getUncategorizedForm() {
let tr = [];
for(let i = 0; i < this.state.uncategorised.settings.length; i++) {
let key = this.state.uncategorised.settings[i].key;
let value = this.state.uncategorised.settings[i].value;
tr.push(
<tr key={"uncategorised-" + i} className={(i % 2 === 0) ? "even" : "odd"}>
<td>
<input className={"form-control"} type={"text"} value={key} maxLength={32} placeholder={"Key"}
onChange={(e) => this.onChangeUncategorisedValue(e, i, true)} />
</td>
<td>
<input className={"form-control"} type={"text"} value={value} placeholder={"value"}
onChange={(e) => this.onChangeUncategorisedValue(e, i, false)} />
</td>
<td className={"text-center align-middle"}>
<ReactTooltip id={"tooltip-uncategorised-" + i} />
<Icon icon={"trash"} className={"text-danger"} style={{cursor: "pointer"}}
onClick={() => this.onDeleteUncategorisedProp(i)} data-type={"error"}
data-tip={"Delete property"} data-place={"right"} data-effect={"solid"}
data-for={"tooltip-uncategorised-" + i}
/>
</td>
</tr>
);
}
return <>
<table className={"table table-bordered table-hover dataTable dtr-inline"}>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th className={"text-center"}><Icon icon={"tools"}/></th>
</tr>
</thead>
<tbody>
{tr}
</tbody>
</table>
<div className={"mt-2 mb-3"}>
<button className={"btn btn-info"} onClick={() => this.onAddUncategorisedProperty()} >
<Icon icon={"plus"} className={"mr-2"} /> Add property
</button>
</div>
</>
}
render() {
let errors = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i}
onClose={() => this.removeError("errors", i)} {...this.state.errors[i]}/>)
}
const categories = {
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
"recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
};
let cards = [];
for (let name in categories) {
let category = categories[name];
cards.push(this.createCard(name, category.color, category.icon, category.title, category.content));
}
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">Settings</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">Settings</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
{errors}
<div>
{cards}
</div>
</div>
<ReactTooltip />
</>
}
onChangeValue(event) {
const target = event.target;
const name = target.name;
const type = target.type;
let value = target.value;
if (type === "checkbox") {
value = event.target.checked ? "1" : "0";
}
let changedMailSettings = false;
if (this.state.mail.keys.includes(name)) {
changedMailSettings = true;
}
let newState = {...this.state, settings: {...this.state.settings, [name]: value}};
if (changedMailSettings) {
newState.mail = {...this.state.mail, unsavedMailSettings: true};
}
this.setState(newState);
}
onReset(category) {
this.setState({...this.state, [category]: {...this.state[category], isResetting: true}});
this.parent.api.getSettings().then((res) => {
if (!res.success) {
let alerts = this.state[category].alerts.slice();
alerts.push({title: "Error fetching settings", message: res.msg});
this.setState({
...this.state,
[category]: {...this.state[category], alerts: alerts, isResetting: false}
});
} else {
let newState = { ...this.state };
let categoryUpdated = {...this.state[category], isResetting: false};
let newSettings = {...this.state.settings};
if (category === "uncategorised") {
categoryUpdated.settings = this.getUncategorisedValues(res);
for (let key in res.settings) {
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
newSettings[key] = res.settings[key] ?? "";
}
}
} else {
for (let key of this.state[category].keys) {
newSettings[key] = res.settings[key] ?? "";
}
if (category === "mail") {
categoryUpdated.unsavedMailSettings = false;
}
}
newState.settings = newSettings;
newState[category] = categoryUpdated;
this.setState(newState);
}
});
}
onSave(category) {
this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
let values = {};
if (category === "uncategorised") {
for (let prop of this.state.uncategorised.settings) {
if (prop.key) {
values[prop.key] = prop.value;
if (this.isDefaultKey(prop.key)) {
this.parent.showDialog("You cannot use this key as property key: " + prop.key, "System specific key");
this.setState({...this.state, [category]: {...this.state[category], isSaving: false}});
return;
}
}
}
for (let key in this.state.settings) {
if (this.state.settings.hasOwnProperty(key) && !this.isDefaultKey(key) && !values.hasOwnProperty(key)) {
values[key] = null;
}
}
} else {
for (let key of this.state[category].keys) {
if (this.hiddenKeys.includes(key) && !this.state.settings[key]) {
continue;
}
values[key] = this.state.settings[key];
}
}
this.parent.api.saveSettings(values).then((res) => {
let alerts = this.state[category].alerts.slice();
let categoryUpdated = {...this.state[category], isSaving: false};
if (!res.success) {
alerts.push({title: "Error saving settings", message: res.msg});
} else {
alerts.push({title: "Success", message: "Settings were successfully saved.", type: "success"});
if (category === "mail") categoryUpdated.unsavedMailSettings = false;
this.setState({...this.state, [category]: categoryUpdated});
}
categoryUpdated.alerts = alerts;
this.setState({...this.state, [category]: categoryUpdated});
});
}
onSendTestMail() {
this.setState({...this.state, mail: {...this.state.mail, isSending: true}});
this.parent.api.sendTestMail(this.state.mail.test_email).then((res) => {
let alerts = this.state.mail.alerts.slice();
let newState = {...this.state.mail, isSending: false};
if (!res.success) {
alerts.push({title: "Error sending email", message: res.msg});
} else {
alerts.push({
title: "Success!",
message: "E-Mail was successfully sent, check your inbox.",
type: "success"
});
newState.test_email = "";
}
newState.alerts = alerts;
this.setState({...this.state, mail: newState});
});
}
}

@ -1,418 +0,0 @@
import * as React from "react";
import Icon from "../elements/icon";
import {Link} from "react-router-dom";
import {getPeriodString} from "../global";
import Alert from "../elements/alert";
import ReactTooltip from "react-tooltip";
const TABLE_SIZE = 10;
export default class UserOverview extends React.Component {
constructor(props) {
super(props);
this.parent = {
showDialog: props.showDialog || function () { },
api: props.api,
};
this.state = {
loaded: false,
users: {
data: {},
page: 1,
pageCount: 1,
totalCount: 0,
},
groups: {
data: {},
page: 1,
pageCount: 1,
totalCount: 0,
},
errors: [],
rowCount: 0
};
}
fetchGroups(page) {
page = page || this.state.groups.page;
this.setState({...this.state, groups: {...this.state.groups, data: {}, page: 1, totalCount: 0}});
this.parent.api.fetchGroups(page, TABLE_SIZE).then((res) => {
if (res.success) {
this.setState({
...this.state,
groups: {
data: res.groups,
pageCount: res.pageCount,
page: page,
totalCount: res.totalCount,
},
rowCount: Math.max(this.state.rowCount, Object.keys(res.groups).length)
});
} else {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching groups", message: res.msg});
this.setState({
...this.state,
errors: errors
});
}
if (!this.state.loaded) {
this.fetchUsers(1)
}
});
}
fetchUsers(page) {
page = page || this.state.users.page;
this.setState({...this.state, users: {...this.state.users, data: {}, pageCount: 1, totalCount: 0}});
this.parent.api.fetchUsers(page, TABLE_SIZE).then((res) => {
if (res.success) {
this.setState({
...this.state,
loaded: true,
users: {
data: res.users,
pageCount: res.pageCount,
page: page,
totalCount: res.totalCount,
},
rowCount: Math.max(this.state.rowCount, Object.keys(res.users).length)
});
} else {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching users", message: res.msg});
this.setState({
...this.state,
loaded: true,
errors: errors
});
}
});
}
componentDidMount() {
this.setState({...this.state, loaded: false});
this.fetchGroups(1);
}
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});
}
}
render() {
if (!this.state.loaded) {
return <div className={"text-center mt-4"}>
<h3>Loading&nbsp;<Icon icon={"spinner"}/></h3>
</div>
}
let errors = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
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">Users & Groups</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">Users</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
{errors}
<div className={"content-fluid"}>
<div className={"row"}>
<div className={"col-lg-6"}>
{this.createUserCard()}
</div>
<div className={"col-lg-6"}>
{this.createGroupCard()}
</div>
</div>
<div className={"row"}>
<div className={"col-12"}>
<Link to={"/admin/user/permissions"} className={"btn btn-primary"}>
<Icon icon={"user-check"} className={"mr-2"}/>
Edit Permissions
</Link>
</div>
</div>
</div>
</div>
<ReactTooltip />
</>;
}
createUserCard() {
let userRows = [];
for (let id in this.state.users.data) {
if (!this.state.users.data.hasOwnProperty(id)) {
continue;
}
let user = this.state.users.data[id];
let confirmedIcon = <Icon icon={user["confirmed"] ? "check" : "times"}/>;
let groups = [];
for (let groupId in user.groups) {
if (user.groups.hasOwnProperty(groupId)) {
let groupName = user.groups[groupId].name;
let groupColor = user.groups[groupId].color;
groups.push(
<span key={"group-" + groupId} className={"mr-1 badge text-white"} style={{backgroundColor: groupColor}}>
{groupName}
</span>
);
}
}
userRows.push(
<tr key={"user-" + id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{groups}</td>
<td>
<span data-effect={"solid"}
data-tip={user["registered_at"]}
data-place={"bottom"}>
{getPeriodString(user["registered_at"])}
</span>
</td>
<td className={"text-center"}>{confirmedIcon}</td>
<td>
<Link to={"/admin/user/edit/" + id} className={"text-reset"}>
<Icon icon={"pencil-alt"} data-effect={"solid"}
data-tip={"Modify user details & group membership"}
data-type={"info"} data-place={"right"}/>
</Link>
</td>
</tr>
);
}
while(userRows.length < this.state.rowCount) {
userRows.push(
<tr key={"empty-row-" + userRows.length}>
<td>&nbsp;</td>
<td/>
<td/>
<td/>
<td/>
<td/>
</tr>
);
}
let pages = [];
let previousDisabled = (this.state.users.page === 1 ? " disabled" : "");
let nextDisabled = (this.state.users.page >= this.state.users.pageCount ? " disabled" : "");
for (let i = 1; i <= this.state.users.pageCount; i++) {
let active = (this.state.users.page === i ? " active" : "");
pages.push(
<li key={"page-" + i} className={"page-item" + active}>
<a className={"page-link"} href={"#"} onClick={() => { if (this.state.users.page !== i) this.fetchUsers(i) }}>
{i}
</a>
</li>
);
}
return <div className={"card"}>
<div className={"card-header border-0"}>
<h3 className={"card-title"}>Users</h3>
<div className={"card-tools"}>
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/user/add"} >
<Icon icon={"plus"}/>
</Link>
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchUsers()}>
<Icon icon={"sync"}/>
</a>
</div>
</div>
<div className={"card-body table-responsive p-0"}>
<table className={"table table-striped table-valign-middle"}>
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Groups</th>
<th>Registered</th>
<th className={"text-center"}>Confirmed</th>
<th><Icon icon={"tools"} /></th>
</tr>
</thead>
<tbody>
{userRows}
</tbody>
</table>
<nav className={"row m-0"}>
<div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
Total: {this.state.users.totalCount}
</div>
<div className={"col-6 p-0"}>
<ul className={"pagination p-2 m-0 justify-content-end"}>
<li className={"page-item" + previousDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchUsers(this.state.users.page - 1)}>
Previous
</a>
</li>
{pages}
<li className={"page-item" + nextDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchUsers(this.state.users.page + 1)}>
Next
</a>
</li>
</ul>
</div>
</nav>
</div>
</div>;
}
createGroupCard() {
let groupRows = [];
for (let id in this.state.groups.data) {
if (!this.state.groups.data.hasOwnProperty(id)) {
continue;
}
let group = this.state.groups.data[id];
groupRows.push(
<tr key={"group-" + id}>
<td>{group.name}</td>
<td className={"text-center"}>{group["memberCount"]}</td>
<td>
<span className={"badge text-white mr-1"} style={{backgroundColor: group.color, fontFamily: "monospace"}}>
{group.color}
</span>
</td>
<td>
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
onClick={(e) => this.onDeleteGroup(e, id)} data-effect={"solid"}
data-tip={"Delete"} data-type={"error"}
data-place={"bottom"}/>
</td>
</tr>
);
}
while(groupRows.length < this.state.rowCount) {
groupRows.push(
<tr key={"empty-row-" + groupRows.length}>
<td>&nbsp;</td>
<td/>
<td/>
<td/>
</tr>
);
}
let pages = [];
let previousDisabled = (this.state.groups.page === 1 ? " disabled" : "");
let nextDisabled = (this.state.groups.page >= this.state.groups.pageCount ? " disabled" : "");
for (let i = 1; i <= this.state.groups.pageCount; i++) {
let active = (this.state.groups.page === i ? " active" : "");
pages.push(
<li key={"page-" + i} className={"page-item" + active}>
<a className={"page-link"} href={"#"} onClick={() => { if (this.state.groups.page !== i) this.fetchGroups(i) }}>
{i}
</a>
</li>
);
}
return <div className={"card"}>
<div className={"card-header border-0"}>
<h3 className={"card-title"}>Groups</h3>
<div className={"card-tools"}>
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/group/add"} >
<Icon icon={"plus"}/>
</Link>
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchGroups()}>
<Icon icon={"sync"}/>
</a>
</div>
</div>
<div className={"card-body table-responsive p-0"}>
<table className={"table table-striped table-valign-middle"}>
<thead>
<tr>
<th>Name</th>
<th className={"text-center"}>Members</th>
<th>Color</th>
<th><Icon icon={"tools"} /></th>
</tr>
</thead>
<tbody>
{groupRows}
</tbody>
</table>
<nav className={"row m-0"}>
<div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
Total: {this.state.groups.totalCount}
</div>
<div className={"col-6 p-0"}>
<ul className={"pagination p-2 m-0 justify-content-end"}>
<li className={"page-item" + previousDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchGroups(this.state.groups.page - 1)}>
Previous
</a>
</li>
{pages}
<li className={"page-item" + nextDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchGroups(this.state.groups.page + 1)}>
Next
</a>
</li>
</ul>
</div>
</nav>
</div>
</div>;
}
onDeleteGroup(e, id) {
e.stopPropagation();
this.parent.showDialog("Are you really sure you want to delete this group?", "Delete Group?", ["Yes", "No"], (btn) => {
if (btn === "Yes") {
this.parent.api.deleteGroup(id).then((res) => {
if (!res.success) {
let errors = this.state.errors.slice();
errors.push({title: "Error deleting group", message: res.msg});
this.setState({
...this.state,
errors: errors
});
} else {
this.setState({ ...this.state, loaded: false });
this.fetchGroups();
}
});
}
});
}
}

@ -1,208 +0,0 @@
import {Link} from "react-router-dom";
import * as React from "react";
import Alert from "../elements/alert";
import moment from 'moment'
import {Bar} from "react-chartjs-2";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
export default class Visitors extends React.Component {
constructor(props) {
super(props);
this.state = {
alerts: [],
date: new Date(),
type: 'monthly',
visitors: { }
};
this.parent = {
api: props.api,
}
}
componentDidMount() {
this.fetchData(this.state.type, this.state.date);
}
fetchData(type, date) {
this.setState({...this.state, type: type, date: date });
this.parent.api.getVisitors(type, moment(date).format("YYYY-MM-DD")).then((res) => {
if(!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching Visitor Statistics" });
this.setState({ ...this.state, alerts: alerts });
} else {
this.setState({
...this.state,
visitors: res.visitors
});
}
});
}
removeError(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
showData(type) {
if (type === this.state.type) {
return;
}
this.fetchData(type, this.state.date);
}
createLabels() {
if (this.state.type === 'weekly') {
return moment.weekdays();
} else if(this.state.type === 'monthly') {
const numDays = moment().daysInMonth();
return Array.from(Array(numDays), (_, i) => i + 1);
} else if(this.state.type === 'yearly') {
return moment.monthsShort();
} else {
return [];
}
}
createTitle() {
if (this.state.type === 'weekly') {
return "Week " + moment(this.state.date).week();
} else if(this.state.type === 'monthly') {
return moment(this.state.date).format('MMMM');
} else if(this.state.type === 'yearly') {
return moment(this.state.date).format('YYYY');
} else {
return "";
}
}
fillData(data = []) {
for (let date in this.state.visitors) {
if (!this.state.visitors.hasOwnProperty(date)) {
continue;
}
let parts = date.split("/");
let count = parseInt(this.state.visitors[date]);
if (this.state.type === 'weekly') {
let day = moment(date).day();
if (day >= 0 && day < 7) {
data[day] = count;
}
} else if(this.state.type === 'monthly') {
let day = parseInt(parts[2]) - 1;
if (day >= 0 && day < data.length) {
data[day] = count;
}
} else if(this.state.type === 'yearly') {
let month = parseInt(parts[1]) - 1;
if (month >= 0 && month < 12) {
data[month] = count;
}
}
}
}
handleChange(date) {
this.fetchData(this.state.type, date);
}
render() {
let alerts = [];
for (let i = 0; i < this.state.alerts.length; i++) {
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.alerts[i]}/>)
}
const viewTypes = ["Weekly", "Monthly", "Yearly"];
let viewOptions = [];
for (let type of viewTypes) {
let isActive = this.state.type === type.toLowerCase();
viewOptions.push(
<label key={"option-" + type.toLowerCase()} className={"btn btn-secondary" + (isActive ? " active" : "")}>
<input type="radio" autoComplete="off" defaultChecked={isActive} onClick={() => this.showData(type.toLowerCase())} />
{type}
</label>
);
}
const labels = this.createLabels();
let data = new Array(labels.length).fill(0);
this.fillData(data);
let colors = [ '#ff4444', '#ffbb33', '#00C851', '#33b5e5' ];
const title = this.createTitle();
while (colors.length < labels.length) {
colors = colors.concat(colors);
}
let chartOptions = {};
let chartData = {
labels: labels,
datasets: [{
label: 'Unique Visitors ' + title,
borderWidth: 1,
data: data,
backgroundColor: colors
}],
maintainAspectRatio: false
};
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"}>Visitor Statistics</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">Visitors</li>
</ol>
</div>
</div>
</div>
</div>
<section className={"content"}>
<div className={"container-fluid"}>
{alerts}
<div className={"row"}>
<div className={"col-4"}>
<p className={"mb-1 lead"}>Show data</p>
<div className="btn-group btn-group-toggle" data-toggle="buttons">
{viewOptions}
</div>
</div>
<div className={"col-4"}>
<p className={"mb-1 lead"}>Select date</p>
<DatePicker className={"text-center"} selected={this.state.date} onChange={(d) => this.handleChange(d)}
showMonthYearPicker={this.state.type === "monthly"}
showYearPicker={this.state.type === "yearly"} />
</div>
</div>
<div className={"row"}>
<div className={"col-12"}>
<div className="chart p-3">
<Bar data={chartData} options={chartOptions} height={100} />
</div>
</div>
</div>
</div>
</section>
</>
}
}