diff --git a/Cargo.lock b/Cargo.lock index ca03e43..0329547 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index a0cfc79..2a2225d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "*" } diff --git a/assets/templates/offers/index.html b/assets/templates/offers/index.html index 748c34b..edc4ada 100644 --- a/assets/templates/offers/index.html +++ b/assets/templates/offers/index.html @@ -1,7 +1,7 @@ {% extends "../base.html" %} {% block content %} -

Sprzedaż niepotrzebych rzeczy

+

Sprzedaż niepotrzebnych rzeczy

{% 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 %} > {% endfor %}
diff --git a/client/src/offers/marketplace-offer.js b/client/src/offers/marketplace-offer.js index af69714..da5bb52 100644 --- a/client/src/offers/marketplace-offer.js +++ b/client/src/offers/marketplace-offer.js @@ -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 }

+

`); + 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 = ` + ${this.#price_range.min}PLN + ${this.#price_range.max}PLN + `; + } + if (this.#price_range.isFixed) { + view.innerHTML = `${this.#price_range.min}PLN`; + } + return ''; + } }); diff --git a/client/src/offers/offer-form.js b/client/src/offers/offer-form.js index 04177e7..a5379ec 100644 --- a/client/src/offers/offer-form.js +++ b/client/src/offers/offer-form.js @@ -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 }
@@ -32,6 +42,18 @@ customElements.define('offer-form', class extends Component { +
+
+ + + +
+
+ + + +
+
@@ -39,6 +61,15 @@ customElements.define('offer-form', class extends Component {
`); + 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; }); diff --git a/client/src/shared.js b/client/src/shared.js index 7ec140b..0d6ac68 100644 --- a/client/src/shared.js +++ b/client/src/shared.js @@ -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(); diff --git a/migrations/20220720060805_add_offer_price.sql b/migrations/20220720060805_add_offer_price.sql new file mode 100644 index 0000000..9d8b9c6 --- /dev/null +++ b/migrations/20220720060805_add_offer_price.sql @@ -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)'; diff --git a/src/model/db.rs b/src/model/db.rs index f5a8bdd..ff15367 100644 --- a/src/model/db.rs +++ b/src/model/db.rs @@ -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 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: >::ValueRef) -> Result { + 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 >::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, } diff --git a/src/model/view.rs b/src/model/view.rs index ed48622..0cb6cf3 100644 --- a/src/model/view.rs +++ b/src/model/view.rs @@ -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, } diff --git a/src/queries/mod.rs b/src/queries/mod.rs index 3bbc2c8..e71ff51 100644 --- a/src/queries/mod.rs +++ b/src/queries/mod.rs @@ -1004,6 +1004,7 @@ pub async fn all_offers(t: &mut T<'_>) -> Result> { SELECT id, owner_id, + price_range, name, description, picture_url, @@ -1028,6 +1029,7 @@ pub async fn visible_offers(t: &mut T<'_>) -> Result> { 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, input: db::CreateOfferInput) -> Result { 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| { diff --git a/src/routes/restricted/offers.rs b/src/routes/restricted/offers.rs index 285f26c..f6c339d 100644 --- a/src/routes/restricted/offers.rs +++ b/src/routes/restricted/offers.rs @@ -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