added search, unified controls and input elements, rendering improved

This commit is contained in:
Jean Jacques Avril 2022-03-12 23:54:14 +01:00
parent a0adef9ec5
commit 32e6018415
17 changed files with 5039 additions and 25560 deletions

30231
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,20 +18,14 @@
]
},
"devDependencies": {
"enzyme": "^3.10.0",
"enzyme-adapter-preact-pure": "^2.0.0",
"eslint": "^8.10.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",
"sass-loader": "^10.2.1",
"sirv-cli": "1.0.3"
"preact-render-to-string": "^5.1.4",
"sass-loader": "^10.2.1"
},
"dependencies": {
"preact": "^10.3.2",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1",
"sass": "^1.49.9"
},

View File

View File

@ -1,11 +1,30 @@
const server = 'http://192.168.4.22'
async function restoreSession(reducer=null) {
let token = localStorage.getItem('token');
let username = localStorage.getItem('username');
console.log({type:'start', token, username});
let active = token!==null && username!==null && await checkAuth(token);
if(active&&reducer!==null){
reducer({type:'start', token, username});
console.log({type:'start', token, username});
}
return (active ? { active: true, token, username } : {active:false});
}
async function storeSession(username, token) {
token = await token;
localStorage.setItem('token', token);
localStorage.setItem('username', username);
return token
}
async function login(username, password) {
let formData = new FormData();
formData.append('username', username)
formData.append('password', password);
formData.append('action', 'login');
const resp = fetch(`${server}/api/auth`, { method: 'POST', mode: 'cors', body: formData }).then(resp => resp.text());
const resp = fetch(`${server}/api/auth`, { method: 'POST', mode: 'cors', body: formData }).then(resp => storeSession(username, resp.text()));
return resp;
}
async function logout(token) {
@ -13,10 +32,15 @@ async function logout(token) {
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;
}
function checkAuth(token) {
return token === 'DUMMYTOKEN';
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') {
//fetch(`${server}/api/userdb`).then()
@ -65,6 +89,6 @@ function parsedb(raw) {
return users;
}
const publicfunctions = { login, logout, checkAuth, parsedb,fetchdb, createCsvTable, updateUser, deleteUser, createUser };
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser,restoreSession };
export default { ...publicfunctions }
export { login, logout, checkAuth, parsedb,fetchdb, createCsvTable, updateUser, deleteUser, createUser }
export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession }

View File

@ -1,16 +1,29 @@
import { h } from "preact";
import { Router, route } from "preact-router";
import { useReducer } from "preact/hooks";
import { useEffect, useReducer } from "preact/hooks";
import { Header, Menu } from "./index.js";
import { Home, Users, EditUser, Login, Logout, System } from "../route";
import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store";
import api from "../api/index.js";
function App() {
// useReducer
const menu = useReducer(menuReducer, false);
const session = useReducer(sessionReducer, { active:true ,token: "0t6BF94Y92" });
const session = useReducer(sessionReducer, {});
const [usertable, userreducer] = useReducer(userTableReducer, []);
console.log(session[0]);
if(!session[0]||(session[0]&&session[0].active===undefined))
api.restoreSession(session[1]);
useEffect(()=>{
if(session[0]&&session[0].active){
console.log("triggerimport");
api.fetchdb(session[0].token).then(imported => {
let action = { type: 'import', imported };
userreducer(action);
});
}},[session]);
console.log(session);
this.menu_items = [
{ text: "Übersicht", path: "/" },
{ text: "Benutzer anlegen", path: "/newuser" },

View File

@ -0,0 +1,2 @@
import {TextBox,CheckBox,Button} from './input'
export {TextBox,CheckBox,Button}

View File

@ -0,0 +1,22 @@
import { h } from 'preact'
function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength }) {
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.value });
return (<div className={'textbox'} >
<input placeholder=" " disabled={disabled} type={type} value={formdata[id]?formdata[id]:''} id={id} onInput={onChange} maxlength={maxlength} name={`form-${id}`} />
<label for={id}>{label}</label>
</div>)
}
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 }

View File

@ -24,9 +24,9 @@ function UserList({userlist,editUser, deleteUser, start, end}) {
return (
<>
<div className="row">
{userlist&&(start!==undefined&&end?userlist.slice(start,end).map((user)=>displayUser(user)):userlist.map((user)=>displayUser(user)))}
</>
</div>
);
}

View File

