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/form-navigation.js";
import "./shared/image-popup.js";
@ -8,6 +12,7 @@ import "./shared/price/price-input.js";
import "./shared/price/price-view.js";
import "./ow-account/ow-account.js";
import "./ow-account/account-view.js";
import "./local-businesses/local-businesses.js";
import "./local-businesses/local-business-item.js";
@ -16,12 +21,12 @@ import "./local-businesses/local-business.js";
import "./login-form.js";
import "./register-form.js";
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-details-form.js";
import "./register-form/register-account-type.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 "./business-items/business-item.js";
@ -64,15 +69,3 @@ if (!document.querySelector('#facebook-jssdk')) {
js.src = "https://connect.facebook.net/en_US/sdk.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 "../register-form/register-item-form-row";
import "../register-form/register-business-item-form";
customElements.define('business-item-editor', class extends Component {
#idx;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import { Component, FORM_STYLE } from "../shared";
import '../shared/facebook-button';
import './account-view';
import { Component, FORM_STYLE, goTo, historyDetails } from "../shared";
customElements.define('ow-account', class extends Component {
static get observedAttributes() {
return ['mode', "id", "name", 'email', "facebook-id"]
return ['mode']
}
constructor() {
@ -12,23 +10,15 @@ customElements.define('ow-account', class extends Component {
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#form > * {
#switch-register, #switch-login {
display: none;
}
:host([mode="login"]) #form > login-form, :host([mode="login"]) #switch-register {
:host([mode="login"]) #switch-register {
display: block !important;
}
:host([mode="register"]) #form > register-form, :host([mode="register"]) #switch-login {
:host([mode="register"]) #switch-login {
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 {
display: block;
cursor: pointer;
@ -41,9 +31,8 @@ customElements.define('ow-account', class extends Component {
${ FORM_STYLE }
</style>
<article id="form">
<login-form></login-form>
<register-form></register-form>
<article>
<slot></slot>
<section id="switch-register">
<a class="btn">Nie masz konta? Utwórz nowe</a>
</section>
@ -51,31 +40,40 @@ customElements.define('ow-account', class extends Component {
<a class="btn">Posiadasz konto? Zaloguj się</a>
</section>
</article>
<account-view></account-view>
`);
this.shadowRoot.querySelector('#switch-login > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'login';
goTo('/login');
});
this.shadowRoot.querySelector('#switch-register > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'register';
goTo('/register/account-type');
});
this.addEventListener('facebook:account', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'facebook';
});
}
connectedCallback() {
if (this.mode === '') this.mode = 'login';
this.name = this.name;
this.email = this.email;
this.facebook_id = this.facebook_id;
super.connectedCallback();
const parts = historyDetails().parts;
switch (parts.first) {
case 'register': {
this.mode = 'register';
break;
}
default:
this.mode = 'login';
break;
}
}
listenHistory = ({ parts }) => {
console.warn(parts);
}
attributeChangedCallback(name, oldV, newV) {
@ -90,31 +88,4 @@ customElements.define('ow-account', class extends Component {
value = ['login', 'register', 'display'].includes(value) ? value : 'login';
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 { 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>
*/
import { FORM_STYLE, Component, goTo } from "./shared.js";
import { RegisterForm } from "./register-form/model.js";
customElements.define('register-form', class extends Component {
#form = new RegisterForm;
static get observedAttributes() {
return ['step']
return ['current']
}
constructor() {
@ -26,13 +13,6 @@ customElements.define('register-form', class extends Component {
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
[id^="step-"] { display: none; }
:host([step="0"]) #step-0 { display: block; }
:host([step="1"]) #step-1 { display: block; }
:host([step="2"]) #step-2 { display: block; }
:host([step="3"]) #step-3 { display: block; }
:host([step="4"]) #step-4 { display: block; }
:host([step="40"]) #step-40 { display: block; }
.actions {
display: flex;
@ -58,139 +38,239 @@ customElements.define('register-form', class extends Component {
${ FORM_STYLE }
</style>
<article id="host">
<article>
<slot></slot>
</article>
`);
this.shadowRoot.addEventListener('form:next', ev => {
ev.stopPropagation();
this.step = this.step + 1;
this.#transfer('next');
});
this.shadowRoot.addEventListener('form:prev', ev => {
ev.stopPropagation();
this.step = this.step - 1;
this.#transfer('prev');
});
this.shadowRoot.addEventListener('account:type', ev => {
ev.stopPropagation();
this.#form.account_type = ev.detail;
if (this.#form.account_type === 'User') {
this.#showUserAccountForm();
} else {
this.#showBusinessAccountForm();
}
this.#copyDetail(ev);
});
this.addEventListener('account:basic', ev => {
this.#copyDetail(ev)
this.#showBusinessDetailsForm();
this.#copyDetail(ev);
});
this.addEventListener('account:business', ev => {
this.#copyDetail(ev)
this.#copyDetail(ev);
});
this.addEventListener('account:items', ev => {
const items = Object.entries(ev.detail).reduce((items, [fieldName, value]) => {
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.#copyDetail(ev);
});
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) {
ev.stopPropagation();
console.info('ev.detail', ev.detail);
for (const [key, value] of Object.entries(ev.detail)) {
this.#form[key] = value;
}
localStorage.setItem('register', JSON.stringify(this.#form.payload));
}
connectedCallback() {
super.connectedCallback();
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);
listenHistory = ({ parts }) => {
this.current = parts.last;
}
#host(body) {
this.shadowRoot.querySelector('#host').innerHTML = body;
this.innerHTML = body;
}
#showAccountTypeForm() {
this.#host(`
<register-user-type> </register-user-type>
<register-account-type></register-account-type>
`);
}
#showUserAccountForm() {
this.#host(`
<register-user-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-user-account-form>
<register-user-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-user-account-form>
`);
}
#showBusinessAccountForm() {
this.#host(`
<register-business-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-business-account-form>
<register-business-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-business-account-form>
`);
}
#showBusinessDetailsForm() {
this.#host(`
<register-business-details-form
name="${ this.#form.name }"
description="${ this.#form.description }"
></register-business-details-form>
<register-business-details-form
name="${ this.#form.name || '' }"
description="${ this.#form.description || '' }"
></register-business-details-form>
`);
}
#transfer(page, direction) {
const current = direction === 'next'
? this.#nextPage(page)
: this.#prevPage(page);
if (!current) return;
#showBusinessItemsForm() {
this.#host(`
<register-business-items-form>
${ this.#form.items.map(
({ 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) {
switch (page) {
#showBusinessContactsForm() {
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":
return this.#form.account_type === 'User'
? 'user-account'
: 'business-account';
case "user-account": return null;
case "business-account": return 'business-details';
case "business-details": return 'business-items';
case "business-items": return 'business-contact';
case "business-contact": return 'business-submit';
case "business-submit": return null;
case "user-account":
return null;
case "business-account":
return 'business-details';
case "business-details":
return 'business-items';
case "business-items":
return 'business-contacts';
case "business-contacts":
return 'business-submit';
case "business-submit":
return null;
}
}
#prevPage(page) {
switch (page) {
#prevPage() {
switch (this.current) {
case "account-type":
return null;
case "user-account": return 'account-type';
case "business-account": return 'account-type';
case "business-details": return 'business-account';
case "business-items": return 'business-details';
case "business-contact": return 'business-items';
case "business-submit": return 'business-contact';
case "user-account":
return 'account-type';
case "business-account":
return 'account-type';
case "business-details":
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;
}
mountFormHandler() {
mountFormHandler(dispatchForm) {
if (this.#mounted) return;
this.#mounted = true;
if (!dispatchForm) dispatchForm = () => this.#dispatchForm();
const form = this.shadowRoot.querySelector('form');
form.addEventListener('submit', ev => {
ev.preventDefault();
@ -21,7 +23,7 @@ export class RegisterFormComponent extends PseudoForm {
}
});
this.addEventListener('form:next', () => {
this.#dispatchForm()
dispatchForm()
});
}
@ -30,7 +32,6 @@ export class RegisterFormComponent extends PseudoForm {
...memo,
[el.name]: el.value,
}), {});
console.warn('#dispatchForm', detail, this.elements)
this.dispatchEvent(new CustomEvent(this.submitEventName, { composed: true, bubbles: true, detail }));
}
@ -106,25 +107,19 @@ export class RegisterForm {
}
get items() {
return this.#items;
return this.#items || [];
}
set items(a) {
this.#items = a;
}
appendItem(item) {
this.#items = this.#items || [];
this.#items.push(item);
}
get contacts() {
return this.#contacts;
return this.#contacts || [];
}
appendContact(contact) {
this.#contacts = this.#contacts || [];
this.#contacts.push(contact);
set contacts(c) {
this.#contacts = c;
}
get name() {
@ -163,4 +158,22 @@ export class RegisterForm {
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 => {
ev.preventDefault();
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();
});
const service = this.shadowRoot.querySelector('#local-service');
service.addEventListener('click', ev => {
ev.preventDefault();
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();
});
}

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 { RegisterFormComponent } from "./model";
customElements.define('register-item-form-row', class extends RegisterFormComponent {
customElements.define('register-business-item-form', class extends RegisterFormComponent {
static get observedAttributes() {
return ['idx', 'name', 'picture-url', 'action', 'remove', 'save']
return ['idx', 'name', 'price', 'picture-url', 'action', 'remove', 'save']
}
constructor() {
@ -29,23 +29,22 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
<image-input send-original="true"></image-input>
<div id="name">
<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 id="price">
<div id="priceWrapper">
<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>
</div>
<input id="submit-button" type="submit" value="Zapisz" />
<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>
</form>
</section>
`);
const form = this.shadowRoot.querySelector('form');
const imageInput = this.shadowRoot.querySelector('image-input');
this.addEventListener('item:removed', () => {
this.setAttribute('removed', 'removed');
@ -58,17 +57,40 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
ev.stopPropagation();
this.picture_url = imageInput.url;
});
this.shadowRoot.querySelector('form').addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
const detail = {
name: form.querySelector('.item-name').value,
price: form.querySelector('price-input').value,
picture_url: this.picture_url,
item_order: this.idx,
};
this.shadowRoot.querySelector('form').addEventListener('change', ev => {
ev.stopPropagation();
const input = ev.target;
const { name, value } = input;
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()) {
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 }));
}
});
@ -77,21 +99,13 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false }));
});
}
get submitEventName() {
return 'account:items';
}
connectedCallback() {
super.connectedCallback();
this.#updateNames(this.idx);
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
static attr2Field(name) {
const field = super.attr2Field(name);
if (field === 'remove') return 'showRemove';
@ -99,30 +113,6 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
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() {
const idx = parseInt(this.getAttribute('idx'));
return isNaN(idx) ? null : idx;
@ -130,7 +120,6 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
set idx(idx) {
this.setAttribute('idx', idx);
this.#updateNames(idx);
}
get name() {
@ -143,12 +132,14 @@ customElements.define('register-item-form-row', class extends RegisterFormCompon
}
get price() {
return this.getAttribute('name');
return this.getAttribute('price');
}
set price(v) {
v = parseInt(v);
console.info('set price', v);
this.setAttribute('price', v);
this.shadowRoot.querySelector('.item-price').value = v;
this.shadowRoot.querySelector('#price').value = v / 100.0;
}
get picture_url() {

View File

@ -1,9 +1,9 @@
import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
const updateItems = (form) => {
const updateItems = (rows) => {
let idx = 0;
for (const el of form.querySelectorAll('register-item-form-row')) {
for (const el of rows) {
el.idx = idx++;
}
return idx;
@ -20,6 +20,10 @@ customElements.define('register-business-items-form', class extends RegisterForm
display: flex;
}
::slotted(register-business-item-form) {
margin-bottom: 18px;
}
${ FORM_STYLE }
</style>
<form>
@ -35,11 +39,11 @@ customElements.define('register-business-items-form', class extends RegisterForm
`);
this.addEventListener('item:removed', ev => {
ev.stopPropagation();
updateItems(this)
updateItems(this.#rows)
});
this.addEventListener('form:next', ev => {
updateItems(this);
for (const el of this.querySelectorAll('item-form-row')) {
updateItems(this.#rows);
for (const el of this.#rows) {
if (!el.reportValidity()) {
ev.stopPropagation();
ev.preventDefault();
@ -49,17 +53,25 @@ customElements.define('register-business-items-form', class extends RegisterForm
this.shadowRoot.querySelector('#add-item').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.appendChild(document.createElement('register-item-form-row'));
updateItems(this);
this.appendChild(document.createElement('register-business-item-form'));
updateItems(this.#rows);
});
this.mountFormHandler();
this.mountFormHandler(() => this.#emitChange());
}
get submitEventName() {
return 'account:items';
}
get elements() {
return Array.from(this.querySelectorAll("register-item-form-row")).map(form => form.elements).flat();
get #rows() {
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 {
static get observedAttributes() {
return ['name', 'description', 'login', 'email', 'password', 'account_type']
return ['name', 'description', 'login', 'email', 'password', 'account-type']
}
constructor() {
@ -24,35 +24,36 @@ customElements.define('register-business-submit-form', class extends PseudoForm
}
</style>
<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>
<label>Login</label>
<input readonly id="preview-login">
<input readonly id="login">
</div>
<div>
<label>Email</label>
<input readonly id="preview-email">
<input readonly id="email">
</div>
<div>
<label>Password</label>
<input readonly id="preview-pass" type="password">
<input readonly id="password" type="password">
</div>
<div>
<label>Name</label>
<input readonly id="preview-name">
<input readonly id="name">
</div>
<div>
<label>Description</label>
<input readonly id="preview-description">
<input readonly id="description">
</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">
<h3>Produkty/usługi</h3>
<slot name="items"></slot>
</div>
<div class="actions">
@ -61,32 +62,25 @@ customElements.define('register-business-submit-form', class extends PseudoForm
</div>
</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) {
this.shadowRoot.querySelector(`[id="hidden-${ name }"]`).value = value;
this.shadowRoot.querySelector(`[id="preview-${ name }"]`).value = value;
}
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>
`;
}
if (res.ok) {
goTo("/account?success");
} else {
const { error } = await res.json();
document.querySelector('error-message').message = error;
}
});
}
set account_type(v) {
@ -96,35 +90,70 @@ customElements.define('register-business-submit-form', class extends PseudoForm
get name() {
return this.#hidden('name').value
}
set name(v) {
this.#hidden('name').value = v;
}
get description() {
return this.#hidden('description').value
}
set description(v) {
this.#hidden('description').value = v;
}
get login() {
return this.#hidden('login').value
}
set login(v) {
this.#hidden('login').value = v;
}
get email() {
return this.#hidden('email').value
}
set email(v) {
this.#hidden('email').value = v;
}
get password() {
return this.#hidden('password').value
}
set password(v) {
this.#hidden('password').value = v;
}
#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;
this.#setFieldValue(field, this[field]);
}
{
const listener = this.listenHistory;
listener && listener(historyDetails());
this.#dropHistory = subscribeHistory(listener);
}
}
disconnectedCallback() {
if (this.#dropHistory) this.#dropHistory();
}
#dropHistory;
listenHistory = null
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV)
return;
@ -279,3 +288,23 @@ export const runFbReady = (fn) => {
};
const fbQueue = [];
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,
detail: path
}));
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: path
}));
});
}

View File

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

View File

@ -14,8 +14,9 @@
<main>
{% match error %}
{% when Some with (e) %}
<p class="error">{{e}}</p>
<error-message message="{{e}"></error-message>
{% when None %}
<error-message></error-message>
{% endmatch %}
<article>
{% 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)]
pub struct CreateContactInfoInput {
#[serde(rename = "type")]
pub contact_type: String,
pub content: String,
}

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tracing::error;
use crate::model::db::AccountType;
use crate::model::view::Page;
use crate::model::{db, view};
use crate::routes::unrestricted::IndexTemplate;
@ -15,198 +16,6 @@ use crate::routes::{HttpResult, Identity};
use crate::view::Helper;
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")]
#[tracing::instrument]
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)]
struct LoginForm {
email: 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")]
#[tracing::instrument]
async fn login(
async fn display_sign_in_form(
req: HttpRequest,
form: web::Form<LoginForm>,
db: Data<PgPool>,
@ -273,33 +105,45 @@ async fn login(
)
}
#[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
#[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 {
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))
}
}
#[get("/account")]
#[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 mut t = crate::ok_or_internal!(&req, pool.begin().await);
let account = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await,
_ => {
id.forget();
None
return HttpResult::goto(&req, "/login");
}
};
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)]
#[serde(rename_all = "snake_case")]
enum RegisterPage {
#[default]
UserType = 0,
Basic = 1,
Business = 2,
Items = 3,
Contact = 4,
Submit = 5,
User = 40,
#[derive(Default, Serialize, Template)]
#[template(path = "register.html")]
struct RegisterTemplate {
account: Option<db::Account>,
error: Option<String>,
#[serde(skip)]
page: Page,
h: Helper,
register_page: RegisterPage,
}
impl Iterator for RegisterPage {
type Item = Self;
impl RegisterTemplate {
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> {
match self {
RegisterPage::UserType => Some(RegisterPage::Basic),
RegisterPage::Basic => Some(RegisterPage::Business),
RegisterPage::Business => Some(RegisterPage::Items),
RegisterPage::Items => Some(RegisterPage::Contact),
RegisterPage::Contact => Some(RegisterPage::Submit),
RegisterPage::Submit => Some(RegisterPage::User),
RegisterPage::User => None,
match self.0 {
RegisterPage::AccountType if self.1 == AccountType::Business => {
Some(RegisterPage::BusinessAccount)
}
RegisterPage::AccountType => Some(RegisterPage::UserAccount),
RegisterPage::UserAccount => 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 {
pub fn as_str(&self) -> &str {
match self {
RegisterPage::UserType => "user-type",
RegisterPage::User => "user-account",
RegisterPage::Basic => "business-account",
RegisterPage::Business => "business-details",
RegisterPage::Items => "items",
RegisterPage::Submit => "submit",
RegisterPage::Contact => "contact",
RegisterPage::AccountType => "account-type",
RegisterPage::UserAccount => "user-account",
RegisterPage::BusinessAccount => "business-account",
RegisterPage::BusinessDetails => "business-details",
RegisterPage::BusinessItems => "business-items",
RegisterPage::BusinessContacts => "business-contacts",
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}")]
#[tracing::instrument]
async fn register_page(
async fn display_register_page(
req: HttpRequest,
id: Identity,
db: Data<PgPool>,
@ -374,13 +409,16 @@ async fn register_page(
None
}
};
if account.is_some() {
return HttpResult::goto(&req, "/account");
}
let page = path.into_inner().0;
t.commit().await.ok();
HttpResult::res(
&req,
StatusCode::OK,
AccountTemplate {
RegisterTemplate {
account,
page: Page::Account,
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) {
config
.service(register)
.service(logout)
.service(login)
.service(display_account_info)
.service(perform_sign_in)
.service(display_sign_in_form)
.service(save_account_details)
.service(display_register_page)
.service(upload)
.service(account_page)
.service(register_page);
.service(logout);
}
#[cfg(test)]