Create local business

This commit is contained in:
eraden 2022-07-06 22:24:59 +02:00
parent 7abf25345f
commit a727a78bcb
19 changed files with 312 additions and 127 deletions

114
client/dist/app.js vendored
View File

@ -8,6 +8,9 @@ form legend {
font-weight: bold;
font-size: 20px;
}
form.inline div {
display: flex;
}
form > div {
display: block;
margin-bottom: 1rem;
@ -127,6 +130,9 @@ customElements.define("form-navigation", class extends HTMLElement {
display: flex;
justify-content: space-between;
}
input {
max-width: 200px;
}
input.hidden {
display: none !important;
}
@ -138,17 +144,9 @@ customElements.define("form-navigation", class extends HTMLElement {
</div>
</form>
`, c.querySelector("#prev").addEventListener("click", (a)=>{
a.stopPropagation(), a.preventDefault(), this.dispatchEvent(new CustomEvent("form:prev", {
bubbles: !0,
composed: !0,
detail: this.parentElement
}));
a.stopPropagation(), a.preventDefault(), this.prev();
}), c.querySelector("#next").addEventListener("click", (a)=>{
a.stopPropagation(), a.preventDefault(), this.dispatchEvent(new CustomEvent("form:next", {
bubbles: !0,
composed: !0,
detail: this.parentElement
}));
a.stopPropagation(), a.preventDefault(), this.next();
});
}
attributeChangedCallback(b, c, d) {
@ -160,6 +158,20 @@ customElements.define("form-navigation", class extends HTMLElement {
this[S].querySelector("#prev").className = "hidden" === d ? "hidden" : "";
}
}
next() {
this.dispatchEvent(new CustomEvent("form:next", {
bubbles: !0,
composed: !0,
detail: this.parentElement
}));
}
prev() {
this.dispatchEvent(new CustomEvent("form:prev", {
bubbles: !0,
composed: !0,
detail: this.parentElement
}));
}
});
customElements.define("local-service", class extends HTMLElement {
static get observedAttributes() {
@ -372,17 +384,19 @@ customElements.define("ow-account", class extends HTMLElement {
#form > * {
display: none;
}
:host([mode="login"]) login-form, :host([mode="login"]) #switch-register {
display: block;
:host([mode="login"]) #form > login-form, :host([mode="login"]) #switch-register {
display: block !important;
}
:host([mode="register"]) register-form, :host([mode="register"]) #switch-login {
display: block;
:host([mode="register"]) #form > register-form, :host([mode="register"]) #switch-login {
display: block !important;
}
#display {
display: none;
}
:host([mode="display"]) #display { display: block; }
:host([mode="display"]) #form { display: none; }
:host([mode="form"]) #form,
:host([mode="login"]) #form { display: block; }
a{
display: block;
@ -406,6 +420,7 @@ customElements.define("ow-account", class extends HTMLElement {
<a>Masz konta? Zaloguj się</a>
</section>
</article>
<article id="display">
<div>
<input id="id" name="id" readonly type="hidden" />
@ -658,13 +673,20 @@ customElements.define("price-input", class extends HTMLElement {
${FORM_STYLE}
</style>
<div id="view">
<input id="price" type="number" min="0.00" max="10000.00" step="0.01" />
<input
id="price" type="number" min="0.00" max="10000.00" step="0.01"
placeholder="Cena, np: 12.23"
/>
<span id="currency"></span>
</div>
`;
let d = c.querySelector("#price");
d.addEventListener("change", (a)=>{
a.stopPropagation(), this.value = d.value;
});
}
connectedCallback() {
this[S].querySelector("#currency").textContent = this.currency;
this[S].querySelector("#currency").textContent = this.currency, this[S].querySelector("#price").value = this.value;
}
attributeChangedCallback(b, c, d) {
if (c === d) return;
@ -681,6 +703,9 @@ customElements.define("price-input", class extends HTMLElement {
break;
case "readonly":
d ? e.setAttribute("readonly", "readonly") : e.removeAttribute("readonly");
break;
case "name":
this.setAttribute("name", d);
}
}
get value() {
@ -721,7 +746,7 @@ customElements.define("register-basic-form", class extends PseudoForm {
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required />
<input id="login" name="login" placeholder="Login" type="text" required autofocus />
</div>
<div>
<label>E-Mail</label>
@ -736,11 +761,7 @@ customElements.define("register-basic-form", class extends PseudoForm {
`;
let d = c.querySelector("form");
d.addEventListener("submit", (a)=>{
a.preventDefault(), a.stopPropagation(), this.dispatchEvent(new CustomEvent("form:next", {
bubbles: !0,
composed: !0,
detail: d
}));
a.preventDefault(), a.stopPropagation(), c.querySelector("form-navigation").next();
});
}
});
@ -771,8 +792,13 @@ customElements.define("register-item-form-row", class extends PseudoForm {
* { font-family: 'Noto Sans', sans-serif; }
${FORM_STYLE}
section {
form {
display: flex;
justify-content: space-between;
}
div {
min-width: 100px;
max-width: 48%;
}
</style>
<form class="inline">
@ -801,8 +827,8 @@ customElements.define("register-item-form-row", class extends PseudoForm {
}
get inputs() {
return [
this[S].querySelector(".item-name").cloneNode(!0),
this[S].querySelector(".item-price").cloneNode(!0),
d(this[S].querySelector(".item-name")),
d(this[S].querySelector(".item-price")),
];
}
updateNames() {
@ -822,7 +848,11 @@ customElements.define("register-item-form-row", class extends PseudoForm {
return super.reportValidity() && this[S].querySelector("price-input").reportValidity();
}
});
let d = (a)=>{
let d = ({ name: a , value: b })=>({
name: a,
value: b
});
let d1 = (a)=>{
let b = 0;
for (let c of a.querySelectorAll("register-item-form-row"))c.idx = b++;
return b;
@ -856,11 +886,11 @@ customElements.define("register-items-form", class extends PseudoForm {
<form-navigation></form-navigation>
</form>
`, this.addEventListener("item:removed", (a)=>{
a.stopPropagation(), d(this);
a.stopPropagation(), d1(this);
}), this.addEventListener("form:next", (a)=>{
for (let b of this.querySelectorAll("item-form-row"))b.reportValidity() || (a.stopPropagation(), a.preventDefault());
}), c.querySelector("#add-item").addEventListener("click", (a)=>{
a.stopPropagation(), a.preventDefault(), this.appendChild(document.createElement("register-item-form-row")), d(this);
a.stopPropagation(), a.preventDefault(), this.appendChild(document.createElement("register-item-form-row")), d1(this);
});
}
get inputs() {
@ -883,7 +913,7 @@ customElements.define("register-company-form", class extends PseudoForm {
</style>
<form id="step-2">
<div>
<input name="name" placeholder="Nazwa usługi" type="text" required />
<input name="name" placeholder="Nazwa usługi" type="text" required autofocus />
</div>
<div>
<label>description</label>
@ -891,7 +921,9 @@ customElements.define("register-company-form", class extends PseudoForm {
</div>
<form-navigation></form-navigation>
</form>
`;
`, c.querySelector("form").addEventListener("submit", (a)=>{
a.preventDefault(), a.stopPropagation(), c.querySelector("form-navigation").next();
});
}
});
customElements.define("register-submit-form", class extends PseudoForm {
@ -905,12 +937,20 @@ customElements.define("register-submit-form", class extends PseudoForm {
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${FORM_STYLE}
.item-view {
display: flex;
justify-content: space-between;
}
.item-view > * {
min-width: 100px;
max-width: 48%;
}
</style>
<form id="step-4">
<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-pass" name="pass" type="hidden" />
<input id="hidden-pass" name="password" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<input id="hidden-description" name="description" type="hidden" />
</div>
@ -951,8 +991,14 @@ customElements.define("register-submit-form", class extends PseudoForm {
setItems(a) {
let c = this[S].querySelector("#items");
for (let d of (c.innerHTML = "", a)){
let e = c.appendChild(document.createElement("div")), [f, g] = d;
f.setAttribute("readonly", "readonly"), e.appendChild(f), e.appendChild(document.createElement("price-view")).value = g.value, g.setAttribute("readonly", "readonly"), g.setAttribute("type", "hidden"), e.appendChild(g);
let e = c.appendChild(document.createElement("div"));
e.className = "item-view";
let [f, g] = d;
e.innerHTML = `
<input type="text" name="${f.name}" value="${f.value}" readonly />
<input type="hidden" name="${g.name}" value="${g.value}" readonly />
<price-view value="${g.value}"></price-view>
`;
}
}
set accountType(a) {

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,9 @@ customElements.define('form-navigation', class extends HTMLElement {
display: flex;
justify-content: space-between;
}
input {
max-width: 200px;
}
input.hidden {
display: none !important;
}
@ -32,20 +35,12 @@ customElements.define('form-navigation', class extends HTMLElement {
shadow.querySelector('#prev').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.dispatchEvent(new CustomEvent('form:prev', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
this.prev();
});
shadow.querySelector('#next').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.dispatchEvent(new CustomEvent('form:next', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
this.next();
});
}
@ -62,4 +57,20 @@ customElements.define('form-navigation', class extends HTMLElement {
}
}
}
next() {
this.dispatchEvent(new CustomEvent('form:next', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
}
prev() {
this.dispatchEvent(new CustomEvent('form:prev', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
}
});

View File

@ -1,4 +1,4 @@
import { S } from "./shared";
import { S } from "../shared";
customElements.define('ow-nav', class extends HTMLElement {
constructor() {

View File

@ -1,4 +1,4 @@
import { S } from "./shared";
import { S } from "../shared";
customElements.define('ow-path', class extends HTMLElement {
static get observedAttributes() {

View File

@ -15,17 +15,19 @@ customElements.define('ow-account', class extends HTMLElement {
#form > * {
display: none;
}
:host([mode="login"]) login-form, :host([mode="login"]) #switch-register {
display: block;
:host([mode="login"]) #form > login-form, :host([mode="login"]) #switch-register {
display: block !important;
}
:host([mode="register"]) register-form, :host([mode="register"]) #switch-login {
display: block;
:host([mode="register"]) #form > register-form, :host([mode="register"]) #switch-login {
display: block !important;
}
#display {
display: none;
}
:host([mode="display"]) #display { display: block; }
:host([mode="display"]) #form { display: none; }
:host([mode="form"]) #form,
:host([mode="login"]) #form { display: block; }
a{
display: block;
@ -49,6 +51,7 @@ customElements.define('ow-account', class extends HTMLElement {
<a>Masz konta? Zaloguj się</a>
</section>
</article>
<article id="display">
<div>
<input id="id" name="id" readonly type="hidden" />

View File

@ -25,15 +25,24 @@ customElements.define('price-input', class extends HTMLElement {
${FORM_STYLE}
</style>
<div id="view">
<input id="price" type="number" min="0.00" max="10000.00" step="0.01" />
<input
id="price" type="number" min="0.00" max="10000.00" step="0.01"
placeholder="Cena, np: 12.23"
/>
<span id="currency"></span>
</div>
`;
const price = shadow.querySelector('#price');
price.addEventListener('change', ev => {
ev.stopPropagation();
this.value = price.value;
});
}
connectedCallback() {
this[S].querySelector('#currency').textContent = this.currency;
// this[S].querySelector('#price').textContent = this.formatted;
this[S].querySelector('#price').value = this.value;
}
attributeChangedCallback(name, oldV, newV) {
@ -61,6 +70,7 @@ customElements.define('price-input', class extends HTMLElement {
break;
}
case 'name': {
this.setAttribute('name', newV);
break;
}
}

View File

@ -16,7 +16,7 @@ customElements.define('register-basic-form', class extends PseudoForm {
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required />
<input id="login" name="login" placeholder="Login" type="text" required autofocus />
</div>
<div>
<label>E-Mail</label>
@ -34,7 +34,7 @@ customElements.define('register-basic-form', class extends PseudoForm {
form.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('form:next', { bubbles: true, composed: true, detail: form }));
shadow.querySelector('form-navigation').next();
})
}
});

View File

@ -14,7 +14,7 @@ customElements.define('register-company-form', class extends PseudoForm {
</style>
<form id="step-2">
<div>
<input name="name" placeholder="Nazwa usługi" type="text" required />
<input name="name" placeholder="Nazwa usługi" type="text" required autofocus />
</div>
<div>
<label>description</label>
@ -23,5 +23,11 @@ customElements.define('register-company-form', class extends PseudoForm {
<form-navigation></form-navigation>
</form>
`;
shadow.querySelector('form').addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
shadow.querySelector('form-navigation').next();
});
}
})

View File

@ -25,8 +25,13 @@ customElements.define('register-item-form-row', class extends PseudoForm {
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
section {
form {
display: flex;
justify-content: space-between;
}
div {
min-width: 100px;
max-width: 48%;
}
</style>
<form class="inline">
@ -65,11 +70,10 @@ customElements.define('register-item-form-row', class extends PseudoForm {
}
}
get inputs() {
return [
this[S].querySelector('.item-name').cloneNode(true),
this[S].querySelector('.item-price').cloneNode(true),
extract(this[S].querySelector('.item-name')),
extract(this[S].querySelector('.item-price')),
];
}
@ -93,3 +97,5 @@ customElements.define('register-item-form-row', class extends PseudoForm {
return super.reportValidity() && this[S].querySelector('price-input').reportValidity();
}
});
const extract = ({ name, value }) => ({ name, value })

View File

@ -11,12 +11,20 @@ customElements.define('register-submit-form', class extends PseudoForm {
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
.item-view {
display: flex;
justify-content: space-between;
}
.item-view > * {
min-width: 100px;
max-width: 48%;
}
</style>
<form id="step-4">
<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-pass" name="pass" type="hidden" />
<input id="hidden-pass" name="password" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<input id="hidden-description" name="description" type="hidden" />
</div>
@ -62,15 +70,14 @@ customElements.define('register-submit-form', class extends PseudoForm {
host.innerHTML = ``;
for (const row of items) {
const el = host.appendChild(document.createElement('div'));
el.className = 'item-view';
const [name, price] = row;
name.setAttribute('readonly', 'readonly');
el.appendChild(name);
el.appendChild(document.createElement('price-view')).value = price.value;
price.setAttribute('readonly', 'readonly');
price.setAttribute('type', 'hidden');
el.appendChild(price);
el.innerHTML = `
<input type="text" name="${name.name}" value="${name.value}" readonly />
<input type="hidden" name="${price.name}" value="${price.value}" readonly />
<price-view value="${price.value}"></price-view>
`;
}
}

View File

@ -9,6 +9,9 @@ form legend {
font-weight: bold;
font-size: 20px;
}
form.inline div {
display: flex;
}
form > div {
display: block;
margin-bottom: 1rem;

View File

@ -1 +1,2 @@
ALTER TABLE accounts ADD COLUMN email text not null default '' unique;
ALTER TABLE accounts
ADD COLUMN email text not null default '' unique;

View File

@ -1 +1,2 @@
ALTER TABLE accounts ADD COLUMN facebook_id TEXT DEFAULT NULL;
ALTER TABLE accounts
ADD COLUMN facebook_id TEXT DEFAULT NULL;

View File

@ -2,7 +2,7 @@
use crate::routes::render_index;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, web::Data, App, HttpResponse, HttpServer};
use actix_web::{web, web::Data, App, HttpServer};
mod auth;
mod model;

View File

@ -4,7 +4,7 @@ use sqlx::{FromRow, Type};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, PartialOrd, PartialEq, Serialize, Deserialize, Type)]
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Serialize, Deserialize, Type)]
pub enum AccountType {
User,
Business,
@ -63,7 +63,7 @@ pub struct Token {
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LocalService {
pub struct LocalBusiness {
pub id: i32,
pub owner_id: i32,
pub name: String,
@ -72,7 +72,7 @@ pub struct LocalService {
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LocalServiceItem {
pub struct LocalBusinessItem {
pub id: i32,
pub local_service_id: i32,
pub name: String,

View File

@ -36,6 +36,12 @@ impl Page {
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BusinessItemInput {
pub name: String,
pub price: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LocalService {
pub id: i32,
@ -43,11 +49,11 @@ pub struct LocalService {
pub name: String,
pub description: String,
pub state: db::LocalServiceState,
pub items: Vec<db::LocalServiceItem>,
pub items: Vec<db::LocalBusinessItem>,
}
impl<'v> From<(db::LocalService, &'v mut Vec<db::LocalServiceItem>)> for LocalService {
fn from((service, items): (db::LocalService, &'v mut Vec<db::LocalServiceItem>)) -> Self {
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalService {
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self {
Self {
id: service.id,
owner_id: service.owner_id,

View File

@ -1,6 +1,6 @@
use crate::model::db;
use crate::model::db::AccountType;
use crate::model::view::Page;
use crate::model::view::{self, Page};
use crate::utils;
use actix_files::Files;
use actix_identity::Identity;
@ -39,8 +39,8 @@ pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse {
_ => None,
};
let (services, mut items) = {
use crate::model::db::{LocalService, LocalServiceItem};
let services: Vec<LocalService> = sqlx::query_as(
use crate::model::db::{LocalBusiness, LocalBusinessItem};
let services: Vec<LocalBusiness> = sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_services
@ -58,7 +58,7 @@ ORDER BY id
})
.unwrap_or_default();
let items: Vec<LocalServiceItem> = sqlx::query_as(
let items: Vec<LocalBusinessItem> = sqlx::query_as(
r#"
SELECT
id,
@ -132,6 +132,9 @@ struct RegisterForm {
password: String,
facebook_id: Option<String>,
account_type: db::AccountType,
items: Option<Vec<view::BusinessItemInput>>,
name: Option<String>,
description: Option<String>,
}
#[post("/register")]
@ -143,13 +146,17 @@ async fn register(
let form = form.into_inner();
let pool = db.into_inner();
if form.account_type == AccountType::Admin {
return HttpResponse::BadRequest().body("Breach attempt detected!");
return HttpResponse::BadRequest().body("Security breach attempt detected!");
}
let mut t = pool.begin().await.unwrap();
let pass = match utils::encrypt(&form.password) {
Ok(pass) => pass,
Err(e) => {
tracing::error!("{:?}", e);
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest().body(
AccountTemplate {
account: None,
@ -165,7 +172,7 @@ async fn register(
let res: sqlx::Result<db::Account> = sqlx::query_as(
r#"
INSERT INTO accounts (login, email, pass, facebook_id, account_type)
VALUES ($1, $2, $3, $4)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, login, email, pass, facebook_id, account_type
"#,
)
@ -173,24 +180,19 @@ RETURNING id, login, email, pass, facebook_id, account_type
.bind(form.email)
.bind(pass)
.bind(form.facebook_id)
.fetch_one(&*pool)
.bind(form.account_type)
.fetch_one(&mut t)
.await;
match res {
let account = match res {
Ok(res) => {
id.remember(format!("{}", res.id));
HttpResponse::Ok().body(
AccountTemplate {
account: Some(res),
error: None,
page: Page::Register,
}
.render()
.unwrap(),
)
res
}
Err(e) => {
eprintln!("{e}");
HttpResponse::BadRequest().body(
tracing::error!("{e}");
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest().body(
AccountTemplate {
account: None,
error: Some("Problem z utworzeniem konta".into()),
@ -198,9 +200,92 @@ RETURNING id, login, email, pass, facebook_id, account_type
}
.render()
.unwrap(),
);
}
};
if matches!(form.account_type, 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();
let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as(
r#"
INSERT INTO local_services (name, owner_id, description)
VALUES ($1, $2, $3)
RETURNING id, owner_id, name, description, state
"#,
)
.bind(name)
.bind(owner_id)
.bind(description)
.fetch_one(&mut t)
.await;
let business = match res {
Ok(business) => business,
Err(e) => {
tracing::error!("{e}");
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest().body(
AccountTemplate {
account: None,
error: Some("Problem z utworzeniem konta".into()),
page: Page::Register,
}
.render()
.unwrap(),
);
}
};
for (idx, item) in form.items.as_deref().unwrap_or_default().iter().enumerate() {
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
r#"
INSERT INTO local_service_items (local_service_id, name, price, item_order)
VALUES ($1, $2, $3, $4)
RETURNING id, local_service_id, name, price, item_order
"#,
)
.bind(business.id)
.bind(&item.name)
.bind(item.price as i32)
.bind(idx as i32)
.fetch_one(&mut t)
.await;
match res {
Ok(_) => {}
Err(e) => {
tracing::error!("{e}");
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest().body(
AccountTemplate {
account: None,
error: Some("Problem z utworzeniem konta".into()),
page: Page::Register,
}
.render()
.unwrap(),
);
}
}
}
}
t.commit().await.unwrap();
HttpResponse::SeeOther()
.append_header(("Location", "/"))
.body(
AccountTemplate {
account: Some(account),
error: None,
page: Page::Register,
}
.render()
.unwrap(),
)
}
#[post("/logout")]