Improve register company

This commit is contained in:
Adrian Woźniak 2022-07-28 15:28:28 +02:00
parent d2a00dfc79
commit a9669432ec
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
26 changed files with 886 additions and 585 deletions

View File

@ -1,3 +1,7 @@
import "./poly.js";
import "./shared/error-message.js";
import "./shared/rich-text-editor.js"; import "./shared/rich-text-editor.js";
import "./shared/form-navigation.js"; import "./shared/form-navigation.js";
import "./shared/image-popup.js"; import "./shared/image-popup.js";
@ -8,6 +12,7 @@ import "./shared/price/price-input.js";
import "./shared/price/price-view.js"; import "./shared/price/price-view.js";
import "./ow-account/ow-account.js"; import "./ow-account/ow-account.js";
import "./ow-account/account-view.js";
import "./local-businesses/local-businesses.js"; import "./local-businesses/local-businesses.js";
import "./local-businesses/local-business-item.js"; import "./local-businesses/local-business-item.js";
@ -16,12 +21,12 @@ import "./local-businesses/local-business.js";
import "./login-form.js"; import "./login-form.js";
import "./register-form.js"; import "./register-form.js";
import "./register-form/register-business-account-form"; import "./register-form/register-business-account-form";
import "./register-form/register-item-form-row.js"; import "./register-form/register-business-item-form.js";
import "./register-form/register-business-items-form.js"; import "./register-form/register-business-items-form.js";
import "./register-form/register-business-details-form.js"; import "./register-form/register-business-details-form.js";
import "./register-form/register-account-type.js"; import "./register-form/register-account-type.js";
import "./register-form/register-user-account-form.js"; import "./register-form/register-user-account-form.js";
import "./register-form/register-business-contact-form.js"; import "./register-form/register-business-contacts-form.js";
import "./register-form/register-business-submit-form.js"; import "./register-form/register-business-submit-form.js";
import "./business-items/business-item.js"; import "./business-items/business-item.js";
@ -64,15 +69,3 @@ if (!document.querySelector('#facebook-jssdk')) {
js.src = "https://connect.facebook.net/en_US/sdk.js"; js.src = "https://connect.facebook.net/en_US/sdk.js";
document.head.appendChild(js); document.head.appendChild(js);
} }
Object.prototype.entry = function (key) {
const owner = this;
return {
owner, orElse(v) {
if (owner[key] === undefined) owner[key] = v;
return owner[key];
}, get() {
return owner[key];
}
}
};

View File

