Compare commits

..

7 Commits

37 changed files with 3716 additions and 22647 deletions
+2710 -22431
View File
File diff suppressed because it is too large Load Diff
+4 -9
View File
@@ -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"
}, },
View File
+1 -1
View File
@@ -1 +1 @@
[{"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}]}] [{"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}]}]
+121
View File
@@ -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

+3 -3
View File
@@ -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="#">&gt;</a></li>); //items.push(<li><a href="" >&gt;</a></li>);
return ( return (
<div class="page-nav-bar"> <div class="page-nav-bar">
<ul> <ul>
+53 -34
View File
@@ -1,55 +1,74 @@
import { h } from "preact"; import { h } from "preact";
import { Router, route } from "preact-router"; import { Router, route } from "preact-router";
import {Header, Menu} from "./"; import { useEffect, useReducer, useState } from "preact/hooks";
import { menuReducer, sessionReducer } from "../store/reducers"; import { Header, Menu } from "./index.js";
import { useReducer } from "preact/hooks"; import { Home, Users, EditUser, Login, Logout, System } from "../route";
import { Home, Users, Profile, Login, Logout } from "../route"; import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store";
import { AppStateProvider } from "../store/AppState"; import api from "../api/index.js";
const menu_items = [
{ text: "Übersicht", path: "/" },
{ text: "Benutzer anlegen", path: "/newuser" },
{ text: "Benutzer verwalten", path: "/users" },
{ text: "System", path: "/system" },
{ text: "Abmelden", path: "/logout" }
]
function App() { function App() {
// useReducer // useReducer
const menu = useReducer(menuReducer, false); const menu = useReducer(menuReducer, false);
const session = useReducer(sessionReducer, {active: true}); const session = useReducer(sessionReducer, {});
this.menu_items = [ const [usertable, userreducer] = useReducer(userTableReducer, []);
{ text: "Übersicht", path: "/" }, const [lasturl, setlasturl] = useState("/");
{ text: "Benutzer", path: "/users" }, if(!session[0]||(session[0]&&session[0].active===undefined))
{ text: "System", path: "/system" }, api.restoreSession(session[1]);
{ text: "Profil", path: "/profile" }, useEffect(()=>{
{ text: "Abmelden", path: "/logout" } if(session[0]&&session[0].active){
] api.fetchdb(session[0].token).then(imported => {
this.handleRoute = async e => { let action = { type: 'import', imported };
userreducer(action);
});
}},[session]);
const handleRoute = async e => {
let active = session[0].active;
switch (e.url) { switch (e.url) {
default: default:
if (!session[0].active) route('/login', true); if (!active) route('/login', true);
break; break;
} }
}; };
return ( return (
<AppStateProvider value={{ menu, session}} > <AppStateProvider value={{ menu, session }} >
<div id="wrapper"> <div id="wrapper">
<Header /> <Header title={"Doorlock"} />
<div class="page"> <div class="page">
<Menu items={this.menu_items} /> <Menu items={menu_items} />
{!menu[0] && {!menu[0] &&
<UserTableProvider value={{ usertable, userreducer }} >
<Router onChange={this.handleRoute}> <Router onChange={handleRoute}>
<Home path="/" user="me" /> <Home path="/" user="me" />
<Login path="login" /> <Login path="login" />
<Profile path="/profile" /> <System path="/system" />
<Logout path="/logout" /> <Logout path="/logout" />
<Users path="/users" /> <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>
} </UserTableProvider>
</div> }
<footer>
<div className="container" style={'text-align: center; align-items: center'}>
<span>&copy; Jean Jacques Avril 2022 </span>
</div>
</footer>
</div> </div>
</AppStateProvider> <footer>
<div className="container" style={'text-align: center; align-items: center'}>
<span>&copy; Jean Jacques Avril 2022 </span>
</div>
</footer>
</div>
</AppStateProvider>
); );
} }
+2 -2
View File
@@ -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>
)} )}
+2
View File
@@ -0,0 +1,2 @@
import {TextBox,CheckBox,Button} from './input'
export {TextBox,CheckBox,Button}
+37
View File
@@ -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 }
+2 -2
View File
@@ -1,13 +1,13 @@
import { useContext } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
const Header = () => { const Header = ({title}) => {
let { menu, session } = useContext(AppState); let { menu, session } = useContext(AppState);
let [menu_shown, toggle_menu] = menu; let [menu_shown, toggle_menu] = menu;
let [sessiondata] = session; let [sessiondata] = session;
return ( return (
<header className='header'> <header className='header'>
<div className="container"> <div className="container">
<h1>Login</h1> <h1>{title}</h1>
{ sessiondata.active &&(<div id="hamburger-button" className={`hamburger ${menu_shown && 'hamburger-active'}`} { sessiondata.active &&(<div id="hamburger-button" className={`hamburger ${menu_shown && 'hamburger-active'}`}
onClick={() => toggle_menu('toggle')}> onClick={() => toggle_menu('toggle')}>
<hr /> <hr />
+2 -2
View File
@@ -4,6 +4,6 @@ import Pageselector from "./Pageselector";
import Header from "./header"; import Header from "./header";
import UserList from "./userlist"; import UserList from "./userlist";
import Menu from "./menu"; import Menu from "./menu";
import Warnbox from "./warnbox";
export {App, Breadcrumbs, Pageselector, Header, UserList, Menu} export {App, Breadcrumbs, Pageselector, Header, UserList, Menu, Warnbox}
export default App export default App
+5 -17
View File
@@ -2,35 +2,23 @@ import { h } from 'preact';
import { Link } from 'preact-router'; import { Link } from 'preact-router';
import { useContext } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
function Menu({items}) { function Menu({ items }) {
let [ menu_shown, toggle_menu ] = useContext(AppState).menu; let [menu_shown, toggle_menu] = useContext(AppState).menu;
let menu_items = [ if (!items) return;
{ text: "Übersicht", path: "/" },
{ text: "Benutzer", path: "/users" },
{ text: "System", path: "/system" },
{ text: "Backup", path: "/backup" },
{ text: "Abmelden", path: "/logout" }
]
const onClick = (e) => { const onClick = (e) => {
e.preventDefault(); e.preventDefault();
toggle_menu('hide'); toggle_menu('hide');
} }
if (items)
menu_items = items;
if (menu_shown) if (menu_shown)
return ( return (
<div class="container" > <div class="container" >
<nav className='menu' > <nav className='menu' >
<ul> <ul>
{menu_items.map((element, i) => (<li key={i}><Link href={element.path} onClick={onClick} >{element.text}</Link></li>))} {items.map((element, i) => (<li key={i}><Link href={element.path} onClick={onClick} >{element.text}</Link></li>))}
</ul> </ul>
</nav> </nav>
</div>) </div>);
} }
export default Menu; export default Menu;
+14 -21
View File
@@ -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
+15
View File
@@ -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
+141
View File
@@ -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;
+25 -6
View File
@@ -1,10 +1,29 @@
import { h } from 'preact'; 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>
function Home() {return( </div>
<div class="container"> );
<h1>Home</h1>
<p>This is the Home component.</p>
</div>
);
} }
export default Home; export default Home;
+3 -1
View File
@@ -1,6 +1,8 @@
import Home from "./home" import Home from "./home"
import Users from './users' import Users from './users'
import EditUser from "./edituser"
import Profile from "./profile" import Profile from "./profile"
import Login from "./login" import Login from "./login"
import Logout from './logout' import Logout from './logout'
export {Home, Users, Profile, Login, Logout } import System from './system'
export {Home, Users, EditUser, Profile, Login, Logout, System }
+33 -33
View File
@@ -1,52 +1,52 @@
import { h } from 'preact'; import { h } from 'preact';
import { route } from 'preact-router'; import { route } from 'preact-router';
import { useContext,useState } from 'preact/hooks'; import { useContext, useState } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
import Breadcrumbs from '../../components/breadcrumbs'; import { Breadcrumbs, Warnbox } from '../../components';
import { CheckBox, Button, TextBox } from '../../components/controls';
import api from '../../api'
function Login() { function Login() {
let [sessiondata, setsession]= useContext(AppState).session; let [sessiondata, setsession] = useContext(AppState).session;
const [val, set] = useState({ username: '', password: '', error: null }); const [fromdata, setform] = useState({ username: '', password: '', error: null });
const navigation = ["Login"]; const navigation = ["Login"];
if(sessiondata.active) if (sessiondata.active)
route('/', true); route('/', true);
function onSubmit(e) { function onSubmit(e) {
e.preventDefault(); e.preventDefault();
//sucess api.login(fromdata).then(result => {
if (val.username === 'admin') { if (result.error !== undefined)
let newsession = { setform(result);
type: 'start', else {
username: 'affe', setsession(result);
token: 'dsf4w3qr'
} }
setsession(newsession); })
}
else {
set({ ...val, error: "user" });
}
set({ username: '', password: '' });
} }
return ( return (
<div class="container"> <div class="container">
<Breadcrumbs items={navigation} /> <Breadcrumbs items={navigation} />
<div className={'contentbox'} >
<h2>Anmeldung</h2>
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
<form id="login_form" onSubmit={onSubmit} > {fromdata.error === 'login_failed' && <Warnbox title='Anmeldefehler'>Ungültige Anmeldedaten.<br />Bitte überprüfen Sie den eingebenen Benutzernamen und das Passwort.</Warnbox>}
<p > {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>}
Bitte melden Sie sich mit ihren Nutzerdaten an. <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' />
</p> </div>
{val.error !== null && <span style={'color: red'}>Fehler: Ungültige Anmeldedaten.</span>} <div className='row'>
<div class="input-box"> <CheckBox id="permanent" formdata={fromdata} formchange={setform} label="Angemeldet bleiben?" />
<input id="name" type="text" placeholder="Username" onInput={e => set({ ...val, username: e.target.value })} value={val.username} />
<label for="name">Benutzername</label>
</div>
<div class="input-box">
<input id="pass" type="password" placeholder="Passwort" onInput={e => set({ ...val, password: e.target.value })} value={val.password} />
<label for="pass">Password</label>
</div>
<input type="submit" value="Submit" />
</form> </div>
<Button onClick={onSubmit}>Anmelden</Button>
</form>
</div>
</div> </div>
); );
+15 -6
View File
@@ -1,29 +1,38 @@
import { h } from 'preact'; import { h } from 'preact';
import { Link } from 'preact-router'; import { Link } from 'preact-router';
import { useContext } from 'preact/hooks'; import { useContext, useState } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
import { Breadcrumbs } from '../../components'; import { Breadcrumbs } from '../../components';
import api from '../../api'
function Logout() { function Logout() {
const navigation = ["Logout"]; const navigation = ["Logout"];
let [text, settext] = useState('');
let [sessiondata, setsession] = useContext(AppState).session; let [sessiondata, setsession] = useContext(AppState).session;
this.shouldComponentUpdate = function () { this.shouldComponentUpdate = function () {
console.log('functional component vs closures'); console.log('functional component vs closures');
} }
if (sessiondata.active) { if (sessiondata.active) {
api.logout(sessiondata.token).then((r) => r === 'success' ? settext('Sitzung beendet.') : settext('Sitzung war bereits nicht mehr vorhanden.'));
setsession({ type: 'end' }) setsession({ type: 'end' })
} }
return ( return (
<div class="container"> <div class="container">
<Breadcrumbs items={navigation} /> <Breadcrumbs items={navigation} />
<div className={'contentbox'} >
<h2>
Erfolgreich abgemeldet:
</h2>
<p>
{text}<br />
<Link className={'button'} href="/login">Erneut Anmelden</Link>
</p>
</div>
<p>
Erfolgreich abgemeldet.
<Link href="/login">Erneut Anmelden</Link>
</p>
</div> </div>
); );
+1 -2
View File
@@ -1,6 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import style from './style.css';
// Note: `user` comes from the URL, courtesy of our router // Note: `user` comes from the URL, courtesy of our router
const Profile = ({ user }) => { const Profile = ({ user }) => {
@@ -14,7 +13,7 @@ const Profile = ({ user }) => {
return ( return (
<div className='container'> <div className='container'>
<div class={style.profile}> <div >
<h1>Profile: {user}</h1> <h1>Profile: {user}</h1>
<p>This is the user profile for a user named {user}.</p> <p>This is the user profile for a user named {user}.</p>
-5
View File
@@ -1,5 +0,0 @@
.profile {
padding: 56px 20px;
min-height: 100%;
width: 100%;
}
+113
View File
@@ -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
+89 -17
View File
@@ -1,24 +1,96 @@
import { Component } from "preact"; import { h } from "preact";
import UserList from "../../components/userlist"; import { UserList, Pageselector, Breadcrumbs } from "../../components";
import Pageselector from "../../components/Pageselector"; import api from '../../api'
class Users extends Component { import { route } from 'preact-router';
shouldComponentUpdate() { 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 return false
} }
userlist = [ const results = useMemo(() => (viewstate.query !== '') ? usertable.filter(u => matchUser(u, viewstate.query)) : usertable, [viewstate.query, usertable]);
{ 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> 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; export default Users;
+3 -4
View File
@@ -1,6 +1,5 @@
import { createContext } from "preact"; import { createContext } from "preact";
const AppState = createContext({}); const AppState = createContext({});
const AppStateProvider = AppState.Provider;
export const AppStateProvider = AppState.Provider; export default AppState;
export default AppState; export {AppStateProvider};
+8
View File
@@ -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}
+5
View File
@@ -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}
+35 -2
View File
@@ -9,8 +9,41 @@ export const menuReducer = (state, action) => {
export const sessionReducer = (state, action) => { export const sessionReducer = (state, action) => {
switch (action.type) { switch (action.type) {
case 'start': return {active: true, token: action.token, username: action.username} case 'start': return { active: true, token: action.token, username: action.username }
case 'end': return {active: false, token: null, username: null, exiry: null} case 'end': return { active: false, token: null, username: null, exiry: null }
default: throw new Error("action type unknown to session reducer"); 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 -1
View File
@@ -5,7 +5,7 @@
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 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
+53
View File
@@ -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%
+104
View File
@@ -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
View File
@@ -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
+6 -3
View File
@@ -15,10 +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 &:hover
background: #bbb padding-left: .5em
color: #333 background: #888
color: #fff
text-shadow: 0 0 1em #fff
&::after &::after
top: 100% top: 100%
left: 0 left: 0
@@ -27,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
+8 -2
View File
@@ -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
+31
View File
@@ -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
+67 -12
View File
@@ -4,9 +4,13 @@
@use 'breadcrumbs' @use 'breadcrumbs'
@use 'pageselector' @use 'pageselector'
@use 'footer' @use 'footer'
@use 'button'
@use 'input'
@import 'warnbox'
* *
//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
@@ -30,11 +34,12 @@ body
position: fixed position: fixed
top: 0em top: 0em
width: 100% width: 100%
max-width: 100vw
height: 4rem 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
@@ -49,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
@@ -69,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
display: inline-flex top: 0
flex-direction: column right: 0
align-self: flex-end bottom: 0
justify-self: flex-end display: inline-flex
flex-direction: column
align-items: center
justify-content: center
@mixin button() @mixin button()
position: relative position: relative
border: none border: none
@@ -131,4 +145,45 @@ body
background-image: url("../assets/icons/edit-icon3.svg") background-image: url("../assets/icons/edit-icon3.svg")
footer footer
@include footer.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