Add items, choose user type

This commit is contained in:
Adrian Woźniak 2022-07-05 16:02:27 +02:00
parent 277aae9341
commit 4a21ba9a18
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
28 changed files with 1602 additions and 706 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
node_modules

View File

@ -24,6 +24,7 @@
* {
--hover-color: #f18902;
--border-slim-color: #495057;
--border-light-gray-color: #e5e5e5;
}
main {

View File

@ -1,705 +0,0 @@
const S = Symbol();
customElements.define('ow-nav', class extends HTMLElement {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
section {
display: flex;
align-items: stretch;
align-content: stretch;
margin-bottom: 12px;
color: #2b2727;
font-family: Raleway, sans-serif;
font-size: 14px;
line-height: 32px;
}
section::after {
display: block;
content: ' ';
border: none;
padding: 10px 18px;
text-decoration: none;
color: #2b2727;
text-transform: uppercase;
align-self: stretch;
flex-shrink: 20;
flex-grow: 20;
}
</style>
<section>
<slot></slot>
</section>
`;
}
});
customElements.define('ow-path', class extends HTMLElement {
static get observedAttributes() {
return ['selected', 'path'];
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
a {
display: block;
padding: 10px 18px;
text-decoration: none;
color: #495057;
text-transform: uppercase;
border: none;
border-bottom: 1px solid var(--border-slim-color);
}
a:hover {
color: var(--hover-color);
}
:host(:not([selected])) a {
border: none;
}
</style>
<a><slot></slot></a>
`;
}
connectedCallback() {
this.selected = this.getAttribute('selected');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'selected':
return this.selected = newV;
case 'path':
return this.path = newV;
}
}
get selected() {
return this.getAttribute('selected') === 'selected';
}
set selected(value) {
if (value === 'selected') this.setAttribute('selected', 'selected');
else this.removeAttribute('selected');
}
get path() {
return this.getAttribute('path') || ''
}
set path(value) {
if (!value || value === '') {
this.removeAttribute('path');
return;
}
this.setAttribute('path', value);
this[S].querySelector('a').setAttribute('href', value);
}
});
customElements.define('local-services', class extends HTMLElement {
static get observedAttributes() {
return ['filter']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
::slotted(local-service[local-services-visible="invisible"]) {
display: none;
}
input {
font-size: 20pt;
line-height: 2.6em;
height: 2.6em;
margin: 0;
padding: 0;
width: 100%;
border:none;
outline:none;
display: block;
background: transparent;
border-bottom: 1px solid #ccc;
text-indent: 20px;
}
</style>
<section>
<input type="text" id="filter" placeholder="Filtruj" />
</section>
<section id="items">
<slot name="services"></slot>
</section>
`;
{
const filter = shadow.querySelector('#filter');
let t = null;
filter.addEventListener('change', ev => {
ev.stopPropagation();
this.filter = ev.target.value;
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
const value = ev.target.value;
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = value;
t = null;
}, 1000 / 3);
});
}
}
connectedCallback() {
this.filter = this.getAttribute('filter');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'filter':
return this.filter = newV;
}
}
get filter() {
return this.getAttribute('filter');
}
set filter(value) {
if (!value || value === '') {
this.removeAttribute('filter');
for (const el of this.querySelectorAll('local-service')) {
el.removeAttribute('local-services-visible');
}
} else {
this.setAttribute('filter', value);
for (const el of this.querySelectorAll('local-service')) {
if (!el.name) continue;
if (el.name.includes(value)) {
el.setAttribute('local-services-visible', 'visible');
} else {
el.setAttribute('local-services-visible', 'invisible');
}
}
}
}
});
customElements.define('local-service', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'service-id', 'state']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name':
return this[S].querySelector('#name').textContent = newV;
}
}
get name() {
return this.getAttribute('name') || ''
}
});
customElements.define('local-service-item', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'price']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#item {
display: flex;
justify-content: space-between;
}
h3 {
font-weight: normal;
}
#price {
font-weight: bold;
}
</style>
<section id="item">
<h3 id="name"></h3>
<ow-price id="price" />
</section>
`;
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
this[S].querySelector('#price').value = this.price();
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name':
return this[S].querySelector('#name').textContent = newV;
case 'price':
return this[S].querySelector('#price').value = newV;
}
}
price(s) {
const n = parseInt(s || this.getAttribute('price'));
return isNaN(n) ? 0 : n;
}
});
customElements.define('ow-price', class extends HTMLElement {
static get observedAttributes() {
return ['value', 'currency']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#price {
font-weight: bold;
}
</style>
<span id="price"></span>
`;
}
connectedCallback() {
this[S].querySelector('#price').textContent = this.formatted;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'price': {
this.value = newV;
this[S].querySelector('#price').textContent = this.formatted;
break;
}
}
}
get formatted() {
let v = this.value;
let major = Math.ceil(v / 100);
let minor = v % 100;
let formatted = `${ major },${ minor < 10 ? `0${ minor }` : minor }`;
return `${ formatted }${ this.currency }`
}
get value() {
const n = parseInt(this.getAttribute('value'));
return isNaN(n) ? 0 : n;
}
set value(v) {
this.setAttribute('value', v)
}
get currency() {
return this.getAttribute('currency') || 'PLN'
}
});
customElements.define('ow-account', class extends HTMLElement {
static get observedAttributes() {
return ['mode']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
article > * {
display: none;
}
:host([mode="login"]) login-form, :host([mode="login"]) #switch-register {
display: block;
}
:host([mode="register"]) register-form, :host([mode="register"]) #switch-login {
display: block;
}
a{
display: block;
cursor: pointer;
}
a:hover {
color: var(--hover-color);
border-bottom-color: var(--hover-color);
text-decoration: none;
}
</style>
<article>
<login-form></login-form>
<register-form></register-form>
<section id="switch-login">
<a>Nie masz konta? Utwórz nowe</a>
</section>
<section id="switch-register">
<a>Masz konta? Zaloguj się</a>
</section>
</article>
`;
shadow.querySelector('#switch-login > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'login';
});
shadow.querySelector('#switch-register > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'register';
});
}
connectedCallback() {
this.mode = 'login';
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get mode() {
return this.getAttribute('mode');
}
set mode(value) {
value = value === 'login' || value === 'register' ? value : 'login';
this.setAttribute('mode', value);
}
});
const FORM_STYLE = `
form {
display: block;
}
form > div {
display: block;
margin-bottom: 1rem;
}
form > div > input, form > div > textarea {
font-size: 16px;
border: none;
border-bottom-style: none;
border-bottom-width: medium;
border-bottom: 1px solid rgba(0,0,0,.1);
border-radius: 2px;
padding: 0;
height: 36px;
background: #fff;
color: rgba(0,0,0,.8);
font-size: 14px;
box-shadow: none !important;
display: block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-clip: padding-box;
transition: border-color .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
}
form > div > input[type="text"],
form > div > input[type="number"],
form > div > input[type="email"],
form > div > input[type="password"],
form > div > textarea {
width: calc(100% - 1.5rem - 2px);
}
form > div > label {
color: #000;
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
display: inline-block;
margin-bottom: .5rem;
}
form > div > input[type="button"], form > div > input[type="submit"] {
padding: 12px 16px;
cursor: pointer;
border: none;
border-width: 1px;
border-radius: 5px;
font-size: 14px;
font-weight: 400;
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
position: relative;
margin-bottom: 20px;
transition: .3s;
background: #46b5d1;
color: #fff;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out,
-webkit-box-shadow .15s ease-in-out;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
}
`;
customElements.define('register-form', class extends HTMLElement {
static get observedAttributes() {
return ['step']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
article > form { display: none; }
:host([step="1"]) #step-1 { display: block; }
:host([step="2"]) #step-2 { display: block; }
:host([step="3"]) #step-3 { display: block; }
${ FORM_STYLE }
.actions {
display: flex;
justify-content: space-between;
}
.actions > input:first-child {
margin-right: 8px;
}
.actions > input:last-child {
margin-left: 8px;
}
</style>
<article>
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>E-Mail</label>
<input name="email" placeholder="Email" type="email" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="submit" value="Następny" />
</div>
</form>
<form id="step-2">
<div>
<input name="name" placeholder="Nazwa usługi" type="text" required />
</div>
<div>
<label>description</label>
<textarea name="description" required></textarea>
</div>
<div class="actions">
<input type="button" value="Wróć" />
<input type="submit" value="Następny" />
</div>
</form>
<form id="step-3">
<input id="hidden-login" name="login" type="hidden" />
<input id="hidden-email" name="email" type="hidden" />
<input id="hidden-pass" name="pass" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<div class="actions">
<input type="button" value="Wróć" />
<input type="submit" value="Dodaj usługi/produkty" />
</div>
</form>
</article>
`;
const copyForm = (form) => {
for (const el of form.elements) {
if (el.name === '') continue;
finalForm.querySelector(`[id="hidden-${el.name}"]`).value = el.value;
}
};
const finalForm = shadow.querySelector('#step-3');
{
const el = shadow.querySelector('#step-1');
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
this.step = 2;
});
}
{
const el = shadow.querySelector('#step-2');
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
this.step = 3;
});
el.querySelector('.actions > input[type="button"]').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.step = 1;
});
}
{
const el = finalForm;
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
let content = [
...shadow.querySelector('#step-1').elements,
...shadow.querySelector('#step-2').elements,
...shadow.querySelector('#step-3').elements,
]
.filter(el => el && el.name !== '')
.reduce((mem, el) => ({ ...mem, [el.name]: el.value }), {});
console.info(content);
});
el.querySelector('.actions > input[type="button"]').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.step = 2;
});
}
}
connectedCallback() {
this.step = 1;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get step() {
const step = parseInt(this.getAttribute('step'));
return isNaN(step) ? 1 : step;
}
set step(n) {
this.setAttribute('step', n);
}
});
customElements.define('login-form', class extends HTMLElement {
static get observedAttributes() {
return []
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
</style>
<form>
<div>
<label>Login</label>
<input name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="button" value="Zaloguj" />
</div>
</form>
`;
}
connectedCallback() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
});

