added files

This commit is contained in:
Jean Jacques Avril 2022-03-04 20:29:15 +01:00
parent 275839a2f7
commit 981dcb3789
36 changed files with 54952 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
/build
/*.log

54050
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"private": true,
"name": "doorlock_pwa",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "preact build",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch",
"lint": "eslint src",
"test": "jest"
},
"eslintConfig": {
"extends": "preact",
"ignorePatterns": [
"build/"
]
},
"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"
},
"dependencies": {
"preact": "^10.3.2",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1",
"sass": "^1.49.9"
},
"jest": {
"preset": "jest-preset-preact",
"setupFiles": [
"<rootDir>/tests/__mocks__/browserMocks.js",
"<rootDir>/tests/__mocks__/setupTests.js"
]
}
}

0
preact.config.js Normal file
View File

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.2" baseProfile="tiny" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 500 500" overflow="visible" xml:space="preserve">
<path fill="#CCCCCC" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M349.5,421.5h-199c-8.28,0-15-6.72-15-15
v-255h229v255C364.5,414.78,357.78,421.5,349.5,421.5z"/>
<path fill="#CCCCCC" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M386.81,99.5H314.5V80.38
c0-9.88-8.01-17.88-17.88-17.88h-92.24c-9.88,0-17.88,8.01-17.88,17.88V99.5h-73.31c-5.35,0-9.69,4.34-9.69,9.69v20.62
c0,5.35,4.34,9.69,9.69,9.69h273.62c5.35,0,9.69-4.34,9.69-9.69v-20.62C396.5,103.84,392.16,99.5,386.81,99.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.2" baseProfile="tiny" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 500 500" overflow="visible" xml:space="preserve">
<polyline fill="#CCCCCC" stroke="#000000" stroke-miterlimit="10" points="181.5,395.5 86.5,395.5 86.5,300.5 "/>
<g>
<rect x="355.37" y="40.63" transform="matrix(0.7071 -0.7071 0.7071 0.7071 31.9147 294.7168)" fill="#CCCCCC" stroke="#000000" stroke-width="1" stroke-miterlimit="9.9999" width="32.69" height="136.4"/>
<rect x="94.59" y="168.11" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.5641 241.9096)" fill="#CCCCCC" stroke="#000000" stroke-width="1" stroke-miterlimit="9.9999" width="299.29" height="136.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View File

@ -0,0 +1,23 @@
import { h } from "preact";
const Pageselector = (props) => {
let start = 1;
let end = 7;
return (
<div class="page-nav-bar">
<ul>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">&gt;</a></li>
</ul>
</div>);
}
export default Pageselector;

23
src/components/app.js Normal file
View File

@ -0,0 +1,23 @@
import { createRef, h } from 'preact';
import { Router } from 'preact-router';
import Header from './header';
import Login from '../routes/login';
import Profile from '../routes/profile';
import Menu from './menu';
import Users from '../routes/users';
const menu = createRef();
const App = () => (
<div id="wrapper">
<Header menu={menu} />
<Menu ref={menu} />
<Login />
<Router>
<Profile path="/profile/" user="me" />
<Profile path="/profile/:user" />
<Users path="/users" />
</Router>
</div>
)
export default App;

View File

@ -0,0 +1,10 @@
import {h} from 'preact';
const Breadcrumbs = (props) =>{ if(props.items) return(
<div class="breadcrumb">
<ul>
{props.items.map((text,i)=><li key={i}><a href="#">{text}</a> </li>)}
</ul>
</div>
)}
export default Breadcrumbs;

View File

@ -0,0 +1,37 @@
import { Component, createRef, h } from 'preact';
//import { Link } from 'preact-router/match';
class Header extends Component {
state = { shown: true }
hamburger = createRef();
togglemenu() {
if (this.props.menu.current) {
this.setState(prev => ({ shown: !prev.shown }));
this.props.menu.current.toggle(this.state);
if (this.state.shown) {
this.hamburger.current.classList.add('hamburger-active');
}
else {
this.hamburger.current.classList.remove('hamburger-active');
}
}
}
render(props){
return (
<header className='header'>
<div className="container">
<h1>Login</h1>
<div id="hamburger-button" ref={this.hamburger} className='hamburger' onClick={() => this.togglemenu(props)}>
<hr />
<hr />
<hr />
</div>
</div>
</header>
);
}
}
export default Header;

View File

@ -0,0 +1,37 @@
import { Component, h } from 'preact';
class Menu extends Component {
menu_items = [
{ text: "Übersicht", path: "/" },
{ text: "Benutzer", path: "/user" },
{ text: "System", path: "/system" },
{ text: "Backup", path: "/backup" },
{ text: "Abmelden", path: "/" }
]
state = { shown: false }
toggle(val) {
this.setState(val);
}
getAlert() {
alert("getAlert from Child");
}
render(props, state) {
return (<div id="menu-screen" className='page' style={`display: ${state.shown ? 'flex' : 'none'}`}>
<div class="container">
<nav className='menu'>
<ul>
{this.menu_items.map((element,i) => (<li key={i}><a href={element.link}>{element.text}</a></li>))}
</ul>
</nav>
</div>
</div>)
}
}
export default Menu;

View File

@ -0,0 +1,40 @@
import { Component } from "preact";
class UserList extends Component {
deleteUser(user){
alert(`delete: ${user.uid}`);
}
editUser(user){
alert(`edit: ${user.uid}`);
}
displayUser(user,key){
return (
<div key={key} class="user-list-item">
<div class="user-attributes">
<span><b>UID:</b> {user.uid}</span> <span>(<b>Aktiv</b>)</span><br />
<span><b>Vorname:</b> {user.first_name}</span><br />
<span><b>Nachname:</b> {user.last_name}</span><br />
<span><b>RFID:</b> {user.rfid}</span>
<span><b>PIN:</b> {user.pin}</span>
</div>
<div class="btn-group">
<button class="btn-trash" onClick={()=>this.deleteUser(user)}>Löschen</button>
<button class="btn-edit" onClick={()=>this.editUser(user)}>Bearbeiten</button>
</div>
</div>);
}
state = {};
render() {
return (
<>
{this.props.userlist&&this.props.userlist.map((user,i)=>this.displayUser(user,i))}
</>
);
}
}
export default UserList

4
src/index.js Normal file
View File

@ -0,0 +1,4 @@
import './style/style.sass';
import App from './components/app';
export default App;

21
src/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "doorlock_pwa",
"short_name": "doorlock_pwa",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#fff",
"theme_color": "#673ab8",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/assets/icons/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

11
src/routes/home/index.js Normal file
View File

@ -0,0 +1,11 @@
import { h } from 'preact';
import style from './style.css';
const Home = () => (
<div class={style.home}>
<h1>Home</h1>
<p>This is the Home component.</p>
</div>
);
export default Home;

View File

@ -0,0 +1,5 @@
.home {
padding: 56px 20px;
min-height: 100%;
width: 100%;
}

View File

@ -0,0 +1,39 @@
import { Component, h } from 'preact';
import Breadcrumbs from '../../components/breadcrumbs';
class Login extends Component {
state = { name: '', password: '' };
navigation = ["Login"];
onSubmit = (e) => {
e.preventDefault();
console.log(this.state);
this.setState({ name: '', password: '' });
}
render() {
return (<div id="login-screen" class="page">
<div class="container">
<Breadcrumbs items={this.navigation} />
<form id="login_form" onSubmit={this.onSubmit} >
<p>
Bitte melden Sie sich mit ihren Nutzerdaten an.
</p>
<div class="input-box">
<input id="name" type="text" placeholder="Username" onInput={e => this.setState(prev => ({ ...prev, name: e.target.value }))} value={this.state.name} />
<label for="name">Benutzername</label>
</div>
<div class="input-box">
<input id="pass" type="password" placeholder="Passwort" onInput={e => this.setState(prev => ({ ...prev, password: e.target.value }))} value={this.state.password} />
<label for="pass">Password</label>
</div>
<input type="submit" value="Submit" />
</form>
</div>
</div>);
}
}
export default Login;

View File

@ -0,0 +1,31 @@
import { h } from 'preact';
import {useEffect, useState} from "preact/hooks";
import style from './style.css';
// Note: `user` comes from the URL, courtesy of our router
const Profile = ({ user }) => {
const [time, setTime] = useState(Date.now());
const [count, setCount] = useState(10);
useEffect(() => {
let timer = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
return (
<div class={style.profile}>
<h1>Profile: {user}</h1>
<p>This is the user profile for a user named { user }.</p>
<div>Current time: {new Date(time).toLocaleString()}</div>
<p>
<button onClick={() => setCount((count) => count + 1)}>Click Me</button>
{' '}
Clicked {count} times.
</p>
</div>
);
}
export default Profile;

View File

@ -0,0 +1,5 @@
.profile {
padding: 56px 20px;
min-height: 100%;
width: 100%;
}

View File

@ -0,0 +1,21 @@
import { Component } from "preact";
import UserList from "../../components/userlist";
import Pageselector from "../../components/Pageselector";
class Users extends Component {
userlist = [
{ uid: 1234, first_name: 'Max', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
{ uid: 12341, first_name: 'Max1', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
{ uid: 12342, first_name: 'Max2', last_name: 'Muster', rfid: 'D3A2E35E', pin: 1234 },
];
render() {
return (
<div id="user-screen" class="page">
<div class="container">
<UserList userlist={this.userlist} />
<Pageselector />
</div>
</div>);
}
}
export default Users;

View File

@ -0,0 +1,52 @@
@mixin breadcrumbs
padding: 1em 0
ul
display: flex
background: #eee
box-shadow: inset 0 0 .3em #ccc, 0 0 .5em #ddd
padding: .3em
border: 0.05em solid #fff
border-radius: .3em
list-style-type: none
li
a
position: relative
display: inline-flex
vertical-align: middle
text-align: center
align-items: center
text-decoration: none
color: #fff
background: #ccc
height: 2em
padding: .5em .5em
&::after
content: ''
position: absolute
top: 0
left: 100%
border-top: 1em solid transparent
border-bottom: 1em solid transparent
border-left: 1em solid #ccc
z-index: 2
&:hover
background-color: #777
&::after
border-left: 1em solid #777
&:first-child a
border-top-left-radius: .3em
border-bottom-left-radius: .3em
&:not(:first-child) a
margin-left: .2em
padding-left: 1.5em
&::before
z-index: 1
content: ''
position: absolute
top: 0
left: 0%
border-top: 1em solid transparent
border-bottom: 1em solid transparent
border-left: 1em solid #eee

39
src/style/_hamburger.sass Normal file
View File

@ -0,0 +1,39 @@
@mixin hamburger-rect
width: 3em
height: .5em
border: none
margin: 0
background: #fff
border-radius: .2em
box-shadow: inset 0 0 .2em #000
position: relative
transition: 250ms all
@mixin hamburger-rect-active
background: black
box-shadow: 0 0 .1em #000
@mixin hamburger-rect-transparent
@mixin hamburger
width: 3em
display: inline-block
position: relative
float: right
hr
@include hamburger-rect
&:nth-child(1)
transform-origin: 0% 0%
&:nth-child(2)
margin: .5em 0
&:nth-child(3)
transform-origin: 0% 100%
&-active
hr:nth-child(1)
transform: translate(.25em, 0em) rotate(45deg)
@include hamburger-rect-active
hr:nth-child(2)
opacity: 0
transform: rotate(0deg) scale(0.2, 0.2)
hr:nth-child(3)
transform: translate(.25em, 0em) rotate(-45deg)
@include hamburger-rect-active

40
src/style/_login.sass Normal file
View File

@ -0,0 +1,40 @@
@mixin text-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
padding: 1em
margin: 0 auto
display: flex
flex-direction: column
.input-box
@include text-input
p
font-size: 2em
input[type=submit]
border: none
border-radius: none
font-size: 2em

28
src/style/_menu.sass Normal file
View File

@ -0,0 +1,28 @@
@mixin nav
ul
list-style: none
padding: 0
margin: 0
display: flex
flex-direction: column
li a
display: block
position: relative
padding: 1em
font-size: 2em
font-style: none
text-decoration: none
color: #666
//text-shadow: 0 0 .2em #000
width: 100%
//background: #ccc
&::after
top: 100%
left: 0
z-index: 2
content: ''
position: absolute
width: 100%
height: .1em
background: #8a8080
//border-bottom-left-radius: 2em 1em

View File

@ -0,0 +1,39 @@
@mixin hamburger-rect
width: 3em
height: .5em
border: none
margin: 0
background: #fff
border-radius: .2em
box-shadow: inset 0 0 .2em #000
position: relative
transition: 250ms all
@mixin hamburger-rect-active
background: black
box-shadow: 0 0 .1em #000
@mixin hamburger-rect-transparent
@mixin hamburger
width: 3em
display: inline-block
position: relative
float: right
hr
@include hamburger-rect
&:nth-child(1)
transform-origin: 0% 0%
&:nth-child(2)
margin: .5em 0
&:nth-child(3)
transform-origin: 0% 100%
.activated
hr:nth-child(1)
transform: translate(.25em, 0em) rotate(45deg)
@include hamburger-rect-active
hr:nth-child(2)
opacity: 0
transform: rotate(0deg) scale(0.2, 0.2)
hr:nth-child(3)
transform: translate(.25em, 0em) rotate(-45deg)
@include hamburger-rect-active

32
src/style/back/index.sass Normal file
View File

@ -0,0 +1,32 @@
*
//border: red 1px dotted
box-sizing: border-box
html
font-family: Helvetica, sans-serif
font-size: 16px
body
margin: 0
padding: 0
background-color: white
//min-height: 100vh
width: 100%
#wrapper
display: flex
flex-direction: column
justify-content: flex-start
align-items: stretch
min-height: 100%
width: 100%
.container
width: clamp(5ch, 100%,75ch)
margin: 0 auto
.page
//overflow: auto
display: flex
background: white
width: 100%
padding-top: 3em

21
src/style/back/style.sass Normal file
View File

@ -0,0 +1,21 @@
@use 'hamburger'
.header
z-index: 100
position: fixed
width: 100%
height: 4em
display: flex
flex-direction: column
justify-content: flex-end
padding: .5em 0
margin: 0 0 0 0
background: linear-gradient(#9e9e9e 0% , #d1d1d1 100%)
box-shadow: 0 0 .2em #444
h1
display: inline-block
margin: 0 0
color: white
.hamburger
font-size: 1em
@include hamburger.hamburger

146
src/style/style.sass Normal file
View File

@ -0,0 +1,146 @@
@use 'hamburger'
@use 'menu'
@use 'login'
@use 'breadcrumbs'
*
//border: red 1px dotted
box-sizing: border-box
html
font-family: Helvetica, sans-serif
font-size: 16px
body
margin: 0
padding: 0
background-color: white
//min-height: 100vh
width: 100%
#wrapper
display: flex
flex-direction: column
justify-content: flex-start
align-items: stretch
min-height: 100%
width: 100%
.header
z-index: 100
position: fixed
width: 100%
height: 4em
display: flex
flex-direction: column
justify-content: flex-end
padding: .5em 0
margin: 0 0 0 0
background: linear-gradient(#9e9e9e 0% , #d1d1d1 100%)
box-shadow: 0 0 .2em #444
h1
display: inline-block
margin: 0 0
color: white
.hamburger
font-size: 1em
@include hamburger.hamburger
.container
width: clamp(5ch, 100%,75ch)
margin: 0 auto
.breadcrumb
@include breadcrumbs.breadcrumbs
.page
//overflow: auto
display: flex
background: white
width: 100%
padding-top: 3em
.menu
@include menu.nav
#login_form
@include login.login-form
.page-nav-bar
text-align: center
//background-color: #ccc
margin: 1em
ul
list-style: none
justify-content: center
margin: 0
padding: 0
display: flex
li a
text-decoration: none
font-weight: bold
padding: .2em .5em
margin: .3em
border-radius: .3em
background: #eee
color: #ccc
border: .1em solid #ccc
&:hover
color: #333
background: #fff
border: .1em solid #ccc
.user-list-item
overflow: hidden
display: flex
flex-direction: row
background: #eee
padding: .5em 0
border-radius: .3em
&:not(:last-child)
margin-bottom: .5em
.user-attributes
padding: .5em
.btn-group
margin-left: auto
display: inline-flex
flex-direction: column
align-self: flex-end
justify-self: flex-end
@mixin button()
position: relative
border: none
margin-left: auto
background: #eee
//background-size: 80% 80%
background-repeat: no-repeat
background-position: left
background-origin: border-box
transition: 150ms background-size, 200ms padding, 100ms width
padding: 0em
padding-left: 3em
width: 3em
height: 3em
border-bottom-left-radius: 1.5em
border-top-left-radius: 1.5em
&:not(:last-of-type)
margin-bottom: .5em
&:hover
width: auto
filter: invert(120%)
padding-right: .5em
background-color: #000
color: #ccc
box-shadow: 0 0 .5em #333
background-position: 00%
//background-size: 90% 90%
&::before
position: absolute
top: 0
left: 0
bottom: 0
right: 0
content: ''
border-radius: 50%
//border: .1em solid #333
.btn-trash
@include button()
background-image: url("../assets/icons/delete-icon3.svg")
.btn-edit
@include button()
background-image: url("../assets/icons/edit-icon3.svg")

4
src/sw.js Normal file
View File

@ -0,0 +1,4 @@
import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
setupRouting();
setupPrecaching(getFiles());

15
src/template.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><% preact.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
<% preact.headEnd %>
</head>
<body>
<% preact.bodyEnd %>
</body>
</html>

View File

@ -0,0 +1,21 @@
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
* An example how to mock localStorage is given below 👇
*/
/*
// Mocks localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: (key) => store[key] || null,
setItem: (key, value) => store[key] = value.toString(),
clear: () => store = {}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}); */

