Add price range
This commit is contained in:
parent
12f95612d6
commit
de9f1742d1
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1308,6 +1308,7 @@ dependencies = [
|
||||
"argon2",
|
||||
"askama",
|
||||
"base64",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-util",
|
||||
@ -1317,6 +1318,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
"tracing",
|
||||
"tracing-actix-web",
|
||||
"tracing-subscriber",
|
||||
|
@ -15,6 +15,7 @@ actix-utils = { version = "3.0.0" }
|
||||
actix-web = { version = "*" }
|
||||
argon2 = { version = "0.4.1" }
|
||||
askama = { version = "*", features = ["serde-json"] }
|
||||
byteorder = { version = "1.4.3" }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
||||
futures-util = { version = "0.3.21", features = [] }
|
||||
@ -24,6 +25,7 @@ rand = { version = "0.8.5", features = [] }
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = { version = "*" }
|
||||
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
||||
sqlx-core = { version = "0.6.0" }
|
||||
tracing = { version = "*" }
|
||||
tracing-actix-web = { version = "*" }
|
||||
tracing-subscriber = { version = "*" }
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "../base.html" %}
|
||||
{% block content %}
|
||||
<marketplace-offers>
|
||||
<h1>Sprzedaż niepotrzebych rzeczy</h1>
|
||||
<h1>Sprzedaż niepotrzebnych rzeczy</h1>
|
||||
|
||||
<offer-form></offer-form>
|
||||
{% for offer in offers %}
|
||||
@ -9,6 +9,15 @@
|
||||
offer-id="{{offer.id}}"
|
||||
description="{{offer.description}}"
|
||||
picture-url="{{offer.picture_url}}"
|
||||
{% match offer.price_range %}
|
||||
{% when PriceRange::Free %}
|
||||
price-range="free"
|
||||
{% when PriceRange::Fixed with { value } %}
|
||||
price-range="{{value}}"
|
||||
{% when PriceRange::Range with { min, max } %}
|
||||
price-range-min="{{min}}"
|
||||
price-range-max="{{max}}"
|
||||
{% endmatch %}
|
||||
></marketplace-offer>
|
||||
{% endfor %}
|
||||
</marketplace-offers>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Component, INPUT_STYLE } from "../shared";
|
||||
import { Component, INPUT_STYLE, PriceRange } from "../shared";
|
||||
|
||||
customElements.define('marketplace-offer', class extends Component {
|
||||
#price_range;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['offer-id', 'description', 'picture-url']
|
||||
return ['offer-id', 'description', 'picture-url', "price-range", "price-range-min", "price-range-max"]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -36,15 +38,17 @@ customElements.define('marketplace-offer', class extends Component {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
${INPUT_STYLE}
|
||||
${ INPUT_STYLE }
|
||||
</style>
|
||||
<section>
|
||||
<div id="preview">
|
||||
<img alt="" src="" id="picture" />
|
||||
</div>
|
||||
<p id="description"></p>
|
||||
<p id="price"></p>
|
||||
</section>
|
||||
`);
|
||||
this.#price_range = new PriceRange(0, 0);
|
||||
}
|
||||
|
||||
get offer_id() {
|
||||
@ -74,4 +78,56 @@ customElements.define('marketplace-offer', class extends Component {
|
||||
this.setAttribute('picture-url', v);
|
||||
this.shadowRoot.querySelector('#picture').src = v;
|
||||
}
|
||||
|
||||
get price_range() {
|
||||
return this.#price_range
|
||||
}
|
||||
|
||||
set price_range(v) {
|
||||
v = v + '';
|
||||
if (!v.match(/free|(\d+([,.]\d{2})?)(,(\d+([,.]\d{2})?))?/i))
|
||||
return;
|
||||
|
||||
if (v === 'free') return this.#price_range = new PriceRange(v, 0);
|
||||
if (v.includes(',')) {
|
||||
const [min, max, ...r] = v.split(',');
|
||||
this.#price_range = new PriceRange(parseInt(min), parseInt(max));
|
||||
}
|
||||
this.#displayPrice();
|
||||
}
|
||||
|
||||
get price_range_min() {
|
||||
return this.#price_range.min
|
||||
}
|
||||
|
||||
set price_range_min(v) {
|
||||
this.#price_range.min = v;
|
||||
this.#displayPrice();
|
||||
}
|
||||
|
||||
get price_range_max() {
|
||||
return this.#price_range.max
|
||||
}
|
||||
|
||||
set price_range_max(v) {
|
||||
this.#price_range.max = v;
|
||||
this.#displayPrice();
|
||||
}
|
||||
|
||||
#displayPrice() {
|
||||
const view = this.shadowRoot.querySelector('#price');
|
||||
if (this.#price_range.isFree) {
|
||||
view.innerHTML = 'Za darmo';
|
||||
}
|
||||
if (this.#price_range.isRange) {
|
||||
view.innerHTML = `
|
||||
<span>${this.#price_range.min}PLN</span>
|
||||
<span>${this.#price_range.max}PLN</span>
|
||||
`;
|
||||
}
|
||||
if (this.#price_range.isFixed) {
|
||||
view.innerHTML = `${this.#price_range.min}PLN`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
@ -20,6 +20,16 @@ customElements.define('offer-form', class extends Component {
|
||||
#descriptionSection {
|
||||
width: calc(100% - 230px);
|
||||
}
|
||||
@media only screen and (min-device-width: 1200px) {
|
||||
#priceSection {
|
||||
width: 300px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
#descriptionSection {
|
||||
width: calc(100% - 550px);
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<section>
|
||||
@ -32,6 +42,18 @@ customElements.define('offer-form', class extends Component {
|
||||
<label for="description">Opis</label>
|
||||
<input name="description" id="description" type="text" />
|
||||
</div>
|
||||
<div id="priceSection">
|
||||
<div>
|
||||
<label>Cena minimalna</label>
|
||||
<price-input id="priceMinUI" value="0"></price-input>
|
||||
<input name="price_min" id="priceMin" type="hidden" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Cena maksymalna</label>
|
||||
<price-input id="priceMaxUI" value="0"></price-input>
|
||||
<input name="price_max" id="priceMax" type="hidden" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="submit" type="submit" value="Utwórz" />
|
||||
</div>
|
||||
@ -39,6 +61,15 @@ customElements.define('offer-form', class extends Component {
|
||||
</section>
|
||||
`);
|
||||
|
||||
this.shadowRoot.querySelector('#priceMinUI').addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.shadowRoot.querySelector('#priceMin').value = ev.target.value;
|
||||
});
|
||||
|
||||
this.shadowRoot.querySelector('#priceMaxUI').addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.shadowRoot.querySelector('#priceMax').value = ev.target.value;
|
||||
});
|
||||
this.addEventListener('image-input:uploaded', ev => {
|
||||
this.picture_url = ev.detail;
|
||||
});
|
||||
|
@ -185,6 +185,44 @@ export class PseudoForm extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export class PriceRange {
|
||||
#min;
|
||||
#max;
|
||||
|
||||
constructor(min, max) {
|
||||
this.#min = min || 0;
|
||||
this.#max = max || 0;
|
||||
}
|
||||
|
||||
get isFree() {
|
||||
return this.#min === 'free' || (this.#min === 0 && this.#max === 0)
|
||||
}
|
||||
|
||||
get isRange() {
|
||||
return this.#max > 0;
|
||||
}
|
||||
|
||||
get isFixed() {
|
||||
return !this.isFree && !this.isRange
|
||||
}
|
||||
|
||||
get min() {
|
||||
return this.#min
|
||||
}
|
||||
|
||||
set min(v) {
|
||||
this.#min = v;
|
||||
}
|
||||
|
||||
get max() {
|
||||
return this.#max
|
||||
}
|
||||
|
||||
set max(v) {
|
||||
this.#max = v;
|
||||
}
|
||||
}
|
||||
|
||||
export const fireFbReady = () => {
|
||||
fbReady = true;
|
||||
for (const fn of fbQueue) fn();
|
||||
|
8
migrations/20220720060805_add_offer_price.sql
Normal file
8
migrations/20220720060805_add_offer_price.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TYPE "PriceRange" AS
|
||||
(
|
||||
min int,
|
||||
max int
|
||||
);
|
||||
|
||||
ALTER TABLE offers
|
||||
ADD COLUMN price_range "PriceRange" NOT NULL DEFAULT '(0, 0)';
|
@ -1,9 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use byteorder::ByteOrder;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Type};
|
||||
use sqlx_core::database::{HasArguments, HasValueRef};
|
||||
use sqlx_core::encode::IsNull;
|
||||
use sqlx_core::error::BoxDynError;
|
||||
use sqlx_core::postgres::{PgValueFormat, Postgres};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Default, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Type)]
|
||||
@ -165,10 +170,89 @@ pub struct UpdateLocalBusinessInput {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum PriceRange {
|
||||
Free,
|
||||
Fixed { value: i32 },
|
||||
Range { min: i32, max: i32 },
|
||||
}
|
||||
|
||||
impl From<(i32, i32)> for PriceRange {
|
||||
fn from((min, max): (i32, i32)) -> Self {
|
||||
match (min, max) {
|
||||
(0, 0) => Self::Free,
|
||||
(_, 0) => Self::Fixed { value: min },
|
||||
_ => Self::Range { min, max },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sqlx::Type<Postgres> for PriceRange {
|
||||
fn type_info() -> sqlx::postgres::PgTypeInfo {
|
||||
sqlx::postgres::PgTypeInfo::with_name("PriceRange")
|
||||
}
|
||||
}
|
||||
|
||||
fn take_i32(bytes: &mut &[u8]) -> i32 {
|
||||
let value = byteorder::BigEndian::read_i32(&bytes[0..4]);
|
||||
*bytes = &bytes[4..];
|
||||
value
|
||||
}
|
||||
|
||||
impl<'l> sqlx::Decode<'l, Postgres> for PriceRange {
|
||||
fn decode(value: <Postgres as HasValueRef<'l>>::ValueRef) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
PgValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
eprintln!("{s:?}");
|
||||
Ok(Self::Free)
|
||||
}
|
||||
PgValueFormat::Binary => {
|
||||
let mut bytes = value.as_bytes()?;
|
||||
|
||||
let _len = take_i32(&mut bytes);
|
||||
|
||||
let _ty = take_i32(&mut bytes);
|
||||
let _min_len = take_i32(&mut bytes);
|
||||
let min = take_i32(&mut bytes);
|
||||
|
||||
let _ty = take_i32(&mut bytes);
|
||||
let _max_len = take_i32(&mut bytes);
|
||||
let max = take_i32(&mut bytes);
|
||||
|
||||
Ok((min, max).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> sqlx::Encode<'l, Postgres> for PriceRange {
|
||||
fn encode_by_ref(&self, buf: &mut <Postgres as HasArguments<'l>>::ArgumentBuffer) -> IsNull {
|
||||
match self {
|
||||
PriceRange::Free => {
|
||||
let _ = 0.encode(buf);
|
||||
let _ = 0.encode(buf);
|
||||
true.encode(buf)
|
||||
}
|
||||
PriceRange::Fixed { value } => {
|
||||
let _ = value.encode(buf);
|
||||
let _ = 0.encode(buf);
|
||||
false.encode(buf)
|
||||
}
|
||||
PriceRange::Range { min, max } => {
|
||||
let _ = min.encode(buf);
|
||||
let _ = max.encode(buf);
|
||||
false.encode(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Offer {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub price_range: PriceRange,
|
||||
pub description: String,
|
||||
pub picture_url: String,
|
||||
pub state: OfferState,
|
||||
@ -194,7 +278,7 @@ pub struct UpdateLocalBusinessItemInput {
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteNewsArticleInput {
|
||||
pub id: i32,
|
||||
}
|
||||
@ -229,6 +313,7 @@ pub struct CreateOfferInput {
|
||||
pub picture_url: String,
|
||||
pub state: OfferState,
|
||||
pub owner_id: i32,
|
||||
pub price_range: PriceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -237,4 +322,5 @@ pub struct UpdateOfferInput {
|
||||
pub description: String,
|
||||
pub picture_url: String,
|
||||
pub state: OfferState,
|
||||
pub price_range: PriceRange,
|
||||
}
|
||||
|
@ -226,6 +226,8 @@ pub struct DeleteContactInfoInput {
|
||||
pub struct CreateOfferInput {
|
||||
pub description: String,
|
||||
pub picture_url: String,
|
||||
pub price_min: i32,
|
||||
pub price_max: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -233,4 +235,6 @@ pub struct UpdateOfferInput {
|
||||
pub id: i32,
|
||||
pub description: String,
|
||||
pub picture_url: String,
|
||||
pub price_min: i32,
|
||||
pub price_max: i32,
|
||||
}
|
||||
|
@ -1004,6 +1004,7 @@ pub async fn all_offers(t: &mut T<'_>) -> Result<Vec<db::Offer>> {
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
price_range,
|
||||
name,
|
||||
description,
|
||||
picture_url,
|
||||
@ -1028,6 +1029,7 @@ pub async fn visible_offers(t: &mut T<'_>) -> Result<Vec<db::Offer>> {
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
price_range,
|
||||
description,
|
||||
picture_url,
|
||||
state,
|
||||
@ -1052,6 +1054,7 @@ pub async fn account_offers(t: &mut T<'_>, account_id: i32) -> Result<Vec<db::Of
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
price_range,
|
||||
description,
|
||||
picture_url,
|
||||
state,
|
||||
@ -1074,11 +1077,12 @@ WHERE owner_id = $1
|
||||
pub async fn create_offer(t: &mut T<'_>, input: db::CreateOfferInput) -> Result<db::Offer> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO offers (description, picture_url, state, search, owner_id)
|
||||
VALUES ($1, $2, $3, to_tsvector('polish', $4), $5)
|
||||
INSERT INTO offers (description, picture_url, state, search, owner_id, price_range)
|
||||
VALUES ($1, $2, $3, to_tsvector('polish', $4), $5, $6)
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
price_range,
|
||||
description,
|
||||
picture_url,
|
||||
state,
|
||||
@ -1090,6 +1094,7 @@ RETURNING
|
||||
.bind(input.state)
|
||||
.bind(&input.description)
|
||||
.bind(input.owner_id)
|
||||
.bind(input.price_range)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@ -1107,11 +1112,13 @@ UPDATE offers
|
||||
SET description = $2,
|
||||
picture_url = $3,
|
||||
state = $4,
|
||||
search = to_tsvector('polish', $5)
|
||||
search = to_tsvector('polish', $5),
|
||||
price_range = $6
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
price_range,
|
||||
description,
|
||||
picture_url,
|
||||
state,
|
||||
@ -1123,6 +1130,7 @@ RETURNING
|
||||
.bind(&input.picture_url)
|
||||
.bind(input.state)
|
||||
.bind(&input.description)
|
||||
.bind(&input.price_range)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
@ -3,9 +3,9 @@ use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::model::db::OfferState;
|
||||
use crate::model::db::{self, OfferState, PriceRange};
|
||||
use crate::model::view;
|
||||
use crate::model::view::Page;
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Identity, Result};
|
||||
use crate::view::Helper;
|
||||
use crate::{authorize, not_xss, ok_or_internal, queries};
|
||||
@ -66,6 +66,7 @@ async fn create_offer(
|
||||
picture_url: form.picture_url,
|
||||
state: OfferState::Pending,
|
||||
owner_id: account.id,
|
||||
price_range: (form.price_min, form.price_max).into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@ -119,6 +120,7 @@ async fn update_offer(
|
||||
description: form.description,
|
||||
picture_url: form.picture_url,
|
||||
state: Default::default(),
|
||||
price_range: (form.price_min, form.price_max).into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
Loading…
Reference in New Issue
Block a user