Upload image

This commit is contained in:
Adrian Woźniak 2022-07-08 15:28:30 +02:00
parent 25986ec594
commit be70ed2b46
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
35 changed files with 998 additions and 493 deletions

View File

@ -3,9 +3,11 @@
<business-items> <business-items>
{% for item in items %} {% for item in items %}
<business-item <business-item
item-id="{{item.id}}"
name="{{item.name}}" name="{{item.name}}"
price="{{item.price}}" price="{{item.price}}"
url="{{item.picture_url}}" picture-url="{{item.picture_url}}"
item-order="{{item.item_order}}"
> >
</business-item> </business-item>
{% endfor %} {% endfor %}

View File

@ -9,7 +9,16 @@
}, },
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "ecmascript" "syntax": "ecmascript",
"dynamicImport": true,
"privateMethod": true,
"functionBind": true,
"exportDefaultFrom": true,
"exportNamespaceFrom": true,
"decorators": true,
"decoratorsBeforeExport": true,
"topLevelAwait": true,
"importMeta": true
}, },
"minify": { "minify": {
"compress": true, "compress": true,

542
client/dist/app.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
import { S } from "./shared"
import "./business-items/business-item"; import "./business-items/business-item";
customElements.define('business-items', class extends HTMLElement { customElements.define('business-items', class extends HTMLElement {
#form;
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>

View File

@ -1,30 +1,61 @@
import { S } from "../shared.js";
import "../shared/image-input"; import "../shared/image-input";
customElements.define('business-item', class extends HTMLElement { customElements.define('business-item', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['name', 'price', 'picture-url'] return ['item-id', 'name', 'price', 'picture-url', 'item-order']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
section { display: flex; justify-content: space-between; } section { display: flex; justify-content: space-between; }
#form {
display: none;
}
</style> </style>
<section> <section>
<image-input></image-input>
<div id="name"></div> <div id="name"></div>
<price-input></price-input> <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> </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() { connectedCallback() {
this.item_id = this.item_id;
this.name = this.name; this.name = this.name;
this.price = this.price; this.price = this.price;
this.picture_url = this.picture_url; this.picture_url = this.picture_url;
@ -33,35 +64,53 @@ customElements.define('business-item', class extends HTMLElement {
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return; if (oldV === newV) return;
switch (name) { switch (name) {
case 'item-id': return this.item_id = newV;
case 'name': return this.name = newV; case 'name': return this.name = newV;
case 'price': return this.price = newV / 100.0; case 'price': return this.price = newV / 100.0;
case 'picture-url': return this.picture_url = newV; 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() { get name() {
return this.getAttribute('name'); return this.getAttribute('name');
} }
set name(v) { set name(v) {
this.setAttribute('name', v); this.setAttribute('name', v);
this[S].querySelector('#name').textContent = v; this.#form.querySelector('#name').textContent = v;
} }
get price() { get price() {
return this.getAttribute('price'); return this.#form.querySelector('price-input').value;
}
set price(v) {
this.setAttribute('price', v);
this[S].querySelector('price-input').value = v;
} }
set price(v) {
this.setAttribute('price', v);
this.#form.querySelector('price-input').value = v;
}
get picture_url() { get picture_url() {
return this.getAttribute('picture-url'); return this.getAttribute('picture-url');
} }
set picture_url(v) { set picture_url(v) {
this.setAttribute('picture-url', v); this.setAttribute('picture-url', v);
this[S].querySelector('image-input').src = v; this.#form.querySelector('image-input').url = v;
} }
}); });

View File

@ -1,6 +1,8 @@
import { S, FORM_STYLE } from "./shared"; import { FORM_STYLE } from "./shared";
customElements.define('form-navigation', class extends HTMLElement { customElements.define('form-navigation', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['next', 'prev'] return ['next', 'prev']
} }
@ -8,7 +10,7 @@ customElements.define('form-navigation', class extends HTMLElement {
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -48,11 +50,11 @@ customElements.define('form-navigation', class extends HTMLElement {
if (oldV === newV) return; if (oldV === newV) return;
switch (name) { switch (name) {
case 'next': { case 'next': {
this[S].querySelector('#next').className = newV === 'hidden' ? 'hidden' : ''; this.#form.querySelector('#next').className = newV === 'hidden' ? 'hidden' : '';
break; break;
} }
case 'prev': { case 'prev': {
this[S].querySelector('#prev').className = newV === 'hidden' ? 'hidden' : ''; this.#form.querySelector('#prev').className = newV === 'hidden' ? 'hidden' : '';
break; break;
} }
} }

View File

@ -1,16 +1,16 @@
import { S } from "./shared";
import "./local-businesses/local-business-item"; import "./local-businesses/local-business-item";
import "./local-businesses/local-business"; import "./local-businesses/local-business";
customElements.define('local-businesses', class extends HTMLElement { customElements.define('local-businesses', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['filter'] return ['filter']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }

View File

@ -1,13 +1,13 @@
import { S } from "../shared";
customElements.define('local-business-item', class extends HTMLElement { customElements.define('local-business-item', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['name', 'price'] return ['name', 'price']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -31,17 +31,17 @@ customElements.define('local-business-item', class extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name'); this.#form.querySelector('#name').textContent = this.getAttribute('name');
this[S].querySelector('#price').value = this.price(); this.#form.querySelector('#price').value = this.price();
} }
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return; if (oldV === newV) return;
switch (name) { switch (name) {
case 'name': case 'name':
return this[S].querySelector('#name').textContent = newV; return this.#form.querySelector('#name').textContent = newV;
case 'price': case 'price':
return this[S].querySelector('#price').value = newV; return this.#form.querySelector('#price').value = newV;
} }
} }

View File

@ -1,13 +1,15 @@
import { S } from "../shared"; import { S } from "../shared";
customElements.define('local-business', class extends HTMLElement { customElements.define('local-business', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['name', 'service-id', 'state'] return ['name', 'service-id', 'state']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -25,14 +27,14 @@ customElements.define('local-business', class extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name'); this.#form.querySelector('#name').textContent = this.getAttribute('name');
} }
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return; if (oldV === newV) return;
switch (name) { switch (name) {
case 'name': case 'name':
return this[S].querySelector('#name').textContent = newV; return this.#form.querySelector('#name').textContent = newV;
} }
} }

View File

@ -1,13 +1,15 @@
import { FORM_STYLE, S } from "./shared"; import { FORM_STYLE, S } from "./shared";
customElements.define('login-form', class extends HTMLElement { customElements.define('login-form', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return [] return []
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }

View File

@ -1,9 +1,9 @@
import { S } from "../shared";
customElements.define('ow-nav', class extends HTMLElement { customElements.define('ow-nav', class extends HTMLElement {
#form;
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }

View File

@ -1,13 +1,13 @@
import { S } from "../shared";
customElements.define('ow-path', class extends HTMLElement { customElements.define('ow-path', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['selected', 'path']; return ['selected', 'path'];
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -66,6 +66,6 @@ customElements.define('ow-path', class extends HTMLElement {
return; return;
} }
this.setAttribute('path', value); this.setAttribute('path', value);
this[S].querySelector('a').setAttribute('href', value); this.#form.querySelector('a').setAttribute('href', value);
} }
}); });

View File

@ -1,13 +1,15 @@
import { S, FORM_STYLE } from "./shared"; import { FORM_STYLE } from "./shared";
customElements.define('ow-account', class extends HTMLElement { customElements.define('ow-account', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['mode', "id", "name", 'email', "facebook-id"] return ['mode', "id", "name", 'email', "facebook-id"]
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -127,7 +129,7 @@ customElements.define('ow-account', class extends HTMLElement {
set name(v) { set name(v) {
this.setAttribute('name', v); this.setAttribute('name', v);
this[S].querySelector('#display #name').value = v; this.#form.querySelector('#display #name').value = v;
} }
get email() { get email() {
@ -136,7 +138,7 @@ customElements.define('ow-account', class extends HTMLElement {
set email(v) { set email(v) {
this.setAttribute('email', v); this.setAttribute('email', v);
this[S].querySelector('#display #email').value = v; this.#form.querySelector('#display #email').value = v;
} }
get facebook_id() { get facebook_id() {
@ -145,6 +147,6 @@ customElements.define('ow-account', class extends HTMLElement {
set facebook_id(v) { set facebook_id(v) {
this.setAttribute('facebook-id', v); this.setAttribute('facebook-id', v);
this[S].querySelector('#display #facebook_id').value = v; this.#form.querySelector('#display #facebook_id').value = v;
} }
}); });

View File

@ -1,13 +1,15 @@
import { S, FORM_STYLE } from "../shared"; import { FORM_STYLE } from "../shared";
customElements.define('price-input', class extends HTMLElement { customElements.define('price-input', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['value', 'currency', 'required', 'name'] return ['value', 'currency', 'required', 'name']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -41,13 +43,13 @@ customElements.define('price-input', class extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this[S].querySelector('#currency').textContent = this.currency; this.#form.querySelector('#currency').textContent = this.currency;
this[S].querySelector('#price').value = this.value; this.#form.querySelector('#price').value = this.value;
} }
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return; if (oldV === newV) return;
const price = this[S].querySelector('#price'); const price = this.#form.querySelector('#price');
switch (name) { switch (name) {
case 'price': { case 'price': {
this.value = newV; this.value = newV;
@ -77,12 +79,12 @@ customElements.define('price-input', class extends HTMLElement {
} }
get value() { get value() {
return this[S].querySelector('#price').value * 100; return Math.floor(parseFloat(this.#form.querySelector('#price').value) * 100);
} }
set value(v) { set value(v) {
this.setAttribute('value', v); this.setAttribute('value', v);
this[S].querySelector('#price').value = v; this.#form.querySelector('#price').value = v;
} }
get currency() { get currency() {
@ -91,11 +93,11 @@ customElements.define('price-input', class extends HTMLElement {
set currency(value) { set currency(value) {
this.setAttribute('currency', value); this.setAttribute('currency', value);
this[S].querySelector('#currency').textContent = this.currency; this.#form.querySelector('#currency').textContent = this.currency;
} }
reportValidity() { reportValidity() {
return this[S].querySelector('input').reportValidity(); return this.#form.querySelector('input').reportValidity();
} }
get name() { get name() {

View File

@ -1,13 +1,13 @@
import { S } from "../shared";
customElements.define('price-view', class extends HTMLElement { customElements.define('price-view', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['value', 'currency'] return ['value', 'currency']
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
@ -21,7 +21,7 @@ customElements.define('price-view', class extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this[S].querySelector('#price').textContent = this.formatted; this.#form.querySelector('#price').textContent = this.formatted;
} }
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
@ -49,7 +49,7 @@ customElements.define('price-view', class extends HTMLElement {
set value(v) { set value(v) {
this.setAttribute('value', v); this.setAttribute('value', v);
this[S].querySelector('#price').textContent = this.formatted; this.#form.querySelector('#price').textContent = this.formatted;
} }
get currency() { get currency() {

View File

@ -8,26 +8,6 @@ import "./register-form/register-submit-form";
import "./register-form/register-user-type"; import "./register-form/register-user-type";
import "./register-form/register-user-form"; 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 { customElements.define('register-form', class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ['step'] return ['step']
@ -48,8 +28,6 @@ customElements.define('register-form', class extends HTMLElement {
:host([step="4"]) #step-4 { display: block; } :host([step="4"]) #step-4 { display: block; }
:host([step="40"]) #step-40 { display: block; } :host([step="40"]) #step-40 { display: block; }
${ FORM_STYLE }
.actions { .actions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -63,6 +41,8 @@ customElements.define('register-form', class extends HTMLElement {
#step-4 > #copied { #step-4 > #copied {
display: none; display: none;
} }
${ FORM_STYLE }
</style> </style>
<article id="host"> <article id="host">
</article> </article>
@ -90,7 +70,7 @@ customElements.define('register-form', class extends HTMLElement {
this[S].addEventListener('form:next', ev => { this[S].addEventListener('form:next', ev => {
ev.stopPropagation(); ev.stopPropagation();
const form = shadow.querySelector(`#step-${ this.step }`); const form = shadow.querySelector(`#step-${ this.step }`);
if (copyForm(form, finalForm)) { if (this.#copyForm(form, finalForm)) {
this.step = this.step + 1; this.step = this.step + 1;
} }
}); });
@ -98,12 +78,10 @@ customElements.define('register-form', class extends HTMLElement {
ev.stopPropagation(); ev.stopPropagation();
this.step = this.step - 1; this.step = this.step - 1;
}); });
{ finalForm.addEventListener('submit', ev => {
finalForm.addEventListener('submit', ev => { ev.preventDefault();
ev.preventDefault(); ev.stopPropagation();
ev.stopPropagation(); });
});
}
} }
connectedCallback() { connectedCallback() {
@ -125,4 +103,24 @@ customElements.define('register-form', class extends HTMLElement {
if (n < 0) return; if (n < 0) return;
this.setAttribute('step', n); 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;
}
}); });

View File

@ -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 { customElements.define('register-basic-form', class extends PseudoForm {
#form;
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>

View File

@ -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 { customElements.define('register-business-form', class extends PseudoForm {
#form;
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({mode: "closed"}); const shadow = this.#form = this.attachShadow({mode: "closed"});
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>

View File

@ -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 { customElements.define('register-item-form-row', class extends PseudoForm {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['idx', 'name'] return ['idx', 'name']
} }
constructor() { constructor() {
super(); super();
this[S] = this.attachShadow({ mode: 'closed' }); this.#form = this.attachShadow({ mode: 'closed' });
this.addEventListener('item:removed', () => { this.addEventListener('item:removed', () => {
this.setAttribute('removed', 'removed'); this.setAttribute('removed', 'removed');
@ -18,44 +20,64 @@ customElements.define('register-item-form-row', class extends PseudoForm {
} }
connectedCallback() { connectedCallback() {
const idx = this.getAttribute('idx'); const idx = this.idx;
this[S].innerHTML = `
this.#form.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }
* { font-family: 'Noto Sans', sans-serif; } * { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
form { form {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-start;
} }
div { #fields > *:not(:last-child) {
min-width: 100px; margin-right: 16px;
max-width: 48%;
} }
#fields > div > label {
margin-right: 16px;
}
#fields {
display: flex;
justify-content: flex-start;
width: 100%;
max-width: 100%;
}
${ FORM_STYLE }
</style> </style>
<form class="inline"> <form class="inline">
<div id="name" class="field"> <div id="fields">
<label>Nazwa</label> <image-input></image-input>
<input class="item-name" name="items[${ idx }][name]" type="text" required> <input type="hidden" name="picture_url" id="picture_url" />
</div> <div id="name" class="field">
<div id="price" class="field"> <label>Nazwa</label>
<label>Cena</label> <input class="item-name" name="items[${ idx }][name]" type="text" required>
<price-input class="item-price" name="items[${ idx }][price]" required> </div>
</price-input> <div id="price" class="field">
<label>Cena</label>
<price-input class="item-price" name="items[${ idx }][price]" required>
</price-input>
</div>
</div> </div>
<div><input class="remove" type="button" value="Usuń"></div> <div><input class="remove" type="button" value="Usuń"></div>
</form> </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.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.reportValidity(); this.reportValidity();
}); });
this[S].querySelector('.remove').addEventListener('click', ev => { this.#form.querySelector('.remove').addEventListener('click', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false })); 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) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return; if (oldV === newV) return;
switch (name) { switch (name) {
case 'idx': { case 'idx':return this.updateNames();
this.updateNames(); case 'picture-url': return this.picture_url = newV;
break;
}
} }
} }
get inputs() { get inputs() {
return [ return [
extract(this[S].querySelector('.item-name')), extract(this.#form.querySelector('.item-name')),
extract(this[S].querySelector('.item-price')), extract(this.#form.querySelector('.item-price')),
]; ];
} }
updateNames() { updateNames() {
const idx = this.getAttribute('idx'); 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; const id = el.id;
el.querySelector('input, price-input').setAttribute('name', `items[${ idx }][${ 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); 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() { reportValidity() {
return super.reportValidity() && this[S].querySelector('price-input').reportValidity(); return super.reportValidity() && this.#form.querySelector('price-input').reportValidity();
} }
}); });

View File

@ -1,4 +1,4 @@
import { FORM_STYLE, S, PseudoForm } from "../shared"; import { FORM_STYLE, PseudoForm } from "../shared";
import "./register-item-form-row" import "./register-item-form-row"
@ -11,13 +11,15 @@ const updateItems = (form) => {
} }
customElements.define('register-items-form', class extends PseudoForm { customElements.define('register-items-form', class extends PseudoForm {
#form;
static get observedAttributes() { static get observedAttributes() {
return [] return []
} }
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' }); const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
:host { display: block; } :host { display: block; }

View File

@ -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 { customElements.define('register-submit-form', class extends PseudoForm {
#form;
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
@ -61,12 +63,12 @@ customElements.define('register-submit-form', class extends PseudoForm {
} }
updateField(name, value) { updateField(name, value) {
this[S].querySelector(`[id="hidden-${ name }"]`).value = value; this.#form.querySelector(`[id="hidden-${ name }"]`).value = value;
this[S].querySelector(`[id="preview-${ name }"]`).value = value; this.#form.querySelector(`[id="preview-${ name }"]`).value = value;
} }
setItems(items) { setItems(items) {
const host = this[S].querySelector('#items'); const host = this.#form.querySelector('#items');
host.innerHTML = ``; host.innerHTML = ``;
for (const row of items) { for (const row of items) {
const el = host.appendChild(document.createElement('div')); const el = host.appendChild(document.createElement('div'));
@ -82,6 +84,6 @@ customElements.define('register-submit-form', class extends PseudoForm {
} }
set accountType(v) { set accountType(v) {
this[S].querySelector('#account_type').value = v; this.#form.querySelector('#account_type').value = v;
} }
}); });

View File

@ -1,6 +1,8 @@
import { S, FORM_STYLE } from "../shared"; import { FORM_STYLE } from "../shared";
customElements.define('register-user-form', class extends HTMLElement { customElements.define('register-user-form', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['mode'] return ['mode']
} }
@ -8,7 +10,7 @@ customElements.define('register-user-form', class extends HTMLElement {
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>

View File

@ -1,8 +1,10 @@
customElements.define('register-user-type', class extends HTMLElement { customElements.define('register-user-type', class extends HTMLElement {
#form;
constructor() { constructor() {
super(); super();
const shadow = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <style>
@ -37,7 +39,7 @@ customElements.define('register-user-type', class extends HTMLElement {
<ul> <ul>
<li> <li>
<a id="user"> <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"/> <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> </svg>
<div>Użytkownik</div> <div>Użytkownik</div>
@ -45,7 +47,10 @@ customElements.define('register-user-type', class extends HTMLElement {
</li> </li>
<li> <li>
<a id="local-service"> <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> <div>Usługodawca</div>
</a> </a>
</li> </li>

View File

@ -1,5 +1,44 @@
export const S = Symbol(); 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 = ` export const FORM_STYLE = `
form { form {
display: block; display: block;
@ -62,42 +101,7 @@ label {
display: inline-block; display: inline-block;
margin-bottom: .5rem; margin-bottom: .5rem;
} }
input[type="button"], input[type="submit"] { ${BUTTON_STYLE}
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 class PseudoForm extends HTMLElement { export class PseudoForm extends HTMLElement {

View File

@ -1,6 +1,8 @@
import { S } from "../shared.js"; import { BUTTON_STYLE, S } from "../shared.js";
customElements.define('image-input', class extends HTMLElement { customElements.define('image-input', class extends HTMLElement {
#form;
static get observedAttributes() { static get observedAttributes() {
return ['width', 'height', "account-id", "url"] return ['width', 'height', "account-id", "url"]
} }
@ -8,15 +10,28 @@ customElements.define('image-input', class extends HTMLElement {
constructor() { constructor() {
super(); super();
const shadow = this[S] = this.attachShadow({ mode: "closed" }); const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = ` shadow.innerHTML = `
<style> <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; } #hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; } input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
#view { width: 200px; height: 200px; cursor: pointer; } #view { width: 200px; height: 200px; cursor: pointer; }
canvas { width: 200px; height: 200px; } 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> </style>
<article> <article>
<section id="hidden"> <section id="hidden">
@ -35,7 +50,12 @@ customElements.define('image-input', class extends HTMLElement {
shadow.querySelector('#save').addEventListener('click', ev => { shadow.querySelector('#save').addEventListener('click', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); 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 = []; const array = [];
for (let i = 0; i < blobBin.length; i++) { for (let i = 0; i < blobBin.length; i++) {
array.push(blobBin.charCodeAt(i)); array.push(blobBin.charCodeAt(i));
@ -46,7 +66,10 @@ customElements.define('image-input', class extends HTMLElement {
fetch("/upload", { fetch("/upload", {
method: "POST", method: "POST",
body: form, 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(); const f = new FileReader();
@ -129,6 +152,6 @@ customElements.define('image-input', class extends HTMLElement {
set url(v) { set url(v) {
this.setAttribute('url', v); this.setAttribute('url', v);
this[S].querySelector('img').src = v; this.#form.querySelector('img').src = v;
} }
}); });

View File

@ -1,2 +1,2 @@
ALTER TABLE local_business_items 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
View 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"

View File

@ -1,9 +1,11 @@
#![feature(drain_filter)] #![feature(drain_filter)]
#![feature(option_get_or_insert_default)] #![feature(option_get_or_insert_default)]
use crate::routes::render_index;
use actix_identity::{CookieIdentityPolicy, IdentityService}; 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 auth;
mod model; mod model;

View File

@ -1,7 +1,8 @@
use std::collections::HashMap;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Type}; use sqlx::{FromRow, Type};
use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Serialize, Deserialize, Type)] #[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Serialize, Deserialize, Type)]

View File

@ -1,6 +1,7 @@
use crate::model::db;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::model::db;
#[derive(Debug)] #[derive(Debug)]
pub enum Page { pub enum Page {
LocalBusinesses, LocalBusinesses,
@ -52,16 +53,6 @@ pub struct BusinessItemInput {
pub picture_url: String, 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct LocalBusiness { pub struct LocalBusiness {
pub id: i32, 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,
}

View File

@ -1,11 +1,14 @@
use actix_web::web::ServiceConfig; use std::fmt::{Debug, Display, Formatter};
use actix_web::{FromRequest, HttpRequest};
use serde::Serializer;
use std::fmt::{Debug, Formatter};
use std::future::Future; use std::future::Future;
use std::ops::Deref; use std::ops::Deref;
use std::pin::Pin; 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 restricted;
mod unrestricted; mod unrestricted;
@ -35,7 +38,7 @@ impl Deref for Identity {
impl FromRequest for Identity { impl FromRequest for Identity {
type Error = actix_web::Error; 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] #[inline]
fn from_request(req: &HttpRequest, p: &mut actix_http::Payload) -> Self::Future { 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))
}
}

View File

@ -1,44 +1,46 @@
use crate::model::db; use std::sync::Arc;
use crate::model::view::Page;
use crate::routes::Identity; use actix_web::web::{Data, Form, ServiceConfig};
use crate::utils; use actix_web::{get, post, HttpResponse};
use actix_web::web::{Data, ServiceConfig};
use actix_web::{get, HttpResponse};
use askama::*; use askama::*;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::info;
use tracing::log::error;
use crate::model::{db, view};
use crate::routes::{Error, Identity, Result};
#[derive(Debug, Template)] #[derive(Debug, Template)]
#[template(path = "business-items.html")] #[template(path = "business-items.html")]
struct BusinessItemsTemplate { struct BusinessItemsTemplate {
page: Page, page: view::Page,
error: Option<String>, error: Option<String>,
account: Option<db::Account>, account: Option<db::Account>,
items: Vec<db::LocalBusinessItem>, items: Vec<db::LocalBusinessItem>,
} }
fn render_unauthorized() -> HttpResponse {
HttpResponse::Unauthorized()
.append_header(("Location", "/"))
.body("")
}
macro_rules! authorize { macro_rules! authorize {
($id: expr, $pool: expr) => {{ ($id: expr, $pool: expr) => {{
let account = match $id.identity() { let account = match $id.identity() {
None => return render_unauthorized(), None => return Err(crate::routes::Error::Unauthorized),
Some(id) => utils::user_by_id(id, &*$pool).await, Some(id) => crate::utils::user_by_id(id, &*$pool).await,
}; };
match account { match account {
Some(account) => account, Some(account) => account,
_ => return render_unauthorized(), _ => return Err(crate::routes::Error::Unauthorized),
} }
}}; }};
} }
#[get("/account/business-items")] #[get("/account/business-items")]
async fn business_items_page(db: Data<PgPool>, id: Identity) -> HttpResponse { #[tracing::instrument]
let pool = db.into_inner(); 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 account = authorize!(id, pool);
let items: Vec<db::LocalBusinessItem> = sqlx::query_as( let items: Vec<db::LocalBusinessItem> = sqlx::query_as(
r#" r#"
SELECT SELECT
@ -46,7 +48,8 @@ SELECT
local_business_id, local_business_id,
name, name,
price, price,
item_order item_order,
picture_url
FROM local_business_items FROM local_business_items
ORDER BY item_order DESC ORDER BY item_order DESC
"#, "#,
@ -61,14 +64,92 @@ ORDER BY item_order DESC
}) })
.unwrap_or_default(); .unwrap_or_default();
let page = BusinessItemsTemplate { let page = BusinessItemsTemplate {
page: Page::BusinessItems, page: view::Page::BusinessItems,
error: None, error: None,
account: Some(account), account: Some(account),
items, 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) { pub fn configure(config: &mut ServiceConfig) {
config.service(business_items_page); config
.service(business_items_page)
.service(update_business_item);
} }

View File

@ -1,18 +1,19 @@
use crate::model::db; use std::collections::HashMap;
use crate::model::view::{self, Page}; use std::path::PathBuf;
use crate::routes::Identity;
use crate::utils;
use actix_files::Files; use actix_files::Files;
use actix_web::web::{Data, ServiceConfig}; use actix_web::web::{Data, ServiceConfig};
use actix_web::*; use actix_web::*;
use askama::Template; use askama::Template;
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::*; use tracing::*;
use crate::model::db;
use crate::model::view::{self, Page};
use crate::routes::{Error, Identity, JsonResult, Result};
use crate::utils;
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
pub struct IndexTemplate { pub struct IndexTemplate {
@ -24,21 +25,23 @@ pub struct IndexTemplate {
#[tracing::instrument] #[tracing::instrument]
pub async fn render_index() -> HttpResponse { pub async fn render_index() -> HttpResponse {
HttpResponse::NotFound().body( HttpResponse::NotFound()
IndexTemplate { .append_header(("Content-Type", "text/html"))
services: vec![], .body(
account: None, IndexTemplate {
error: None, services: vec![],
page: Page::LocalBusinesses, account: None,
} error: None,
.render() page: Page::LocalBusinesses,
.unwrap(), }
) .render()
.unwrap(),
)
} }
#[get("/")] #[get("/")]
#[tracing::instrument] #[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 pool = db.into_inner();
let record = match id.identity() { let record = match id.identity() {
Some(id) => utils::user_by_id(id, &pool).await, Some(id) => utils::user_by_id(id, &pool).await,
@ -104,7 +107,9 @@ ORDER BY item_order DESC
} }
.render() .render()
.unwrap(); .unwrap();
HttpResponse::Ok().body(body) Ok(HttpResponse::Ok()
.append_header(("Content-Type", "text/html"))
.body(body))
} }
#[derive(Template)] #[derive(Template)]
@ -117,7 +122,7 @@ struct AccountTemplate {
#[get("/account")] #[get("/account")]
#[tracing::instrument] #[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 pool = db.into_inner();
let record = match id.identity() { let record = match id.identity() {
Some(id) => utils::user_by_id(id, &pool).await, 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 None
} }
}; };
HttpResponse::Ok().body( Ok(HttpResponse::Ok().body(
AccountTemplate { AccountTemplate {
account: record, account: record,
error: None, error: None,
@ -134,7 +139,7 @@ async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
} }
.render() .render()
.unwrap(), .unwrap(),
) ))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -320,7 +325,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
.bind(&item.name) .bind(&item.name)
.bind(item.price as i32) .bind(item.price as i32)
.bind(idx as i32) .bind(idx as i32)
.bind(item.picture_url) .bind(&item.picture_url)
.fetch_one(&mut t) .fetch_one(&mut t)
.await; .await;
match res { match res {
@ -329,15 +334,17 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
tracing::error!("{e}"); tracing::error!("{e}");
dbg!(e); dbg!(e);
t.rollback().await.unwrap(); t.rollback().await.unwrap();
return HttpResponse::BadRequest().body( return HttpResponse::BadRequest()
AccountTemplate { .append_header(("Content-Type", "text/html"))
account: None, .body(
error: Some("Problem z utworzeniem konta".into()), AccountTemplate {
page: Page::Register, account: None,
} error: Some("Problem z utworzeniem konta".into()),
.render() page: Page::Register,
.unwrap(), }
); .render()
.unwrap(),
);
} }
} }
} }
@ -424,15 +431,17 @@ WHERE email = $1
); );
} }
id.remember(format!("{}", record.id)); id.remember(format!("{}", record.id));
HttpResponse::Ok().body( HttpResponse::Ok()
AccountTemplate { .append_header(("Content-Type", "text/html"))
account: Some(record), .body(
error: None, AccountTemplate {
page: Page::Login, account: Some(record),
} error: None,
.render() page: Page::Login,
.unwrap(), }
) .render()
.unwrap(),
)
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -441,30 +450,54 @@ struct UploadResponse {
} }
#[post("/upload")] #[post("/upload")]
async fn upload( async fn upload(mut payload: actix_multipart::Multipart, id: Identity) -> JsonResult<HttpResponse> {
mut payload: actix_multipart::Multipart,
id: Identity,
) -> Result<HttpResponse, actix_web::Error> {
let path = PathBuf::new().join( let path = PathBuf::new().join(
id.identity() id.identity()
.map(|id| format!("./uploads/{id}")) .map(|id| format!("./uploads/{id}"))
.unwrap_or_else(|| "./uploads/tmp".into()), .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 { 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(); let name = field.name();
tracing::info!("Writing file {:?}", name); info!("Writing file {:?}", name);
let path = path.join(name); let path = path.join(name);
while let Some(chunk) = field.next().await { while let Some(chunk) = field.next().await {
let chunk = chunk?; let chunk = chunk.map_err(|e| {
std::fs::write(&path, chunk)?; 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 { 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 { } else {
Ok(HttpResponse::BadRequest().finish()) Ok(HttpResponse::BadRequest().finish())
@ -494,9 +527,21 @@ pub fn configure(config: &mut ServiceConfig) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::model::view;
use std::collections::HashMap; 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] #[test]
fn parse_items() { fn parse_items() {
let mut items = Vec::with_capacity(0); let mut items = Vec::with_capacity(0);
@ -507,8 +552,8 @@ mod tests {
names.insert("items[1][price]".into(), "20".into()); names.insert("items[1][price]".into(), "20".into());
super::process_items(&mut items, names); super::process_items(&mut items, names);
let expected = vec![ let expected = vec![
view::BusinessItemInput::new("a", 10), view::BusinessItemInput::new("a", 10, "/a"),
view::BusinessItemInput::new("b", 20), view::BusinessItemInput::new("b", 20, "/b"),
]; ];
assert_eq!(items, expected); assert_eq!(items, expected);
} }

View File

@ -1,7 +1,8 @@
use crate::model::db;
use argon2::{Algorithm, Argon2, Params, Version}; use argon2::{Algorithm, Argon2, Params, Version};
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use crate::model::db;
#[tracing::instrument] #[tracing::instrument]
pub fn encrypt(pass: &str) -> password_hash::Result<String> { pub fn encrypt(pass: &str) -> password_hash::Result<String> {
tracing::debug!("Hashing password {:?}", pass); tracing::debug!("Hashing password {:?}", pass);