From 4e1755895ddcab320dee52662b2423316bde3e90 Mon Sep 17 00:00:00 2001 From: eraden Date: Sun, 31 Jul 2022 21:34:43 +0200 Subject: [PATCH] Improve UI, enable add contacts in Add offer, fix business management --- client/src/api.js | 48 +++++++++ client/src/app.js | 2 + client/src/business-editor.js | 73 +++++++++++++ .../business-items/business-item-editor.js | 1 + client/src/contacts/contact-info-editor.js | 42 ++++---- client/src/contacts/edit-contact-info.js | 30 +++++- client/src/marketplace/marketplace-editor.js | 73 +++++++++++++ .../register-business-contacts-form.js | 5 +- .../register-business-submit-form.js | 46 ++++---- client/src/shared.js | 19 ++++ client/src/shared/popup-window.js | 101 ++++++++++++++++++ .../assets/templates/businesses/editor.html | 41 +++---- server/assets/templates/marketplace/new.html | 50 +++++---- server/src/model/view.rs | 1 + server/src/queries/contacts.rs | 1 + server/src/routes/mod.rs | 4 +- server/src/routes/restricted.rs | 6 +- server/src/routes/restricted/contacts.rs | 35 ++++-- 18 files changed, 485 insertions(+), 93 deletions(-) create mode 100644 client/src/api.js create mode 100644 client/src/business-editor.js create mode 100644 client/src/marketplace/marketplace-editor.js create mode 100644 client/src/shared/popup-window.js diff --git a/client/src/api.js b/client/src/api.js new file mode 100644 index 0000000..f30022f --- /dev/null +++ b/client/src/api.js @@ -0,0 +1,48 @@ +const JSON_HEADER = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', +}; + +export const accountContacts = () => + fetch('/contacts/list.json', { + headers: { + ...JSON_HEADER, + }, + method: 'GET', + }).then(res => res.json()) + +export const createContact = (body) => + fetch('/contacts/create', { + headers: { + ...JSON_HEADER, + }, + method: 'POST', + body: JSON.stringify(body), + }).then(res => res.json()) + +export const updateContact = (body) => + fetch('/contacts/update', { + headers: { + ...JSON_HEADER, + }, + method: 'POST', + body: JSON.stringify(body), + }).then(res => res.json()) + +export const deleteContact = async (body) => + fetch('/contacts/delete', { + headers: { + ...JSON_HEADER, + }, + method: 'POST', + body: JSON.stringify(body), + }).then(res => res.json()) + +export const register = (body) => + fetch('/register', { + headers: { + ...JSON_HEADER, + }, + method: 'POST', + body: JSON.stringify(body), + }).then(res => res.json()) diff --git a/client/src/app.js b/client/src/app.js index 24d67f0..95d682b 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -30,6 +30,7 @@ import "./register-form/register-user-account-form.js"; import "./register-form/register-business-contacts-form.js"; import "./register-form/register-business-submit-form.js"; +import "./business-editor.js"; import "./business-items/business-item.js"; import "./business-items/business-item-editor.js"; @@ -46,6 +47,7 @@ import "./marketplace/marketplace-offer.js"; import "./marketplace/marketplace-offers.js"; import "./marketplace/offer-form.js"; import "./marketplace/user-edit-offer.js"; +import "./marketplace/marketplace-editor.js"; import "./terms_and_conditions/terms-and-conditions.js"; import "./terms_and_conditions/privacy-policy.js"; diff --git a/client/src/business-editor.js b/client/src/business-editor.js new file mode 100644 index 0000000..5c88128 --- /dev/null +++ b/client/src/business-editor.js @@ -0,0 +1,73 @@ +import { Component } from "./shared.js"; +import * as api from "./api"; + +customElements.define('business-editor', class extends Component { + constructor() { + super(` + +
+
+ +
+
+ +
+
+ `); + + this.addEventListener('contact:create', async ({ detail }) => { + await this.#createContact(detail); + }); + this.addEventListener('contact:update', async ({ detail }) => { + await this.#updateContact(detail); + }); + this.addEventListener('contact:delete', async ({ detail }) => { + await this.#deleteContact(detail) + }); + } + + async #createContact({ content, type }) { + await api.createContact({ content, type }); + const { contacts } = await api.accountContacts(); + this.#contacts = this.#formatContacts(contacts); + } + + async #updateContact({ id, content, type }) { + console.info(1); + await api.updateContact({ id, content, type }); + console.info(2); + const { contacts } = await api.accountContacts(); + console.info(3, contacts); + this.#contacts = this.#formatContacts(contacts); + } + + async #deleteContact({ id }) { + await api.deleteContact({ id }); + const { contacts } = await api.accountContacts(); + this.#contacts = this.#formatContacts(contacts); + } + + #formatContacts(contacts) { + return contacts.map(({ id, content, contact_type }) => ` + + + + `).join(''); + } + + set #contacts(html) { + this.querySelector('contact-info-editor') + .innerHTML = html; + } +}); diff --git a/client/src/business-items/business-item-editor.js b/client/src/business-items/business-item-editor.js index 17741b1..f9befb0 100644 --- a/client/src/business-items/business-item-editor.js +++ b/client/src/business-items/business-item-editor.js @@ -142,6 +142,7 @@ customElements.define('business-item-editor', class extends Component { } const el = this.shadowRoot.querySelector('register-item-form-row'); + if (!el) return; el.idx = this.#idx + 1; } diff --git a/client/src/contacts/contact-info-editor.js b/client/src/contacts/contact-info-editor.js index 64430e5..e34d7a8 100644 --- a/client/src/contacts/contact-info-editor.js +++ b/client/src/contacts/contact-info-editor.js @@ -1,4 +1,4 @@ -import { Component, FORM_STYLE } from "../shared"; +import { Component, FORM_STYLE, onKeyDown } from "../shared.js"; customElements.define('contact-info-editor', class extends Component { static get observedAttributes() { @@ -64,29 +64,30 @@ customElements.define('contact-info-editor', class extends Component { `); - { - let timeout; - const input = this.shadowRoot.querySelector('#content'); - input.addEventListener('change', ev => { - ev.stopPropagation(); - this.#updateContactType(input.value, null); - this.content = input.value; - }); - input.addEventListener('keyup', ev => { - ev.stopPropagation(); - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - timeout = null; - this.#updateContactType(input.value, null); - this.content = input.value; - }, 1000 / 3) - }); - } + onKeyDown(this.shadowRoot.querySelector('#content'), (ev, input) => { + this.#updateContactType(input.value, null); + this.content = input.value; + }); this.shadowRoot.querySelector('#type').addEventListener('change', ev => { ev.stopPropagation(); this.#updateContactType(null, ev.target.value); }); + + this.shadowRoot.querySelector('form').addEventListener('submit', ev => { + ev.preventDefault(); + ev.stopPropagation(); + this.dispatchEvent(new CustomEvent( + this.contact_id + ? 'contact:update' + : 'contact:create', + { + composed: true, + bubbles: true, + detail: { type: this.type, content: this.content, id: this.contact_id } + } + )); + }); } get type() { @@ -107,7 +108,8 @@ customElements.define('contact-info-editor', class extends Component { } get contact_id() { - return this.getAttribute('contact-id'); + const v = parseInt(this.getAttribute('contact-id')); + return isNaN(v) ? null : v; } set contact_id(v) { diff --git a/client/src/contacts/edit-contact-info.js b/client/src/contacts/edit-contact-info.js index b818b48..271892c 100644 --- a/client/src/contacts/edit-contact-info.js +++ b/client/src/contacts/edit-contact-info.js @@ -20,6 +20,9 @@ customElements.define('edit-contact-info', class extends Component { #actions input { margin-right: 8px; } + #cancel, #edit { + display: none; + } :host([mode = 'view']) contact-info-editor { display: none; } @@ -30,6 +33,12 @@ customElements.define('edit-contact-info', class extends Component { display: block; min-width: 50%; } + :host([mode = 'edit']) #cancel { + display: block; + } + :host([mode = 'view']) #edit { + display: block; + } :host([mode = 'view']) ::slotted(contact-info) { display: block; } @@ -58,6 +67,7 @@ customElements.define('edit-contact-info', class extends Component {
+
@@ -75,16 +85,32 @@ customElements.define('edit-contact-info', class extends Component { const info = this.querySelector('contact-info'); if (!info) return; - form.contact_id = info.contact_id; + form.contact_id = this.contact_id; form.type = info.type; form.content = info.content; this.mode = 'edit'; }); + this.shadowRoot.querySelector('#cancel').addEventListener('click', ev => { + ev.preventDefault(); + ev.stopPropagation(); + this.mode = 'view'; + }); + const deleteForm = this.shadowRoot.querySelector('#deleteButton'); + deleteForm.addEventListener('submit', ev => { + ev.preventDefault(); + ev.stopPropagation(); + this.dispatchEvent(new CustomEvent('contact:delete', { + composed: true, + bubbles: true, + detail: { id: this.contact_id } + })) + }); } get contact_id() { - return this.getAttribute('contact-id'); + const v = parseInt(this.getAttribute('contact-id')); + return isNaN(v) ? null : v; } set contact_id(v) { diff --git a/client/src/marketplace/marketplace-editor.js b/client/src/marketplace/marketplace-editor.js new file mode 100644 index 0000000..e1a015f --- /dev/null +++ b/client/src/marketplace/marketplace-editor.js @@ -0,0 +1,73 @@ +import { Component } from "../shared.js"; +import * as api from "../api.js"; + +customElements.define('marketplace-editor', class extends Component { + constructor() { + super(` + +
+
+ +
+
+ +
+
+ `); + + this.addEventListener('contact:create', async ({ detail }) => { + await this.#createContact(detail); + }); + this.addEventListener('contact:update', async ({ detail }) => { + await this.#updateContact(detail); + }); + this.addEventListener('contact:delete', async ({ detail }) => { + await this.#deleteContact(detail) + }); + } + + async #createContact({ content, type }) { + await api.createContact({ content, type }); + const { contacts } = await api.accountContacts(); + this.#contacts = this.#formatContacts(contacts); + } + + async #updateContact({ id, content, type }) { + await api.updateContact({ id, content, type }); + const { contacts } = await api.accountContacts(); + this.#contacts = this.#formatContacts(contacts); + } + + async #deleteContact({ id }) { + await api.deleteContact({ id }); + const { contacts } = await api.accountContacts(); + this.#contacts = this.#formatContacts(contacts); + } + + #formatContacts(contacts) { + return contacts.map(({ id, content, contact_type }) => ` + + + + `).join(''); + } + + set #contacts(html) { + for (const el of this.querySelectorAll('edit-contact-info')) + el.remove(); + const fragment = document.createElement('template'); + fragment.innerHTML = html; + this.appendChild(fragment.content); + } +}); diff --git a/client/src/register-form/register-business-contacts-form.js b/client/src/register-form/register-business-contacts-form.js index 77f8a62..0c47134 100644 --- a/client/src/register-form/register-business-contacts-form.js +++ b/client/src/register-form/register-business-contacts-form.js @@ -17,12 +17,15 @@ customElements.define('register-business-contacts-form', class extends RegisterF ::slotted(edit-contact-info) { margin-bottom: 16px; } + #form { + margin-bottom: 16px; + } ${ BUTTON_STYLE }

Edycja listy danych kontaktowych

-
+
diff --git a/client/src/register-form/register-business-submit-form.js b/client/src/register-form/register-business-submit-form.js index 8859477..595d9ca 100644 --- a/client/src/register-form/register-business-submit-form.js +++ b/client/src/register-form/register-business-submit-form.js @@ -1,5 +1,6 @@ -import { FORM_STYLE, Router, PseudoForm } from "../shared.js"; -import { ErrorMessage } from "../shared/error-message.js"; +import { FORM_STYLE, PseudoForm } from "../shared.js"; +import { ErrorMessage } from "../shared/error-message.js"; +import * as api from "../api.js"; customElements.define('register-business-submit-form', class extends PseudoForm { static get observedAttributes() { @@ -17,7 +18,21 @@ customElements.define('register-business-submit-form', class extends PseudoForm width: 100%; max-width: 100%; } + form > .field, form > .field > label, form > .field > input { + display: block; + width: 100%; + } @media only screen and (min-device-width: 1000px) { + form > .field { + display: flex; + } + form > .field > label { + min-width: 200px; + } + form > .field > input { + align-self: stretch; + width: calc(100% - 220px); + } .item-view { display: flex; justify-content: space-between; @@ -28,24 +43,24 @@ customElements.define('register-business-submit-form', class extends PseudoForm } } - -
+ +
-
+
-
+
-
+
-
+
@@ -70,20 +85,13 @@ customElements.define('register-business-submit-form', class extends PseudoForm 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', - }, - }); + const { error } = await api.register(this.#form); + console.info(error); - if (res.ok) { + if (error) { // Router.goTo("/account?success"); - location.href ='/account?success'; + location.href = '/account?success'; } else { - const { error } = await res.json(); ErrorMessage.errorMessage = error; } }); diff --git a/client/src/shared.js b/client/src/shared.js index a9ddf2f..e12891f 100644 --- a/client/src/shared.js +++ b/client/src/shared.js @@ -307,3 +307,22 @@ export class Router { } window.addEventListener('popstate', () => Router.onChange()); + +export const onKeyDown = (input, callback) => { + let timeout; + input.addEventListener('change', ev => { + if (timeout) clearTimeout(timeout); + timeout = null; + + ev.stopPropagation(); + callback(ev, input) + }); + input.addEventListener('keyup', ev => { + ev.stopPropagation(); + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + timeout = null; + callback(ev, input); + }, 1000 / 3) + }); +} diff --git a/client/src/shared/popup-window.js b/client/src/shared/popup-window.js new file mode 100644 index 0000000..5a348ca --- /dev/null +++ b/client/src/shared/popup-window.js @@ -0,0 +1,101 @@ +import { Component, BUTTON_STYLE } from "../shared.js"; + +customElements.define('popup-window', class extends Component { + static get observedAttributes() { + return ['index', 'required', 'open']; + } + + constructor() { + super(` + +
+
 
+
+
+
+
+ + +
+
+
+ `); + } + + static attr2Field(name) { + if (name === 'open') + return 'is_open'; + super.attr2Field(name); + } + + get required() { + return this.getAttribute('required') === 'yes'; + } + + set required(v) { + if (v === true || v === 'yes') + this.setAttribute('required', 'yes'); + else + this.removeAttribute('required'); + } + + get index() { + const v = parseInt(this.getAttribute('index')); + return isNaN(v) ? null : v; + } + + set index(v) { + v = parseInt(v); + if (isNaN(v)) return; + this.setAttribute('index', v.toString()); + this.style.zIndex = v; + } + + get is_open() { + return this.getAttribute('open') === 'true'; + } + + set is_open(v) { + if (v === 'true' || v === true) + this.setAttribute('open', 'true'); + else + this.removeAttribute('open'); + } + + close() { + this.is_open = false; + } + + open() { + this.is_open = true; + } +}); diff --git a/server/assets/templates/businesses/editor.html b/server/assets/templates/businesses/editor.html index 0f8bafc..637aa1d 100644 --- a/server/assets/templates/businesses/editor.html +++ b/server/assets/templates/businesses/editor.html @@ -1,35 +1,36 @@ {% extends "../base.html" %} {% block content %} - - + + {% for contact in contacts %} {% endfor %} - - + - - {% for item in items %} - - - {% endfor %} + {% for item in items %} + + + {% endfor %} - + + {% endblock %} diff --git a/server/assets/templates/marketplace/new.html b/server/assets/templates/marketplace/new.html index ae86fa4..21695f3 100644 --- a/server/assets/templates/marketplace/new.html +++ b/server/assets/templates/marketplace/new.html @@ -1,23 +1,35 @@ {% extends "../base.html" %} {% block content %} -

Tworzenie oferty

- + +
+

Tworzenie oferty

+ +
-

Lista danych kontaktowych

- - {% for contact in contacts -%} - - - - {%- endfor %} - +
+

Lista danych kontaktowych

+

+ Możesz dodać dodatkowe dane kontaktowe jak numer telefonu lub link do profilu Facebook. +

+

+ Numer telefonu zostanie zabezpieczony i będzie wymagał wciśnięcia ikony w celu odszyfrowania. +

+ + {% for contact in contacts -%} + + + + {%- endfor %} + +
+
{% endblock %} diff --git a/server/src/model/view.rs b/server/src/model/view.rs index 1ff36d0..1c47d2b 100644 --- a/server/src/model/view.rs +++ b/server/src/model/view.rs @@ -212,6 +212,7 @@ pub struct CreateContactInfoInput { #[derive(Debug, Deserialize)] pub struct UpdateContactInfoInput { pub id: i32, + #[serde(rename = "type")] pub contact_type: String, pub content: String, } diff --git a/server/src/queries/contacts.rs b/server/src/queries/contacts.rs index 5ffabce..fb3bb62 100644 --- a/server/src/queries/contacts.rs +++ b/server/src/queries/contacts.rs @@ -38,6 +38,7 @@ FROM contacts WHERE owner_id = $1 +ORDER BY contact_type, id ASC "#, ) .bind(account_id) diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index bb49a42..05a9c39 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -349,9 +349,7 @@ impl Responder for HttpResult { HttpResult::GoTo { location, content_type: ContentType::Json, - } => HttpResponse::SeeOther() - .append_header(("Location", location.as_str())) - .body(format!("{{\"location\":{:?}}}", location.as_str())), + } => HttpResponse::SeeOther().body(format!("{{\"location\":{:?}}}", location.as_str())), } } } diff --git a/server/src/routes/restricted.rs b/server/src/routes/restricted.rs index 6a8a2f3..d2ec0f6 100644 --- a/server/src/routes/restricted.rs +++ b/server/src/routes/restricted.rs @@ -55,14 +55,14 @@ async fn handle_business_items_page( t: &mut sqlx::Transaction<'_, sqlx::postgres::Postgres>, id: Identity, ) -> HttpResult { - let account = authorize!(&req, t, id); + let account = authorize!(req, t, id); let business = crate::ok_or_internal!( - &req, + req, queries::account_business_by_owner_id(t, account.id).await ); let items = queries::account_items(t, account.id).await; - let contacts = crate::ok_or_internal!(&req, queries::account_contacts(t, account.id).await); + let contacts = crate::ok_or_internal!(req, queries::account_contacts(t, account.id).await); HttpResult::res( req, diff --git a/server/src/routes/restricted/contacts.rs b/server/src/routes/restricted/contacts.rs index 0bfb118..2e39123 100644 --- a/server/src/routes/restricted/contacts.rs +++ b/server/src/routes/restricted/contacts.rs @@ -1,22 +1,44 @@ -use actix_web::web::{Data, Form, ServiceConfig}; -use actix_web::{post, web, HttpRequest}; +use actix_http::StatusCode; +use actix_web::web::{Data, ServiceConfig}; +use actix_web::{get, post, web, HttpRequest}; +use serde::Serialize; use sqlx::PgPool; use crate::model::{db, view}; use crate::routes::{HttpResult, Identity}; use crate::{authorize, not_xss, ok_or_internal, queries, routes}; +#[derive(Debug, Default, Serialize)] +struct ContactListTemplate { + contacts: Vec, +} + +#[get("/list.json")] +async fn contact_list(req: HttpRequest, id: Identity, db: Data) -> HttpResult { + let pool = db.into_inner(); + let mut t = ok_or_internal!(&req, pool.begin().await); + let account = authorize!(&req, &mut t, id); + + let contacts = queries::account_contacts(&mut t, account.id) + .await + .unwrap_or_default(); + + t.commit().await.ok(); + + HttpResult::json(StatusCode::OK, ContactListTemplate { contacts }) +} + #[post("/create")] async fn create_contact( req: HttpRequest, id: Identity, db: Data, - form: Form, + form: web::Json, ) -> HttpResult { let form = form.into_inner(); dbg!(&form); let pool = db.into_inner(); - let mut t = crate::ok_or_internal!(&req, pool.begin().await); + let mut t = ok_or_internal!(&req, pool.begin().await); let account = authorize!(&req, &mut t, id); not_xss!(&req, &form.contact_type, t); not_xss!(&req, &form.content, t); @@ -48,7 +70,7 @@ async fn update_contact( req: HttpRequest, id: Identity, db: Data, - form: Form, + form: web::Json, ) -> HttpResult { let form = form.into_inner(); dbg!(&form); @@ -86,7 +108,7 @@ async fn delete_contact( req: HttpRequest, id: Identity, db: Data, - form: Form, + form: web::Json, ) -> HttpResult { let form = form.into_inner(); dbg!(&form); @@ -110,6 +132,7 @@ async fn delete_contact( pub fn configure(config: &mut ServiceConfig) { config.service( web::scope("contacts") + .service(contact_list) .service(create_contact) .service(update_contact) .service(delete_contact),