@ -1,6 +1,6 @@
import { Component, FORM_STYLE } from "../shared"; import { Component, FORM_STYLE } from "../shared";
import "../register-form/register-item-form-row"; import "../register-form/register-business-item-form";
customElements.define('business-item-editor', class extends Component { customElements.define('business-item-editor', class extends Component {
#idx; #idx;

View File

@ -2,7 +2,7 @@ import { Component, FORM_STYLE } from "../shared";
customElements.define('contact-info-editor', class extends Component { customElements.define('contact-info-editor', class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['type', "contact-id", "content"]; return ['type', "contact-id", "content", 'save'];
} }
constructor() { constructor() {
@ -26,6 +26,9 @@ customElements.define('contact-info-editor', class extends Component {
article { article {
margin: 8px; margin: 8px;
} }
:host([save="false"]) #submit {
display: none;
}
@media only screen and (min-device-width: 1000px) { @media only screen and (min-device-width: 1000px) {
article { article {
margin: 0; margin: 0;
@ -41,14 +44,14 @@ customElements.define('contact-info-editor', class extends Component {
<label>E-Mail</label> <label>E-Mail</label>
<div id="input"> <div id="input">
<div id="icon"> <div id="icon">
<contact-type-icon contact-type="email"></contact-type-icon> <contact-type-icon type="email"></contact-type-icon>
</div> </div>
<input type="text" id="content" name="content" /> <input type="text" id="content" name="content" />
<input type="hidden" id="contact_type" name="contact_type" /> <input type="hidden" id="type" name="type" />
</div> </div>
</div> </div>
<div> <div id="submit">
<input type="submit" value="Dodaj" /> <input type="submit" value="Dodaj" />
</div> </div>
</form> </form>
@ -65,6 +68,7 @@ customElements.define('contact-info-editor', class extends Component {
input.addEventListener('change', ev => { input.addEventListener('change', ev => {
ev.stopPropagation(); ev.stopPropagation();
this.#updateContactType(input.value, null); this.#updateContactType(input.value, null);
this.content = input.value;
}); });
input.addEventListener('keyup', ev => { input.addEventListener('keyup', ev => {
ev.stopPropagation(); ev.stopPropagation();
@ -72,11 +76,12 @@ customElements.define('contact-info-editor', class extends Component {
timeout = setTimeout(() => { timeout = setTimeout(() => {
timeout = null; timeout = null;
this.#updateContactType(input.value, null); this.#updateContactType(input.value, null);
this.content = input.value;
}, 1000 / 3) }, 1000 / 3)
}); });
} }
this.shadowRoot.querySelector('#contact_type').addEventListener('change', ev => { this.shadowRoot.querySelector('#type').addEventListener('change', ev => {
ev.stopPropagation(); ev.stopPropagation();
this.#updateContactType(null, ev.target.value); this.#updateContactType(null, ev.target.value);
}); });
@ -136,8 +141,8 @@ customElements.define('contact-info-editor', class extends Component {
type = type || this.#resolveContactType(value); type = type || this.#resolveContactType(value);
this.setAttribute('type', type); this.setAttribute('type', type);
const icon = this.shadowRoot.querySelector('contact-type-icon'); const icon = this.shadowRoot.querySelector('contact-type-icon');
icon.setAttribute('contact-type', type); icon.type = type;
this.shadowRoot.querySelector('#contact_type').value = type; this.shadowRoot.querySelector('#type').value = type;
} }
#resolveContactType(s) { #resolveContactType(s) {

View File

@ -2,7 +2,7 @@ import { Component } from "../shared";
customElements.define('contact-info', class extends Component { customElements.define('contact-info', class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['contact-type', 'content', 'contact-id', 'mode']; return ['type', 'content', 'contact-id', 'mode'];
} }
constructor() { constructor() {
@ -24,13 +24,13 @@ customElements.define('contact-info', class extends Component {
<slot></slot> <slot></slot>
<section> <section>
<a target="_blank"> <a target="_blank">
<contact-type-icon contact-type="email"></contact-type-icon> <contact-type-icon type="email"></contact-type-icon>
<div id="content"></div> <div id="content"></div>
</a> </a>
</section> </section>
`); `);
this.shadowRoot.querySelector('a').addEventListener('click', ev => { this.shadowRoot.querySelector('a').addEventListener('click', ev => {
if (this.contact_type === 'mobile') { if (this.type === 'mobile') {
ev.preventDefault(); ev.preventDefault();
const decoded = atob(this.content); const decoded = atob(this.content);
if (this.#isMobile()) { if (this.#isMobile()) {
@ -44,16 +44,16 @@ customElements.define('contact-info', class extends Component {
}); });
} }
set contact_type(v) { set type(v) {
if (!v) return; if (!v) return;
this.setAttribute('contact-type', v); this.setAttribute('type', v);
this.shadowRoot.querySelector('contact-type-icon').setAttribute('contact-type', v); this.shadowRoot.querySelector('contact-type-icon').type = v;
this.#setHref(); this.#setHref();
} }
get contact_type() { get type() {
return this.getAttribute('contact-type'); return this.getAttribute('type');
} }
set content(v) { set content(v) {
@ -90,7 +90,7 @@ customElements.define('contact-info', class extends Component {
#createLinkPath() { #createLinkPath() {
const s = this.shadowRoot.querySelector('#content').textContent || ''; const s = this.shadowRoot.querySelector('#content').textContent || '';
switch (this.contact_type) { switch (this.type) {
case 'email': case 'email':
return `mailto:${ s }`; return `mailto:${ s }`;
default: default:

View File

@ -3,7 +3,7 @@ import { Component } from "../shared";
customElements.define('contact-type-icon', customElements.define('contact-type-icon',
class extends Component { class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['contact-type']; return ['type'];
} }
constructor() { constructor() {
@ -11,16 +11,16 @@ customElements.define('contact-type-icon',
<style> <style>
:host { display: block; } :host { display: block; }
svg { display: none; } svg { display: none; }
:host([contact-type="email"]) #email-icon { :host([type="email"]) #email-icon {
display: block; display: block;
} }
:host([contact-type="facebook"]) #fb-icon { :host([type="facebook"]) #fb-icon {
display: block; display: block;
} }
:host([contact-type="other"]) #other-icon { :host([type="other"]) #other-icon {
display: block; display: block;
} }
:host([contact-type="mobile"]) #mobile-icon { :host([type="mobile"]) #mobile-icon {
display: block; display: block;
} }
path { path {
@ -48,15 +48,11 @@ customElements.define('contact-type-icon',
`); `);
} }
set contact_type(v) { set type(v) {
this.contactType = v; this.setAttribute('type', v || 'email');
} }
set contactType(v) { get type() {
this.setAttribute('contact-type', v || 'email'); return this.getAttribute('type')
}
get contact_type() {
return this.getAttribute('contact-type')
} }
}); });

View File

@ -2,7 +2,7 @@ import { Component, BUTTON_STYLE } from "../shared";
customElements.define('edit-contact-info', class extends Component { customElements.define('edit-contact-info', class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['contact-id', "mode"]; return ['contact-id', "mode", "delete", 'type'];
} }
constructor() { constructor() {
@ -41,6 +41,9 @@ customElements.define('edit-contact-info', class extends Component {
:host([mode = 'edit']) ::slotted(contact-info) { :host([mode = 'edit']) ::slotted(contact-info) {
display: none; display: none;
} }
:host([delete = "false"]) #deleteButton {
display: none;
}
${ BUTTON_STYLE } ${ BUTTON_STYLE }
</style> </style>
<article> <article>
@ -49,7 +52,7 @@ customElements.define('edit-contact-info', class extends Component {
<contact-info-editor></contact-info-editor> <contact-info-editor></contact-info-editor>
<div id="buttons"> <div id="buttons">
<input type="button" value="Edytuj" id="edit" /> <input type="button" value="Edytuj" id="edit" />
<form action="/contacts/delete" method="post"> <form id="deleteButton" action="/contacts/delete" method="post">
<input type="hidden" name="id" id="remove-id" /> <input type="hidden" name="id" id="remove-id" />
<input type="submit" value="Usuń" id="remove" /> <input type="submit" value="Usuń" id="remove" />
</form> </form>
@ -67,7 +70,7 @@ customElements.define('edit-contact-info', class extends Component {
if (!info) return; if (!info) return;
form.contact_id = info.contact_id; form.contact_id = info.contact_id;
form.type = info.contact_type; form.type = info.type;
form.content = info.content; form.content = info.content;
this.mode = 'edit'; this.mode = 'edit';

View File

@ -129,6 +129,7 @@ customElements.define('account-view', class extends Component {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.register_success = (location.search || '').includes('success'); this.register_success = (location.search || '').includes('success');
localStorage.removeItem('register');
} }
get name() { get name() {

View File

@ -1,10 +1,8 @@
import { Component, FORM_STYLE } from "../shared"; import { Component, FORM_STYLE, goTo, historyDetails } from "../shared";
import '../shared/facebook-button';
import './account-view';
customElements.define('ow-account', class extends Component { customElements.define('ow-account', class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['mode', "id", "name", 'email', "facebook-id"] return ['mode']
} }
constructor() { constructor() {
@ -12,23 +10,15 @@ customElements.define('ow-account', class extends Component {
<style> <style>
:host { display: block; } :host { display: block; }
* { font-family: 'Noto Sans', sans-serif; } * { font-family: 'Noto Sans', sans-serif; }
#form > * { #switch-register, #switch-login {
display: none; display: none;
} }
:host([mode="login"]) #form > login-form, :host([mode="login"]) #switch-register { :host([mode="login"]) #switch-register {
display: block !important; display: block !important;
} }
:host([mode="register"]) #form > register-form, :host([mode="register"]) #switch-login { :host([mode="register"]) #switch-login {
display: block !important; display: block !important;
} }
account-view {
display: none;
}
:host([mode="display"]) account-view { display: block; }
:host([mode="display"]) #form { display: none; }
:host([mode="form"]) #form,
:host([mode="login"]) #form { display: block; }
a { a {
display: block; display: block;
cursor: pointer; cursor: pointer;
@ -41,9 +31,8 @@ customElements.define('ow-account', class extends Component {
${ FORM_STYLE } ${ FORM_STYLE }
</style> </style>
<article id="form"> <article>
<login-form></login-form> <slot></slot>
<register-form></register-form>
<section id="switch-register"> <section id="switch-register">
<a class="btn">Nie masz konta? Utwórz nowe</a> <a class="btn">Nie masz konta? Utwórz nowe</a>
</section> </section>
@ -51,31 +40,40 @@ customElements.define('ow-account', class extends Component {
<a class="btn">Posiadasz konto? Zaloguj się</a> <a class="btn">Posiadasz konto? Zaloguj się</a>
</section> </section>
</article> </article>
<account-view></account-view>
`); `);
this.shadowRoot.querySelector('#switch-login > a').addEventListener('click', ev => { this.shadowRoot.querySelector('#switch-login > a').addEventListener('click', ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.mode = 'login'; goTo('/login');
}); });
this.shadowRoot.querySelector('#switch-register > a').addEventListener('click', ev => { this.shadowRoot.querySelector('#switch-register > a').addEventListener('click', ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.mode = 'register'; goTo('/register/account-type');
}); });
this.addEventListener('facebook:account', ev => { this.addEventListener('facebook:account', ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.mode = 'facebook';
}); });
} }
connectedCallback() { connectedCallback() {
if (this.mode === '') this.mode = 'login'; super.connectedCallback();
this.name = this.name; const parts = historyDetails().parts;
this.email = this.email; switch (parts.first) {
this.facebook_id = this.facebook_id; case 'register': {
this.mode = 'register';
break;
}
default:
this.mode = 'login';
break;
}
}
listenHistory = ({ parts }) => {
console.warn(parts);
} }
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
@ -90,31 +88,4 @@ customElements.define('ow-account', class extends Component {
value = ['login', 'register', 'display'].includes(value) ? value : 'login'; value = ['login', 'register', 'display'].includes(value) ? value : 'login';
this.setAttribute('mode', value); this.setAttribute('mode', value);
} }
get name() {
return this.getAttribute('name') || '';
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('account-view').name = v;
}
get email() {
return this.getAttribute('email') || '';
}
set email(v) {
this.setAttribute('email', v);
this.shadowRoot.querySelector('account-view').email = v;
}
get facebook_id() {
return this.getAttribute('facebook-id');
}
set facebook_id(v) {
this.setAttribute('facebook-id', v);
this.shadowRoot.querySelector('account-view').facebook_id = v;
}
}); });

22
client/src/poly.js Normal file
View File

@ -0,0 +1,22 @@
Object.defineProperties(Object.prototype, {
'entry': {
value(key) {
const owner = this;
return {
owner, orElse(v) {
if (owner[key] === undefined) owner[key] = v;
return owner[key];
}, get() {
return owner[key];
}
}
}
}
})
Object.defineProperties(Array.prototype, {
'last': { get() { return this[this.length - 1] } },
'tail': { get() { return this[this.length - 1] } },
'first': { get() { return this[0] } },
'head': { get() { return this[0] } },
})

View File

@ -1,24 +1,11 @@
import { FORM_STYLE, Component } from "./shared.js"; import { FORM_STYLE, Component, goTo } from "./shared.js";
import { RegisterForm } from "./register-form/model.js"; import { RegisterForm } from "./register-form/model.js";
/*
<article>
<register-user-type id="step-0"> </register-user-type>
<register-basic-form id="step-1"></register-basic-form>
<register-business-form id="step-2"></register-business-form>
<register-items-form id="step-3"></register-items-form>
<register-business-contacts id="step-4"></register-business-contacts>
<register-submit-form id="step-5"></register-submit-form>
<register-user-form id="step-40"></register-user-form>
</article>
*/
customElements.define('register-form', class extends Component { customElements.define('register-form', class extends Component {
#form = new RegisterForm; #form = new RegisterForm;
static get observedAttributes() { static get observedAttributes() {
return ['step'] return ['current']
} }
constructor() { constructor() {
@ -26,13 +13,6 @@ customElements.define('register-form', class extends Component {
<style> <style>
:host { display: block; } :host { display: block; }
* { font-family: 'Noto Sans', sans-serif; } * { font-family: 'Noto Sans', sans-serif; }
[id^="step-"] { display: none; }
:host([step="0"]) #step-0 { display: block; }
:host([step="1"]) #step-1 { display: block; }
:host([step="2"]) #step-2 { display: block; }
:host([step="3"]) #step-3 { display: block; }
:host([step="4"]) #step-4 { display: block; }
:host([step="40"]) #step-40 { display: block; }
.actions { .actions {
display: flex; display: flex;
@ -58,139 +38,239 @@ customElements.define('register-form', class extends Component {
${ FORM_STYLE } ${ FORM_STYLE }
</style> </style>
<article id="host"> <article>
<slot></slot>
</article> </article>
`); `);
this.shadowRoot.addEventListener('form:next', ev => { this.shadowRoot.addEventListener('form:next', ev => {
ev.stopPropagation(); ev.stopPropagation();
this.step = this.step + 1; this.#transfer('next');
}); });
this.shadowRoot.addEventListener('form:prev', ev => { this.shadowRoot.addEventListener('form:prev', ev => {
ev.stopPropagation(); ev.stopPropagation();
this.step = this.step - 1; this.#transfer('prev');
}); });
this.shadowRoot.addEventListener('account:type', ev => { this.shadowRoot.addEventListener('account:type', ev => {
ev.stopPropagation(); ev.stopPropagation();
this.#form.account_type = ev.detail; this.#copyDetail(ev);
if (this.#form.account_type === 'User') {
this.#showUserAccountForm();
} else {
this.#showBusinessAccountForm();
}
}); });
this.addEventListener('account:basic', ev => { this.addEventListener('account:basic', ev => {
this.#copyDetail(ev) this.#copyDetail(ev);
this.#showBusinessDetailsForm();
}); });
this.addEventListener('account:business', ev => { this.addEventListener('account:business', ev => {
this.#copyDetail(ev) this.#copyDetail(ev);
}); });
this.addEventListener('account:items', ev => { this.addEventListener('account:items', ev => {
const items = Object.entries(ev.detail).reduce((items, [fieldName, value]) => { this.#copyDetail(ev);
const m = fieldName.match(/(items)\[(\d+)]\[(\w+)]/);
if (!Array.isArray(m)) return items;
const [_full, _items, n, name] = m;
items.entry(n).orElse({})[name] = value;
return items;
}, {});
this.#form.items = Array.from(Object.values(items));
}); });
this.addEventListener('account:contacts', ev => {
this.#copyDetail(ev);
});
this.#loadCache();
}
#loadCache() {
let register = {};
try {
register = JSON.parse(localStorage.getItem('register'));
} catch (e) {
}
this.#form.from(register);
} }
#copyDetail(ev) { #copyDetail(ev) {
ev.stopPropagation(); ev.stopPropagation();
console.info('ev.detail', ev.detail);
for (const [key, value] of Object.entries(ev.detail)) { for (const [key, value] of Object.entries(ev.detail)) {
this.#form[key] = value; this.#form[key] = value;
} }
localStorage.setItem('register', JSON.stringify(this.#form.payload));
} }
connectedCallback() { listenHistory = ({ parts }) => {
super.connectedCallback(); this.current = parts.last;
this.#showAccountTypeForm();
}
get step() {
const step = parseInt(this.getAttribute('step'));
return isNaN(step) ? 1 : step;
}
set step(n) {
if (n < 0) return;
this.setAttribute('step', n);
} }
#host(body) { #host(body) {
this.shadowRoot.querySelector('#host').innerHTML = body; this.innerHTML = body;
} }
#showAccountTypeForm() { #showAccountTypeForm() {
this.#host(` this.#host(`
<register-user-type> </register-user-type> <register-account-type></register-account-type>
`); `);
} }
#showUserAccountForm() { #showUserAccountForm() {
this.#host(` this.#host(`
<register-user-account-form <register-user-account-form
login="${ this.#form.login }" login="${ this.#form.login }"
email="${ this.#form.email }" email="${ this.#form.email }"
password="${ this.#form.password }" password="${ this.#form.password }"
></register-user-account-form> ></register-user-account-form>
`); `);
} }
#showBusinessAccountForm() { #showBusinessAccountForm() {
this.#host(` this.#host(`
<register-business-account-form <register-business-account-form
login="${ this.#form.login }" login="${ this.#form.login }"
email="${ this.#form.email }" email="${ this.#form.email }"
password="${ this.#form.password }" password="${ this.#form.password }"
></register-business-account-form> ></register-business-account-form>
`); `);
} }
#showBusinessDetailsForm() { #showBusinessDetailsForm() {
this.#host(` this.#host(`
<register-business-details-form <register-business-details-form
name="${ this.#form.name }" name="${ this.#form.name || '' }"
description="${ this.#form.description }" description="${ this.#form.description || '' }"
></register-business-details-form> ></register-business-details-form>
`); `);
} }
#transfer(page, direction) { #showBusinessItemsForm() {
const current = direction === 'next' this.#host(`
? this.#nextPage(page) <register-business-items-form>
: this.#prevPage(page); ${ this.#form.items.map(
if (!current) return; ({ name, price, picture_url }) => `
<register-business-item-form
name="${ name }"
price="${ price }"
picture-url="${ picture_url }"
></register-business-item-form>
`
).join('') }
</register-business-items-form>
`);
} }
#nextPage(page) { #showBusinessContactsForm() {
switch (page) { this.#host(`
<register-business-contacts-form>
${ this.#form.contacts.map(
({ type, content }) => `
<edit-contact-info
mode="view"
delete="false"
>
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).join('') }
</register-business-contacts-form>
`);
}
#showBusinessSubmitForm() {
this.#host(`
<register-business-submit-form
name="${ this.#form.name }"
description="${ this.#form.description }"
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
account-type="${ this.#form.account_type }"
>
${ this.#form.items.map(
({ name, price, picture_url }) => `
<local-business-item
slot="items"
name="${ name }"
price="${ price }"
picture-url="${ picture_url }"
></local-business-item>
`
).join('') }
${ this.#form.contacts.map(
({ type, content }) => `
<contact-info
slot="contacts"
type="${ type }"
content="${ content }"
></contact-info>
`).join('') }
</register-business-submit-form>
`);
}
#transfer(direction) {
const current = direction === 'next'
? this.#nextPage()
: this.#prevPage();
this.current = current;
goTo(`/register/${ current }`);
}
get current() {
return this.getAttribute('current');
}
set current(current) {
if (!current) return;
this.setAttribute('current', current);
switch (current) {
case "account-type":
return this.#showAccountTypeForm();
case "user-account":
return this.#showUserAccountForm();
case "business-account":
return this.#showBusinessAccountForm();
case "business-details":
return this.#showBusinessDetailsForm();
case "business-items":
return this.#showBusinessItemsForm();
case "business-contacts":
return this.#showBusinessContactsForm();
case "business-submit":
return this.#showBusinessSubmitForm();
default:
throw new Error(`Unknown page "${ current }"`);
}
}
#nextPage() {
switch (this.current) {
case "account-type": case "account-type":
return this.#form.account_type === 'User' return this.#form.account_type === 'User'
? 'user-account' ? 'user-account'
: 'business-account'; : 'business-account';
case "user-account": return null; case "user-account":
case "business-account": return 'business-details'; return null;
case "business-details": return 'business-items'; case "business-account":
case "business-items": return 'business-contact'; return 'business-details';
case "business-contact": return 'business-submit'; case "business-details":
case "business-submit": return null; return 'business-items';
case "business-items":
return 'business-contacts';
case "business-contacts":
return 'business-submit';
case "business-submit":
return null;
} }
} }
#prevPage(page) { #prevPage() {
switch (page) { switch (this.current) {
case "account-type": case "account-type":
return null; return null;
case "user-account": return 'account-type'; case "user-account":
case "business-account": return 'account-type'; return 'account-type';
case "business-details": return 'business-account'; case "business-account":
case "business-items": return 'business-details'; return 'account-type';
case "business-contact": return 'business-items'; case "business-details":
case "business-submit": return 'business-contact'; return 'business-account';
case "business-items":
return 'business-details';
case "business-contacts":
return 'business-items';
case "business-submit":
return 'business-contacts';
} }
} }
}); });

View File

@ -7,10 +7,12 @@ export class RegisterFormComponent extends PseudoForm {
return null; return null;
} }
mountFormHandler() { mountFormHandler(dispatchForm) {
if (this.#mounted) return; if (this.#mounted) return;
this.#mounted = true; this.#mounted = true;
if (!dispatchForm) dispatchForm = () => this.#dispatchForm();
const form = this.shadowRoot.querySelector('form'); const form = this.shadowRoot.querySelector('form');
form.addEventListener('submit', ev => { form.addEventListener('submit', ev => {
ev.preventDefault(); ev.preventDefault();
@ -21,7 +23,7 @@ export class RegisterFormComponent extends PseudoForm {
} }
}); });
this.addEventListener('form:next', () => { this.addEventListener('form:next', () => {
this.#dispatchForm() dispatchForm()
}); });
} }
@ -30,7 +32,6 @@ export class RegisterFormComponent extends PseudoForm {
...memo, ...memo,
[el.name]: el.value, [el.name]: el.value,
}), {}); }), {});
console.warn('#dispatchForm', detail, this.elements)
this.dispatchEvent(new CustomEvent(this.submitEventName, { composed: true, bubbles: true, detail })); this.dispatchEvent(new CustomEvent(this.submitEventName, { composed: true, bubbles: true, detail }));
} }
@ -106,25 +107,19 @@ export class RegisterForm {
} }
get items() { get items() {
return this.#items; return this.#items || [];
} }
set items(a) { set items(a) {
this.#items = a; this.#items = a;
} }
appendItem(item) {
this.#items = this.#items || [];
this.#items.push(item);
}
get contacts() { get contacts() {
return this.#contacts; return this.#contacts || [];
} }
appendContact(contact) { set contacts(c) {
this.#contacts = this.#contacts || []; this.#contacts = c;
this.#contacts.push(contact);
} }
get name() { get name() {
@ -163,4 +158,22 @@ export class RegisterForm {
return m; return m;
}, {}) }, {})
} }
from(object) {
[
'email',
'login',
'password',
'facebook_id',
'account_type',
'items',
'contacts',
'name',
'description'
].forEach(key => {
const value = object[key];
if (!value) return;
this[key] = value;
});
}
} }

View File

@ -117,14 +117,22 @@ customElements.define('register-account-type', class extends Component {
user.addEventListener('click', ev => { user.addEventListener('click', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type', { bubbles: true, composed: true, detail: 'User' })); this.dispatchEvent(new CustomEvent('account:type', {
bubbles: true,
composed: true,
detail: { account_type: 'User' }
}));
this.shadowRoot.querySelector('form-navigation').next(); this.shadowRoot.querySelector('form-navigation').next();
}); });
const service = this.shadowRoot.querySelector('#local-service'); const service = this.shadowRoot.querySelector('#local-service');
service.addEventListener('click', ev => { service.addEventListener('click', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type', { bubbles: true, composed: true, detail: 'Business' })); this.dispatchEvent(new CustomEvent('account:type', {
bubbles: true,
composed: true,
detail: { account_type: 'Business' }
}));
this.shadowRoot.querySelector('form-navigation').next(); this.shadowRoot.querySelector('form-navigation').next();
}); });
} }

View File

@ -1,22 +0,0 @@
import { RegisterFormComponent } from "./model.js";
customElements.define('register-business-contact-form', class extends RegisterFormComponent {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<article>
<form>
<form-navigation></form-navigation>
</form>
</article>
`);
this.mountFormHandler();
}
get submitEventName() {
return 'account:contacts';
}
});

View File

@ -0,0 +1,65 @@
import { BUTTON_STYLE } from "../shared.js";
import { RegisterFormComponent } from "./model.js";
customElements.define('register-business-contacts-form', class extends RegisterFormComponent {
constructor() {
super(`
<style>
:host { display: block; }
section {
display: flex;
justify-content: space-between;
}
${ BUTTON_STYLE }
</style>
<article>
<form>
<section>
<contact-info-editor
save="false"
></contact-info-editor>
<input type="button" id="addButton" value="Dodaj" />
</section>
<slot></slot>
<form-navigation></form-navigation>
</form>
</article>
`);
this.shadowRoot.querySelector('#addButton').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
const form = this.shadowRoot.querySelector('contact-info-editor');
const { type, content } = form;
this.innerHTML += `
<edit-contact-info mode="view" delete="false">
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`;
});
this.mountFormHandler(() => this.#emitChange());
}
get submitEventName() {
return 'account:contacts';
}
get #rows() {
return Array.from(this.querySelectorAll('contact-info'));
}
#emitChange() {
const rows = this.#rows;
const contacts = rows.map(({ type, content }) => ({
type,
content
}));
this.dispatchEvent(new CustomEvent(this.submitEventName, {
bubbles: true, composed: true, detail: { contacts: contacts.length ? contacts : null },
}));
}
});

View File

@ -1,9 +1,9 @@
import { FORM_STYLE } from "../shared"; import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model"; import { RegisterFormComponent } from "./model";
customElements.define('register-item-form-row', class extends RegisterFormComponent { customElements.define('register-business-item-form', class extends RegisterFormComponent {
static get observedAttributes() { static get observedAttributes() {
return ['idx', 'name', 'picture-url', 'action', 'remove', 'save'] return ['idx', 'name', 'price', 'picture-url', 'action', 'remove', 'save']
} }
constructor() { constructor() {
@ -29,23 +29,22 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
<image-input send-original="true"></image-input> <image-input send-original="true"></image-input>
<div id="name"> <div id="name">
<label>Nazwa</label> <label>Nazwa</label>
<input id="name" class="item-name" name="items[none][name]" type="text" required /> <input id="name" class="item-name" name="name" type="text" required />
</div> </div>
<div id="price"> <div id="priceWrapper">
<label>Cena</label> <label>Cena</label>
<price-input id="price" class="item-price" name="items[none][price]" required > <price-input id="price" class="item-price" name="price" required >
</price-input> </price-input>
</div> </div>
<input id="submit-button" type="submit" value="Zapisz" /> <input id="submit-button" type="submit" value="Zapisz" />
<input id="remove-button" type="submit" value="Usuń" /> <input id="remove-button" type="submit" value="Usuń" />
<input type="hidden" name="items[none][picture_url]" id="picture_url" /> <input type="hidden" name="picture_url" id="picture_url" />
<slot name="tail"></slot> <slot name="tail"></slot>
</form> </form>
</section> </section>
`); `);
const form = this.shadowRoot.querySelector('form');
const imageInput = this.shadowRoot.querySelector('image-input'); const imageInput = this.shadowRoot.querySelector('image-input');
this.addEventListener('item:removed', () => { this.addEventListener('item:removed', () => {
this.setAttribute('removed', 'removed'); this.setAttribute('removed', 'removed');
@ -58,17 +57,40 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
ev.stopPropagation(); ev.stopPropagation();
this.picture_url = imageInput.url; this.picture_url = imageInput.url;
}); });
this.shadowRoot.querySelector('form').addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
const detail = { this.shadowRoot.querySelector('form').addEventListener('change', ev => {
name: form.querySelector('.item-name').value, ev.stopPropagation();
price: form.querySelector('price-input').value, const input = ev.target;
picture_url: this.picture_url, const { name, value } = input;
item_order: this.idx,
}; switch (name) {
case 'price': {
this.price = parseInt(value);
break;
}
case 'name': {
this.name = value;
break;
}
case 'picture_url': {
this.picture_url = value;
break;
}
default: {
break;
}
}
});
this.shadowRoot.querySelector('form').addEventListener('submit', ev => {
ev.stopPropagation();
ev.preventDefault();
if (this.reportValidity()) { if (this.reportValidity()) {
const detail = {
name: this.name,
price: this.price,
picture_url: this.picture_url,
item_order: this.idx,
};
this.dispatchEvent(new CustomEvent('item:submit', { bubbles: true, composed: true, detail })); this.dispatchEvent(new CustomEvent('item:submit', { bubbles: true, composed: true, detail }));
} }
}); });
@ -77,21 +99,13 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
ev.stopPropagation(); ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false })); this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false }));
}); });
} }
get submitEventName() { get submitEventName() {
return 'account:items'; return 'account:items';
} }
connectedCallback() {
super.connectedCallback();
this.#updateNames(this.idx);
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
static attr2Field(name) { static attr2Field(name) {
const field = super.attr2Field(name); const field = super.attr2Field(name);
if (field === 'remove') return 'showRemove'; if (field === 'remove') return 'showRemove';
@ -99,30 +113,6 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
return field; return field;
} }
get inputs() {
return this.#inputs.map(extract);
}
get elements() {
console.warn('item form elements', this.#inputs)
return this.#inputs
}
get #inputs() {
return [
this.shadowRoot.querySelector('.item-name'),
this.shadowRoot.querySelector('.item-price'),
this.shadowRoot.querySelector('#picture_url'),
];
}
#updateNames() {
const idx = this.idx;
for (const el of this.#inputs) {
el.setAttribute('name', `items[${ idx }][${ el.id }]`);
}
}
get idx() { get idx() {
const idx = parseInt(this.getAttribute('idx')); const idx = parseInt(this.getAttribute('idx'));
return isNaN(idx) ? null : idx; return isNaN(idx) ? null : idx;
@ -130,7 +120,6 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
set idx(idx) { set idx(idx) {
this.setAttribute('idx', idx); this.setAttribute('idx', idx);
this.#updateNames(idx);
} }
get name() { get name() {
@ -143,12 +132,14 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
} }
get price() { get price() {
return this.getAttribute('name'); return this.getAttribute('price');
} }
set price(v) { set price(v) {
v = parseInt(v);
console.info('set price', v);
this.setAttribute('price', v); this.setAttribute('price', v);
this.shadowRoot.querySelector('.item-price').value = v; this.shadowRoot.querySelector('#price').value = v / 100.0;
} }
get picture_url() { get picture_url() {

View File

@ -1,9 +1,9 @@
import { FORM_STYLE } from "../shared"; import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model"; import { RegisterFormComponent } from "./model";
const updateItems = (form) => { const updateItems = (rows) => {
let idx = 0; let idx = 0;
for (const el of form.querySelectorAll('register-item-form-row')) { for (const el of rows) {
el.idx = idx++; el.idx = idx++;
} }
return idx; return idx;
@ -20,6 +20,10 @@ customElements.define('register-business-items-form', class extends RegisterForm
display: flex; display: flex;
} }
::slotted(register-business-item-form) {
margin-bottom: 18px;
}
${ FORM_STYLE } ${ FORM_STYLE }
</style> </style>
<form> <form>
@ -35,11 +39,11 @@ customElements.define('register-business-items-form', class extends RegisterForm
`); `);
this.addEventListener('item:removed', ev => { this.addEventListener('item:removed', ev => {
ev.stopPropagation(); ev.stopPropagation();
updateItems(this) updateItems(this.#rows)
}); });
this.addEventListener('form:next', ev => { this.addEventListener('form:next', ev => {
updateItems(this); updateItems(this.#rows);
for (const el of this.querySelectorAll('item-form-row')) { for (const el of this.#rows) {
if (!el.reportValidity()) { if (!el.reportValidity()) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -49,17 +53,25 @@ customElements.define('register-business-items-form', class extends RegisterForm
this.shadowRoot.querySelector('#add-item').addEventListener('click', ev => { this.shadowRoot.querySelector('#add-item').addEventListener('click', ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.appendChild(document.createElement('register-item-form-row')); this.appendChild(document.createElement('register-business-item-form'));
updateItems(this); updateItems(this.#rows);
}); });
this.mountFormHandler(); this.mountFormHandler(() => this.#emitChange());
} }
get submitEventName() { get submitEventName() {
return 'account:items'; return 'account:items';
} }
get elements() { get #rows() {
return Array.from(this.querySelectorAll("register-item-form-row")).map(form => form.elements).flat(); return Array.from(this.querySelectorAll("register-business-item-form"));
}
#emitChange() {
const rows = this.#rows;
console.warn(rows);
const items = this.#rows.map(({ price, picture_url, name }) => ({ price, picture_url, name }));
console.warn(items);
this.dispatchEvent(new CustomEvent(this.submitEventName, { bubbles: true, composed: true, detail: { items } }));
} }
}); });

View File

@ -1,8 +1,8 @@
import { FORM_STYLE, PseudoForm } from "../shared"; import { FORM_STYLE, goTo, PseudoForm } from "../shared.js";
customElements.define('register-business-submit-form', class extends PseudoForm { customElements.define('register-business-submit-form', class extends PseudoForm {
static get observedAttributes() { static get observedAttributes() {
return ['name', 'description', 'login', 'email', 'password', 'account_type'] return ['name', 'description', 'login', 'email', 'password', 'account-type']
} }
constructor() { constructor() {
@ -24,35 +24,36 @@ customElements.define('register-business-submit-form', class extends PseudoForm
} }
</style> </style>
<form id="step-4" method="post" action="/register"> <form id="step-4" method="post" action="/register">
<div id="copied">
<input id="hidden-login" name="login" type="hidden" />
<input id="hidden-email" name="email" type="hidden" />
<input id="hidden-password" name="password" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<input id="hidden-description" name="description" type="hidden" />
</div>
<div> <div>
<label>Login</label> <label>Login</label>
<input readonly id="preview-login"> <input readonly id="login">
</div> </div>
<div> <div>
<label>Email</label> <label>Email</label>
<input readonly id="preview-email"> <input readonly id="email">
</div> </div>
<div> <div>
<label>Password</label> <label>Password</label>
<input readonly id="preview-pass" type="password"> <input readonly id="password" type="password">
</div> </div>
<div> <div>
<label>Name</label> <label>Name</label>
<input readonly id="preview-name"> <input readonly id="name">
</div> </div>
<div> <div>
<label>Description</label> <label>Description</label>
<input readonly id="preview-description"> <input readonly id="description">
</div> </div>
<input type="hidden" name="account_type" id="account_type" /> <input type="hidden" name="account_type" id="account_type" value="Business" />
<div id="contacts">
<h3>Dane kontaktowe</h3>
<slot name="contacts"></slot>
</div>
<div id="items"> <div id="items">
<h3>Produkty/usługi</h3>
<slot name="items"></slot>
</div> </div>
<div class="actions"> <div class="actions">
@ -61,32 +62,25 @@ customElements.define('register-business-submit-form', class extends PseudoForm
</div> </div>
</form> </form>
`); `);
} this.shadowRoot.querySelector('form').addEventListener('submit', async (ev) => {
ev.stopPropagation();
ev.preventDefault();
const res = await fetch('/register', {
method: 'POST',
body: JSON.stringify(this.#form),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
updateField(name, value) { if (res.ok) {
this.shadowRoot.querySelector(`[id="hidden-${ name }"]`).value = value; goTo("/account?success");
this.shadowRoot.querySelector(`[id="preview-${ name }"]`).value = value; } else {
} const { error } = await res.json();
document.querySelector('error-message').message = error;
set items(items) { }
const host = this.shadowRoot.querySelector('#items'); });
host.innerHTML = ``;
for (const row of items) {
const el = host.appendChild(document.createElement('div'));
el.className = 'item-view';
const [name, price, img] = row;
el.innerHTML = `
${ img.value !== ''
? `<img src="${img.value}" alt="" />`
: `<span>Brak zdjęcia</span>`
}
<input type="text" name="${ name.name }" value="${ name.value }" readonly />
<input type="hidden" name="${ price.name }" value="${ price.value }" readonly />
<input type="hidden" name="${ img.name }" value="${ img.value }" readonly />
<price-view value="${ price.value }"></price-view>
`;
}
} }
set account_type(v) { set account_type(v) {
@ -96,35 +90,70 @@ customElements.define('register-business-submit-form', class extends PseudoForm
get name() { get name() {
return this.#hidden('name').value return this.#hidden('name').value
} }
set name(v) { set name(v) {
this.#hidden('name').value = v; this.#hidden('name').value = v;
} }
get description() { get description() {
return this.#hidden('description').value return this.#hidden('description').value
} }
set description(v) { set description(v) {
this.#hidden('description').value = v; this.#hidden('description').value = v;
} }
get login() { get login() {
return this.#hidden('login').value return this.#hidden('login').value
} }
set login(v) { set login(v) {
this.#hidden('login').value = v; this.#hidden('login').value = v;
} }
get email() { get email() {
return this.#hidden('email').value return this.#hidden('email').value
} }
set email(v) { set email(v) {
this.#hidden('email').value = v; this.#hidden('email').value = v;
} }
get password() { get password() {
return this.#hidden('password').value return this.#hidden('password').value
} }
set password(v) { set password(v) {
this.#hidden('password').value = v; this.#hidden('password').value = v;
} }
#hidden(selector) { #hidden(selector) {
return this.shadowRoot.querySelector(`#hidden-${selector}`) return this.shadowRoot.querySelector(`#${ selector }`)
}
get #form() {
return [
'login',
'email',
'password',
'name',
'description',
'account_type',
].reduce((memo, name) => ({
...memo,
[name]: this.#hidden(name).value,
}), {
items: Array.from(this.querySelectorAll('local-business-item')).map(({ name, price, picture_url }) => ({
name, price, picture_url
})),
contacts: Array.from(this.querySelectorAll('contact-info')).map(({ content, type }) => ({
content,
type
})),
});
}
get payload() {
return this.#form
} }
}); });

View File

@ -168,11 +168,20 @@ export class Component extends HTMLElement {
if (!field) continue; if (!field) continue;
this.#setFieldValue(field, this[field]); this.#setFieldValue(field, this[field]);
} }
{
const listener = this.listenHistory;
listener && listener(historyDetails());
this.#dropHistory = subscribeHistory(listener);
}
} }
disconnectedCallback() { disconnectedCallback() {
if (this.#dropHistory) this.#dropHistory();
} }
#dropHistory;
listenHistory = null
attributeChangedCallback(name, oldV, newV) { attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) if (oldV === newV)
return; return;
@ -279,3 +288,23 @@ export const runFbReady = (fn) => {
}; };
const fbQueue = []; const fbQueue = [];
let fbReady = false; let fbReady = false;
const historyQueue = new Set;
export const goTo = (url) => {
history.pushState({}, document.title, url);
for (const call of historyQueue) call();
document.dispatchEvent(new CustomEvent('history:push', { composed: true, bubbles: true, details: historyDetails() }));
}
export const historyDetails = () => {
const parts = location.pathname.split('/').filter(s => s && s.length)
return { parts }
}
const subscribeHistory = (cb) => {
if (cb) {
const call = () => {
cb(historyDetails());
}
historyQueue.add(call);
return () => historyQueue.delete(call);
}
}

View File

@ -0,0 +1,33 @@
import { Component } from "../shared.js";
customElements.define('error-message', class extends Component {
static get observedAttributes() {
return ['message'];
}
constructor() {
super(`
<style>
:host { display: block; }
div {
width: 1280px;
background: #ffe0e0;
border: 1px solid var(--red-color);
margin: 8px auto auto;
padding: 8px;
color: var(--red-color);
}
</style>
<div></div>
`);
}
get message() {
return this.getAttribute('message');
}
set message(m) {
this.setAttribute('message', m);
this.shadowRoot.querySelector('div').textContent = m;
}
})

View File

@ -188,6 +188,11 @@ customElements.define('image-input', class extends Component {
composed: true, composed: true,
detail: path detail: path
})); }));
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: path
}));
}); });
} }

