Compare commits
11 Commits
642083bad6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c5354afb9 | |||
| 37f134fd13 | |||
| 32e6018415 | |||
| a0adef9ec5 | |||
| c97d12e669 | |||
| 154366c20e | |||
| 54bcf8aa68 | |||
| 68501897e4 | |||
| 6e3419019b | |||
| d990c46a02 | |||
| ea3b847268 |
Generated
+2678
-22399
File diff suppressed because it is too large
Load Diff
+4
-9
@@ -8,7 +8,8 @@
|
|||||||
"serve": "sirv build --port 8080 --cors --single",
|
"serve": "sirv build --port 8080 --cors --single",
|
||||||
"dev": "preact watch",
|
"dev": "preact watch",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"buildmin": "preact build --no-sw --no-esm --no-prerender"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "preact",
|
"extends": "preact",
|
||||||
@@ -17,20 +18,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"enzyme": "^3.10.0",
|
|
||||||
"enzyme-adapter-preact-pure": "^2.0.0",
|
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-preact": "^1.1.0",
|
"eslint-config-preact": "^1.1.0",
|
||||||
"jest": "^27.5.1",
|
|
||||||
"jest-preset-preact": "^1.0.0",
|
|
||||||
"node-sass": "^6.0.1",
|
|
||||||
"preact-cli": "^3.3.5",
|
"preact-cli": "^3.3.5",
|
||||||
"sass-loader": "^10.2.1",
|
"preact-render-to-string": "^5.1.4",
|
||||||
"sirv-cli": "1.0.3"
|
"sass-loader": "^10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"preact": "^10.3.2",
|
"preact": "^10.3.2",
|
||||||
"preact-render-to-string": "^5.1.4",
|
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
"sass": "^1.49.9"
|
"sass": "^1.49.9"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
[{"timestamp":1646927116411,"files":[{"filename":"index.html","previous":487,"size":487,"diff":0},{"filename":"bundle.45d14.css","previous":1691,"size":1691,"diff":0},{"filename":"bundle.*****.js","previous":14680,"size":14692,"diff":12},{"filename":"polyfills.*****.js","previous":2288,"size":2288,"diff":0}]},{"timestamp":1646924506348,"files":[{"filename":"index.html","previous":487,"size":487,"diff":0},{"filename":"bundle.45d14.css","previous":1691,"size":1691,"diff":0},{"filename":"bundle.*****.js","previous":12153,"size":14680,"diff":2527},{"filename":"polyfills.*****.js","previous":2288,"size":2288,"diff":0}]},{"timestamp":1646755354217,"files":[{"filename":"bundle.*****.esm.js","previous":9839,"size":0,"diff":-9839},{"filename":"polyfills.*****.esm.js","previous":2187,"size":0,"diff":-2187},{"filename":"sw.js","previous":10597,"size":0,"diff":-10597},{"filename":"sw-esm.js","previous":10603,"size":0,"diff":-10603},{"filename":"polyfills.058fb.js","previous":2288,"size":0,"diff":-2288},{"filename":"index.html","previous":521,"size":487,"diff":-34},{"filename":"200.html","previous":521,"size":0,"diff":-521},{"filename":"bundle.45d14.css","previous":1691,"size":1691,"diff":0},{"filename":"bundle.caa2d.js","previous":12219,"size":0,"diff":-12219},{"filename":"bundle.*****.js","previous":0,"size":12153,"diff":12153},{"filename":"polyfills.*****.js","previous":0,"size":2288,"diff":2288}]},{"timestamp":1646755194517,"files":[{"filename":"bundle.2a54a.css","previous":1709,"size":0,"diff":-1709},{"filename":"bundle.*****.esm.js","previous":9842,"size":9839,"diff":-3},{"filename":"polyfills.*****.esm.js","previous":2187,"size":2187,"diff":0},{"filename":"sw.js","previous":10595,"size":10597,"diff":2},{"filename":"sw-esm.js","previous":10600,"size":10603,"diff":3},{"filename":"polyfills.058fb.js","previous":2288,"size":2288,"diff":0},{"filename":"index.html","previous":536,"size":521,"diff":-15},{"filename":"200.html","previous":536,"size":521,"diff":-15},{"filename":"bundle.3030b.js","previous":12224,"size":0,"diff":-12224},{"filename":"bundle.45d14.css","previous":0,"size":1691,"diff":1691},{"filename":"bundle.caa2d.js","previous":0,"size":12219,"diff":12219}]},{"timestamp":1646755054393,"files":[{"filename":"bundle.2a54a.css","previous":1709,"size":1709,"diff":0},{"filename":"bundle.*****.esm.js","previous":9856,"size":9842,"diff":-14},{"filename":"polyfills.*****.esm.js","previous":2187,"size":2187,"diff":0},{"filename":"sw.js","previous":10599,"size":10595,"diff":-4},{"filename":"sw-esm.js","previous":10603,"size":10600,"diff":-3},{"filename":"bundle.1a7a6.js","previous":11947,"size":0,"diff":-11947},{"filename":"polyfills.058fb.js","previous":2288,"size":2288,"diff":0},{"filename":"index.html","previous":536,"size":536,"diff":0},{"filename":"200.html","previous":536,"size":536,"diff":0},{"filename":"bundle.3030b.js","previous":0,"size":12224,"diff":12224}]},{"timestamp":1646751614979,"files":[{"filename":"bundle.2a54a.css","previous":0,"size":1709,"diff":1709},{"filename":"bundle.*****.esm.js","previous":0,"size":9856,"diff":9856},{"filename":"polyfills.*****.esm.js","previous":0,"size":2187,"diff":2187},{"filename":"sw.js","previous":0,"size":10599,"diff":10599},{"filename":"sw-esm.js","previous":0,"size":10603,"diff":10603},{"filename":"bundle.1a7a6.js","previous":0,"size":11947,"diff":11947},{"filename":"polyfills.058fb.js","previous":0,"size":2288,"diff":2288},{"filename":"index.html","previous":0,"size":536,"diff":536},{"filename":"200.html","previous":0,"size":536,"diff":536}]}]
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
const server = 'http://192.168.20.1'
|
||||||
|
const timeout = 1500;
|
||||||
|
async function restoreSession(reducer = null) {
|
||||||
|
|
||||||
|
let token = localStorage.getItem('token');
|
||||||
|
let username = localStorage.getItem('username');
|
||||||
|
let active = token !== null && username !== null && await checkAuth(token);
|
||||||
|
if (active && reducer !== null) {
|
||||||
|
reducer({ type: 'start', token, username });
|
||||||
|
}
|
||||||
|
return (active ? { active: true, token, username } : { active: false });
|
||||||
|
}
|
||||||
|
async function storeSession(result, token) {
|
||||||
|
result.token = await token;
|
||||||
|
result.type = 'start';
|
||||||
|
console.log(result)
|
||||||
|
if (result.permanent) {
|
||||||
|
localStorage.setItem('token', result.token);
|
||||||
|
localStorage.setItem('username', result.username);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(formdata) {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('username', formdata.username)
|
||||||
|
formData.append('password', formdata.password);
|
||||||
|
formData.append('action', 'login');
|
||||||
|
let result = { username: formdata.username, permanent: formdata.permanent }
|
||||||
|
const controller = new AbortController();
|
||||||
|
const id = setTimeout(() => controller.abort(), timeout);
|
||||||
|
const resp = await fetch(`${server}/api/auth`, { signal: controller.signal, method: 'POST', mode: 'cors', body: formData })
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.ok) return storeSession(result, resp.text());
|
||||||
|
else if (resp.status === 401)
|
||||||
|
result.error = 'login_failed';
|
||||||
|
else throw new Error(resp.error);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
result.error = 'network_connection';
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async function logout(token) {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('token', token);
|
||||||
|
formData.append('action', 'logout');
|
||||||
|
const resp = fetch(`${server}/api/auth`, { method: 'POST', mode: 'cors', body: formData }).then(resp => resp.text());
|
||||||
|
localStorage.clear();
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
async function checkAuth(token) {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('token', token);
|
||||||
|
formData.append('action', 'check');
|
||||||
|
const resp = await fetch(`${server}/api/auth`, { method: 'POST', mode: 'cors', body: formData }).then(resp => resp.text()).then(text => text === 'valid');
|
||||||
|
return await resp;
|
||||||
|
}
|
||||||
|
async function fetchdb(token = 'azif7eqCl5') {
|
||||||
|
const resp = await fetch(`${server}/api/userdb`, { method: 'GET', mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.text()).then(text => parsedb(text))
|
||||||
|
return resp;
|
||||||
|
|
||||||
|
}
|
||||||
|
function createCsvTable(userdb) {
|
||||||
|
let res = '';
|
||||||
|
let first = true;
|
||||||
|
for (let u of userdb) {
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
res += `${!first && '\n'}${u.uid},${u.first_name},${u.last_name},${u.rfid_uid},${u.user_pin},${u.enabled ? '1' : '0'}`;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
async function updateUser(token, user) {
|
||||||
|
const resp = await fetch(`${server}/api/user/`, { method: 'POST', body: JSON.stringify(user), mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.json());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
async function deleteUser(token, user) {
|
||||||
|
const resp = await fetch(`${server}/api/user/${user.uid}`, { method: 'DELETE', body: JSON.stringify(user), mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.json());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
async function createUser(token, user) {
|
||||||
|
const resp = await fetch(`${server}/api/user/`, { method: 'PUT', body: JSON.stringify(user), mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.json());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
function parsedb(raw) {
|
||||||
|
|
||||||
|
let lines = raw.split('\n');
|
||||||
|
let users = [];
|
||||||
|
lines.map((l, line) => {
|
||||||
|
let [uid, first_name, last_name, rfid_uid, user_pin, enabled] = l.split([';']);
|
||||||
|
users.push({ line, uid, first_name, last_name, rfid_uid, user_pin, enabled: enabled[0] === '1' });
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function catchRFID(token) {
|
||||||
|
const resp = await fetch(`${server}/api/rfid`, { method: 'GET', mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.json());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function config(token, payload) {
|
||||||
|
let reqdata = new FormData();
|
||||||
|
for (let [k, v] of Object.entries(payload))
|
||||||
|
if (['number', 'string'].includes(typeof (v)))
|
||||||
|
reqdata.append(k, v);
|
||||||
|
const abort = new AbortController();
|
||||||
|
const aborttimer = setTimeout(() => abort.abort(), timeout);
|
||||||
|
return fetch(`${server}/api/config`, { signal: abort.signal, method: 'POST', mode: 'cors', body: reqdata, headers:{Authentification: token} }).
|
||||||
|
then(resp => resp.ok ? resp.json() : { error: resp.status });
|
||||||
|
}
|
||||||
|
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID, config };
|
||||||
|
export default { ...publicfunctions }
|
||||||
|
export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID, config }
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,15 +1,15 @@
|
|||||||
import { h } from "preact";
|
import { h } from "preact";
|
||||||
|
|
||||||
|
import { Link } from "preact-router";
|
||||||
|
|
||||||
|
|
||||||
const Pageselector = (props) => {
|
const Pageselector = (props) => {
|
||||||
var items = [];
|
var items = [];
|
||||||
if(!isNaN(props.start)&&!isNaN(props.end)&&!isNaN(props.current))
|
if(!isNaN(props.start)&&!isNaN(props.end)&&!isNaN(props.current))
|
||||||
for(var i=props.start; i<=props.end; i++){
|
for(var i=props.start; i<=props.end; i++){
|
||||||
items.push(<li><a {...(i==props.current?{className:'active'}:{})} href="#">{i}</a></li>);
|
items.push(<li><Link {...(i==props.current?{className:'active'}:{})} href={`/users/${i}`} value={i} >{i}</Link></li>);
|
||||||
}
|
}
|
||||||
items.push(<li><a href="#">></a></li>);
|
//items.push(<li><a href="" >></a></li>);
|
||||||
return (
|
return (
|
||||||
<div class="page-nav-bar">
|
<div class="page-nav-bar">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
+64
-45
@@ -1,57 +1,76 @@
|
|||||||
import { Component, createRef, h } from "preact";
|
import { h } from "preact";
|
||||||
import { Router } from "preact-router";
|
import { Router, route } from "preact-router";
|
||||||
import Header from "./header";
|
import { useEffect, useReducer, useState } from "preact/hooks";
|
||||||
import Login from "../routes/login";
|
import { Header, Menu } from "./index.js";
|
||||||
import Profile from "../routes/profile";
|
import { Home, Users, EditUser, Login, Logout, System } from "../route";
|
||||||
import Menu from "./menu";
|
import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store";
|
||||||
import Users from "../routes/users";
|
import api from "../api/index.js";
|
||||||
import { useCallback, useState } from "preact/hooks";
|
|
||||||
const menu = createRef();
|
const menu_items = [
|
||||||
class App extends Component {
|
{ text: "Übersicht", path: "/" },
|
||||||
toggleMenu() {
|
{ text: "Benutzer anlegen", path: "/newuser" },
|
||||||
const [visible, setValue] = useState(false);
|
{ text: "Benutzer verwalten", path: "/users" },
|
||||||
const toggle = useCallback(() => {
|
{ text: "System", path: "/system" },
|
||||||
setValue(!visible);
|
{ text: "Abmelden", path: "/logout" }
|
||||||
return !visible;
|
]
|
||||||
}, [visible]);
|
|
||||||
return { visible, toggle };
|
function App() {
|
||||||
}
|
// useReducer
|
||||||
authenticateUser() {
|
const menu = useReducer(menuReducer, false);
|
||||||
const [session, setValue] = useState({ token: null });
|
const session = useReducer(sessionReducer, {});
|
||||||
const login = (username, password) => {
|
const [usertable, userreducer] = useReducer(userTableReducer, []);
|
||||||
if (username == "admin" && password == "admin") {
|
const [lasturl, setlasturl] = useState("/");
|
||||||
setValue({ ...session, token: "ABCDEFG" });
|
if(!session[0]||(session[0]&&session[0].active===undefined))
|
||||||
return true;
|
api.restoreSession(session[1]);
|
||||||
|
useEffect(()=>{
|
||||||
|
if(session[0]&&session[0].active){
|
||||||
|
api.fetchdb(session[0].token).then(imported => {
|
||||||
|
let action = { type: 'import', imported };
|
||||||
|
userreducer(action);
|
||||||
|
});
|
||||||
|
}},[session]);
|
||||||
|
const handleRoute = async e => {
|
||||||
|
let active = session[0].active;
|
||||||
|
|
||||||
|
switch (e.url) {
|
||||||
|
default:
|
||||||
|
if (!active) route('/login', true);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const logout = () => setValue({ ...session, token: null });
|
|
||||||
const isAuthenticated = () => session.token != null;
|
|
||||||
return { login, logout, isAuthenticated };
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const menu = this.toggleMenu();
|
|
||||||
const auth = this.authenticateUser();
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
|
<AppStateProvider value={{ menu, session }} >
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<Header menu={menu} auth={auth} />
|
<Header title={"Doorlock"} />
|
||||||
<Menu menu={menu} />
|
|
||||||
{!menu.visible &&
|
|
||||||
(!auth.isAuthenticated() ? (
|
|
||||||
<Login auth={auth} />
|
|
||||||
) : (
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<Router>
|
<Menu items={menu_items} />
|
||||||
<Profile path="/test/" user="me" />
|
{!menu[0] &&
|
||||||
<Profile path="/profile/:user" />
|
<UserTableProvider value={{ usertable, userreducer }} >
|
||||||
<Users path="/users" />
|
<Router onChange={handleRoute}>
|
||||||
|
<Home path="/" user="me" />
|
||||||
|
<Login path="login" />
|
||||||
|
<System path="/system" />
|
||||||
|
<Logout path="/logout" />
|
||||||
|
<Users path="/users/:pageid?" />
|
||||||
|
<EditUser path="/edituser/:userid?" />
|
||||||
|
<EditUser path="/newuser" />
|
||||||
<div class="container" default>Error 404</div>
|
<div class="container" default>Error 404</div>
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</UserTableProvider>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div className="container" style={'text-align: center; align-items: center'}>
|
||||||
|
<span>© Jean Jacques Avril 2022 </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {h} from 'preact';
|
import {h} from 'preact';
|
||||||
|
|
||||||
const Breadcrumbs = (props) =>{ if(props.items) return(
|
const Breadcrumbs = ({items}) =>{ if(items) return(
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<ul>
|
<ul>
|
||||||
{props.items.map((text,i)=><li key={i}><a href="#">{text}</a> </li>)}
|
{items.map((text,i)=><li key={i}><a href="#">{text}</a></li>)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import {TextBox,CheckBox,Button} from './input'
|
||||||
|
export {TextBox,CheckBox,Button}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { h, render } from 'preact'
|
||||||
|
|
||||||
|
function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength, overridevalue, onChange, onInput, checkInput, allowdChars, showavailable,hinttext }) {
|
||||||
|
|
||||||
|
if (typeof (onInput) === 'undefined' && typeof (formchange) === 'function') {
|
||||||
|
onInput = (e) => formchange(p => {
|
||||||
|
if (typeof (checkInput) === 'function' && !checkInput(e.target.value)) {
|
||||||
|
return ({ ...p, error: { [e.target.id]: 'input_error' } })
|
||||||
|
}
|
||||||
|
return ({ ...p, [e.target.id]: e.target.value, error: { [e.target.id]: undefined } })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const onBlur = (e)=>{
|
||||||
|
formchange(p =>({ ...p, error: { [e.target.id]: undefined } }));
|
||||||
|
}
|
||||||
|
return (<>
|
||||||
|
<div className={'textbox'} >
|
||||||
|
<input placeholder=" " disabled={disabled} type={type} value={overridevalue ? overridevalue : formdata[id] ? formdata[id] : ''} id={id} onBlur={onBlur} onInput={onInput} onChange={onChange} maxlength={maxlength} name={`form-${id}`} />
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
{formdata&&formdata.error&&formdata.error[id]&&hinttext&&<div className='textbox__hint'>{hinttext}</div>}
|
||||||
|
</div>
|
||||||
|
{showavailable && <span class="textbox__info"> {formdata[id] ? formdata[id].length : 0} von {maxlength} Zeichen verwendet.</span>}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
function CheckBox({ id, disabled, label, formdata, formchange }) {
|
||||||
|
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.checked });
|
||||||
|
return (<div className={'checkbox'} >
|
||||||
|
<input disabled={disabled} type={'checkbox'} checked={formdata[id] ? 'on' : ''} id={id} onChange={onChange} name={`from-${id}`} />
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
function Button({ onClick, children, type }) {
|
||||||
|
return (<div className={'button'} onClick={onClick} >
|
||||||
|
<button type={type} >{children}</button>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
export { TextBox, CheckBox, Button }
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Component, createRef, h } from 'preact';
|
import { useContext } from 'preact/hooks';
|
||||||
//import { Link } from 'preact-router/match';
|
import AppState from '../../store/AppState';
|
||||||
|
const Header = ({title}) => {
|
||||||
const Header = (props) => (
|
let { menu, session } = useContext(AppState);
|
||||||
|
let [menu_shown, toggle_menu] = menu;
|
||||||
|
let [sessiondata] = session;
|
||||||
|
return (
|
||||||
<header className='header'>
|
<header className='header'>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h1>Login</h1>
|
<h1>{title}</h1>
|
||||||
{props.auth.isAuthenticated() && (<div id="hamburger-button" className={`hamburger ${props.menu.visible && 'hamburger-active'}`}
|
{ sessiondata.active &&(<div id="hamburger-button" className={`hamburger ${menu_shown && 'hamburger-active'}`}
|
||||||
onClick={() => props.menu.toggle()}>
|
onClick={() => toggle_menu('toggle')}>
|
||||||
<hr />
|
<hr />
|
||||||
<hr />
|
<hr />
|
||||||
<hr />
|
<hr />
|
||||||
@@ -14,7 +17,7 @@ const Header = (props) => (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import App from "./app";
|
||||||
|
import Breadcrumbs from "./breadcrumbs";
|
||||||
|
import Pageselector from "./Pageselector";
|
||||||
|
import Header from "./header";
|
||||||
|
import UserList from "./userlist";
|
||||||
|
import Menu from "./menu";
|
||||||
|
import Warnbox from "./warnbox";
|
||||||
|
export {App, Breadcrumbs, Pageselector, Header, UserList, Menu, Warnbox}
|
||||||
|
export default App
|
||||||
@@ -1,35 +1,24 @@
|
|||||||
import { Component, h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { Link } from 'preact-router';
|
import { Link } from 'preact-router';
|
||||||
class Menu extends Component {
|
import { useContext } from 'preact/hooks';
|
||||||
|
import AppState from '../../store/AppState';
|
||||||
menu_items = [
|
function Menu({ items }) {
|
||||||
{ text: "Übersicht", path: "/" },
|
let [menu_shown, toggle_menu] = useContext(AppState).menu;
|
||||||
{ text: "Benutzer", path: "/users" },
|
if (!items) return;
|
||||||
{ text: "System", path: "/system" },
|
const onClick = (e) => {
|
||||||
{ text: "Backup", path: "/backup" },
|
|
||||||
{ text: "Abmelden", path: "/logout" }
|
|
||||||
]
|
|
||||||
|
|
||||||
getAlert() {
|
|
||||||
alert("getAlert from Child");
|
|
||||||
}
|
|
||||||
onClick = (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.menu.toggle();
|
toggle_menu('hide');
|
||||||
}
|
}
|
||||||
render(props, state) {
|
if (menu_shown)
|
||||||
if (props.menu.visible)
|
|
||||||
return (
|
return (
|
||||||
<div class="container" >
|
<div class="container" >
|
||||||
<nav className='menu' >
|
<nav className='menu' >
|
||||||
<ul>
|
<ul>
|
||||||
<li><Link href="/test" >Test</Link></li>
|
{items.map((element, i) => (<li key={i}><Link href={element.path} onClick={onClick} >{element.text}</Link></li>))}
|
||||||
{this.menu_items.map((element, i) => (<li key={i}><Link href={element.path} onClick={this.onClick} >{element.text}</Link></li>))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</div>)
|
</div>);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Menu;
|
export default Menu;
|
||||||
@@ -1,40 +1,33 @@
|
|||||||
import { Component } from "preact";
|
import { h } from "preact";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserList extends Component {
|
function UserList({userlist,editUser, deleteUser, start, end}) {
|
||||||
|
|
||||||
|
|
||||||
deleteUser(user){
|
const displayUser=(user)=>{
|
||||||
alert(`delete: ${user.uid}`);
|
|
||||||
}
|
|
||||||
editUser(user){
|
|
||||||
alert(`edit: ${user.uid}`);
|
|
||||||
}
|
|
||||||
displayUser(user,key){
|
|
||||||
return (
|
return (
|
||||||
<div key={key} class="user-list-item">
|
<div key={`user${user.line}`} class="user-list-item" >
|
||||||
<div class="user-attributes">
|
<div class="user-attributes">
|
||||||
<span><b>UID:</b> {user.uid}</span> <span>(<b>Aktiv</b>)</span><br />
|
<span><b>UID:</b> {user.uid}</span> <span>(<b>{user.enabled?'Aktiv':'Inaktiv'}</b>)</span><br />
|
||||||
<span><b>Vorname:</b> {user.first_name}</span><br />
|
<span><b>Vorname:</b> {user.first_name}</span><br />
|
||||||
<span><b>Nachname:</b> {user.last_name}</span><br />
|
<span><b>Nachname:</b> {user.last_name}</span><br />
|
||||||
<span><b>RFID:</b> {user.rfid}</span>
|
<span><b>RFID:</b> {user.rfid_uid}</span>
|
||||||
<span><b>PIN:</b> {user.pin}</span>
|
<span><b>PIN:</b> {user.user_pin}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn-trash" onClick={()=>this.deleteUser(user)}>Löschen</button>
|
<button class="btn-trash" onClick={()=>deleteUser(user)}>Löschen</button>
|
||||||
<button class="btn-edit" onClick={()=>this.editUser(user)}>Bearbeiten</button>
|
<button class="btn-edit" onClick={()=>editUser(user)}>Bearbeiten</button>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {};
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="row">
|
||||||
{this.props.userlist&&this.props.userlist.map((user,i)=>this.displayUser(user,i))}
|
{userlist&&(start!==undefined&&end?userlist.slice(start,end).map((user)=>displayUser(user)):userlist.map((user)=>displayUser(user)))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export default UserList
|
export default UserList
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
function Warnbox({ title = "Fehler", children }) {
|
||||||
|
return (<div className={'warnbox'} >
|
||||||
|
|
||||||
|
<span className='warnbox__icon'>⚠</span>
|
||||||
|
|
||||||
|
<div className="column">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{children && <span> {children}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Warnbox
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import './style/style.sass';
|
import './style/style.sass';
|
||||||
import App from './components/app';
|
import App from './components';
|
||||||
import { h, render, Component } from "preact"
|
import { h, render } from "preact"
|
||||||
|
|
||||||
render(<App />, document.body)
|
render(<App />, document.body)
|
||||||
//export default App;
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useContext, useEffect, useState, useCallback } from 'preact/hooks';
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import AppState, { UserTable } from "../../store";
|
||||||
|
import api from '../../api'
|
||||||
|
import { Breadcrumbs } from "../../components";
|
||||||
|
import { CheckBox, Button, TextBox } from '../../components/controls';
|
||||||
|
function EditUser({ userid }) {
|
||||||
|
let [sessiondata,] = useContext(AppState).session;
|
||||||
|
const { usertable, userreducer } = useContext(UserTable);
|
||||||
|
const [formdata, formchange] = useState({});
|
||||||
|
const [rfidscan, setscan] = useState({active:false, rfidscaninterval:null});
|
||||||
|
let rfidscaninterval = null;
|
||||||
|
let maxlength_uid = 10;
|
||||||
|
let maxlength_name = 25;
|
||||||
|
let maxlength_pin = 10;
|
||||||
|
let maxlength_rfid = 8;
|
||||||
|
useEffect(() => {
|
||||||
|
if (rfidscan.rfidscaninterval === null && rfidscan.active) {
|
||||||
|
rfidscan.rfidscaninterval = setInterval(() => {
|
||||||
|
api.catchRFID(sessiondata.token).then(result => {
|
||||||
|
if (result.rfid_uid) {
|
||||||
|
formchange(old => ({ ...old, rfid_uid: result.rfid_uid }))
|
||||||
|
setscan(p => ({...p,active:false}))
|
||||||
|
}
|
||||||
|
}).catch(() => { setscan(p => ({...p,active:false})) });
|
||||||
|
}, 1000);
|
||||||
|
} else if (rfidscan.rfidscaninterval !== null && !rfidscan.active) {
|
||||||
|
clearInterval(rfidscan.rfidscaninterval);
|
||||||
|
setscan(p => ({...p,rfidscaninterval:null}));
|
||||||
|
}
|
||||||
|
}, [rfidscan]);
|
||||||
|
useEffect(() => {
|
||||||
|
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
||||||
|
if (user !== undefined) {
|
||||||
|
formchange(user);
|
||||||
|
}
|
||||||
|
}, [userid, usertable]);
|
||||||
|
function uidCount(state, user) {
|
||||||
|
return state.reduce((pre, curr) => pre + curr.uid == user.uid ? 1 : 0, 0);
|
||||||
|
}
|
||||||
|
const onReset = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
||||||
|
formchange(user !== undefined ? user : {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const runScan = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setscan(p => ({...p,active:!p.active}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const onSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let idcount = uidCount(usertable, formdata);
|
||||||
|
console.log(idcount);
|
||||||
|
if (formdata.line !== undefined) {
|
||||||
|
api.updateUser(sessiondata.token, formdata).then(r => {
|
||||||
|
userreducer({ type: 'update', user: r })
|
||||||
|
});
|
||||||
|
route('/users');
|
||||||
|
} else if (idcount === 0) {
|
||||||
|
api.createUser(sessiondata.token, formdata).then(r => {
|
||||||
|
userreducer({ type: 'create', user: r })
|
||||||
|
});
|
||||||
|
route('/users');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert(`UID ${formdata.uid} wurde bereits ${idcount} mal benutzt`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='container'>
|
||||||
|
<Breadcrumbs items={['Benutzerverwaltung', formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"]} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<h2>{formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"}</h2>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<h3>Eindeutige Identifikationsnummer</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<b>Info</b>
|
||||||
|
<span> Nummer muss einmalig sein</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Persönliche Daten</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox formdata={formdata} formchange={formchange} id="first_name" label="Vorname" maxlength={maxlength_name} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} showavailable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Authentifizierung</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} disabled={rfidscan.active} overridevalue={rfidscan.active&&"Scannen..."} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={runScan}>{rfidscan.active ? "Abbrechen" : "Scannen"}</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<b>Info</b>
|
||||||
|
<span>Achten Sie darauf, eine PIN <br />nicht mehrfach zu benutzen.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Status</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<CheckBox formdata={formdata} formchange={formchange} id="enabled" type={'checkbox'} label="Benutzer darf sich anmelden" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='row' style={'justify-content: space-between'}>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={onSubmit} >Speichern</Button>
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={onReset} >Zurücksetzen</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default EditUser;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { Link } from 'preact-router';
|
||||||
|
import { useContext } from 'preact/hooks';
|
||||||
|
import AppState from '../../store/AppState';
|
||||||
|
import { Breadcrumbs } from "../../components";
|
||||||
|
function Home() {
|
||||||
|
let [sessiondata, ] = useContext(AppState).session;
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumbs items={['Startseite']} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<h2>Startseite</h2>
|
||||||
|
<p>Willkommen zurück {sessiondata.username} </p>
|
||||||
|
<div>
|
||||||
|
<h3>Status</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Aktiv seit: 12.01.2022</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Hier können Sie den Türöffner manuell aktivieren.</p>
|
||||||
|
<p>Um Benutzer anzulegen, öffnen sie das Menü und tippen auf <Link href="/newuser">Benutzer anlegen</Link> </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default Home;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import Home from "./home"
|
||||||
|
import Users from './users'
|
||||||
|
import EditUser from "./edituser"
|
||||||
|
import Profile from "./profile"
|
||||||
|
import Login from "./login"
|
||||||
|
import Logout from './logout'
|
||||||
|
import System from './system'
|
||||||
|
export {Home, Users, EditUser, Profile, Login, Logout, System }
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import { useContext, useState } from 'preact/hooks';
|
||||||
|
import AppState from '../../store/AppState';
|
||||||
|
import { Breadcrumbs, Warnbox } from '../../components';
|
||||||
|
import { CheckBox, Button, TextBox } from '../../components/controls';
|
||||||
|
import api from '../../api'
|
||||||
|
function Login() {
|
||||||
|
let [sessiondata, setsession] = useContext(AppState).session;
|
||||||
|
const [fromdata, setform] = useState({ username: '', password: '', error: null });
|
||||||
|
const navigation = ["Login"];
|
||||||
|
if (sessiondata.active)
|
||||||
|
route('/', true);
|
||||||
|
function onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
api.login(fromdata).then(result => {
|
||||||
|
if (result.error !== undefined)
|
||||||
|
setform(result);
|
||||||
|
else {
|
||||||
|
setsession(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumbs items={navigation} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<h2>Anmeldung</h2>
|
||||||
|
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
|
||||||
|
|
||||||
|
{fromdata.error === 'login_failed' && <Warnbox title='Anmeldefehler'>Ungültige Anmeldedaten.<br />Bitte überprüfen Sie den eingebenen Benutzernamen und das Passwort.</Warnbox>}
|
||||||
|
{fromdata.error === 'network_connection' && <Warnbox title='Netwerkfehler'>Die Kommunikation mit dem Gerät ist zurzeit nicht möglich.<br />Bitte überprüfen Sie die Netzwerkverbindung.</Warnbox>}
|
||||||
|
<form id="login_form" onSubmit={onSubmit} >
|
||||||
|
<div className='row'>
|
||||||
|
<TextBox maxlength={25} formdata={fromdata} formchange={setform} id="username" label="Benutzername" />
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<TextBox maxlength={25} formdata={fromdata} formchange={setform} id="password" label="Passwort" type='password' />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<CheckBox id="permanent" formdata={fromdata} formchange={setform} label="Angemeldet bleiben?" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<Button onClick={onSubmit}>Anmelden</Button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { Link } from 'preact-router';
|
||||||
|
import { useContext, useState } from 'preact/hooks';
|
||||||
|
import AppState from '../../store/AppState';
|
||||||
|
import { Breadcrumbs } from '../../components';
|
||||||
|
import api from '../../api'
|
||||||
|
function Logout() {
|
||||||
|
const navigation = ["Logout"];
|
||||||
|
let [text, settext] = useState('');
|
||||||
|
let [sessiondata, setsession] = useContext(AppState).session;
|
||||||
|
this.shouldComponentUpdate = function () {
|
||||||
|
console.log('functional component vs closures');
|
||||||
|
}
|
||||||
|
if (sessiondata.active) {
|
||||||
|
api.logout(sessiondata.token).then((r) => r === 'success' ? settext('Sitzung beendet.') : settext('Sitzung war bereits nicht mehr vorhanden.'));
|
||||||
|
setsession({ type: 'end' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumbs items={navigation} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<h2>
|
||||||
|
Erfolgreich abgemeldet:
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{text}<br />
|
||||||
|
<Link className={'button'} href="/login">Erneut Anmelden</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logout;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
// Note: `user` comes from the URL, courtesy of our router
|
||||||
|
const Profile = ({ user }) => {
|
||||||
|
const [time, setTime] = useState(Date.now());
|
||||||
|
const [count, setCount] = useState(10);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer = setInterval(() => setTime(Date.now()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container'>
|
||||||
|
<div >
|
||||||
|
<h1>Profile: {user}</h1>
|
||||||
|
<p>This is the user profile for a user named {user}.</p>
|
||||||
|
|
||||||
|
<div>Current time: {new Date(time).toLocaleString()}</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button onClick={() => setCount((count) => count + 1)}>Click Me</button>
|
||||||
|
{' '}
|
||||||
|
Clicked {count} times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { Breadcrumbs } from "../../components";
|
||||||
|
import { useContext, useEffect, useState } from 'preact/hooks';
|
||||||
|
import { CheckBox, Button, TextBox } from '../../components/controls';
|
||||||
|
import AppState from '../../store/AppState';
|
||||||
|
import api from '../../api'
|
||||||
|
const checkIP = (v)=>{
|
||||||
|
let parts = v.split('.');
|
||||||
|
if(parts.length>4) return false;
|
||||||
|
for(let part of parts)
|
||||||
|
if(isNaN(part)||part>255||part<0||part.length>3)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function System() {
|
||||||
|
let [session,] = useContext(AppState).session;
|
||||||
|
let [formdata, setform] = useState({});
|
||||||
|
useEffect(() => {
|
||||||
|
api.config(session.token, { action: "get" }).then(r => setform(p => ({ ...p, ...r })));
|
||||||
|
}, [session])
|
||||||
|
return (
|
||||||
|
<div className='container'>
|
||||||
|
<Breadcrumbs items={['Systemeinstellungen']} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<h2>System</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<h3>Geräteeinstellungen</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="mode" label="Modus" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=1} maxlength={1} showavailable hinttext="Nur Zahlen 0 bis 1" />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="fail_timeout" label="Login Timeout" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=255} maxlength={3} showavailable hinttext="Nur Zahlen 0 bis 255" />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="hold_time" label="Haltezeit" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=255} maxlength={3} showavailable hinttext="Nur Zahlen 0 bis 255"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>WiFi Setup</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="SSID" label="WiFi Name" maxlength={31} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="PASS" label="WiFi Passwort" maxlength={31} showavailable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="ip" label="IP Adresse" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 192.168.20.1" />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Netzmaske" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 255.255.255.0"/>
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="gw" label="Gateway" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 0.0.0.0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={() => null} >Speichern</Button>
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={() => null} >Neustart</Button>
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={() => null} >Zurücksetzen</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form>
|
||||||
|
<h3>Admin User</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="ip" label="Name" maxlength={15} showavailable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Passwort" maxlength={15} showavailable />
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Passwort wiederholen" maxlength={15} showavailable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={() => null} >Speichern</Button>
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={() => null} >Zurücksetzen</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h3>Datenbank Backup</h3>
|
||||||
|
<div className='row'>
|
||||||
|
<div className="column">
|
||||||
|
<h4>Backup einspielen</h4>
|
||||||
|
<form>
|
||||||
|
<input type={'file'} />
|
||||||
|
<input type={'submit'} value={'Hochladen'} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<h4>Backup herunterladen</h4>
|
||||||
|
<button>Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
export default System
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { h } from "preact";
|
||||||
|
import { UserList, Pageselector, Breadcrumbs } from "../../components";
|
||||||
|
import api from '../../api'
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import { useContext, useEffect, useState, useMemo } from "preact/hooks";
|
||||||
|
import AppState, { UserTable } from "../../store";
|
||||||
|
function Users({ pageid }) {
|
||||||
|
const { usertable, userreducer } = useContext(UserTable);
|
||||||
|
let [sessiondata,] = useContext(AppState).session;
|
||||||
|
const [viewstate, setview] = useState({ limit: 100, page: 1, pages: 0, query: '' });
|
||||||
|
const matchUser = (user, query, fields = ['uid', 'first_name', 'last_name'], exact = false) => {
|
||||||
|
let words = query.toLowerCase().split(' ');
|
||||||
|
let matches = 0;
|
||||||
|
|
||||||
|
for (let word of words) {
|
||||||
|
let match = false;
|
||||||
|
for (let key of fields){
|
||||||
|
let cmp = user[key].toLowerCase();
|
||||||
|
if (exact ? cmp == word : cmp.includes(word)) {
|
||||||
|
match = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
matches += 1;
|
||||||
|
if(matches >= words.length) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const results = useMemo(() => (viewstate.query !== '') ? usertable.filter(u => matchUser(u, viewstate.query)) : usertable, [viewstate.query, usertable]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const setPage = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(e)
|
||||||
|
let page = e.target.text;
|
||||||
|
setview(state => ({ ...state, page }));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setview((state) => {
|
||||||
|
let pages = Math.ceil(results.length / state.limit)
|
||||||
|
if (state.page >= pages) return ({ ...state, pages, page: pages })
|
||||||
|
return ({ ...state, pages })
|
||||||
|
})
|
||||||
|
}, [results.length, viewstate.limit])
|
||||||
|
useEffect(() => {
|
||||||
|
if (pageid && !isNaN(pageid))
|
||||||
|
setview((state) => ({ ...state, page: pageid }))
|
||||||
|
}, [pageid])
|
||||||
|
|
||||||
|
const deleteUser = (user) => {
|
||||||
|
api.deleteUser(sessiondata.token, user).then(r => {
|
||||||
|
let action = {
|
||||||
|
type: 'delete',
|
||||||
|
user, r
|
||||||
|
}
|
||||||
|
userreducer(action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const calculateView = () => {
|
||||||
|
let start = viewstate.limit * (viewstate.page - 1);
|
||||||
|
let end = viewstate.limit * (viewstate.page);
|
||||||
|
return { start, end }
|
||||||
|
}
|
||||||
|
const editUser = (user) => {
|
||||||
|
route(`/edituser/${user.uid}`);
|
||||||
|
}
|
||||||
|
const setLimit = (e) => {
|
||||||
|
setview(state => ({ ...state, limit: e.target.value }));
|
||||||
|
console.log(e.target.value);
|
||||||
|
console.log(viewstate);
|
||||||
|
}
|
||||||
|
const navigation = ["Benutzerverwaltung",'Übersicht'];
|
||||||
|
//
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumbs items={navigation} />
|
||||||
|
<div className={'contentbox'} >
|
||||||
|
<div>Suche: <input type={'text'} onInput={(e) => setview(state => ({ ...state, query: e.target.value }))} value={viewstate.query} />
|
||||||
|
<button>Hinzufügen</button>
|
||||||
|
Limit: <select onChange={setLimit} value={viewstate.limit}><optgroup label={'Anzahl'}><option>10</option><option>25</option><option>50</option><option>100</option><option>200</option></optgroup><option>Alle</option></select></div>
|
||||||
|
</div>
|
||||||
|
<UserList {...calculateView()} userlist={results} deleteUser={deleteUser} editUser={editUser} />
|
||||||
|
<Pageselector start={1} end={viewstate.pages} current={viewstate.page} setPage={setPage} />
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Users;
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import style from './style.css';
|
|
||||||
|
|
||||||
const Home = () => (
|
|
||||||
<div class={style.home}>
|
|
||||||
<h1>Home</h1>
|
|
||||||
<p>This is the Home component.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Home;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.home {
|
|
||||||
padding: 56px 20px;
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Component, h } from 'preact';
|
|
||||||
import Breadcrumbs from '../../components/breadcrumbs';
|
|
||||||
|
|
||||||
class Login extends Component {
|
|
||||||
state = { username: '', password: '' };
|
|
||||||
navigation = ["Login"];
|
|
||||||
onSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log(this.state);
|
|
||||||
this.setState({ username: '', password: '' });
|
|
||||||
if(!this.props.auth.login(this.state.username,this.state.password))
|
|
||||||
alert("Wrong login")
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (<div id="login-screen" class="page">
|
|
||||||
<div class="container">
|
|
||||||
<Breadcrumbs items={this.navigation} />
|
|
||||||
|
|
||||||
<form id="login_form" onSubmit={this.onSubmit} >
|
|
||||||
<p>
|
|
||||||
Bitte melden Sie sich mit ihren Nutzerdaten an.
|
|
||||||
</p>
|
|
||||||
<div class="input-box">
|
|
||||||
<input id="name" type="text" placeholder="Username" onInput={e => this.setState(prev => ({ ...prev, username: e.target.value }))} value={this.state.username} />
|
|
||||||
<label for="name">Benutzername</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-box">
|
|
||||||
<input id="pass" type="password" placeholder="Passwort" onInput={e => this.setState(prev => ({ ...prev, password: e.target.value }))} value={this.state.password} />
|
|
||||||
<label for="pass">Password</label>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Submit" />
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import {useEffect, useState} from "preact/hooks";
|
|
||||||
import style from './style.css';
|
|
||||||
|
|
||||||
// Note: `user` comes from the URL, courtesy of our router
|
|
||||||
const Profile = ({ user }) => {
|
|
||||||
const [time, setTime] = useState(Date.now());
|
|
||||||
const [count, setCount] = useState(10);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer = setInterval(() => setTime(Date.now()), 1000);
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={style.profile}>
|
|
||||||
<h1>Profile: {user}</h1>
|
|
||||||
<p>This is the user profile for a user named { user }.</p>
|
|
||||||
|
|
||||||
<div>Current time: {new Date(time).toLocaleString()}</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>Click Me</button>
|
|
||||||
{' '}
|
|
||||||
Clicked {count} times.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Profile;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.profile {
|
|
||||||
padding: 56px 20px;
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Component } from "preact";
|
|
||||||
import UserList from "../../components/userlist";
|
|
||||||
import Pageselector from "../../components/Pageselector";
|
|
||||||
class Users extends Component {
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
userlist = [
|
|
||||||
{ uid: 1234, first_name: 'Max', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
|
|
||||||
{ uid: 12341, first_name: 'Max1', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
|
|
||||||
{ uid: 12342, first_name: 'Max2', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
|
|
||||||
];
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<UserList userlist={this.userlist} />
|
|
||||||
<Pageselector start={1} end={9} current={2} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Users;
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from "preact";
|
||||||
|
const AppState = createContext({});
|
||||||
|
const AppStateProvider = AppState.Provider;
|
||||||
|
export default AppState;
|
||||||
|
export {AppStateProvider};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createContext } from "preact";
|
||||||
|
export function CreateUserTable(){
|
||||||
|
const usercontext = createContext();
|
||||||
|
return usercontext;
|
||||||
|
}
|
||||||
|
const UserTable = CreateUserTable();
|
||||||
|
export default UserTable.Provider;
|
||||||
|
export {UserTable}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import AppState, {AppStateProvider} from "./AppState";
|
||||||
|
import UserTableProvider,{UserTable} from './UserTable';
|
||||||
|
import {menuReducer, sessionReducer, userTableReducer} from './reducers';
|
||||||
|
export default AppState;
|
||||||
|
export {AppStateProvider, UserTableProvider, UserTable, menuReducer, sessionReducer, userTableReducer}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export const menuReducer = (state, action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'show': return true;
|
||||||
|
case 'hide': return false;
|
||||||
|
case 'toggle': return !state;
|
||||||
|
default: throw new Error("menustate unknown to reducer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'start': return { active: true, token: action.token, username: action.username }
|
||||||
|
case 'end': return { active: false, token: null, username: null, exiry: null }
|
||||||
|
default: throw new Error("action type unknown to session reducer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userTableReducer = (state, action) => {
|
||||||
|
let user = action.user;
|
||||||
|
switch (action.type) {
|
||||||
|
case 'create': return [...state, { line: state.length, ...user }];
|
||||||
|
case 'delete': {
|
||||||
|
let newstate = [];
|
||||||
|
let newindex = 0;
|
||||||
|
state.forEach((u, i) => {
|
||||||
|
if (user.uid && u.uid != user.uid || user.line && i != user.line)
|
||||||
|
newstate.push({ ...u, line: newindex++ })
|
||||||
|
return newstate;
|
||||||
|
}, []);
|
||||||
|
return newstate;
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
let newstate = [];
|
||||||
|
state.forEach((u, i) => {
|
||||||
|
if (user.uid && u.uid == user.uid || user.line && i == user.line)
|
||||||
|
newstate.push(user);
|
||||||
|
else
|
||||||
|
newstate.push(u);
|
||||||
|
});
|
||||||
|
return [...newstate]
|
||||||
|
}
|
||||||
|
case 'import':
|
||||||
|
return action.imported;
|
||||||
|
case 'reset':
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
throw new Error("action type unknown to usertable reducer");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
@mixin breadcrumbs
|
@mixin breadcrumbs
|
||||||
padding: 1em 0
|
padding-top: 1rem
|
||||||
ul
|
ul
|
||||||
display: flex
|
display: flex
|
||||||
background: #eee
|
background: #eee
|
||||||
box-shadow: inset 0 0 .3em #ccc, 0 0 .5em #ddd
|
box-shadow: inset 0 0 .3em #ccc, 0 0 .5em #ddd
|
||||||
padding: .3em
|
padding: .3em
|
||||||
|
margin: 0 .5em
|
||||||
border: 0.05em solid #fff
|
border: 0.05em solid #fff
|
||||||
border-radius: .3em
|
border-radius: .3em
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@mixin default
|
||||||
|
z-index: 1
|
||||||
|
margin: 0
|
||||||
|
padding: 0.5em
|
||||||
|
text-align: center
|
||||||
|
display: block
|
||||||
|
width: auto
|
||||||
|
position: relative
|
||||||
|
color: #333
|
||||||
|
font-weight: bold
|
||||||
|
text-decoration: none
|
||||||
|
border: solid 1px #999
|
||||||
|
border-radius: .2rem
|
||||||
|
min-width: 100%
|
||||||
|
overflow: hidden
|
||||||
|
&--disabled
|
||||||
|
color: #999
|
||||||
|
|
||||||
|
button
|
||||||
|
display: block
|
||||||
|
background: none
|
||||||
|
width: 100%
|
||||||
|
border: none
|
||||||
|
border-radius: none
|
||||||
|
font-size: 2em
|
||||||
|
transition: all ease-in-out 100ms
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
&::before
|
||||||
|
z-index: -1
|
||||||
|
border-radius: .2rem
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
right: 0
|
||||||
|
content: ''
|
||||||
|
background: transparent
|
||||||
|
width: 0%
|
||||||
|
height: 100%
|
||||||
|
transition: all ease-in-out 250ms
|
||||||
|
&:hover
|
||||||
|
color: #fff
|
||||||
|
button
|
||||||
|
background: #333
|
||||||
|
color: #fff
|
||||||
|
border-radius: .3rem
|
||||||
|
&::before
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: auto
|
||||||
|
content: ''
|
||||||
|
background: #333
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@mixin footer
|
||||||
|
display: block
|
||||||
|
background: #bbb
|
||||||
|
text-shadow: 0 0 .2em #34d
|
||||||
|
color: white
|
||||||
|
flex-shrink: 0
|
||||||
|
padding: 0.5rem 0
|
||||||
|
margin-top: 1rem
|
||||||
|
width: 100%
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
@mixin textfield
|
||||||
|
margin: 0 1em
|
||||||
|
min-width: 100%
|
||||||
|
position: relative
|
||||||
|
display: block
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
background: #fafafa
|
||||||
|
border-radius: .3em
|
||||||
|
border-bottom: 1px solid #ccc
|
||||||
|
&:hover
|
||||||
|
background: #fff
|
||||||
|
input
|
||||||
|
//overflow: hidden
|
||||||
|
display: block
|
||||||
|
background: none
|
||||||
|
width: 100%
|
||||||
|
border-radius: none
|
||||||
|
outline: none
|
||||||
|
border: none
|
||||||
|
font-size: 1em
|
||||||
|
padding: 0.7em
|
||||||
|
+ label
|
||||||
|
user-select: none
|
||||||
|
position: absolute
|
||||||
|
top: 0.15em
|
||||||
|
left: 0.25em
|
||||||
|
right: 0
|
||||||
|
font-size: .7em
|
||||||
|
cursor: text
|
||||||
|
transition: 250ms all
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
&::placeholder
|
||||||
|
color: transparent
|
||||||
|
|
||||||
|
&:placeholder-shown + label
|
||||||
|
color: grey
|
||||||
|
position: absolute
|
||||||
|
padding: .7em
|
||||||
|
top: 0em
|
||||||
|
left: 0em
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
font-size: 1em
|
||||||
|
&__info
|
||||||
|
color: #ccc
|
||||||
|
font-size: 0.7em
|
||||||
|
&__hint
|
||||||
|
position: absolute
|
||||||
|
z-index: 1
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
top: calc(100% - .6em)
|
||||||
|
border-bottom-left-radius: .3em
|
||||||
|
border-bottom-right-radius: .3em
|
||||||
|
color: #fff
|
||||||
|
font-size: .7em
|
||||||
|
background: red
|
||||||
|
|
||||||
|
@mixin checkbox
|
||||||
|
position: relative
|
||||||
|
font-size: 1.5em
|
||||||
|
label
|
||||||
|
user-select: none
|
||||||
|
margin-left: 1em
|
||||||
|
padding-left: .5em
|
||||||
|
transition: all 250ms ease-in-out
|
||||||
|
&::before
|
||||||
|
position: absolute
|
||||||
|
content: ''
|
||||||
|
left: 0
|
||||||
|
top: 0
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
background: radial-gradient(circle, #e2e2e2 0%, #b8b8b8 100%)
|
||||||
|
border-radius: .3em
|
||||||
|
border: solid 1px #ccc
|
||||||
|
box-shadow: 0 0 .5em #fff, inset 0 0 .3em #fff
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: ''
|
||||||
|
left: 0
|
||||||
|
top: 0
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
position: absolute
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
transform: scale(0)
|
||||||
|
padding: 0
|
||||||
|
text-shadow: 0 0 .2em #888
|
||||||
|
transition: all 250ms ease-in-out
|
||||||
|
input
|
||||||
|
display: none
|
||||||
|
&:checked + label
|
||||||
|
text-shadow: 0 0 .5em #888
|
||||||
|
&::before
|
||||||
|
box-shadow: 0 0 .5em #fff, inset 0 0 .5em #fff
|
||||||
|
&::after
|
||||||
|
transform: scale(1)
|
||||||
|
content: '✔'
|
||||||
|
color: #fff
|
||||||
+4
-31
@@ -1,40 +1,13 @@
|
|||||||
@mixin text-input
|
@use 'input'
|
||||||
position: relative
|
|
||||||
input
|
|
||||||
display: block
|
|
||||||
width: 100%
|
|
||||||
border-radius: none
|
|
||||||
outline: none
|
|
||||||
border: none
|
|
||||||
font-size: 1.5em
|
|
||||||
padding: .7em
|
|
||||||
+ label
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
left: 0
|
|
||||||
cursor: text
|
|
||||||
transition: 250ms all
|
|
||||||
|
|
||||||
&::placeholder
|
|
||||||
color: transparent
|
|
||||||
&:placeholder-shown + label
|
|
||||||
color: grey
|
|
||||||
font-size: 1.5em
|
|
||||||
position: absolute
|
|
||||||
padding: .7em
|
|
||||||
|
|
||||||
@mixin login-form
|
@mixin login-form
|
||||||
|
|
||||||
padding: 1em
|
padding: 1em
|
||||||
|
width: 100%
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
.input-box
|
.input-box
|
||||||
@include text-input
|
@include input.textfield
|
||||||
|
font-size: 1.5em
|
||||||
p
|
p
|
||||||
font-size: 2em
|
font-size: 2em
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
border: none
|
|
||||||
border-radius: none
|
|
||||||
font-size: 2em
|
|
||||||
|
|||||||
@@ -15,7 +15,13 @@
|
|||||||
color: #666
|
color: #666
|
||||||
//text-shadow: 0 0 .2em #000
|
//text-shadow: 0 0 .2em #000
|
||||||
width: 100%
|
width: 100%
|
||||||
|
transition: all ease-in-out 150ms
|
||||||
//background: #ccc
|
//background: #ccc
|
||||||
|
&:hover
|
||||||
|
padding-left: .5em
|
||||||
|
background: #888
|
||||||
|
color: #fff
|
||||||
|
text-shadow: 0 0 1em #fff
|
||||||
&::after
|
&::after
|
||||||
top: 100%
|
top: 100%
|
||||||
left: 0
|
left: 0
|
||||||
@@ -24,5 +30,5 @@
|
|||||||
position: absolute
|
position: absolute
|
||||||
width: 100%
|
width: 100%
|
||||||
height: .1em
|
height: .1em
|
||||||
background: #8a8080
|
background: #ccc
|
||||||
//border-bottom-left-radius: 2em 1em
|
//border-bottom-left-radius: 2em 1em
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
@mixin pageselector
|
@mixin pageselector
|
||||||
text-align: center
|
text-align: center
|
||||||
//background-color: #ccc
|
//background-color: #ccc
|
||||||
margin: 1em
|
margin: 2em
|
||||||
ul
|
ul
|
||||||
list-style: none
|
list-style: none
|
||||||
justify-content: center
|
justify-content: center
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
display: flex
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
li a
|
li a
|
||||||
|
display: inline-flex
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
min-width: 2rem
|
||||||
|
min-height: 2rem
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
padding: .2em .5em
|
padding: .2em .5em
|
||||||
margin: .3em
|
margin: .3em
|
||||||
border-radius: .3em
|
border-radius: .3em
|
||||||
@@ -23,4 +29,4 @@
|
|||||||
&:hover
|
&:hover
|
||||||
color: #333
|
color: #333
|
||||||
background: #fff
|
background: #fff
|
||||||
border: .1em solid #ccc
|
border: .1em solid #444
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.warnbox
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
//flex-wrap: wrap
|
||||||
|
background: #ffc964
|
||||||
|
box-shadow: 0 0 1em #ffc964
|
||||||
|
border-radius: .3em
|
||||||
|
border: 1px solid #fff
|
||||||
|
align-items: stretch
|
||||||
|
justify-content: stretch
|
||||||
|
padding: .3em
|
||||||
|
&__icon
|
||||||
|
font-size: 2em
|
||||||
|
line-height: 0.5em
|
||||||
|
//width: 2em
|
||||||
|
border-right: 1px solid #fff
|
||||||
|
color: #fff
|
||||||
|
font-weight: bold
|
||||||
|
text-shadow: 0 0 .2em #fff
|
||||||
|
padding: .25em
|
||||||
|
margin: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
//align-self: flex-start
|
||||||
|
h3
|
||||||
|
margin: .3em
|
||||||
|
color: #fff
|
||||||
|
div span
|
||||||
|
margin-left: 1em
|
||||||
|
|
||||||
+75
-12
@@ -3,18 +3,23 @@
|
|||||||
@use 'login'
|
@use 'login'
|
||||||
@use 'breadcrumbs'
|
@use 'breadcrumbs'
|
||||||
@use 'pageselector'
|
@use 'pageselector'
|
||||||
|
@use 'footer'
|
||||||
|
@use 'button'
|
||||||
|
@use 'input'
|
||||||
|
@import 'warnbox'
|
||||||
*
|
*
|
||||||
//border: red 1px dotted
|
//border: 1px dotted red
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
|
||||||
html
|
html
|
||||||
font-family: Helvetica, sans-serif
|
font-family: Helvetica, sans-serif
|
||||||
font-size: 16px
|
font-size: 16px
|
||||||
|
height: 100%
|
||||||
body
|
body
|
||||||
|
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
background-color: white
|
background-color: white
|
||||||
//min-height: 100vh
|
height: 100%
|
||||||
width: 100%
|
width: 100%
|
||||||
#wrapper
|
#wrapper
|
||||||
display: flex
|
display: flex
|
||||||
@@ -22,16 +27,19 @@ body
|
|||||||
justify-content: flex-start
|
justify-content: flex-start
|
||||||
align-items: stretch
|
align-items: stretch
|
||||||
min-height: 100%
|
min-height: 100%
|
||||||
|
//margin-bottom: -2rem
|
||||||
width: 100%
|
width: 100%
|
||||||
.header
|
.header
|
||||||
z-index: 100
|
z-index: 100
|
||||||
position: fixed
|
position: fixed
|
||||||
|
top: 0em
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 4em
|
max-width: 100vw
|
||||||
|
height: 4rem
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
justify-content: flex-end
|
justify-content: center
|
||||||
padding: .5em 0
|
padding: 0.5em .5em
|
||||||
margin: 0 0 0 0
|
margin: 0 0 0 0
|
||||||
background: linear-gradient(#9e9e9e 0% , #d1d1d1 100%)
|
background: linear-gradient(#9e9e9e 0% , #d1d1d1 100%)
|
||||||
box-shadow: 0 0 .2em #444
|
box-shadow: 0 0 .2em #444
|
||||||
@@ -46,6 +54,7 @@ body
|
|||||||
.container
|
.container
|
||||||
width: clamp(5ch, 100%,75ch)
|
width: clamp(5ch, 100%,75ch)
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
|
|
||||||
.breadcrumb
|
.breadcrumb
|
||||||
@include breadcrumbs.breadcrumbs
|
@include breadcrumbs.breadcrumbs
|
||||||
|
|
||||||
@@ -54,7 +63,9 @@ body
|
|||||||
display: flex
|
display: flex
|
||||||
background: white
|
background: white
|
||||||
width: 100%
|
width: 100%
|
||||||
padding-top: 6em
|
flex: 1
|
||||||
|
margin-top: 4rem
|
||||||
|
|
||||||
.menu
|
.menu
|
||||||
@include menu.nav
|
@include menu.nav
|
||||||
|
|
||||||
@@ -64,22 +75,30 @@ body
|
|||||||
.page-nav-bar
|
.page-nav-bar
|
||||||
@include pageselector.pageselector
|
@include pageselector.pageselector
|
||||||
.user-list-item
|
.user-list-item
|
||||||
|
position: relative
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
display: flex
|
display: inline-flex
|
||||||
|
flex-shrink: 1
|
||||||
|
min-width: calc(50% - 1em)
|
||||||
|
flex-grow: 1
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
background: #eee
|
background: #eee
|
||||||
padding: .5em 0
|
margin: 0 0.5em
|
||||||
|
padding: .5em 3em 0 0
|
||||||
border-radius: .3em
|
border-radius: .3em
|
||||||
&:not(:last-child)
|
&:not(:last-child)
|
||||||
margin-bottom: .5em
|
margin-bottom: .5em
|
||||||
.user-attributes
|
.user-attributes
|
||||||
padding: .5em
|
padding: .5em
|
||||||
.btn-group
|
.btn-group
|
||||||
margin-left: auto
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
display: inline-flex
|
display: inline-flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-self: flex-end
|
align-items: center
|
||||||
justify-self: flex-end
|
justify-content: center
|
||||||
@mixin button()
|
@mixin button()
|
||||||
position: relative
|
position: relative
|
||||||
border: none
|
border: none
|
||||||
@@ -124,3 +143,47 @@ body
|
|||||||
.btn-edit
|
.btn-edit
|
||||||
@include button()
|
@include button()
|
||||||
background-image: url("../assets/icons/edit-icon3.svg")
|
background-image: url("../assets/icons/edit-icon3.svg")
|
||||||
|
|
||||||
|
footer
|
||||||
|
@include footer.footer
|
||||||
|
|
||||||
|
.contentbox
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
flex-direction: column
|
||||||
|
align-content: stretch
|
||||||
|
padding: 1em
|
||||||
|
margin: 1em 0.5em
|
||||||
|
border: 1px solid #fff
|
||||||
|
background: #f4f4f4
|
||||||
|
box-shadow: 0 0 .5em #ddd, inset 0 0 1em #fff
|
||||||
|
border-radius: .5em
|
||||||
|
h2
|
||||||
|
margin: 0
|
||||||
|
display: flex
|
||||||
|
flex-break: after
|
||||||
|
flex-basis: 100%
|
||||||
|
.button
|
||||||
|
@include button.default
|
||||||
|
|
||||||
|
.textbox
|
||||||
|
@include input.textfield
|
||||||
|
font-size: 1.5em
|
||||||
|
.checkbox
|
||||||
|
@include input.checkbox
|
||||||
|
|
||||||
|
.row
|
||||||
|
display: flex
|
||||||
|
justify-content: space-evenly
|
||||||
|
align-items: stretch
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
width: 100%
|
||||||
|
&:not(:last-child)
|
||||||
|
margin-bottom: 1em
|
||||||
|
.column
|
||||||
|
padding: .5em
|
||||||
|
flex-grow: 1
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
flex-wrap: wrap
|
||||||
Reference in New Issue
Block a user