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>
{% 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 %}

View File

@ -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

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";
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>

View File

@ -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;
}
});

View File

@ -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;
}
}

View File

@ -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; }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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; }

View File

@ -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; }

View File

@ -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);
}
});

View File

@ -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;
}
});

View File

@ -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() {

View File

@ -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() {

View File

@ -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;
}
});

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 {
#form;
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
const shadow = this.#form = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<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 {
#form;
constructor() {
super();
const shadow = this[S] = this.attachShadow({mode: "closed"});
const shadow = this.#form = this.attachShadow({mode: "closed"});
shadow.innerHTML = `
<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 {
#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();
}
});

View File

@ -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; }

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 {
#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;
}
});

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 {
#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>

View File

@ -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>

View File

@ -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 {

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 {
#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;
}
});

View File

@ -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
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(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;

View File

@ -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)]

View File

@ -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,
}

View File

@ -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))
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);