Improve UI, enable add contacts in Add offer, fix business management

This commit is contained in:
eraden 2022-07-31 21:34:43 +02:00
parent d02deeb732
commit 4e1755895d
18 changed files with 485 additions and 93 deletions

48
client/src/api.js Normal file
View File

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

View File

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

View File

@ -0,0 +1,73 @@
import { Component } from "./shared.js";
import * as api from "./api";
customElements.define('business-editor', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<article>
<section>
<slot name="items"></slot>
</section>
<section>
<slot name="contacts"></slot>
</section>
</article>
`);
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 }) => `
<edit-contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
mode="view"
>
<contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).join('');
}
set #contacts(html) {
this.querySelector('contact-info-editor')
.innerHTML = html;
}
});

View File

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

View File

@ -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 {
</article>
`);
{
let timeout;
const input = this.shadowRoot.querySelector('#content');
input.addEventListener('change', ev => {
ev.stopPropagation();
onKeyDown(this.shadowRoot.querySelector('#content'), (ev, input) => {
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)
});
}
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) {

View File

@ -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 {
<contact-info-editor></contact-info-editor>
<div id="buttons">
<input type="button" value="Edytuj" id="edit" />
<input type="button" value="Anuluj" id="cancel" />
<form id="deleteButton" action="/contacts/delete" method="post">
<input type="hidden" name="id" id="remove-id" />
<input type="submit" value="Usuń" id="remove" />
@ -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) {

View File

@ -0,0 +1,73 @@
import { Component } from "../shared.js";
import * as api from "../api.js";
customElements.define('marketplace-editor', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<article>
<section>
<slot name="offers"></slot>
</section>
<section>
<slot name="contacts"></slot>
</section>
</article>
`);
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 }) => `
<edit-contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
mode="view"
>
<contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).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);
}
});

View File

@ -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 }
</style>
<article>
<h2>Edycja listy danych kontaktowych</h2>
<form>
<section>
<section id="form">
<contact-info-editor
save="false"
>

View File

@ -1,5 +1,6 @@
import { FORM_STYLE, Router, PseudoForm } from "../shared.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
}
}
</style>
<form id="step-4" method="post" action="/register">
<div>
<form method="post" action="/register">
<div class="field">
<label>Login</label>
<input readonly id="login">
</div>
<div>
<div class="field">
<label>Email</label>
<input readonly id="email">
</div>
<div>
<div class="field">
<label>Password</label>
<input readonly id="password" type="password">
</div>
<div>
<div class="field">
<label>Name</label>
<input readonly id="name">
</div>
<div>
<div class="field">
<label>Description</label>
<input readonly id="description">
</div>
@ -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';
} else {
const { error } = await res.json();
ErrorMessage.errorMessage = error;
}
});

View File

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

View File

@ -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(`
<style>
:host {
display: block;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
article {
position: relative;
}
#bg {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background: rgba(100,100,100, .6);
}
#content {
display: block;
width: 100%;
height: 100%;
}
${ BUTTON_STYLE }
</style>
<article>
<section id="bg">&nbsp;</section>
<section id="content">
<div><slot></slot></div>
<div id="required"><input id="submit" value="Zakończ" /></div>
<div id="optional">
<input id="yes" value="Tak" />
<input id="no" value="Nie" />
</div>
</section>
</article>
`);
}
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;
}
});

View File

@ -1,21 +1,21 @@
{% extends "../base.html" %}
{% block content %}
<contact-info-editor type="email" value="{{h.email(account)}}" {{h.account_id_tag(account)}}>
<contact-info-list>
<business-editor {{h.account_id_tag(account)}}>
<contact-info-editor slot="contacts" type="email" value="{{h.email(account)}}" {{h.account_id_tag(account)}}>
{% for contact in contacts %}
<edit-contact-info contact-id="{{contact.id}}" mode="view">
<contact-info
contact-id="{{contact.id}}"
contact-type="{{contact.contact_type}}"
type="{{contact.contact_type}}"
content="{{contact.content}}"
></contact-info>
</edit-contact-info>
{% endfor %}
</contact-info-list>
</contact-info-editor>
<business-item-editor
slot="items"
business-id="{{business.id}}"
name="{{business.name}}"
description="{{business.description}}"
@ -32,4 +32,5 @@
{% endfor %}
</business-item-editor>
</business-editor>
{% endblock %}

View File

@ -1,9 +1,19 @@
{% extends "../base.html" %}
{% block content %}
<marketplace-editor {{h.account_id_tag(account)}}>
<section slot="offers">
<h2>Tworzenie oferty</h2>
<offer-form {{h.account_id_tag(account)}}></offer-form>
</section>
<section slot="contacts">
<h3>Lista danych kontaktowych</h3>
<p>
Możesz dodać dodatkowe dane kontaktowe jak numer telefonu lub link do profilu Facebook.
</p>
<p>
Numer telefonu zostanie zabezpieczony i będzie wymagał wciśnięcia ikony w celu odszyfrowania.
</p>
<contact-info-editor {{h.account_id_tag(account)}}>
{% for contact in contacts -%}
<edit-contact-info
@ -20,4 +30,6 @@
</edit-contact-info>
{%- endfor %}
</contact-info-editor>
</section>
</marketplace-editor>
{% endblock %}

View File

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

View File

@ -38,6 +38,7 @@ FROM
contacts
WHERE
owner_id = $1
ORDER BY contact_type, id ASC
"#,
)
.bind(account_id)

View File

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

View File

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

View File

@ -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<db::ContactInfo>,
}
#[get("/list.json")]
async fn contact_list(req: HttpRequest, id: Identity, db: Data<PgPool>) -> 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<PgPool>,
form: Form<view::CreateContactInfoInput>,
form: web::Json<view::CreateContactInfoInput>,
) -> 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<PgPool>,
form: Form<view::UpdateContactInfoInput>,
form: web::Json<view::UpdateContactInfoInput>,
) -> HttpResult {
let form = form.into_inner();
dbg!(&form);
@ -86,7 +108,7 @@ async fn delete_contact(
req: HttpRequest,
id: Identity,
db: Data<PgPool>,
form: Form<view::DeleteContactInfoInput>,
form: web::Json<view::DeleteContactInfoInput>,
) -> 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),