21
client/.swcrc Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"module": {
"type": "es6",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
},
"jsc": {
"parser": {
"syntax": "ecmascript"
},
"minify": {
"compress": true,
"mangle": true
},
"target": "es2022"
},
"minify": true
}

8
client/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"dependencies": {
"@swc/cli": "^0.1.57",
"@swc/core": "^1.2.209",
"@swc/helpers": "^0.4.3",
"browserslist": "^4.21.1"
}
}

11
client/spack.config.js Normal file
View File

@ -0,0 +1,11 @@
const { config } = require("@swc/core/spack");
const path = require("path");
module.exports = config({
entry: {
app: __dirname + "/src/app.js",
},
output: {
path: __dirname + "/dist",
},
});

12
client/src/app.js Normal file
View File

@ -0,0 +1,12 @@
import "./form-navigation.js";
import "./local-service.js";
import "./local-service-item.js";
import "./local-services.js";
import "./login-form.js";
import "./ow-account.js";
import "./ow-nav.js";
import "./ow-path.js";
import "./price/price-view";
import "./price/price-input";
import "./register-form.js";
import "./shared.js";

View File

@ -0,0 +1,65 @@
import { S, FORM_STYLE } from "./shared";
customElements.define('form-navigation', class extends HTMLElement {
static get observedAttributes() {
return ['next', 'prev']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
.actions {
display: flex;
justify-content: space-between;
}
input.hidden {
display: none !important;
}
</style>
<form class="inline">
<div class="actions">
<input id="prev" type="button" value="Wróć" />
<input id="next" type="submit" value="Następny" />
</div>
</form>
`;
shadow.querySelector('#prev').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.dispatchEvent(new CustomEvent('form:prev', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
});
shadow.querySelector('#next').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.dispatchEvent(new CustomEvent('form:next', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
});
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'next': {
this[S].querySelector('#next').className = newV === 'hidden' ? 'hidden' : '';
break;
}
case 'prev': {
this[S].querySelector('#prev').className = newV === 'hidden' ? 'hidden' : '';
break;
}
}
}
});

View File

@ -0,0 +1,52 @@
import { S } from "./shared";
customElements.define('local-service-item', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'price']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#item {
display: flex;
justify-content: space-between;
}
h3 {
font-weight: normal;
}
#price {
font-weight: bold;
}
</style>
<section id="item">
<h3 id="name"></h3>
<price-view id="price"></price-view>
</section>
`;
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
this[S].querySelector('#price').value = this.price();
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name':
return this[S].querySelector('#name').textContent = newV;
case 'price':
return this[S].querySelector('#price').value = newV;
}
}
price(s) {
const n = parseInt(s || this.getAttribute('price'));
return isNaN(n) ? 0 : n;
}
});

