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": { "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

View File

@ -1,11 +1,30 @@
const server = 'http://192.168.4.22' 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) { async function login(username, password) {
let formData = new FormData(); let formData = new FormData();
formData.append('username', username) formData.append('username', username)
formData.append('password', password); formData.append('password', password);
formData.append('action', 'login'); 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; return resp;
} }
async function logout(token) { async function logout(token) {
@ -13,10 +32,15 @@ async function logout(token) {
formData.append('token', token); formData.append('token', token);
formData.append('action', 'logout'); formData.append('action', 'logout');
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 => resp.text());
localStorage.clear();
return resp; return resp;
} }
function checkAuth(token) { async function checkAuth(token) {
return token === 'DUMMYTOKEN'; 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') { async function fetchdb(token = 'azif7eqCl5') {
//fetch(`${server}/api/userdb`).then() //fetch(`${server}/api/userdb`).then()
@ -65,6 +89,6 @@ function parsedb(raw) {
return users; 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 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 { h } from "preact";
import { Router, route } from "preact-router"; import { Router, route } from "preact-router";
import { useReducer } from "preact/hooks"; import { useEffect, useReducer } from "preact/hooks";
import { Header, Menu } from "./index.js"; import { Header, Menu } from "./index.js";
import { Home, Users, EditUser, Login, Logout, System } from "../route"; import { Home, Users, EditUser, Login, Logout, System } from "../route";
import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store"; import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store";
import api from "../api/index.js";
function App() { function App() {
// useReducer // useReducer
const menu = useReducer(menuReducer, false); const menu = useReducer(menuReducer, false);
const session = useReducer(sessionReducer, { active:true ,token: "0t6BF94Y92" }); const session = useReducer(sessionReducer, {});
const [usertable, userreducer] = useReducer(userTableReducer, []); 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 = [ this.menu_items = [
{ text: "Übersicht", path: "/" }, { text: "Übersicht", path: "/" },
{ text: "Benutzer anlegen", path: "/newuser" }, { 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 ( return (
<> <div className="row">
{userlist&&(start!==undefined&&end?userlist.slice(start,end).map((user)=>displayUser(user)):userlist.map((user)=>displayUser(user)))} {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 { route } from 'preact-router';
import AppState, { UserTable } from "../../store"; import AppState, { UserTable } from "../../store";
import api from '../../api' import api from '../../api'
import { Breadcrumbs } from "../../components";
import { CheckBox, Button, TextBox } from '../../components/controls';
function EditUser({ userid }) { function EditUser({ userid }) {
let [sessiondata,] = useContext(AppState).session; let [sessiondata,] = useContext(AppState).session;
const { usertable, userreducer } = useContext(UserTable); const { usertable, userreducer } = useContext(UserTable);
const [formdata, formchange] = useState({}); const [formdata, formchange] = useState({});
const onChange = (e) => { let maxlength_uid = 10;
console.log(e); let maxlength_name = 25;
if (e.target.type === 'checkbox') let maxlength_pin = 10;
formchange({ ...formdata, [e.target.id]: e.target.checked }) let maxlength_rfid = 8;
else useEffect(() => {
formchange({ ...formdata, [e.target.id]: e.target.value }) let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
} if (user !== undefined) {
useEffect(()=>{
let user = userid !== undefined?usertable.find(u => u.uid === userid):undefined;
if(user!==undefined){
formchange(user); 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) => { const onSubmit = (e) => {
e.preventDefault(); e.preventDefault();
console.log(api); let idcount = uidCount(usertable, formdata);
if(formdata.line!==undefined){ console.log(idcount);
if (formdata.line !== undefined) {
api.updateUser(sessiondata.token, formdata).then(r => { api.updateUser(sessiondata.token, formdata).then(r => {
userreducer({ type: 'update', user: r }) userreducer({ type: 'update', user: r })
}); });
}else{ route('/users');
} else if (idcount === 0) {
api.createUser(sessiondata.token, formdata).then(r => { api.createUser(sessiondata.token, formdata).then(r => {
userreducer({ type: 'create', user: r }) userreducer({ type: 'create', user: r })
}); });
}
route('/users'); 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 ( return (
<div className='container'> <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}> <form onSubmit={onSubmit}>
<Input id="uid" label="UserID" /> <h3>Eindeutige Identifikationsnummer</h3>
<Input id="first_name" label="Vorname" /> <div className='row'>
<Input id="last_name" label="NachName" /> <div className='column'>
<Input id="rfid_uid" label="RFID" /> <TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} />
<Input id="user_pin" label="Pin" /> {<span class="textbox__info"> {formdata["uid"]?formdata["uid"].length:0} von {maxlength_uid} Zeichen verwendet.</span>}
<Input id="enabled" type={'checkbox'} label="" /> </div>
<div className={'button'} ><input type={'submit'}>Speichern</input> </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> </form>
</div> </div>
</div>
) )
} }
export default EditUser; export default EditUser;

View File

@ -1,12 +1,13 @@
import { h } from 'preact'; import { h } from 'preact';
import { Link } from 'preact-router'; import { Link } from 'preact-router';
import { useContext, useState } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
import { Breadcrumbs } from "../../components";
function Home() { function Home() {
let [sessiondata, ] = useContext(AppState).session; let [sessiondata, ] = useContext(AppState).session;
return ( return (
<div class="container"> <div class="container">
<Breadcrumbs items={['Startseite']} />
<div className={'contentbox'} > <div className={'contentbox'} >
<h2>Startseite</h2> <h2>Startseite</h2>
<p>Willkommen zurück {sessiondata.username} </p> <p>Willkommen zurück {sessiondata.username} </p>
@ -18,7 +19,6 @@ function Home() {
</div> </div>
<div> <div>
<p>Hier können Sie den Türöffner manuell aktivieren.</p> <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> <p>Um Benutzer anzulegen, öffnen sie das Menü und tippen auf <Link href="/newuser">Benutzer anlegen</Link> </p>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -2,48 +2,64 @@ import { h } from "preact";
import { UserList, Pageselector, Breadcrumbs } from "../../components"; import { UserList, Pageselector, Breadcrumbs } from "../../components";
import api from '../../api' import api from '../../api'
import { route } from 'preact-router'; 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"; import AppState, { UserTable } from "../../store";
function Users({ pageid }) { function Users({ pageid }) {
const [viewstate, setview] = useState({ limit: 100, page: 1, pages: null })
const { usertable, userreducer } = useContext(UserTable); const { usertable, userreducer } = useContext(UserTable);
let [sessiondata,] = useContext(AppState).session; 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) => { const setPage = (e) => {
e.preventDefault(); e.preventDefault();
console.log(e) console.log(e)
let page = e.target.text; let page = e.target.text;
setview({ ...viewstate, page }) setview(state => ({ ...state, page }));
} }
useEffect(() => { useEffect(() => {
if (usertable === undefined) setview((state) => {
console.log("TOKEN"); let pages = Math.ceil(results.length / state.limit)
console.log(sessiondata.token); if (state.page >= pages) return ({ ...state, pages, page: pages })
api.fetchdb(sessiondata.token).then(imported => { return ({ ...state, pages })
let action = { type: 'import', imported }; })
userreducer(action); }, [results.length, viewstate.limit])
});
}, [sessiondata]);
useEffect(() => {
setview({ ...viewstate, pages: Math.ceil(usertable.length / viewstate.limit) });
console.log("New View effect")
}, [usertable])
useEffect(() => { useEffect(() => {
if (pageid && !isNaN(pageid)) if (pageid && !isNaN(pageid))
setview({ ...viewstate, page: pageid }) setview((state) => ({ ...state, page: pageid }))
}, [pageid]) }, [pageid])
const deleteUser = (user) => { const deleteUser = (user) => {
api.deleteUser(sessiondata.token,user).then(r=>{ api.deleteUser(sessiondata.token, user).then(r => {
let action = { let action = {
type: 'delete', type: 'delete',
user, r user, r
} }
userreducer(action) userreducer(action)
}) })
} }
const calculateView = () => { const calculateView = () => {
let start = viewstate.limit * (viewstate.page - 1); let start = viewstate.limit * (viewstate.page - 1);
@ -53,15 +69,22 @@ function Users({ pageid }) {
const editUser = (user) => { const editUser = (user) => {
route(`/edituser/${user.uid}`); 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 ( return (
<div class="container"> <div class="container">
<Breadcrumbs items={navigation} /> <Breadcrumbs items={navigation} />
<div className={'contentbox'} > <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> </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} /> <Pageselector start={1} end={viewstate.pages} current={viewstate.page} setPage={setPage} />

View File

@ -18,12 +18,12 @@ export const sessionReducer = (state, action) => {
export const userTableReducer = (state, action) => { export const userTableReducer = (state, action) => {
let user = action.user; let user = action.user;
switch (action.type) { switch (action.type) {
case 'create': return [...state, { line: state.length, ...user }] case 'create': return [...state, { line: state.length, ...user }];
case 'delete': { case 'delete': {
let newstate = []; let newstate = [];
let newindex = 0; let newindex = 0;
state.forEach((u, i) => { state.forEach((u, i) => {
if (user.uid && u.uid != user.uid|| user.line && i != user.line ) if (user.uid && u.uid != user.uid || user.line && i != user.line)
newstate.push({ ...u, line: newindex++ }) newstate.push({ ...u, line: newindex++ })
return newstate; return newstate;
}, []); }, []);

View File

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

View File

@ -1,7 +1,9 @@
@mixin textfield @mixin textfield
margin: 0 1em
width: 100%
position: relative position: relative
display: inline-block display: block
margin: 0 .25em 1em .25em margin: 0
background: #fafafa background: #fafafa
border-radius: .3em border-radius: .3em
border-bottom: 1px solid #ccc border-bottom: 1px solid #ccc
@ -35,3 +37,51 @@
top: 0em top: 0em
left: 0em left: 0em
font-size: 1em 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 .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
margin: 0 0.5em margin: 0 0.5em
padding: .5em 0 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
@ -161,3 +168,21 @@ footer
.textbox .textbox
@include input.textfield @include input.textfield
font-size: 1.5em 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