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 }
`);
+ 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