View File

@ -15,11 +15,5 @@
{%- endif %} {%- endif %}
</account-view> </account-view>
{% when None %} {% when None %}
<ow-account
mode="form"
page="{{register_page.as_str()}}"
>
</ow-account>
{% endmatch %} {% endmatch %}
{% endblock %} {% endblock %}

View File

@ -14,8 +14,9 @@
<main> <main>
{% match error %} {% match error %}
{% when Some with (e) %} {% when Some with (e) %}
<p class="error">{{e}}</p> <error-message message="{{e}"></error-message>
{% when None %} {% when None %}
<error-message></error-message>
{% endmatch %} {% endmatch %}
<article> <article>
{% include "nav.html" %} {% include "nav.html" %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
<ow-account>
<register-form
page="{{register_page.as_str()}}"
>
</register-form>
</ow-account>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<ow-account>
<login-form></login-form>
</ow-account>
{% endblock %}

View File

@ -204,6 +204,7 @@ pub struct DeleteNewsArticleInput {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateContactInfoInput { pub struct CreateContactInfoInput {
#[serde(rename = "type")]
pub contact_type: String, pub contact_type: String,
pub content: String, pub content: String,
} }

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use tracing::error; use tracing::error;
use crate::model::db::AccountType;
use crate::model::view::Page; use crate::model::view::Page;
use crate::model::{db, view}; use crate::model::{db, view};
use crate::routes::unrestricted::IndexTemplate; use crate::routes::unrestricted::IndexTemplate;
@ -15,198 +16,6 @@ use crate::routes::{HttpResult, Identity};
use crate::view::Helper; use crate::view::Helper;
use crate::{not_xss, queries, routes, utils}; use crate::{not_xss, queries, routes, utils};
#[derive(Default, Serialize, Template)]
#[template(path = "account.html")]
struct AccountTemplate {
account: Option<db::Account>,
error: Option<String>,
#[serde(skip)]
page: Page,
h: Helper,
register_page: RegisterPage,
}
impl AccountTemplate {
pub fn error<Error: Into<String>>(error: Error, page: Page) -> Self {
AccountTemplate {
error: Some(error.into()),
page,
..Default::default()
}
}
pub fn bad_request<Error: Into<String>>(
req: &HttpRequest,
error: Error,
page: Page,
) -> HttpResult {
HttpResult::res(
req,
StatusCode::BAD_REQUEST,
AccountTemplate::error(error, page),
)
}
}
#[post("/register")]
#[tracing::instrument]
async fn register(
req: HttpRequest,
form: web::Form<RegisterForm>,
db: Data<PgPool>,
id: Identity,
) -> HttpResult {
let mut form = form.into_inner();
dbg!(&form);
process_items(form.items.get_or_insert_default(), form.names);
let pool = db.into_inner();
if form.account_type == db::AccountType::Admin {
return HttpResult::err(&req, routes::Error::XSS);
}
let mut t = pool.begin().await.unwrap();
let pass = match utils::encrypt(&form.password) {
Ok(pass) => pass,
Err(e) => {
error!("{:?}", e);
dbg!(e);
t.rollback().await.unwrap();
return AccountTemplate::bad_request(
&req,
"Zapisanie hasła nie powiodło się",
Page::Register,
);
}
};
let res = queries::create_account(
&mut t,
db::CreateAccountInput {
login: form.login,
email: form.email,
pass,
facebook_id: form.facebook_id,
account_type: form.account_type,
},
)
.await;
let account = match res {
Ok(res) => {
id.remember(format!("{}", res.id));
res
}
Err(queries::Error::AccountTaken { .. }) => {
return AccountTemplate::bad_request(&req, "Adres e-mail jest zajęty", Page::Register);
}
Err(e) => {
dbg!(e);
t.rollback().await.unwrap();
return AccountTemplate::bad_request(
&req,
"Problem z utworzeniem konta",
Page::Register,
);
}
};
if matches!(form.account_type, db::AccountType::Business) {
let name = form.name.as_deref().unwrap_or_default();
let owner_id = account.id;
let description = form.description.as_deref().unwrap_or_default();
not_xss!(&req, name, t);
not_xss!(&req, description, t);
let res =
queries::create_local_business(&mut t, name.into(), owner_id, description.into()).await;
let business = match res {
Ok(business) => business,
Err(e) => {
dbg!(e);
t.rollback().await.unwrap();
return HttpResult::res(
&req,
StatusCode::BAD_REQUEST,
AccountTemplate {
error: Some("Problem z utworzeniem konta".into()),
page: Page::Register,
..Default::default()
},
);
}
};
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
not_xss!(&req, &item.name, t);
not_xss!(&req, &item.picture_url, t);
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, local_business_id, name, price, item_order, picture_url
"#,
)
.bind(business.id)
.bind(&item.name)
.bind(item.price as i32)
.bind(idx as i32)
.bind(if item.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
let name = item.picture_url.split('/').last().unwrap_or_default();
let dir = utils::item_picture_write_dir(format!("{}", account.id));
if let Err(e) = std::fs::create_dir_all(&dir) {
error!("{e} {:?}", dir);
dbg!(e);
t.rollback().await.unwrap();
return AccountTemplate::bad_request(
&req,
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.",
Page::Register,
);
}
let path = dir.join(name);
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
error!("{e} {:?}", item.picture_url);
dbg!(e);
t.rollback().await.unwrap();
return AccountTemplate::bad_request(
&req,
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.",
Page::Register,
);
}
let path = path.to_str().map(String::from).unwrap_or_default();
path.strip_prefix('.')
.map(String::from)
.unwrap_or_else(|| path)
})
.fetch_one(&mut t)
.await;
match res {
Ok(_) => {}
Err(e) => {
tracing::error!("{e}");
dbg!(e);
t.rollback().await.unwrap();
return AccountTemplate::bad_request(
&req,
"Problem z utworzeniem konta",
Page::Register,
);
}
}
}
}
t.commit().await.unwrap();
HttpResult::goto(&req, "/account?success")
}
#[post("/logout")] #[post("/logout")]
#[tracing::instrument] #[tracing::instrument]
async fn logout(id: Identity) -> HttpResponse { async fn logout(id: Identity) -> HttpResponse {
@ -223,15 +32,38 @@ async fn logout(id: Identity) -> HttpResponse {
) )
} }
#[derive(Debug, Default, Serialize, Template)]
#[template(path = "sign-in.html")]
struct SignInTemplate {
account: Option<db::Account>,
error: Option<String>,
#[serde(skip)]
page: Page,
h: Helper,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct LoginForm { struct LoginForm {
email: String, email: String,
password: String, password: String,
} }
#[get("/login")]
#[tracing::instrument]
async fn perform_sign_in(req: HttpRequest, id: Identity) -> HttpResult {
HttpResult::res(
&req,
StatusCode::OK,
SignInTemplate {
page: Page::Login,
..Default::default()
},
)
}
#[post("/login")] #[post("/login")]
#[tracing::instrument] #[tracing::instrument]
async fn login( async fn display_sign_in_form(
req: HttpRequest, req: HttpRequest,
form: web::Form<LoginForm>, form: web::Form<LoginForm>,
db: Data<PgPool>, db: Data<PgPool>,
@ -273,33 +105,45 @@ async fn login(
) )
} }
#[post("/upload")] #[derive(Default, Serialize, Template)]
async fn upload( #[template(path = "account.html")]
req: HttpRequest, struct AccountTemplate {
payload: actix_multipart::Multipart, account: Option<db::Account>,
db: Data<PgPool>, error: Option<String>,
id: Identity, #[serde(skip)]
) -> HttpResult { page: Page,
let pool = db.into_inner(); h: Helper,
let mut t = crate::ok_or_internal!(&req, pool.begin().await); register_page: RegisterPage,
let id = match id.identity() { }
Some(id) => queries::account_by_id(&mut t, id).await.map(|a| a.id),
_ => None, impl AccountTemplate {
}; pub fn error<Error: Into<String>>(error: Error, page: Page) -> Self {
t.commit().await.ok(); Self {
routes::uploads::hande_upload(&req, payload, id, "accounts").await error: Some(error.into()),
page,
..Default::default()
}
}
pub fn bad_request<Error: Into<String>>(
req: &HttpRequest,
error: Error,
page: Page,
) -> HttpResult {
HttpResult::res(req, StatusCode::BAD_REQUEST, Self::error(error, page))
}
} }
#[get("/account")] #[get("/account")]
#[tracing::instrument] #[tracing::instrument]
async fn account_page(req: HttpRequest, id: Identity, db: Data<PgPool>) -> HttpResult { async fn display_account_info(req: HttpRequest, id: Identity, db: Data<PgPool>) -> HttpResult {
let pool = db.into_inner(); let pool = db.into_inner();
let mut t = crate::ok_or_internal!(&req, pool.begin().await); let mut t = crate::ok_or_internal!(&req, pool.begin().await);
let account = match id.identity() { let account = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await, Some(id) => queries::account_by_id(&mut t, id).await,
_ => { _ => {
id.forget(); id.forget();
None return HttpResult::goto(&req, "/login");
} }
}; };
t.commit().await.ok(); t.commit().await.ok();
@ -314,31 +158,67 @@ async fn account_page(req: HttpRequest, id: Identity, db: Data<PgPool>) -> HttpR
) )
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Default, Serialize, Template)]
#[serde(rename_all = "snake_case")] #[template(path = "register.html")]
enum RegisterPage { struct RegisterTemplate {
#[default] account: Option<db::Account>,
UserType = 0, error: Option<String>,
Basic = 1, #[serde(skip)]
Business = 2, page: Page,
Items = 3, h: Helper,
Contact = 4, register_page: RegisterPage,
Submit = 5,
User = 40,
} }
impl Iterator for RegisterPage { impl RegisterTemplate {
type Item = Self; pub fn error<Error: Into<String>>(error: Error, page: Page) -> Self {
Self {
error: Some(error.into()),
page,
..Default::default()
}
}
pub fn bad_request<Error: Into<String>>(
req: &HttpRequest,
error: Error,
page: Page,
) -> HttpResult {
HttpResult::res(req, StatusCode::BAD_REQUEST, Self::error(error, page))
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum RegisterPage {
#[default]
AccountType,
UserAccount,
BusinessAccount,
BusinessDetails,
BusinessItems,
BusinessContacts,
BusinessSubmit,
}
struct RegisterIterator(RegisterPage, AccountType);
impl Iterator for RegisterIterator {
type Item = RegisterPage;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self.0 {
RegisterPage::UserType => Some(RegisterPage::Basic), RegisterPage::AccountType if self.1 == AccountType::Business => {
RegisterPage::Basic => Some(RegisterPage::Business), Some(RegisterPage::BusinessAccount)
RegisterPage::Business => Some(RegisterPage::Items), }
RegisterPage::Items => Some(RegisterPage::Contact), RegisterPage::AccountType => Some(RegisterPage::UserAccount),
RegisterPage::Contact => Some(RegisterPage::Submit),
RegisterPage::Submit => Some(RegisterPage::User), RegisterPage::UserAccount => None,
RegisterPage::User => None,
RegisterPage::BusinessAccount => Some(RegisterPage::BusinessDetails),
RegisterPage::BusinessDetails => Some(RegisterPage::BusinessItems),
RegisterPage::BusinessItems => Some(RegisterPage::BusinessContacts),
RegisterPage::BusinessContacts => Some(RegisterPage::BusinessSubmit),
RegisterPage::BusinessSubmit => None,
} }
} }
} }
@ -346,20 +226,175 @@ impl Iterator for RegisterPage {
impl RegisterPage { impl RegisterPage {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
RegisterPage::UserType => "user-type", RegisterPage::AccountType => "account-type",
RegisterPage::User => "user-account", RegisterPage::UserAccount => "user-account",
RegisterPage::Basic => "business-account", RegisterPage::BusinessAccount => "business-account",
RegisterPage::Business => "business-details", RegisterPage::BusinessDetails => "business-details",
RegisterPage::Items => "items", RegisterPage::BusinessItems => "business-items",
RegisterPage::Submit => "submit", RegisterPage::BusinessContacts => "business-contacts",
RegisterPage::Contact => "contact", RegisterPage::BusinessSubmit => "business-submit",
} }
} }
} }
#[post("/register")]
#[tracing::instrument]
async fn save_account_details(
req: HttpRequest,
form: web::Json<RegisterForm>,
db: Data<PgPool>,
id: Identity,
) -> HttpResult {
let mut form = form.into_inner();
dbg!(&form);
process_items(form.items.get_or_insert_default(), form.names);
let pool = db.into_inner();
if form.account_type == db::AccountType::Admin {
return HttpResult::err(&req, routes::Error::XSS);
}
let mut t = pool.begin().await.unwrap();
let pass = match utils::encrypt(&form.password) {
Ok(pass) => pass,
Err(e) => {
error!("{:?}", e);
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Zapisanie hasła nie powiodło się",
Page::Register,
);
}
};
let res = queries::create_account(
&mut t,
db::CreateAccountInput {
login: form.login,
email: form.email,
pass,
facebook_id: form.facebook_id,
account_type: form.account_type,
},
)
.await;
let account = match res {
Ok(res) => {
id.remember(format!("{}", res.id));
res
}
Err(queries::Error::AccountTaken { .. }) => {
return RegisterTemplate::bad_request(&req, "Adres e-mail jest zajęty", Page::Register);
}
Err(e) => {
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem konta",
Page::Register,
);
}
};
if matches!(form.account_type, db::AccountType::Business) {
let name = form.name.as_deref().unwrap_or_default();
let owner_id = account.id;
let description = form.description.as_deref().unwrap_or_default();
not_xss!(&req, name, t);
not_xss!(&req, description, t);
let res =
queries::create_local_business(&mut t, name.into(), owner_id, description.into()).await;
let business = match res {
Ok(business) => business,
Err(e) => {
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem konta",
Page::Register,
);
}
};
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
not_xss!(&req, &item.name, t);
not_xss!(&req, &item.picture_url, t);
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, local_business_id, name, price, item_order, picture_url
"#,
)
.bind(business.id)
.bind(&item.name)
.bind(item.price as i32)
.bind(idx as i32)
.bind(if item.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
let name = item.picture_url.split('/').last().unwrap_or_default();
let dir = utils::item_picture_write_dir(format!("{}", account.id));
if let Err(e) = std::fs::create_dir_all(&dir) {
error!("{e} {:?}", dir);
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.",
Page::Register,
);
}
let path = dir.join(name);
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
error!("{e} {:?}", item.picture_url);
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.",
Page::Register,
);
}
let path = path.to_str().map(String::from).unwrap_or_default();
path.strip_prefix('.')
.map(String::from)
.unwrap_or_else(|| path)
})
.fetch_one(&mut t)
.await;
match res {
Ok(_) => {}
Err(e) => {
error!("{e}");
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem konta",
Page::Register,
);
}
}
}
}
t.commit().await.unwrap();
HttpResult::goto(&req, "/account?success")
}
#[get("/register/{step}")] #[get("/register/{step}")]
#[tracing::instrument] #[tracing::instrument]
async fn register_page( async fn display_register_page(
req: HttpRequest, req: HttpRequest,
id: Identity, id: Identity,
db: Data<PgPool>, db: Data<PgPool>,
@ -374,13 +409,16 @@ async fn register_page(
None None
} }
}; };
if account.is_some() {
return HttpResult::goto(&req, "/account");
}
let page = path.into_inner().0; let page = path.into_inner().0;
t.commit().await.ok(); t.commit().await.ok();
HttpResult::res( HttpResult::res(
&req, &req,
StatusCode::OK, StatusCode::OK,
AccountTemplate { RegisterTemplate {
account, account,
page: Page::Account, page: Page::Account,
register_page: page, register_page: page,
@ -451,14 +489,32 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
} }
} }
#[post("/upload")]
async fn upload(
req: HttpRequest,
payload: actix_multipart::Multipart,
db: Data<PgPool>,
id: Identity,
) -> HttpResult {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(&req, pool.begin().await);
let id = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await.map(|a| a.id),
_ => None,
};
t.commit().await.ok();
routes::uploads::hande_upload(&req, payload, id, "accounts").await
}
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
config config
.service(register) .service(display_account_info)
.service(logout) .service(perform_sign_in)
.service(login) .service(display_sign_in_form)
.service(save_account_details)
.service(display_register_page)
.service(upload) .service(upload)
.service(account_page) .service(logout);
.service(register_page);
} }
#[cfg(test)] #[cfg(test)]