View File

@ -0,0 +1,42 @@
import { S } from "./shared";
customElements.define('local-service', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'service-id', 'state']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name':
return this[S].querySelector('#name').textContent = newV;
}
}
get name() {
return this.getAttribute('name') || ''
}
});

View File

@ -0,0 +1,94 @@
import { S } from "./shared";
customElements.define('local-services', class extends HTMLElement {
static get observedAttributes() {
return ['filter']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
::slotted(local-service[local-services-visible="invisible"]) {
display: none;
}
input {
font-size: 20pt;
line-height: 2.6em;
height: 2.6em;
margin: 0;
padding: 0;
width: 100%;
border:none;
outline:none;
display: block;
background: transparent;
border-bottom: 1px solid #ccc;
text-indent: 20px;
}
</style>
<section>
<input type="text" id="filter" placeholder="Filtruj" />
</section>
<section id="items">
<slot name="services"></slot>
</section>
`;
{
const filter = shadow.querySelector('#filter');
let t = null;
filter.addEventListener('change', ev => {
ev.stopPropagation();
this.filter = ev.target.value;
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
const value = ev.target.value;
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = value;
t = null;
}, 1000 / 3);
});
}
}
connectedCallback() {
this.filter = this.getAttribute('filter');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'filter':
return this.filter = newV;
}
}
get filter() {
return this.getAttribute('filter');
}
set filter(value) {
if (!value || value === '') {
this.removeAttribute('filter');
for (const el of this.querySelectorAll('local-service')) {
el.removeAttribute('local-services-visible');
}
} else {
this.setAttribute('filter', value);
for (const el of this.querySelectorAll('local-service')) {
if (!el.name) continue;
if (el.name.includes(value)) {
el.setAttribute('local-services-visible', 'visible');
} else {
el.setAttribute('local-services-visible', 'invisible');
}
}
}
}
});

41
client/src/login-form.js Normal file
View File

@ -0,0 +1,41 @@
import { FORM_STYLE, S } from "./shared";
customElements.define('login-form', class extends HTMLElement {
static get observedAttributes() {
return []
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
</style>
<form>
<div>
<label>Login</label>
<input name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="button" value="Zaloguj" />
</div>
</form>
`;
}
connectedCallback() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
});

75
client/src/ow-account.js Normal file
View File

@ -0,0 +1,75 @@
import { S } from "./shared";
customElements.define('ow-account', class extends HTMLElement {
static get observedAttributes() {
return ['mode']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
article > * {
display: none;
}
:host([mode="login"]) login-form, :host([mode="login"]) #switch-register {
display: block;
}
:host([mode="register"]) register-form, :host([mode="register"]) #switch-login {
display: block;
}
a{
display: block;
cursor: pointer;
}
a:hover {
color: var(--hover-color);
border-bottom-color: var(--hover-color);
text-decoration: none;
}
</style>
<article>
<login-form></login-form>
<register-form></register-form>
<section id="switch-register">
<a>Nie masz konta? Utwórz nowe</a>
</section>
<section id="switch-login">
<a>Masz konta? Zaloguj się</a>
</section>
</article>
`;
shadow.querySelector('#switch-login > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'login';
});
shadow.querySelector('#switch-register > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'register';
});
}
connectedCallback() {
this.mode = 'login';
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get mode() {
return this.getAttribute('mode');
}
set mode(value) {
value = value === 'login' || value === 'register' ? value : 'login';
this.setAttribute('mode', value);
}
});

40
client/src/ow-nav.js Normal file
View File

@ -0,0 +1,40 @@
import { S } from "./shared";
customElements.define('ow-nav', class extends HTMLElement {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
section {
display: flex;
align-items: stretch;
align-content: stretch;
margin-bottom: 12px;
color: #2b2727;
font-family: Raleway, sans-serif;
font-size: 14px;
line-height: 32px;
}
section::after {
display: block;
content: ' ';
border: none;
padding: 10px 18px;
text-decoration: none;
color: #2b2727;
text-transform: uppercase;
align-self: stretch;
flex-shrink: 20;
flex-grow: 20;
}
</style>
<section>
<slot></slot>
</section>
`;
}
});