View File

@ -0,0 +1,3 @@
// This fixed an error related to the CSS and loading gif breaking my Jest test
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
module.exports = 'test-file-stub';

View File

@ -0,0 +1,6 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-preact-pure';
configure({
adapter: new Adapter()
});

12
tests/header.test.js Normal file
View File

@ -0,0 +1,12 @@
import { h } from 'preact';
import Header from '../src/components/header';
// See: https://github.com/preactjs/enzyme-adapter-preact-pure
import { shallow } from 'enzyme';
describe('Initial Test of the Header', () => {
test('Header renders 3 nav items', () => {
const context = shallow(<Header />);
expect(context.find('h1').text()).toBe('Preact App');
expect(context.find('Link').length).toBe(3);
});
});

68
webpack.config.js Normal file
View File

@ -0,0 +1,68 @@
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// App directory
const appDirectory = fs.realpathSync(process.cwd());
// Gets absolute path of file within app directory
const resolveAppPath = relativePath => path.resolve(appDirectory, relativePath);
// Host
const host = process.env.HOST || 'localhost';
// Required for babel-preset-react-app
process.env.NODE_ENV = 'development';
module.exports = {
// Environment mode
mode: 'development',
// Entry point of app
entry: resolveAppPath('src'),
output: {
// Development filename output
filename: 'static/js/bundle.js',
},
devServer: {
// Serve index.html as the base
// Enable compression
compress: true,
// Enable hot reloading
hot: true,
host,
port: 8080,
},
plugins: [
// Re-generate index.html with injected script tag.
// The injected script tag contains a src value of the
// filename output defined above.
new HtmlWebpackPlugin({
inject: true,
template: resolveAppPath('public/index.html'),
}),
],
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
}