Upload image
This commit is contained in:
parent
25986ec594
commit
be70ed2b46
@ -3,9 +3,11 @@
|
||||
<business-items>
|
||||
{% for item in items %}
|
||||
<business-item
|
||||
item-id="{{item.id}}"
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
url="{{item.picture_url}}"
|
||||
picture-url="{{item.picture_url}}"
|
||||
item-order="{{item.item_order}}"
|
||||
>
|
||||
</business-item>
|
||||
{% endfor %}
|
||||
|
@ -9,7 +9,16 @@
|
||||
},
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "ecmascript"
|
||||
"syntax": "ecmascript",
|
||||
"dynamicImport": true,
|
||||
"privateMethod": true,
|
||||
"functionBind": true,
|
||||
"exportDefaultFrom": true,
|
||||
"exportNamespaceFrom": true,
|
||||
"decorators": true,
|
||||
"decoratorsBeforeExport": true,
|
||||
"topLevelAwait": true,
|
||||
"importMeta": true
|
||||
},
|
||||
"minify": {
|
||||
"compress": true,
|
||||
|
530
client/dist/app.js
vendored
530
client/dist/app.js
vendored
File diff suppressed because it is too large
Load Diff
2
client/dist/app.js.map
vendored
2
client/dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,12 +1,12 @@
|
||||
import { S } from "./shared"
|
||||
|
||||
import "./business-items/business-item";
|
||||
|
||||
customElements.define('business-items', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
|
@ -1,30 +1,61 @@
|
||||
import { S } from "../shared.js";
|
||||
import "../shared/image-input";
|
||||
|
||||
customElements.define('business-item', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['name', 'price', 'picture-url']
|
||||
return ['item-id', 'name', 'price', 'picture-url', 'item-order']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
section { display: flex; justify-content: space-between; }
|
||||
#form {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<section>
|
||||
<image-input></image-input>
|
||||
<div id="name"></div>
|
||||
<price-input></price-input>
|
||||
<image-input></image-input>
|
||||
</section>
|
||||
<section id="form">
|
||||
<form action="/business-item/update" method="post">
|
||||
<input name="id" id="id" />
|
||||
<input name="name" id="name" />
|
||||
<input name="price" id="price" />
|
||||
<input name="item_order" id="item_order" />
|
||||
<input name="picture_url" id="picture_url" />
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
|
||||
const imageInput = shadow.querySelector('image-input');
|
||||
|
||||
this.addEventListener('image-input:uploaded', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.picture_url = imageInput.url;
|
||||
|
||||
const form = shadow.querySelector('form');
|
||||
form.querySelector('#id').value = this.item_id;
|
||||
form.querySelector('#name').value = this.name;
|
||||
form.querySelector('#price').value = this.price;
|
||||
form.querySelector('#picture_url').value = this.picture_url;
|
||||
form.querySelector('#item_order').value = this.item_order;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.item_id = this.item_id;
|
||||
this.name = this.name;
|
||||
this.price = this.price;
|
||||
this.picture_url = this.picture_url;
|
||||
@ -33,35 +64,53 @@ customElements.define('business-item', class extends HTMLElement {
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'item-id': return this.item_id = newV;
|
||||
case 'name': return this.name = newV;
|
||||
case 'price': return this.price = newV / 100.0;
|
||||
case 'picture-url': return this.picture_url = newV;
|
||||
}
|
||||
}
|
||||
|
||||
get item_id() {
|
||||
return this.getAttribute('item-id');
|
||||
}
|
||||
|
||||
set item_id(v) {
|
||||
this.setAttribute('item-id', v);
|
||||
}
|
||||
|
||||
get item_order() {
|
||||
return this.getAttribute('item-order');
|
||||
}
|
||||
|
||||
set item_order(v) {
|
||||
this.setAttribute('item-order', v);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name');
|
||||
}
|
||||
|
||||
set name(v) {
|
||||
this.setAttribute('name', v);
|
||||
this[S].querySelector('#name').textContent = v;
|
||||
this.#form.querySelector('#name').textContent = v;
|
||||
}
|
||||
|
||||
|
||||
get price() {
|
||||
return this.getAttribute('price');
|
||||
}
|
||||
set price(v) {
|
||||
this.setAttribute('price', v);
|
||||
this[S].querySelector('price-input').value = v;
|
||||
return this.#form.querySelector('price-input').value;
|
||||
}
|
||||
|
||||
set price(v) {
|
||||
this.setAttribute('price', v);
|
||||
this.#form.querySelector('price-input').value = v;
|
||||
}
|
||||
|
||||
get picture_url() {
|
||||
return this.getAttribute('picture-url');
|
||||
}
|
||||
|
||||
set picture_url(v) {
|
||||
this.setAttribute('picture-url', v);
|
||||
this[S].querySelector('image-input').src = v;
|
||||
this.#form.querySelector('image-input').url = v;
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { S, FORM_STYLE } from "./shared";
|
||||
import { FORM_STYLE } from "./shared";
|
||||
|
||||
customElements.define('form-navigation', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['next', 'prev']
|
||||
}
|
||||
@ -8,7 +10,7 @@ customElements.define('form-navigation', class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -48,11 +50,11 @@ customElements.define('form-navigation', class extends HTMLElement {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'next': {
|
||||
this[S].querySelector('#next').className = newV === 'hidden' ? 'hidden' : '';
|
||||
this.#form.querySelector('#next').className = newV === 'hidden' ? 'hidden' : '';
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
this[S].querySelector('#prev').className = newV === 'hidden' ? 'hidden' : '';
|
||||
this.#form.querySelector('#prev').className = newV === 'hidden' ? 'hidden' : '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { S } from "./shared";
|
||||
|
||||
import "./local-businesses/local-business-item";
|
||||
import "./local-businesses/local-business";
|
||||
|
||||
customElements.define('local-businesses', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['filter']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { S } from "../shared";
|
||||
|
||||
customElements.define('local-business-item', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['name', 'price']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -31,17 +31,17 @@ customElements.define('local-business-item', class extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#name').textContent = this.getAttribute('name');
|
||||
this[S].querySelector('#price').value = this.price();
|
||||
this.#form.querySelector('#name').textContent = this.getAttribute('name');
|
||||
this.#form.querySelector('#price').value = this.price();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'name':
|
||||
return this[S].querySelector('#name').textContent = newV;
|
||||
return this.#form.querySelector('#name').textContent = newV;
|
||||
case 'price':
|
||||
return this[S].querySelector('#price').value = newV;
|
||||
return this.#form.querySelector('#price').value = newV;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { S } from "../shared";
|
||||
|
||||
customElements.define('local-business', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['name', 'service-id', 'state']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -25,14 +27,14 @@ customElements.define('local-business', class extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#name').textContent = this.getAttribute('name');
|
||||
this.#form.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;
|
||||
return this.#form.querySelector('#name').textContent = newV;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { FORM_STYLE, S } from "./shared";
|
||||
|
||||
customElements.define('login-form', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return []
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { S } from "../shared";
|
||||
|
||||
customElements.define('ow-nav', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { S } from "../shared";
|
||||
|
||||
customElements.define('ow-path', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['selected', 'path'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -66,6 +66,6 @@ customElements.define('ow-path', class extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
this.setAttribute('path', value);
|
||||
this[S].querySelector('a').setAttribute('href', value);
|
||||
this.#form.querySelector('a').setAttribute('href', value);
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { S, FORM_STYLE } from "./shared";
|
||||
import { FORM_STYLE } from "./shared";
|
||||
|
||||
customElements.define('ow-account', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['mode', "id", "name", 'email', "facebook-id"]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -127,7 +129,7 @@ customElements.define('ow-account', class extends HTMLElement {
|
||||
|
||||
set name(v) {
|
||||
this.setAttribute('name', v);
|
||||
this[S].querySelector('#display #name').value = v;
|
||||
this.#form.querySelector('#display #name').value = v;
|
||||
}
|
||||
|
||||
get email() {
|
||||
@ -136,7 +138,7 @@ customElements.define('ow-account', class extends HTMLElement {
|
||||
|
||||
set email(v) {
|
||||
this.setAttribute('email', v);
|
||||
this[S].querySelector('#display #email').value = v;
|
||||
this.#form.querySelector('#display #email').value = v;
|
||||
}
|
||||
|
||||
get facebook_id() {
|
||||
@ -145,6 +147,6 @@ customElements.define('ow-account', class extends HTMLElement {
|
||||
|
||||
set facebook_id(v) {
|
||||
this.setAttribute('facebook-id', v);
|
||||
this[S].querySelector('#display #facebook_id').value = v;
|
||||
this.#form.querySelector('#display #facebook_id').value = v;
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { S, FORM_STYLE } from "../shared";
|
||||
import { FORM_STYLE } from "../shared";
|
||||
|
||||
customElements.define('price-input', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['value', 'currency', 'required', 'name']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -41,13 +43,13 @@ customElements.define('price-input', class extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#currency').textContent = this.currency;
|
||||
this[S].querySelector('#price').value = this.value;
|
||||
this.#form.querySelector('#currency').textContent = this.currency;
|
||||
this.#form.querySelector('#price').value = this.value;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
const price = this[S].querySelector('#price');
|
||||
const price = this.#form.querySelector('#price');
|
||||
switch (name) {
|
||||
case 'price': {
|
||||
this.value = newV;
|
||||
@ -77,12 +79,12 @@ customElements.define('price-input', class extends HTMLElement {
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this[S].querySelector('#price').value * 100;
|
||||
return Math.floor(parseFloat(this.#form.querySelector('#price').value) * 100);
|
||||
}
|
||||
|
||||
set value(v) {
|
||||
this.setAttribute('value', v);
|
||||
this[S].querySelector('#price').value = v;
|
||||
this.#form.querySelector('#price').value = v;
|
||||
}
|
||||
|
||||
get currency() {
|
||||
@ -91,11 +93,11 @@ customElements.define('price-input', class extends HTMLElement {
|
||||
|
||||
set currency(value) {
|
||||
this.setAttribute('currency', value);
|
||||
this[S].querySelector('#currency').textContent = this.currency;
|
||||
this.#form.querySelector('#currency').textContent = this.currency;
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
return this[S].querySelector('input').reportValidity();
|
||||
return this.#form.querySelector('input').reportValidity();
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { S } from "../shared";
|
||||
|
||||
customElements.define('price-view', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['value', 'currency']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
@ -21,7 +21,7 @@ customElements.define('price-view', class extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#price').textContent = this.formatted;
|
||||
this.#form.querySelector('#price').textContent = this.formatted;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
@ -49,7 +49,7 @@ customElements.define('price-view', class extends HTMLElement {
|
||||
|
||||
set value(v) {
|
||||
this.setAttribute('value', v);
|
||||
this[S].querySelector('#price').textContent = this.formatted;
|
||||
this.#form.querySelector('#price').textContent = this.formatted;
|
||||
}
|
||||
|
||||
get currency() {
|
||||
|
@ -8,26 +8,6 @@ import "./register-form/register-submit-form";
|
||||
import "./register-form/register-user-type";
|
||||
import "./register-form/register-user-form";
|
||||
|
||||
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']
|
||||
@ -48,8 +28,6 @@ customElements.define('register-form', class extends HTMLElement {
|
||||
:host([step="4"]) #step-4 { display: block; }
|
||||
:host([step="40"]) #step-40 { display: block; }
|
||||
|
||||
${ FORM_STYLE }
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -63,6 +41,8 @@ customElements.define('register-form', class extends HTMLElement {
|
||||
#step-4 > #copied {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<article id="host">
|
||||
</article>
|
||||
@ -90,7 +70,7 @@ customElements.define('register-form', class extends HTMLElement {
|
||||
this[S].addEventListener('form:next', ev => {
|
||||
ev.stopPropagation();
|
||||
const form = shadow.querySelector(`#step-${ this.step }`);
|
||||
if (copyForm(form, finalForm)) {
|
||||
if (this.#copyForm(form, finalForm)) {
|
||||
this.step = this.step + 1;
|
||||
}
|
||||
});
|
||||
@ -98,13 +78,11 @@ customElements.define('register-form', class extends HTMLElement {
|
||||
ev.stopPropagation();
|
||||
this.step = this.step - 1;
|
||||
});
|
||||
{
|
||||
finalForm.addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.step = 0;
|
||||
@ -125,4 +103,24 @@ customElements.define('register-form', class extends HTMLElement {
|
||||
if (n < 0) return;
|
||||
this.setAttribute('step', n);
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
});
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { FORM_STYLE, S, PseudoForm } from "../shared";
|
||||
import { FORM_STYLE, PseudoForm } from "../shared";
|
||||
|
||||
customElements.define('register-basic-form', class extends PseudoForm {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { FORM_STYLE, S, PseudoForm } from "../shared";
|
||||
import { FORM_STYLE, PseudoForm } from "../shared";
|
||||
|
||||
customElements.define('register-business-form', class extends PseudoForm {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({mode: "closed"});
|
||||
const shadow = this.#form = this.attachShadow({mode: "closed"});
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { S, FORM_STYLE, PseudoForm } from "../shared";
|
||||
import { FORM_STYLE, PseudoForm } from "../shared";
|
||||
|
||||
customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['idx', 'name']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this[S] = this.attachShadow({ mode: 'closed' });
|
||||
this.#form = this.attachShadow({ mode: 'closed' });
|
||||
|
||||
this.addEventListener('item:removed', () => {
|
||||
this.setAttribute('removed', 'removed');
|
||||
@ -18,23 +20,36 @@ customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const idx = this.getAttribute('idx');
|
||||
this[S].innerHTML = `
|
||||
const idx = this.idx;
|
||||
|
||||
this.#form.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
${ FORM_STYLE }
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
div {
|
||||
min-width: 100px;
|
||||
max-width: 48%;
|
||||
#fields > *:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
#fields > div > label {
|
||||
margin-right: 16px;
|
||||
}
|
||||
#fields {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<form class="inline">
|
||||
<div id="fields">
|
||||
<image-input></image-input>
|
||||
<input type="hidden" name="picture_url" id="picture_url" />
|
||||
<div id="name" class="field">
|
||||
<label>Nazwa</label>
|
||||
<input class="item-name" name="items[${ idx }][name]" type="text" required>
|
||||
@ -44,18 +59,25 @@ customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
<price-input class="item-price" name="items[${ idx }][price]" required>
|
||||
</price-input>
|
||||
</div>
|
||||
</div>
|
||||
<div><input class="remove" type="button" value="Usuń"></div>
|
||||
</form>
|
||||
`;
|
||||
this[S].querySelector('form').addEventListener('submit', ev => {
|
||||
const imageInput = this.#form.querySelector('image-input');
|
||||
|
||||
this.addEventListener('image-input:uploaded', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.picture_url = imageInput.url;
|
||||
});
|
||||
this.#form.querySelector('form').addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.reportValidity();
|
||||
});
|
||||
this[S].querySelector('.remove').addEventListener('click', ev => {
|
||||
this.#form.querySelector('.remove').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false }));
|
||||
});
|
||||
}
|
||||
@ -63,23 +85,21 @@ customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'idx': {
|
||||
this.updateNames();
|
||||
break;
|
||||
}
|
||||
case 'idx':return this.updateNames();
|
||||
case 'picture-url': return this.picture_url = newV;
|
||||
}
|
||||
}
|
||||
|
||||
get inputs() {
|
||||
return [
|
||||
extract(this[S].querySelector('.item-name')),
|
||||
extract(this[S].querySelector('.item-price')),
|
||||
extract(this.#form.querySelector('.item-name')),
|
||||
extract(this.#form.querySelector('.item-price')),
|
||||
];
|
||||
}
|
||||
|
||||
updateNames() {
|
||||
const idx = this.getAttribute('idx');
|
||||
for (const el of this[S].querySelectorAll('.field')) {
|
||||
for (const el of this.#form.querySelectorAll('.field')) {
|
||||
const id = el.id;
|
||||
el.querySelector('input, price-input').setAttribute('name', `items[${ idx }][${ id }]`);
|
||||
}
|
||||
@ -93,8 +113,18 @@ customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
this.setAttribute('idx', idx);
|
||||
}
|
||||
|
||||
get picture_url() {
|
||||
return this.getAttribute('picture-url');
|
||||
}
|
||||
|
||||
set picture_url(v) {
|
||||
this.setAttribute('picture-url', v);
|
||||
this.#form.querySelector('image-input').url = v;
|
||||
this.#form.querySelector('#picture_url').value = v;
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
return super.reportValidity() && this[S].querySelector('price-input').reportValidity();
|
||||
return super.reportValidity() && this.#form.querySelector('price-input').reportValidity();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FORM_STYLE, S, PseudoForm } from "../shared";
|
||||
import { FORM_STYLE, PseudoForm } from "../shared";
|
||||
|
||||
import "./register-item-form-row"
|
||||
|
||||
@ -11,13 +11,15 @@ const updateItems = (form) => {
|
||||
}
|
||||
|
||||
customElements.define('register-items-form', class extends PseudoForm {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return []
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { FORM_STYLE, S, PseudoForm } from "../shared";
|
||||
import { FORM_STYLE, PseudoForm } from "../shared";
|
||||
|
||||
customElements.define('register-submit-form', class extends PseudoForm {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
@ -61,12 +63,12 @@ customElements.define('register-submit-form', class extends PseudoForm {
|
||||
}
|
||||
|
||||
updateField(name, value) {
|
||||
this[S].querySelector(`[id="hidden-${ name }"]`).value = value;
|
||||
this[S].querySelector(`[id="preview-${ name }"]`).value = value;
|
||||
this.#form.querySelector(`[id="hidden-${ name }"]`).value = value;
|
||||
this.#form.querySelector(`[id="preview-${ name }"]`).value = value;
|
||||
}
|
||||
|
||||
setItems(items) {
|
||||
const host = this[S].querySelector('#items');
|
||||
const host = this.#form.querySelector('#items');
|
||||
host.innerHTML = ``;
|
||||
for (const row of items) {
|
||||
const el = host.appendChild(document.createElement('div'));
|
||||
@ -82,6 +84,6 @@ customElements.define('register-submit-form', class extends PseudoForm {
|
||||
}
|
||||
|
||||
set accountType(v) {
|
||||
this[S].querySelector('#account_type').value = v;
|
||||
this.#form.querySelector('#account_type').value = v;
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { S, FORM_STYLE } from "../shared";
|
||||
import { FORM_STYLE } from "../shared";
|
||||
|
||||
customElements.define('register-user-form', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['mode']
|
||||
}
|
||||
@ -8,7 +10,7 @@ customElements.define('register-user-form', class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
|
@ -1,8 +1,10 @@
|
||||
customElements.define('register-user-type', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
@ -37,7 +39,7 @@ customElements.define('register-user-type', class extends HTMLElement {
|
||||
<ul>
|
||||
<li>
|
||||
<a id="user">
|
||||
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<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>
|
||||
@ -45,7 +47,10 @@ customElements.define('register-user-type', class extends HTMLElement {
|
||||
</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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="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>
|
||||
|
@ -1,5 +1,44 @@
|
||||
export const S = Symbol();
|
||||
|
||||
export const BUTTON_STYLE = `
|
||||
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;
|
||||
|
||||
border: 1px solid #495057;
|
||||
color: #495057;
|
||||
background: white;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FORM_STYLE = `
|
||||
form {
|
||||
display: block;
|
||||
@ -62,42 +101,7 @@ label {
|
||||
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;
|
||||
|
||||
border: 1px solid #495057;
|
||||
color: #495057;
|
||||
background: white;
|
||||
}
|
||||
${BUTTON_STYLE}
|
||||
`;
|
||||
|
||||
export class PseudoForm extends HTMLElement {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { S } from "../shared.js";
|
||||
import { BUTTON_STYLE, S } from "../shared.js";
|
||||
|
||||
customElements.define('image-input', class extends HTMLElement {
|
||||
#form;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['width', 'height', "account-id", "url"]
|
||||
}
|
||||
@ -8,15 +10,28 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this[S] = this.attachShadow({ mode: "closed" });
|
||||
const shadow = this.#form = this.attachShadow({ mode: "closed" });
|
||||
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; border: 1px solid black; }
|
||||
:host {
|
||||
display: block;
|
||||
border: 1px solid var(--border-light-gray-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
||||
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
|
||||
#view { width: 200px; height: 200px; cursor: pointer; }
|
||||
canvas { width: 200px; height: 200px; }
|
||||
div > input[type=button] {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-color: var(--border-light-gray-color);
|
||||
}
|
||||
${BUTTON_STYLE}
|
||||
</style>
|
||||
<article>
|
||||
<section id="hidden">
|
||||
@ -35,7 +50,12 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
shadow.querySelector('#save').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const blobBin = atob(canvas.toDataURL("image/webp", 1.0).split(',')[1]);
|
||||
const c = document.createElement('canvas');
|
||||
c.width = this.width;
|
||||
c.height = this.height;
|
||||
c.getContext('2d').putImageData(ctx.getImageData(0, 0, this.width, this.height), 0, 0);
|
||||
|
||||
const blobBin = atob(c.toDataURL("image/webp", 1.0).split(',')[1]);
|
||||
const array = [];
|
||||
for (let i = 0; i < blobBin.length; i++) {
|
||||
array.push(blobBin.charCodeAt(i));
|
||||
@ -46,7 +66,10 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
fetch("/upload", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
}).then(res => res.json()).then(({ path }) => this.url = path);
|
||||
}).then(res => res.json()).then(({ path }) => {
|
||||
this.url = path;
|
||||
this.dispatchEvent(new CustomEvent('image-input:uploaded', { bubbles: true, composed: true }));
|
||||
});
|
||||
});
|
||||
|
||||
const f = new FileReader();
|
||||
@ -129,6 +152,6 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
|
||||
set url(v) {
|
||||
this.setAttribute('url', v);
|
||||
this[S].querySelector('img').src = v;
|
||||
this.#form.querySelector('img').src = v;
|
||||
}
|
||||
});
|
||||
|
@ -1,2 +1,2 @@
|
||||
ALTER TABLE local_business_items
|
||||
ADD COLUMN picture_url TEXT NOT NULL UNIQUE;
|
||||
ADD COLUMN picture_url TEXT NOT NULL UNIQUE DEFAULT '';
|
||||
|
7
rustfmt.toml
Normal file
7
rustfmt.toml
Normal file
@ -0,0 +1,7 @@
|
||||
imports_granularity = "Module"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_modules = true
|
||||
reorder_imports = true
|
||||
use_field_init_shorthand = true
|
||||
wrap_comments = true
|
||||
edition = "2021"
|
@ -1,9 +1,11 @@
|
||||
#![feature(drain_filter)]
|
||||
#![feature(option_get_or_insert_default)]
|
||||
|
||||
use crate::routes::render_index;
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_web::{web, web::Data, App, HttpServer};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
|
||||
use crate::routes::render_index;
|
||||
|
||||
mod auth;
|
||||
mod model;
|
||||
|
@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Type};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Serialize, Deserialize, Type)]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::model::db;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::db;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Page {
|
||||
LocalBusinesses,
|
||||
@ -52,16 +53,6 @@ pub struct BusinessItemInput {
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
impl BusinessItemInput {
|
||||
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
price,
|
||||
picture_url: picture_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LocalBusiness {
|
||||
pub id: i32,
|
||||
@ -86,3 +77,12 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct UpdateBusinessItemInput {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub price: i32,
|
||||
pub picture_url: String,
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Serializer;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
|
||||
use actix_http::body::BoxBody;
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, Responder, ResponseError};
|
||||
use serde::Serializer;
|
||||
|
||||
mod restricted;
|
||||
mod unrestricted;
|
||||
|
||||
@ -35,7 +38,7 @@ impl Deref for Identity {
|
||||
|
||||
impl FromRequest for Identity {
|
||||
type Error = actix_web::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Identity, actix_web::Error>>>>;
|
||||
type Future = Pin<Box<dyn Future<Output = std::result::Result<Identity, actix_web::Error>>>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, p: &mut actix_http::Payload) -> Self::Future {
|
||||
@ -48,3 +51,120 @@ impl FromRequest for Identity {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type JsonResult<T> = std::result::Result<T, JsonError>;
|
||||
|
||||
pub enum Error {
|
||||
Unauthorized,
|
||||
UploadFailed,
|
||||
OwnedBusinessNotFound { account_id: i32 },
|
||||
OwnedBusinessItemNotFound { account_id: i32, business_id: i32 },
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn to_json(self) -> JsonError {
|
||||
JsonError(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Unauthorized => f.write_str("Brak uprawnień"),
|
||||
Error::OwnedBusinessNotFound { account_id } => f.write_fmt(format_args!(
|
||||
"Nie znaleziono usługi dla użytkownika {}",
|
||||
account_id
|
||||
)),
|
||||
Error::OwnedBusinessItemNotFound {
|
||||
account_id,
|
||||
business_id,
|
||||
} => f.write_fmt(format_args!(
|
||||
"Nie znaleziono przedmiotu usługi {} dla użytkownika {}",
|
||||
business_id, account_id
|
||||
)),
|
||||
Error::UploadFailed => f.write_str("Nie można zapisać pliku"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::Unauthorized => StatusCode::SEE_OTHER,
|
||||
Error::OwnedBusinessNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for Error {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
HttpResponse::build(self.status_code())
|
||||
.append_header(if matches!(self, Self::Unauthorized) {
|
||||
("Location", "/")
|
||||
} else {
|
||||
("X-Error", "1")
|
||||
})
|
||||
.content_type("text/html")
|
||||
.body(format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JsonError(Error);
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonErrorRepr {
|
||||
error: String,
|
||||
}
|
||||
impl Display for JsonError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
&serde_json::to_string(&JsonErrorRepr {
|
||||
error: format!("{}", self.0),
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for JsonError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for JsonError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.0 {
|
||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Error::OwnedBusinessNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for JsonError {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
HttpResponse::build(self.status_code())
|
||||
.append_header(if matches!(self.0, Error::Unauthorized) {
|
||||
("Location", "/")
|
||||
} else {
|
||||
("X-Error", "1")
|
||||
})
|
||||
.content_type("application/json")
|
||||
.body(format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,46 @@
|
||||
use crate::model::db;
|
||||
use crate::model::view::Page;
|
||||
use crate::routes::Identity;
|
||||
use crate::utils;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::{get, post, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
use tracing::info;
|
||||
use tracing::log::error;
|
||||
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Error, Identity, Result};
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "business-items.html")]
|
||||
struct BusinessItemsTemplate {
|
||||
page: Page,
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
items: Vec<db::LocalBusinessItem>,
|
||||
}
|
||||
|
||||
fn render_unauthorized() -> HttpResponse {
|
||||
HttpResponse::Unauthorized()
|
||||
.append_header(("Location", "/"))
|
||||
.body("")
|
||||
}
|
||||
|
||||
macro_rules! authorize {
|
||||
($id: expr, $pool: expr) => {{
|
||||
let account = match $id.identity() {
|
||||
None => return render_unauthorized(),
|
||||
Some(id) => utils::user_by_id(id, &*$pool).await,
|
||||
None => return Err(crate::routes::Error::Unauthorized),
|
||||
Some(id) => crate::utils::user_by_id(id, &*$pool).await,
|
||||
};
|
||||
match account {
|
||||
Some(account) => account,
|
||||
_ => return render_unauthorized(),
|
||||
_ => return Err(crate::routes::Error::Unauthorized),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[get("/account/business-items")]
|
||||
async fn business_items_page(db: Data<PgPool>, id: Identity) -> HttpResponse {
|
||||
let pool = db.into_inner();
|
||||
#[tracing::instrument]
|
||||
async fn business_items_page(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
handle_business_items_page(db.into_inner(), id).await
|
||||
}
|
||||
|
||||
async fn handle_business_items_page(pool: Arc<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let account = authorize!(id, pool);
|
||||
|
||||
let items: Vec<db::LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
@ -46,7 +48,8 @@ SELECT
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order
|
||||
item_order,
|
||||
picture_url
|
||||
FROM local_business_items
|
||||
ORDER BY item_order DESC
|
||||
"#,
|
||||
@ -61,14 +64,92 @@ ORDER BY item_order DESC
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let page = BusinessItemsTemplate {
|
||||
page: Page::BusinessItems,
|
||||
page: view::Page::BusinessItems,
|
||||
error: None,
|
||||
account: Some(account),
|
||||
items,
|
||||
};
|
||||
HttpResponse::Ok().body(page.render().unwrap())
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(page.render().unwrap()))
|
||||
}
|
||||
|
||||
#[post("/business-item/update")]
|
||||
#[tracing::instrument]
|
||||
async fn update_business_item(
|
||||
form: Form<view::UpdateBusinessItemInput>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
dbg!(&form);
|
||||
let pool = db.into_inner();
|
||||
let account = authorize!(id, pool);
|
||||
|
||||
{
|
||||
let business: db::LocalBusiness = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
FROM local_businesses
|
||||
WHERE state != 'Banned' AND owner_id = $1
|
||||
GROUP BY id, state
|
||||
ORDER BY id DESC
|
||||
"#,
|
||||
)
|
||||
.bind(account.id)
|
||||
.fetch_one(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::OwnedBusinessNotFound {
|
||||
account_id: account.id,
|
||||
}
|
||||
})?;
|
||||
|
||||
let item: db::LocalBusinessItem = sqlx::query_as(
|
||||
r#"
|
||||
UPDATE local_business_items
|
||||
SET
|
||||
name = $3,
|
||||
price = $4,
|
||||
picture_url = $5,
|
||||
item_order = $6
|
||||
WHERE
|
||||
local_business_id = $1 AND
|
||||
id = $2
|
||||
RETURNING
|
||||
id,
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order,
|
||||
picture_url
|
||||
"#,
|
||||
)
|
||||
.bind(business.id)
|
||||
.bind(form.id)
|
||||
.bind(form.name)
|
||||
.bind(form.price)
|
||||
.bind(form.picture_url)
|
||||
.bind(form.item_order)
|
||||
.fetch_one(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::OwnedBusinessItemNotFound {
|
||||
account_id: account.id,
|
||||
business_id: business.id,
|
||||
}
|
||||
})?;
|
||||
info!("{:?}", item);
|
||||
}
|
||||
handle_business_items_page(pool, id).await
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(business_items_page);
|
||||
config
|
||||
.service(business_items_page)
|
||||
.service(update_business_item);
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::Identity;
|
||||
use crate::utils;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::*;
|
||||
use askama::Template;
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::*;
|
||||
|
||||
use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::{Error, Identity, JsonResult, Result};
|
||||
use crate::utils;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct IndexTemplate {
|
||||
@ -24,7 +25,9 @@ pub struct IndexTemplate {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn render_index() -> HttpResponse {
|
||||
HttpResponse::NotFound().body(
|
||||
HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
IndexTemplate {
|
||||
services: vec![],
|
||||
account: None,
|
||||
@ -38,7 +41,7 @@ pub async fn render_index() -> HttpResponse {
|
||||
|
||||
#[get("/")]
|
||||
#[tracing::instrument]
|
||||
pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse {
|
||||
pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let record = match id.identity() {
|
||||
Some(id) => utils::user_by_id(id, &pool).await,
|
||||
@ -104,7 +107,9 @@ ORDER BY item_order DESC
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
HttpResponse::Ok().body(body)
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(body))
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -117,7 +122,7 @@ struct AccountTemplate {
|
||||
|
||||
#[get("/account")]
|
||||
#[tracing::instrument]
|
||||
async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
|
||||
async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let record = match id.identity() {
|
||||
Some(id) => utils::user_by_id(id, &pool).await,
|
||||
@ -126,7 +131,7 @@ async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
|
||||
None
|
||||
}
|
||||
};
|
||||
HttpResponse::Ok().body(
|
||||
Ok(HttpResponse::Ok().body(
|
||||
AccountTemplate {
|
||||
account: record,
|
||||
error: None,
|
||||
@ -134,7 +139,7 @@ async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -320,7 +325,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
.bind(&item.name)
|
||||
.bind(item.price as i32)
|
||||
.bind(idx as i32)
|
||||
.bind(item.picture_url)
|
||||
.bind(&item.picture_url)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
match res {
|
||||
@ -329,7 +334,9 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().body(
|
||||
return HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
@ -424,7 +431,9 @@ WHERE email = $1
|
||||
);
|
||||
}
|
||||
id.remember(format!("{}", record.id));
|
||||
HttpResponse::Ok().body(
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
AccountTemplate {
|
||||
account: Some(record),
|
||||
error: None,
|
||||
@ -441,30 +450,54 @@ struct UploadResponse {
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn upload(
|
||||
mut payload: actix_multipart::Multipart,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
async fn upload(mut payload: actix_multipart::Multipart, id: Identity) -> JsonResult<HttpResponse> {
|
||||
let path = PathBuf::new().join(
|
||||
id.identity()
|
||||
.map(|id| format!("./uploads/{id}"))
|
||||
.unwrap_or_else(|| "./uploads/tmp".into()),
|
||||
);
|
||||
std::fs::create_dir_all(&path)?;
|
||||
std::fs::create_dir_all(&path).map_err(|e| {
|
||||
error!("Cannot create upload directory {:?}", path);
|
||||
dbg!(e);
|
||||
Error::UploadFailed.to_json()
|
||||
})?;
|
||||
|
||||
if let Some(item) = payload.next().await {
|
||||
let mut field = item?;
|
||||
let mut field = item.map_err(|e| {
|
||||
warn!("Malformed upload file",);
|
||||
dbg!(e);
|
||||
Error::UploadFailed.to_json()
|
||||
})?;
|
||||
let name = field.name();
|
||||
tracing::info!("Writing file {:?}", name);
|
||||
info!("Writing file {:?}", name);
|
||||
let path = path.join(name);
|
||||
|
||||
while let Some(chunk) = field.next().await {
|
||||
let chunk = chunk?;
|
||||
std::fs::write(&path, chunk)?;
|
||||
let chunk = chunk.map_err(|e| {
|
||||
warn!(
|
||||
"Failed to read uploaded file bytes for {:?}/{:?}",
|
||||
path,
|
||||
field.name()
|
||||
);
|
||||
dbg!(e);
|
||||
Error::UploadFailed.to_json()
|
||||
})?;
|
||||
std::fs::write(&path, chunk).map_err(|e| {
|
||||
warn!(
|
||||
"Failed to write uploaded file bytes for {:?}/{:?}",
|
||||
path,
|
||||
field.name()
|
||||
);
|
||||
dbg!(e);
|
||||
Error::UploadFailed.to_json()
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(UploadResponse {
|
||||
path: path.to_str().unwrap_or_default().into(),
|
||||
path: String::from(path.to_str().unwrap_or_default())
|
||||
.strip_prefix('.')
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
}))
|
||||
} else {
|
||||
Ok(HttpResponse::BadRequest().finish())
|
||||
@ -494,9 +527,21 @@ pub fn configure(config: &mut ServiceConfig) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::model::view;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::view;
|
||||
use crate::model::view::BusinessItemInput;
|
||||
|
||||
impl BusinessItemInput {
|
||||
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
price,
|
||||
picture_url: picture_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_items() {
|
||||
let mut items = Vec::with_capacity(0);
|
||||
@ -507,8 +552,8 @@ mod tests {
|
||||
names.insert("items[1][price]".into(), "20".into());
|
||||
super::process_items(&mut items, names);
|
||||
let expected = vec![
|
||||
view::BusinessItemInput::new("a", 10),
|
||||
view::BusinessItemInput::new("b", 20),
|
||||
view::BusinessItemInput::new("a", 10, "/a"),
|
||||
view::BusinessItemInput::new("b", 20, "/b"),
|
||||
];
|
||||
assert_eq!(items, expected);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::model::db;
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||
|
||||
use crate::model::db;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn encrypt(pass: &str) -> password_hash::Result<String> {
|
||||
tracing::debug!("Hashing password {:?}", pass);
|
||||
|
Loading…
Reference in New Issue
Block a user