71
client/src/ow-path.js Normal file
View File

@ -0,0 +1,71 @@
import { S } from "./shared";
customElements.define('ow-path', class extends HTMLElement {
static get observedAttributes() {
return ['selected', 'path'];
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
a {
display: block;
padding: 10px 18px;
text-decoration: none;
color: #495057;
text-transform: uppercase;
border: none;
border-bottom: 1px solid var(--border-slim-color);
}
a:hover {
color: var(--hover-color);
}
:host(:not([selected])) a {
border: none;
}
</style>
<a><slot></slot></a>
`;
}
connectedCallback() {
this.selected = this.getAttribute('selected');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'selected':
return this.selected = newV;
case 'path':
return this.path = newV;
}
}
get selected() {
return this.getAttribute('selected') === 'selected';
}
set selected(value) {
if (value === 'selected') this.setAttribute('selected', 'selected');
else this.removeAttribute('selected');
}
get path() {
return this.getAttribute('path') || ''
}
set path(value) {
if (!value || value === '') {
this.removeAttribute('path');
return;
}
this.setAttribute('path', value);
this[S].querySelector('a').setAttribute('href', value);
}
});

View File

@ -0,0 +1,98 @@
import { S, FORM_STYLE } from "../shared";
customElements.define('price-input', class extends HTMLElement {
static get observedAttributes() {
return ['value', 'currency', 'required', 'name']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#price {
font-weight: bold;
}
#view {
display: flex;
}
#value {
display: flex;
justify-content: start;
}
${FORM_STYLE}
</style>
<div id="view">
<input id="price" type="number" min="0.00" max="10000.00" step="0.01" />
<span id="currency"></span>
</div>
`;
}
connectedCallback() {
this[S].querySelector('#currency').textContent = this.currency;
// this[S].querySelector('#price').textContent = this.formatted;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
const price = this[S].querySelector('#price');
switch (name) {
case 'price': {
this.value = newV;
break;
}
case 'currency': {
this.currency = newV;
break;
}
case 'required': {
newV
? price.setAttribute('required', 'required')
: price.removeAttribute('required');
break;
}
case 'readonly': {
newV
? price.setAttribute('readonly', 'readonly')
: price.removeAttribute('readonly');
break;
}
case 'name': {
break;
}
}
}
get value() {
return this[S].querySelector('#price').value * 100;
}
set value(v) {
this.setAttribute('value', v);
this[S].querySelector('#price').value = v;
}
get currency() {
return this.getAttribute('currency') || 'PLN';
}
set currency(value) {
this.setAttribute('currency', value);
this[S].querySelector('#currency').textContent = this.currency;
}
reportValidity() {
return this[S].querySelector('input').reportValidity();
}
get name() {
return this.getAttribute('name');
}
set name(value) {
this.setAttribute('name', value);
}
});

View File

@ -0,0 +1,58 @@
import { S } from "../shared";
customElements.define('price-view', class extends HTMLElement {
static get observedAttributes() {
return ['value', 'currency']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#price {
font-weight: bold;
}
</style>
<span id="price"></span>
`;
}
connectedCallback() {
this[S].querySelector('#price').textContent = this.formatted;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'price': {
this.value = newV;
break;
}
}
}
get formatted() {
let v = this.value;
let major = Math.ceil(v / 100);
let minor = v % 100;
let formatted = `${ major },${ minor < 10 ? `0${ minor }` : minor }`;
return `${ formatted }${ this.currency }`
}
get value() {
const n = parseInt(this.getAttribute('value'));
return isNaN(n) ? 0 : n;
}
set value(v) {
this.setAttribute('value', v);
this[S].querySelector('#price').textContent = this.formatted;
}
get currency() {
return this.getAttribute('currency') || 'PLN';
}
});

136
client/src/register-form.js Normal file
View File

@ -0,0 +1,136 @@
import { S, FORM_STYLE } from "./shared";
import "./register-form/register-basic-form";
import "./register-form/register-item-form-row.js";
import "./register-form/register-items-form.js";
import "./register-form/register-company-form";
import "./register-form/register-submit-form";
import "./register-form/register-user-type";
import "./register-form/register-oauth2";
const copyForm = (form, finalForm) => {
form.reportValidity();
for (const el of form.elements) {
if (el.name === '') continue;
if (!el.reportValidity()) {
return false;
}
}
const inputs = form.inputs;
if (inputs)
finalForm.setItems(inputs);
else
for (const el of form.elements) {
if (el.name === '') continue;
finalForm.updateField(el.name, el.value);
}
return true;
};
customElements.define('register-form', class extends HTMLElement {
static get observedAttributes() {
return ['step']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
[id^="step-"] { display: none; }
:host([step="0"]) #step-0 { display: block; }
:host([step="1"]) #step-1 { display: block; }
:host([step="2"]) #step-2 { display: block; }
:host([step="3"]) #step-3 { display: block; }
:host([step="4"]) #step-4 { display: block; }
:host([step="1000"]) #step-1000 { display: block; }
${ FORM_STYLE }
.actions {
display: flex;
justify-content: space-between;
}
.actions > input:first-child {
margin-right: 8px;
}
.actions > input:last-child {
margin-left: 8px;
}
#step-4 > #copied {
display: none;
}
</style>
<article>
<register-user-type id="step-0">
</register-user-type>
<register-basic-form id="step-1">
</register-basic-form>
<register-company-form id="step-2">
</register-company-form>
<register-items-form id="step-3">
</register-items-form>
<register-submit-form id="step-4">
</register-submit-form>
<register-oauth2 id="step-1000">
</register-oauth2>
</article>
`;
const finalForm = shadow.querySelector('#step-4');
this[S].addEventListener('account:type:user', ev => {
ev.stopPropagation();
this.step = 1000;
});
this[S].addEventListener('account:type:local-service', ev => {
ev.stopPropagation();
this.step = 1;
});
this[S].addEventListener('form:next', ev => {
ev.stopPropagation();
const form = shadow.querySelector(`#step-${ this.step }`);
if (copyForm(form, finalForm)) {
this.step = this.step + 1;
}
});
this[S].addEventListener('form:prev', ev => {
ev.stopPropagation();
this.step = this.step - 1;
});
{
const el = finalForm;
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
});
}
}
connectedCallback() {
this.step = 0;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get step() {
const step = parseInt(this.getAttribute('step'));
return isNaN(step) ? 1 : step;
}
set step(n) {
if (n < 0) return;
this.setAttribute('step', n);
}
});