@ -3,54 +3,108 @@ import { useContext, useEffect, useState } 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 onChange = (e) => {
console.log(e);
if (e.target.type === 'checkbox')
formchange({ ...formdata, [e.target.id]: e.target.checked })
else
formchange({ ...formdata, [e.target.id]: e.target.value })
}
let maxlength_uid = 10;
let maxlength_name = 25;
let maxlength_pin = 10;
let maxlength_rfid = 8;
useEffect(() => {
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
if (user !== undefined) {
formchange(user);
}
},[userid]);
}, [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 onSubmit = (e) => {
e.preventDefault();
console.log(api);
let idcount = uidCount(usertable, formdata);
console.log(idcount);
if (formdata.line !== undefined) {
api.updateUser(sessiondata.token, formdata).then(r => {
userreducer({ type: 'update', user: r })
});
}else{
route('/users');
} else if (idcount === 0) {
api.createUser(sessiondata.token, formdata).then(r => {
userreducer({ type: 'create', user: r })
});
}
route('/users');
}
const Input = ({ id, type = 'text', label, disabled }) => (<div className={'textbox'} ><input placeholder=" " disabled={disabled} type={type} checked={type == 'checkbox' && formdata[id] ? 'on' : ''} value={formdata[id]} id={id} onChange={onChange} name={`edit-${id}`} /><label for={id}>{label}</label></div>)
else {
alert(`UID ${formdata.uid} wurde bereits ${idcount} mal benutzt`);
}
}
return (
<div className='container'>
<h1>{formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"}</h1>
<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}>
<Input id="uid" label="UserID" />
<Input id="first_name" label="Vorname" />
<Input id="last_name" label="NachName" />
<Input id="rfid_uid" label="RFID" />
<Input id="user_pin" label="Pin" />
<Input id="enabled" type={'checkbox'} label="" />
<div className={'button'} ><input type={'submit'}>Speichern</input> </div>
<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} />
{<span class="textbox__info"> {formdata["uid"]?formdata["uid"].length:0} von {maxlength_uid} Zeichen verwendet.</span>}
</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} />
{<span class="textbox__info"> {formdata["first_name"] ?formdata["first_name"].length:0} von {maxlength_name} Zeichen verwendet.</span>}
</div>
<div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} />
{<span class="textbox__info"> {formdata["last_name"]?formdata["last_name"].length:0} von {maxlength_name} Zeichen verwendet.</span>}
</div>
</div>
<h3>Authentifizierung</h3>
<div className='row'>
<div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} />
{<span class="textbox__info"> {formdata["rfid_uid"]?formdata["rfid_uid"].length:0} von {maxlength_rfid} Zeichen verwendet.</span>}
</div>
<div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} />
{<span class="textbox__info"> {formdata["user_pin"] ?formdata["user_pin"].length:0} von {maxlength_pin} Zeichen verwendet.</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;

View File

@ -1,12 +1,13 @@
import { h } from 'preact';
import { Link } from 'preact-router';
import { useContext, useState } from 'preact/hooks';
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>
@ -18,7 +19,6 @@ function Home() {
</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>

View File

@ -3,6 +3,7 @@ import { route } from 'preact-router';
import { useContext, useState } from 'preact/hooks';
import AppState from '../../store/AppState';
import Breadcrumbs from '../../components/breadcrumbs';
import { CheckBox, Button, TextBox } from '../../components/controls';
import api from '../../api'
function Login() {
let [sessiondata, setsession] = useContext(AppState).session;
@ -22,7 +23,7 @@ function Login() {
setsession(newsession);
}
else {
set({ ...val, error: "user" });
set(prev=>({ ...prev, error: "user" }));
}
set({ username: '', password: '' });
})
@ -36,17 +37,18 @@ function Login() {
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
{val.error !== null && <span style={'color: red'}>Fehler: Ungültige Anmeldedaten.</span>}
<form id="login_form" onSubmit={onSubmit} >
<div class="input-box">
<input id="name" type="text" placeholder="Username" onInput={e => set({ ...val, username: e.target.value })} value={val.username} />
<label for="name">Benutzername</label>
<div className='row'>
<TextBox maxlength={25} formdata={val} formchange={set} id="username" label="Benutzername" />
</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 className='row'>
<TextBox maxlength={25} formdata={val} formchange={set} id="password" label="Passwort" type='password' />
</div>
<div class={'button'} onClick={onSubmit} ><input type="submit" value="Anmelden" /></div>
<div className='row'>
<CheckBox id="permanent" formdata={val} formchange={set} label="Angemeldet bleiben?" />
</div>
<Button onClick={onSubmit}>Anmelden</Button>
</form>
</div>

View File

@ -1,10 +1,11 @@
import { h } from 'preact';
import { Breadcrumbs } from "../../components";
function System() {
return (
<div className='container'>
<Breadcrumbs items={['Systemeinstellungen']} />
<div className={'contentbox'} >
<h2>System</h2>
<h3>WiFi Setup</h3>

View File

@ -2,39 +2,56 @@ import { h } from "preact";
import { UserList, Pageselector, Breadcrumbs } from "../../components";
import api from '../../api'
import { route } from 'preact-router';
import { useContext, useEffect, useState } from "preact/hooks";
import { useContext, useEffect, useState, useMemo } from "preact/hooks";
import AppState, { UserTable } from "../../store";
function Users({ pageid }) {
const [viewstate, setview] = useState({ limit: 100, page: 1, pages: null })
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({ ...viewstate, page })
setview(state => ({ ...state, page }));
}
useEffect(() => {
if (usertable === undefined)
console.log("TOKEN");
console.log(sessiondata.token);
api.fetchdb(sessiondata.token).then(imported => {
let action = { type: 'import', imported };
userreducer(action);
});
}, [sessiondata]);
useEffect(() => {
setview({ ...viewstate, pages: Math.ceil(usertable.length / viewstate.limit) });
console.log("New View effect")
}, [usertable])
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({ ...viewstate, page: pageid })
setview((state) => ({ ...state, page: pageid }))
}, [pageid])
const deleteUser = (user) => {
api.deleteUser(sessiondata.token, user).then(r => {
let action = {
@ -43,7 +60,6 @@ function Users({ pageid }) {
}
userreducer(action)
})
}
const calculateView = () => {
let start = viewstate.limit * (viewstate.page - 1);
@ -53,15 +69,22 @@ function Users({ pageid }) {
const editUser = (user) => {
route(`/edituser/${user.uid}`);
}
const navigation = ["Users"];
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'} /><button>Hinzufügen</button> Limit: <select><optgroup label={'Anzahl'}><option>10</option><option>25</option><option>50</option><option>100</option></optgroup><option>Alle</option></select></div>
<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={usertable} deleteUser={deleteUser} editUser={editUser} />
<UserList {...calculateView()} userlist={results} deleteUser={deleteUser} editUser={editUser} />
<Pageselector start={1} end={viewstate.pages} current={viewstate.page} setPage={setPage} />

View File

@ -18,7 +18,7 @@ export const sessionReducer = (state, action) => {
export const userTableReducer = (state, action) => {
let user = action.user;
switch (action.type) {
case 'create': return [...state, { line: state.length, ...user }]
case 'create': return [...state, { line: state.length, ...user }];
case 'delete': {
let newstate = [];
let newindex = 0;

View File

@ -10,7 +10,7 @@
text-decoration: none
border: solid 1px #999
border-radius: .2rem
input[type=submit]
button
display: block
background: none
width: 100%
@ -32,7 +32,7 @@
transition: all ease-in-out 250ms
&:hover
color: #fff
input[type=submit]
button
background: #333
color: #fff
border-radius: .3rem

View File

@ -1,7 +1,9 @@
@mixin textfield
margin: 0 1em
width: 100%
position: relative
display: inline-block
margin: 0 .25em 1em .25em
display: block
margin: 0
background: #fafafa
border-radius: .3em
border-bottom: 1px solid #ccc
@ -35,3 +37,51 @@
top: 0em
left: 0em
font-size: 1em
&__info
color: #ccc
font-size: 0.7em
@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

View File

@ -74,23 +74,30 @@ body
.page-nav-bar
@include pageselector.pageselector
.user-list-item
position: relative
overflow: hidden
display: flex
display: inline-flex
flex-shrink: 1
min-width: calc(50% - 1em)
flex-grow: 1
flex-direction: row
background: #eee
margin: 0 0.5em
padding: .5em 0
padding: .5em 3em 0 0
border-radius: .3em
&:not(:last-child)
margin-bottom: .5em
.user-attributes
padding: .5em
.btn-group
margin-left: auto
position: absolute
top: 0
right: 0
bottom: 0
display: inline-flex
flex-direction: column
align-self: flex-end
justify-self: flex-end
align-items: center
justify-content: center
@mixin button()
position: relative
border: none
@ -161,3 +168,21 @@ footer
.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