feat: add more images for product

This commit is contained in:
Vlad Stan 2023-04-03 18:36:14 +03:00
parent 73016c2ce9
commit d032c8b259
8 changed files with 51 additions and 73 deletions

View file

@ -258,7 +258,7 @@ async def create_product(merchant_id: str, data: PartialProduct) -> Product:
await db.execute( await db.execute(
f""" f"""
INSERT INTO nostrmarket.products (merchant_id, id, stall_id, name, image, price, quantity, category_list, meta) INSERT INTO nostrmarket.products (merchant_id, id, stall_id, name, price, quantity, image_urls, category_list, meta)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
@ -266,9 +266,9 @@ async def create_product(merchant_id: str, data: PartialProduct) -> Product:
product_id, product_id,
data.stall_id, data.stall_id,
data.name, data.name,
data.image,
data.price, data.price,
data.quantity, data.quantity,
json.dumps(data.images),
json.dumps(data.categories), json.dumps(data.categories),
json.dumps(data.config.dict()), json.dumps(data.config.dict()),
), ),
@ -283,14 +283,14 @@ async def update_product(merchant_id: str, product: Product) -> Product:
await db.execute( await db.execute(
f""" f"""
UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ? UPDATE nostrmarket.products set name = ?, price = ?, quantity = ?, image_urls = ?, category_list = ?, meta = ?
WHERE merchant_id = ? AND id = ? WHERE merchant_id = ? AND id = ?
""", """,
( (
product.name, product.name,
product.image,
product.price, product.price,
product.quantity, product.quantity,
json.dumps(product.images),
json.dumps(product.categories), json.dumps(product.categories),
json.dumps(product.config.dict()), json.dumps(product.config.dict()),
merchant_id, merchant_id,

View file

@ -43,7 +43,7 @@ async def m001_initial(db):
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
stall_id TEXT NOT NULL, stall_id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
image TEXT, image_urls TEXT DEFAULT '[]',
price REAL NOT NULL, price REAL NOT NULL,
quantity INTEGER NOT NULL, quantity INTEGER NOT NULL,
category_list TEXT DEFAULT '[]', category_list TEXT DEFAULT '[]',

View file

@ -217,30 +217,11 @@ class PartialProduct(BaseModel):
stall_id: str stall_id: str
name: str name: str
categories: List[str] = [] categories: List[str] = []
image: Optional[str] images: List[str] = []
price: float price: float
quantity: int quantity: int
config: ProductConfig = ProductConfig() config: ProductConfig = ProductConfig()
def validate_product(self):
if self.image:
image_is_url = self.image.startswith("https://") or self.image.startswith(
"http://"
)
if not image_is_url:
def size(b64string):
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
image_size = size(self.image) / 1024
if image_size > 100:
raise ValueError(
f"""
Image size is too big, {int(image_size)}Kb.
Max: 100kb, Compress the image at https://tinypng.com, or use an URL."""
)
class Product(PartialProduct, Nostrable): class Product(PartialProduct, Nostrable):
id: str id: str
@ -251,7 +232,7 @@ class Product(PartialProduct, Nostrable):
"stall_id": self.stall_id, "stall_id": self.stall_id,
"name": self.name, "name": self.name,
"description": self.config.description, "description": self.config.description,
"image": self.image, "images": self.images,
"currency": self.config.currency, "currency": self.config.currency,
"price": self.price, "price": self.price,
"quantity": self.quantity, "quantity": self.quantity,
@ -285,6 +266,7 @@ class Product(PartialProduct, Nostrable):
def from_row(cls, row: Row) -> "Product": def from_row(cls, row: Row) -> "Product":
product = cls(**dict(row)) product = cls(**dict(row))
product.config = ProductConfig(**json.loads(row["meta"])) product.config = ProductConfig(**json.loads(row["meta"]))
product.images = json.loads(row["image_urls"])
product.categories = json.loads(row["category_list"]) product.categories = json.loads(row["category_list"])
return product return product

View file

@ -1,6 +1,6 @@
<q-card class="card--product"> <q-card class="card--product">
<q-img <q-img
:src="product.image ? product.image : '/nostrmarket/static/images/placeholder.png'" :src="product.images ? product.images[0] : '/nostrmarket/static/images/placeholder.png'"
alt="Product Image" alt="Product Image"
loading="lazy" loading="lazy"
spinner-color="white" spinner-color="white"

View file

@ -226,43 +226,28 @@
label="Categories (Hit Enter to add)" label="Categories (Hit Enter to add)"
placeholder="crafts,robots,etc" placeholder="crafts,robots,etc"
></q-select> ></q-select>
<q-toggle
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
v-model="productDialog.url"
></q-toggle>
<q-input <q-input
v-if="productDialog.url"
filled filled
dense dense
v-model.trim="productDialog.data.image" v-model.trim="productDialog.data.image"
@keydown.enter="addProductImage"
type="url" type="url"
label="Image URL" label="Image URL"
></q-input>
<q-file
v-else
class="q-pr-md"
filled
dense
capture="environment"
accept="image/jpeg, image/png"
:max-file-size="3*1024**2"
label="Small image (optional)"
clearable
@input="imageAdded"
@clear="imageCleared"
> >
<template v-if="productDialog.data.image" v-slot:before> <q-btn @click="addProductImage" dense flat icon="add"></q-btn
<img style="height: 1em" :src="productDialog.data.image" /> ></q-input>
</template>
<template v-if="productDialog.data.image" v-slot:append>
<q-icon
name="cancel"
@click.stop.prevent="imageCleared"
class="cursor-pointer"
/>
</template>
</q-file>
<q-chip
v-for="imageUrl in productDialog.data.images"
:key="imageUrl"
removable
@remove="removeProductImage(imageUrl)"
color="primary"
text-color="white"
>
<span v-text="imageUrl.split('/').pop()"></span>
</q-chip>
<q-input <q-input
filled filled
dense dense

View file

@ -27,6 +27,7 @@ async function stallDetails(path) {
id: null, id: null,
name: '', name: '',
categories: [], categories: [],
images: [],
image: null, image: null,
price: 0, price: 0,
@ -170,22 +171,23 @@ async function stallDetails(path) {
} }
}) })
}, },
imageAdded(file) { addProductImage: function () {
const image = new Image() if (!isValidImageUrl(this.productDialog.data.image)) {
image.src = URL.createObjectURL(file) this.$q.notify({
image.onload = async () => { type: 'warning',
let fit = imgSizeFit(image) message: 'Not a valid image URL',
let canvas = document.createElement('canvas') timeout: 5000
canvas.setAttribute('width', fit.width) })
canvas.setAttribute('height', fit.height) return
output = await pica.resize(image, canvas)
this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4)
this.productDialog = {...this.productDialog}
} }
}, this.productDialog.data.images.push(this.productDialog.data.image)
imageCleared() {
this.productDialog.data.image = null this.productDialog.data.image = null
this.productDialog = {...this.productDialog} },
removeProductImage: function (imageUrl) {
const index = this.productDialog.data.images.indexOf(imageUrl)
if (index !== -1) {
this.productDialog.data.images.splice(index, 1)
}
}, },
getProducts: async function () { getProducts: async function () {
try { try {
@ -205,7 +207,7 @@ async function stallDetails(path) {
id: this.productDialog.data.id, id: this.productDialog.data.id,
name: this.productDialog.data.name, name: this.productDialog.data.name,
image: this.productDialog.data.image, images: this.productDialog.data.images,
price: this.productDialog.data.price, price: this.productDialog.data.price,
quantity: this.productDialog.data.quantity, quantity: this.productDialog.data.quantity,
categories: this.productDialog.data.categories, categories: this.productDialog.data.categories,
@ -294,6 +296,7 @@ async function stallDetails(path) {
description: '', description: '',
categories: [], categories: [],
image: null, image: null,
images: [],
price: 0, price: 0,
quantity: 0, quantity: 0,
config: { config: {

View file

@ -111,3 +111,13 @@ function timeFromNow(time) {
// Return time from now data // Return time from now data
return `${tfn.time} ${tfn.unitOfTime}` return `${tfn.time} ${tfn.unitOfTime}`
} }
function isValidImageUrl(string) {
let url
try {
url = new URL(string)
} catch (_) {
return false
}
return url.protocol === 'http:' || url.protocol === 'https:'
}

View file

@ -518,7 +518,6 @@ async def api_create_product(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Product: ) -> Product:
try: try:
data.validate_product()
merchant = await get_merchant_for_user(wallet.wallet.user) merchant = await get_merchant_for_user(wallet.wallet.user)
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"
@ -557,7 +556,6 @@ async def api_update_product(
if product_id != product.id: if product_id != product.id:
raise ValueError("Bad product ID") raise ValueError("Bad product ID")
product.validate_product()
merchant = await get_merchant_for_user(wallet.wallet.user) merchant = await get_merchant_for_user(wallet.wallet.user)
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"