View File

@ -0,0 +1,40 @@
import { FORM_STYLE, S, PseudoForm } from "../shared";
customElements.define('register-basic-form', class extends PseudoForm {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host {
display: block;
}
${ FORM_STYLE }
</style>
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>E-Mail</label>
<input name="email" placeholder="Email" type="email" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<form-navigation prev="hidden"></form-navigation>
</form>
`;
const form = shadow.querySelector('form');
form.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('form:next', { bubbles: true, composed: true, detail: form }));
})
}
});

View File

@ -0,0 +1,27 @@
import { FORM_STYLE, S, PseudoForm } from "../shared";
customElements.define('register-company-form', class extends PseudoForm {
constructor() {
super();
const shadow = this[S] = this.attachShadow({mode: "closed"});
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${FORM_STYLE}
</style>
<form id="step-2">
<div>
<input name="name" placeholder="Nazwa usługi" type="text" required />
</div>
<div>
<label>description</label>
<textarea name="description" required></textarea>
</div>
<form-navigation></form-navigation>
</form>
`;
}
})

View File

@ -0,0 +1,95 @@
import { S, FORM_STYLE, PseudoForm } from "../shared";
customElements.define('register-item-form-row', class extends PseudoForm {
static get observedAttributes() {
return ['idx', 'name']
}
constructor() {
super();
this[S] = this.attachShadow({ mode: 'closed' });
this.addEventListener('item:removed', () => {
this.setAttribute('removed', 'removed');
const parent = this.parentElement;
this.remove();
parent.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: true }));
});
}
connectedCallback() {
const idx = this.getAttribute('idx');
this[S].innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
section {
display: flex;
}
</style>
<form class="inline">
<div id="name" class="field">
<label>Nazwa</label>
<input class="item-name" name="items[${ idx }][name]" type="text" required>
</div>
<div id="price" class="field">
<label>Cena</label>
<price-input class="item-price" name="items[${ idx }][price]" required>
</price-input>
</div>
<div><input class="remove" type="button" value="Usuń"></div>
</form>
`;
this[S].querySelector('form').addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.reportValidity();
});
this[S].querySelector('.remove').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false }));
});
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'idx': {
this.updateNames();
break;
}
}
}
get inputs() {
return [
this[S].querySelector('input.item-name').cloneNode(true),
this[S].querySelector('input.item-price').cloneNode(true),
];
}
updateNames() {
const idx = this.getAttribute('idx');
for (const el of this[S].querySelectorAll('.field')) {
const id = el.id;
el.querySelector('input, price-input').setAttribute('name', `items[${ idx }][${ id }]`);
}
}
get idx() {
return this.getAttribute('idx');
}
set idx(idx) {
this.setAttribute('idx', idx);
}
reportValidity() {
return super.reportValidity() && this[S].querySelector('price-input').reportValidity();
}
});

View File

@ -0,0 +1,64 @@
import { FORM_STYLE, S, PseudoForm } from "../shared";
import "./register-item-form-row"
const updateItems = (form) => {
let idx = 0;
for (const el of form.querySelectorAll('register-item-form-row')) {
el.idx = idx++;
}
return idx;
}
customElements.define('register-items-form', class extends PseudoForm {
static get observedAttributes() {
return []
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
::slotted(section) {
display: flex;
}
</style>
<form>
<article id="items">
<slot></slot>
</article>
<div>
<input type="button" id="add-item" value="Dodaj usługę/produkt" />
</div>
<form-navigation></form-navigation>
</form>
`;
this.addEventListener('item:removed', ev => {
ev.stopPropagation();
updateItems(this)
});
this.addEventListener('form:next', ev => {
for (const el of this.querySelectorAll('item-form-row')) {
if (!el.reportValidity()) {
ev.stopPropagation();
ev.preventDefault();
}
}
});
shadow.querySelector('#add-item').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.appendChild(document.createElement('register-item-form-row'));
updateItems(this)
});
}
get inputs() {
return [...this.querySelectorAll("register-item-form-row")].map(form => form.inputs)
}
});

View File

@ -0,0 +1,12 @@
customElements.define('register-oauth2', class extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<a id="google-button" class="btn btn-block btn-social btn-google">
<i class="fa fa-google"></i> Sign in with Google
</a>
`;
}
});

View File

@ -0,0 +1,75 @@
import { FORM_STYLE, S, PseudoForm } from "../shared";
customElements.define('register-submit-form', class extends PseudoForm {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
</style>
<form id="step-4">
<div id="copied">
<input id="hidden-login" name="login" type="hidden" />
<input id="hidden-email" name="email" type="hidden" />
<input id="hidden-pass" name="pass" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<input id="hidden-description" name="description" type="hidden" />
</div>
<div>
<label>Login</label>
<input readonly id="preview-login">
</div>
<div>
<label>Email</label>
<input readonly id="preview-email">
</div>
<div>
<label>Password</label>
<input readonly id="preview-pass" type="password">
</div>
<div>
<label>Name</label>
<input readonly id="preview-name">
</div>
<div>
<label>Description</label>
<input readonly id="preview-description">
</div>
<div id="items">
</div>
<div class="actions">
<form-navigation next="hidden"></form-navigation>
<input type="submit" value="Dodaj usługi/produkty" />
</div>
</form>
`;
}
updateField(name, value) {
this[S].querySelector(`[id="hidden-${ name }"]`).value = value;
this[S].querySelector(`[id="preview-${ name }"]`).value = value;
}
setItems(items) {
const host = this[S].querySelector('#items');
host.innerHTML = ``;
for (const row of items) {
const el = host.appendChild(document.createElement('div'));
const [name, price] = row;
name.setAttribute('readonly', 'readonly');
el.appendChild(name);
el.appendChild(document.createElement('price-view')).value = price.value;
price.setAttribute('readonly', 'readonly');
price.setAttribute('type', 'hidden');
el.appendChild(price);
}
}
});

View File

@ -0,0 +1,69 @@
customElements.define('register-user-type', class extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host {
display: block;
}
ul, li {
list-style: none;
}
ul {
display: flex;
justify-content: space-between;
}
a {
display: block;
border: 1px solid var(--border-slim-color);
border-radius: 16px;
padding: 16px;
text-align: center;
cursor: pointer;
color: var(--border-slim-color);
}
svg {
width: 200px;
height: 200px;
}
svg, path {
fill: var(--border-slim-color) !important;
}
</style>
<article>
<ul>
<li>
<a id="user">
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5.875a3.625 3.625 0 0 0-1.006 7.109c-1.194.145-2.218.567-2.99 1.328-.982.967-1.479 2.408-1.479 4.288a.475.475 0 1 0 .95 0c0-1.72.453-2.88 1.196-3.612.744-.733 1.856-1.113 3.329-1.113s2.585.38 3.33 1.113c.742.733 1.195 1.892 1.195 3.612a.475.475 0 1 0 .95 0c0-1.88-.497-3.32-1.48-4.288-.77-.76-1.795-1.183-2.989-1.328A3.627 3.627 0 0 0 7.5.875ZM4.825 4.5a2.675 2.675 0 1 1 5.35 0 2.675 2.675 0 0 1-5.35 0Z" fill="currentColor"/>
</svg>
<div>Użytkownik</div>
</a>
</li>
<li>
<a id="local-service">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489.4 489.4" style="enable-background:new 0 0 489.4 489.4" xml:space="preserve"><path d="M347.7 263.75h-66.5c-18.2 0-33 14.8-33 33v51c0 18.2 14.8 33 33 33h66.5c18.2 0 33-14.8 33-33v-51c0-18.2-14.8-33-33-33zm9 84c0 5-4.1 9-9 9h-66.5c-5 0-9-4.1-9-9v-51c0-5 4.1-9 9-9h66.5c5 0 9 4.1 9 9v51z"/><path d="M489.4 171.05c0-2.1-.5-4.1-1.6-5.9l-72.8-128c-2.1-3.7-6.1-6.1-10.4-6.1H84.7c-4.3 0-8.3 2.3-10.4 6.1l-72.7 128c-1 1.8-1.6 3.8-1.6 5.9 0 28.7 17.3 53.3 42 64.2v211.1c0 6.6 5.4 12 12 12h381.3c6.6 0 12-5.4 12-12v-209.6c0-.5 0-.9-.1-1.3 24.8-10.9 42.2-35.6 42.2-64.4zM91.7 55.15h305.9l56.9 100.1H34.9l56.8-100.1zm256.6 124c-3.8 21.6-22.7 38-45.4 38s-41.6-16.4-45.4-38h90.8zm-116.3 0c-3.8 21.6-22.7 38-45.4 38s-41.6-16.4-45.5-38H232zm-207.2 0h90.9c-3.8 21.6-22.8 38-45.5 38-22.7.1-41.6-16.4-45.4-38zm176.8 255.2h-69v-129.5c0-9.4 7.6-17.1 17.1-17.1h34.9c9.4 0 17.1 7.6 17.1 17.1v129.5h-.1zm221.7 0H225.6v-129.5c0-22.6-18.4-41.1-41.1-41.1h-34.9c-22.6 0-41.1 18.4-41.1 41.1v129.6H66v-193.3c1.4.1 2.8.1 4.2.1 24.2 0 45.6-12.3 58.2-31 12.6 18.7 34 31 58.2 31s45.5-12.3 58.2-31c12.6 18.7 34 31 58.1 31 24.2 0 45.5-12.3 58.1-31 12.6 18.7 34 31 58.2 31 1.4 0 2.7-.1 4.1-.1v193.2zm-4.1-217.1c-22.7 0-41.6-16.4-45.4-38h90.9c-3.9 21.5-22.8 38-45.5 38z"/></svg>
<div>Usługodawca</div>
</a>
</li>
</ul>
</article>
`;
const user = shadow.querySelector('#user');
user.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type:user', { bubbles: true, composed: true }));
});
const service = shadow.querySelector('#local-service');
service.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type:local-service', { bubbles: true, composed: true }));
});
}
});

107
client/src/shared.js Normal file
View File

@ -0,0 +1,107 @@
export const S = Symbol();
export const FORM_STYLE = `
form {
display: block;
}
form.inline {
display: flex;
justify-content: space-between;
}
form > div {
display: block;
margin-bottom: 1rem;
}
input, textarea {
font-size: 16px;
border: none;
border-bottom-style: none;
border-bottom-width: medium;
border-bottom: 1px solid rgba(0,0,0,.1);
border-radius: 2px;
padding: 0;
height: 36px;
background: #fff;
color: rgba(0,0,0,.8);
font-size: 14px;
box-shadow: none !important;
display: block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-clip: padding-box;
transition: border-color .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
}
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
textarea {
width: calc(100% - 1.5rem - 2px);
}
label {
color: #000;
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
display: inline-block;
margin-bottom: .5rem;
}
input[type="button"], input[type="submit"] {
padding: 12px 16px;
cursor: pointer;
border: none;
border-width: 1px;
border-radius: 5px;
font-size: 14px;
font-weight: 400;
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
position: relative;
margin-bottom: 20px;
transition: .3s;
background: #46b5d1;
color: #fff;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out,
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
}
`;
export class PseudoForm extends HTMLElement {
reportValidity() {
return this[S].querySelector('form').reportValidity();
}
checkValidity() {
return this[S].querySelector('form').checkValidity();
}
get elements() {
return this[S].querySelector('form').elements;
}
}

286
client/yarn.lock Normal file
View File

@ -0,0 +1,286 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@swc/cli@^0.1.57":
version "0.1.57"
resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.1.57.tgz#a9c424de5a217ec20a4b7c2c0e5c343980537e83"
integrity sha512-HxM8TqYHhAg+zp7+RdTU69bnkl4MWdt1ygyp6BDIPjTiaJVH6Dizn2ezbgDS8mnFZI1FyhKvxU/bbaUs8XhzQg==
dependencies:
commander "^7.1.0"
fast-glob "^3.2.5"
slash "3.0.0"
source-map "^0.7.3"
"@swc/core-android-arm-eabi@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.209.tgz#cc3b5a06f73ad79367b417e174e3704aa285d5ec"
integrity sha512-87+4ffqg3f2iTYqdfiQcRvC56wZ62X8OHN7Yh8LwGqPBpt7or0Sj1ryzufrZlwdKwiM2pU86FfCS7UM8EfE5Rw==
"@swc/core-android-arm64@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.209.tgz#b01653e2183817390b353fedcc2d7750e47548d9"
integrity sha512-PfWiBrY4guq6zpTREeaqsjSrbZKFB+B4ZvFGbHlyZDwckNJf+9mBEVoJc5xsUs/INIH/Zz0g2GMdR/Lxp4IDig==
"@swc/core-darwin-arm64@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.209.tgz#37cb3469d1140fa9c40ebbbc76d13931acd7cb69"
integrity sha512-RT4bOKKEAfUXFY2thNe5y/htQl5Pg9zoZF+QRQrehBeB2L66dT5k1cG51ipKbOYtdICp7nLLeF2KW3CCxdlPbQ==
"@swc/core-darwin-x64@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.209.tgz#d473f11e912f867a79c586d399e4678cd1a53641"
integrity sha512-SC312em15Dy5Na/8xgOGxXFcdpxBcSMwORc2sdrkiiWKTJ+fWJAmDnG82TWRfi3V/Q6oXBdTEKWuJudHrlcYSA==
"@swc/core-freebsd-x64@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.209.tgz#d54d504dd8e3f5a8e4446ee9c4ba01e97af80c77"
integrity sha512-ta88pSLq0MpqznnwnCa0201kHUBHzAHrSpeAqlBLIR0Tste18myGXp58dGAN3U0n0hPDWPIj+Y26/qCqNZAnpA==
"@swc/core-linux-arm-gnueabihf@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.209.tgz#3a91d441351deba6169b07ba31d445248f516c75"
integrity sha512-E7rvzd5OK5idheZrbqdgWHyJ2anrgTYVcfRPvt95pk+Eu/RqMcn9qpLbkI1Olttih/7omzrLfGo00Hju+WmzEg==
"@swc/core-linux-arm64-gnu@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.209.tgz#d07a4a7dbfa4cefda9914994d8d3ac634b5cfca6"
integrity sha512-Ejb9eTiFJgACp+JT/Q/bqZptX61GSAPlA6ilOeezp7zgdHzcD3iRZh/AttirA4DQgTr10i3gGBKNxvIwTyxj1g==
"@swc/core-linux-arm64-musl@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.209.tgz#d1cf0a24642e8c90ff875149ce64c4d6237d806f"
integrity sha512-k9L70GpjVljAZLiClSE5R70pfFXDKcDXhn1Rj2miQC/luQjzmWQQLyjACAZmvE//GrWRAV8v+xTTZVhZT1PIbQ==
"@swc/core-linux-x64-gnu@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.209.tgz#62788183ba176af45bb48bfa4410194ac71eb481"
integrity sha512-Fkh5NS/SRbqVsM7URhMHJ3r0piI+iU7BNSLMvQZN8jzWEdxBb26j+DZuEX/4eKBwQ5P6ycCpYYbMBrxfmHfxlA==
"@swc/core-linux-x64-musl@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.209.tgz#ed1d64e335e1160ca6d23c279593f6848f680fab"
integrity sha512-9YaOGD5ODMfkMxCF9iRxlAcENUhzGSZ752r6oedymPWU/FnjIiw2AGdNKL+jCgplh+/5Q/P1NthSciBlChzzgQ==
"@swc/core-win32-arm64-msvc@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.209.tgz#369e861fd22f089c944bb3a4fcaf1fceb9c45978"
integrity sha512-TMljLtDO7DlLGZI2DIw6cAdvedADFKB/d7NK/1pZYYGD8QgjO4NvsLg2fG1TqRf2UwPazWn1B8EQ0B+1NtRIHg==
"@swc/core-win32-ia32-msvc@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.209.tgz#2712745fa873d1bfa543614c58c750247a22b751"
integrity sha512-n/zAh+2YkSMVMLaDpVnsrm8SEcpKLhRSI2/MKDBwOxW9RwB8KqsjidKRijW1hOX89PyNwnVAeEljc0RIDJHTCw==
"@swc/core-win32-x64-msvc@1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.209.tgz#3561f7a242e06c6119c5c4ec6fa73861c274e6e2"
integrity sha512-0tN/pOHfKpTTgkn0eTfQj0MQfIplZ37/iuXHTsrQEULRcqHxxi+2POnbsLfKChrHOcSzrqFWl8I9szHEnYcyow==
"@swc/core@^1.2.209":
version "1.2.209"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.209.tgz#6bb2f93cc39159b07d53dfcfdb1c297d21fb8c99"
integrity sha512-UHRTGPAnr0tT7tP/5v7H8dO+Dy9ELymbVm2sqetDK3UXAODs/PsWSiDH6EC5W1yPj6DmzGhfDM+APxsu17VqrQ==
optionalDependencies:
"@swc/core-android-arm-eabi" "1.2.209"
"@swc/core-android-arm64" "1.2.209"
"@swc/core-darwin-arm64" "1.2.209"
"@swc/core-darwin-x64" "1.2.209"
"@swc/core-freebsd-x64" "1.2.209"
"@swc/core-linux-arm-gnueabihf" "1.2.209"
"@swc/core-linux-arm64-gnu" "1.2.209"
"@swc/core-linux-arm64-musl" "1.2.209"
"@swc/core-linux-x64-gnu" "1.2.209"
"@swc/core-linux-x64-musl" "1.2.209"
"@swc/core-win32-arm64-msvc" "1.2.209"
"@swc/core-win32-ia32-msvc" "1.2.209"
"@swc/core-win32-x64-msvc" "1.2.209"
"@swc/helpers@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.3.tgz#16593dfc248c53b699d4b5026040f88ddb497012"
integrity sha512-6JrF+fdUK2zbGpJIlN7G3v966PQjyx/dPt1T9km2wj+EUBqgrxCk3uX4Kct16MIm9gGxfKRcfax2hVf5jvlTzA==
dependencies:
tslib "^2.4.0"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
browserslist@^4.21.1:
version "4.21.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.1.tgz#c9b9b0a54c7607e8dc3e01a0d311727188011a00"
integrity sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==
dependencies:
caniuse-lite "^1.0.30001359"
electron-to-chromium "^1.4.172"
node-releases "^2.0.5"
update-browserslist-db "^1.0.4"
caniuse-lite@^1.0.30001359:
version "1.0.30001363"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15"
integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg==
commander@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
electron-to-chromium@^1.4.172:
version "1.4.178"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.178.tgz#3dae6fda486007bb54bbfed420ebd40881a3de45"
integrity sha512-aWuhJXkwIdoQzGR8p2QvR3N0OzdUKZSP8+P/hzuMzNQIPZoEa8HiCGM75bQBHjyz+eKT5PB9dVCzkK/tyQ4B5Q==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
fast-glob@^3.2.5:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
dependencies:
reusify "^1.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.2"
picomatch "^2.3.1"
node-releases@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
slash@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
source-map@^0.7.3:
version "0.7.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
update-browserslist-db@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz#dbfc5a789caa26b1db8990796c2c8ebbce304824"
integrity sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"

View File

@ -80,7 +80,7 @@ pub fn configure(config: &mut ServiceConfig) {
.service(Files::new("/assets/images", "./assets/images"))
.service(Files::new("/assets/css", "./assets/css"))
.service(
Files::new("/assets/js", "./assets/js")
Files::new("/assets/js", "./client/dist")
.use_etag(true)
.prefer_utf8(true)
.show_files_listing(),