From ad83fc1099d8b5e29229573d2270ecaaa7fa5243 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sat, 8 Aug 2026 15:36:41 +0330 Subject: [PATCH] Add store-item bulk create, unlimited stock, and contact-for-price UI. Co-authored-by: Cursor --- .../ConfirmBulkAddStoreItemsModal.module.css | 191 +++++++++++++++ .../ConfirmBulkAddStoreItemsModal.tsx | 164 +++++++++++++ .../CreateStoreItemsModal.module.css | 81 ++++++- .../src/components/CreateStoreItemsModal.tsx | 219 ++++++++++++++---- .../src/components/EditStoreItemsModal.tsx | 72 ++++-- .../src/components/StoreItemCard.module.css | 10 +- .../business/src/components/StoreItemCard.tsx | 20 +- .../src/components/StoreItemPrice.module.css | 11 + .../src/components/StoreItemPrice.tsx | 9 +- apps/business/src/i18n/messages.ts | 36 +++ .../business/src/services/storeItemService.ts | 16 +- apps/business/src/utils/storeItemRows.ts | 29 +++ .../src/components/StoreItemPrice.module.css | 11 + .../src/components/StoreItemPrice.tsx | 9 +- apps/customer/src/i18n/messages.ts | 2 + 15 files changed, 803 insertions(+), 77 deletions(-) create mode 100644 apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css create mode 100644 apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx diff --git a/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css new file mode 100644 index 0000000..8691260 --- /dev/null +++ b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css @@ -0,0 +1,191 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.28); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1200; + padding: 20px; +} + +.modal { + position: relative; + width: 100%; + max-width: 440px; + padding: 32px 28px 24px; + text-align: start; + background: rgba(255, 255, 255, 0.94); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid rgba(245, 158, 11, 0.28); + border-radius: var(--radius); + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.18); + font-family: var(--font-ui); +} + +.iconWrap { + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + background: rgba(245, 158, 11, 0.14); + color: #d97706; + border-radius: 50%; +} + +.title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; + text-align: center; + font-family: var(--font-ui); +} + +.message { + font-size: 14px; + line-height: 1.6; + color: var(--text-secondary); + margin-bottom: 12px; + text-align: center; + font-family: var(--font-ui); +} + +.points { + margin: 0 0 18px; + padding-inline-start: 1.25rem; + font-size: 13px; + line-height: 1.65; + color: var(--text-secondary); + font-family: var(--font-ui); +} + +.points li + li { + margin-top: 4px; +} + +.agree { + display: flex; + align-items: flex-start; + gap: 10px; + margin-bottom: 22px; + padding: 12px 14px; + font-size: 13px; + line-height: 1.45; + color: var(--text-primary); + font-family: var(--font-ui); + background: rgba(148, 163, 184, 0.1); + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: var(--radius-sm); + cursor: pointer; + text-align: start; +} + +.agreeCheck { + width: 16px; + height: 16px; + margin-top: 2px; + flex-shrink: 0; + accent-color: var(--primary); + cursor: pointer; +} + +.agreeCheck:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.actions { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; +} + +.cancelBtn { + padding: 10px 20px; + min-height: var(--field-height); + font-size: 14px; + font-weight: 500; + font-family: var(--font-ui); + color: var(--text-secondary); + border-radius: var(--radius-sm); + transition: background 0.2s; +} + +.cancelBtn:hover:not(:disabled) { + background: rgba(148, 163, 184, 0.15); +} + +.cancelBtn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.confirmBtn { + padding: 10px 22px; + min-height: var(--field-height); + font-size: 14px; + font-weight: 600; + font-family: var(--font-ui); + color: white; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + border-radius: var(--radius-sm); + box-shadow: 0 4px 12px rgba(var(--primary-rgb) / 0.3); + transition: transform 0.2s, opacity 0.2s; +} + +.confirmBtn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.confirmBtn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.closeBtn { + position: absolute; + top: 14px; + inset-inline-end: 14px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--text-muted); + transition: background 0.2s, color 0.2s; +} + +.closeBtn:hover:not(:disabled) { + background: rgba(245, 158, 11, 0.12); + color: #d97706; +} + +.closeBtn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.overlayIn { animation: overlayFadeIn 0.22s ease forwards; } +.overlayOut { animation: overlayFadeOut 0.22s ease forwards; } +.modalIn { animation: modalFadeIn 0.25s ease forwards; } +.modalOut { animation: modalFadeOut 0.22s ease forwards; } + +@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } } +@keyframes modalFadeIn { + from { opacity: 0; transform: translateY(12px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes modalFadeOut { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(8px) scale(0.98); } +} diff --git a/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx new file mode 100644 index 0000000..8bb2ca1 --- /dev/null +++ b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { AlertTriangle, PackagePlus, X } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' +import { useT } from '../i18n/useT' +import styles from './ConfirmBulkAddStoreItemsModal.module.css' + +export interface BulkAddResult { + created: number + skipped: number + failed: number +} + +interface ConfirmBulkAddStoreItemsModalProps { + open: boolean + busy: boolean + result: BulkAddResult | null + onConfirm: () => void + onClose: () => void +} + +const ANIMATION_MS = 220 + +export function ConfirmBulkAddStoreItemsModal({ + open, + busy, + result, + onConfirm, + onClose, +}: ConfirmBulkAddStoreItemsModalProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' + const [mounted, setMounted] = useState(open) + const [closing, setClosing] = useState(false) + const [agreed, setAgreed] = useState(false) + + useEffect(() => { + if (open) { + setMounted(true) + setClosing(false) + setAgreed(false) + } else if (mounted) { + setClosing(true) + const timer = setTimeout(() => { + setMounted(false) + setClosing(false) + setAgreed(false) + }, ANIMATION_MS) + return () => clearTimeout(timer) + } + }, [open, mounted]) + + useEffect(() => { + if (!mounted || closing) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !busy) onClose() + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [mounted, closing, onClose, busy]) + + if (!mounted) return null + + const showResult = result !== null + + return createPortal( +
{ + if (!busy) onClose() + }} + > +
e.stopPropagation()} + role="alertdialog" + aria-modal="true" + aria-labelledby="confirm-bulk-add-title" + lang={isFa ? 'fa' : 'en'} + dir={isFa ? 'rtl' : 'ltr'} + > +
+ {showResult ? : } +
+ +

+ {showResult + ? t('storeItems.create.addAllResultTitle') + : t('storeItems.create.addAllConfirmTitle')} +

+ + {showResult ? ( +

+ {t('storeItems.create.addAllDone', { + created: String(result.created), + skipped: String(result.skipped), + failed: String(result.failed), + })} +

+ ) : ( + <> +

{t('storeItems.create.addAllConfirmIntro')}

+
    +
  • {t('storeItems.create.addAllConfirmMax')}
  • +
  • {t('storeItems.create.addAllConfirmSkip')}
  • +
  • {t('storeItems.create.addAllConfirmHeavy')}
  • +
+ + + + )} + +
+ {showResult ? ( + + ) : ( + <> + + + + )} +
+ + +
+
, + document.body, + ) +} diff --git a/apps/business/src/components/CreateStoreItemsModal.module.css b/apps/business/src/components/CreateStoreItemsModal.module.css index 7241338..768be58 100644 --- a/apps/business/src/components/CreateStoreItemsModal.module.css +++ b/apps/business/src/components/CreateStoreItemsModal.module.css @@ -65,6 +65,14 @@ cursor: pointer; } +.rowActions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + .addRowBtn { display: inline-flex; align-items: center; @@ -76,13 +84,82 @@ color: var(--primary); border-radius: var(--radius-sm); transition: background 0.2s; - margin-bottom: 8px; } -.addRowBtn:hover { +.addRowBtn:hover:not(:disabled) { background: rgba(var(--primary-rgb) / 0.1); } +.addRowBtn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.footerActions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-top: 16px; + margin-top: 8px; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.footerBulkBtn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + min-height: var(--field-height); + font-size: 13px; + font-weight: 500; + font-family: var(--font-ui); + color: var(--primary); + border-radius: var(--radius-sm); + transition: background 0.2s; +} + +.footerBulkBtn:hover:not(:disabled) { + background: rgba(var(--primary-rgb) / 0.1); +} + +.footerBulkBtn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.footerEnd { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-inline-start: auto; +} + +.unlimitedCell { + display: flex; + align-items: center; + justify-content: center; +} + +.unlimitedCheck { + width: 16px; + height: 16px; + accent-color: var(--primary); + cursor: pointer; +} + +.unlimitedCheck:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.compactInput:disabled { + opacity: 0.55; + cursor: not-allowed; +} + .modalWide { max-width: 920px; } diff --git a/apps/business/src/components/CreateStoreItemsModal.tsx b/apps/business/src/components/CreateStoreItemsModal.tsx index 2b89808..e3886aa 100644 --- a/apps/business/src/components/CreateStoreItemsModal.tsx +++ b/apps/business/src/components/CreateStoreItemsModal.tsx @@ -1,7 +1,8 @@ import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' -import { Plus, X } from 'lucide-react' +import { PackagePlus, Plus, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' +import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import { ApiError } from '../lib/api' import { listAllProducts } from '../services/productService' @@ -16,10 +17,15 @@ import { import { createId } from '../utils/id' import { formatIrtInput, parseIrtInput } from '../utils/irtPrice' import { + buildBulkCreateItems, createEmptyStoreItemRow, getVariationOptions, type StoreItemDraftRow, } from '../utils/storeItemRows' +import { + ConfirmBulkAddStoreItemsModal, + type BulkAddResult, +} from './ConfirmBulkAddStoreItemsModal' import { ProductSearchSelect } from './ProductSearchSelect' import { Tooltip } from './Tooltip' import modalStyles from './VariationsModal.module.css' @@ -44,6 +50,7 @@ export function CreateStoreItemsModal({ }: CreateStoreItemsModalProps) { const t = useT() const { locale } = useLocale() + const { showToast } = useToast() const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [products, setProducts] = useState<{ id: string; title: string; nameFa: string }[]>([]) @@ -53,8 +60,13 @@ export function CreateStoreItemsModal({ const [isLoadingProducts, setIsLoadingProducts] = useState(false) const [isLoadingVariations, setIsLoadingVariations] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) + const [isBulkCreating, setIsBulkCreating] = useState(false) + const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false) + const [bulkResult, setBulkResult] = useState(null) const [error, setError] = useState('') + const busy = isSubmitting || isBulkCreating + const formVariations = useMemo( () => variations.filter((variation) => getVariationOptions(variation).length > 0), [variations], @@ -62,8 +74,10 @@ export function CreateStoreItemsModal({ const gridTemplate = useMemo(() => { const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ') - const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '32px'].filter((part) => part).join(' ') - return cols || 'minmax(120px, 1fr) 90px 32px' + const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '72px', '32px'] + .filter((part) => part) + .join(' ') + return cols || 'minmax(120px, 1fr) 90px 72px 32px' }, [formVariations]) useEffect(() => { @@ -79,6 +93,8 @@ export function CreateStoreItemsModal({ setVariations([]) setRows([]) setError('') + setBulkConfirmOpen(false) + setBulkResult(null) }, ANIMATION_MS) return () => clearTimeout(timer) } @@ -154,11 +170,11 @@ export function CreateStoreItemsModal({ useEffect(() => { if (!mounted || closing) return const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose() + if (e.key === 'Escape' && !busy && !bulkConfirmOpen) onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) - }, [mounted, closing, onClose]) + }, [mounted, closing, onClose, busy, bulkConfirmOpen]) if (!mounted) return null @@ -205,8 +221,12 @@ export function CreateStoreItemsModal({ if (!optionId) return [] return [{ variationId: variation.id, optionId }] }), - price: parseIrtInput(row.price) ?? undefined, - stockQuantity: row.stock.trim() ? Number(row.stock) : undefined, + price: parseIrtInput(row.price), + stockQuantity: row.unlimitedStock + ? null + : row.stock.trim() + ? Number(row.stock) + : null, })) } @@ -215,10 +235,12 @@ export function CreateStoreItemsModal({ rows.length > 0 && rows.every((row) => { const price = parseIrtInput(row.price) + if (price !== null && price < 0) return false + if (row.unlimitedStock) return true const stock = Number(row.stock) - return price !== null && price >= 0 && row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 + return row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 }) && - !isSubmitting && + !busy && !isLoadingVariations async function handleSubmit(e: React.FormEvent) { @@ -246,10 +268,91 @@ export function CreateStoreItemsModal({ } } + function openBulkConfirm() { + if (busy || isLoadingProducts || products.length === 0) return + setBulkResult(null) + setBulkConfirmOpen(true) + } + + function closeBulkConfirm() { + if (isBulkCreating) return + const hadResult = bulkResult !== null + const createdCount = bulkResult?.created ?? 0 + setBulkConfirmOpen(false) + setBulkResult(null) + if (hadResult) { + onCreated?.() + if (createdCount > 0) onClose() + } + } + + async function handleAddAllProducts() { + if (isBulkCreating || isLoadingProducts) return + + setIsBulkCreating(true) + setError('') + setBulkResult(null) + + let created = 0 + let skipped = 0 + let failed = 0 + const skip = new Set(existingProductIds) + + try { + const catalog = + products.length > 0 + ? products + : (await listAllProducts()).map((item) => ({ + id: item.id, + title: item.title, + nameFa: item.nameFa, + })) + + for (const product of catalog) { + if (skip.has(product.id)) { + skipped += 1 + continue + } + + try { + const data = await getProductVariationValues(product.id) + const items = buildBulkCreateItems(data.variations) + await batchCreateStoreItems({ productId: product.id, items }) + created += 1 + skip.add(product.id) + } catch { + failed += 1 + } + } + + const result = { created, skipped, failed } + setBulkResult(result) + showToast( + t('storeItems.create.addAllDone', { + created: String(created), + skipped: String(skipped), + failed: String(failed), + }), + failed > 0 && created === 0 ? 'error' : 'success', + ) + } catch (err) { + if (err instanceof ApiError) { + setError(err.message) + } else { + setError(t('storeItems.create.errorCreate')) + } + setBulkConfirmOpen(false) + } finally { + setIsBulkCreating(false) + } + } + return createPortal(
{ + if (!busy && !bulkConfirmOpen) onClose() + }} >
@@ -284,7 +388,7 @@ export function CreateStoreItemsModal({ value={productId} onChange={handleProductChange} placeholder={t('storeItems.create.productPlaceholder')} - disabled={isLoadingProducts || isSubmitting} + disabled={isLoadingProducts || busy} aria-label={t('storeItems.create.productPlaceholder')} />
@@ -304,6 +408,7 @@ export function CreateStoreItemsModal({ ))} {t('storeItems.create.price')} {t('storeItems.create.stock')} + {t('storeItems.create.unlimited')}
@@ -324,7 +429,7 @@ export function CreateStoreItemsModal({ onChange={(e) => updateRowSelection(row.id, variation.id, e.target.value) } - disabled={isSubmitting} + disabled={busy} > {options.map((option) => ( @@ -341,25 +446,38 @@ export function CreateStoreItemsModal({ className={rowStyles.compactInput} value={row.price} onChange={(e) => updateRow(row.id, { price: formatIrtInput(e.target.value) })} - placeholder="0" - disabled={isSubmitting} + placeholder={t('storeItems.price.contact')} + disabled={busy} /> updateRow(row.id, { stock: e.target.value })} - disabled={isSubmitting} + disabled={busy || row.unlimitedStock} + placeholder={row.unlimitedStock ? '∞' : '0'} /> +
+ + updateRow(row.id, { unlimitedStock: e.target.checked }) + } + disabled={busy} + aria-label={t('storeItems.create.unlimited')} + /> +
- +
+ +
)} {error &&

{error}

} -
+
- +
+ + +
+ + void handleAddAllProducts()} + onClose={closeBulkConfirm} + /> , document.body, diff --git a/apps/business/src/components/EditStoreItemsModal.tsx b/apps/business/src/components/EditStoreItemsModal.tsx index e30e982..ba5ed3f 100644 --- a/apps/business/src/components/EditStoreItemsModal.tsx +++ b/apps/business/src/components/EditStoreItemsModal.tsx @@ -43,12 +43,15 @@ function itemsToRows(storeItems: StoreItem[]): StoreItemDraftRow[] { selections[selection.variationId] = selection.optionId } + const unlimited = item.stockQuantity === null + return { id: createId(), storeItemId: item.id, selections, price: item.price !== null ? formatIrtInput(String(item.price)) : '', - stock: item.stockQuantity !== null ? String(item.stockQuantity) : '1', + stock: unlimited ? '1' : String(item.stockQuantity), + unlimitedStock: unlimited, } }) } @@ -82,8 +85,10 @@ export function EditStoreItemsModal({ const gridTemplate = useMemo(() => { const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ') - const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '32px'].filter((part) => part).join(' ') - return cols || 'minmax(120px, 1fr) 90px 32px' + const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '72px', '32px'] + .filter((part) => part) + .join(' ') + return cols || 'minmax(120px, 1fr) 90px 72px 32px' }, [formVariations]) useEffect(() => { @@ -128,11 +133,11 @@ export function EditStoreItemsModal({ useEffect(() => { if (!mounted || closing) return const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose() + if (e.key === 'Escape' && !isSubmitting) onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) - }, [mounted, closing, onClose]) + }, [mounted, closing, onClose, isSubmitting]) if (!mounted || !productId) return null @@ -174,8 +179,10 @@ export function EditStoreItemsModal({ rows.length > 0 && rows.every((row) => { const price = parseIrtInput(row.price) + if (price !== null && price < 0) return false + if (row.unlimitedStock) return true const stock = Number(row.stock) - return price !== null && price >= 0 && row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 + return row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 }) && !isSubmitting && !isLoading @@ -197,8 +204,12 @@ export function EditStoreItemsModal({ if (!optionId) return [] return [{ variationId: variation.id, optionId }] }), - price: parseIrtInput(row.price)!, - stockQuantity: Number(row.stock), + price: parseIrtInput(row.price), + stockQuantity: row.unlimitedStock + ? null + : row.stock.trim() + ? Number(row.stock) + : null, })), removedIds, }) @@ -222,7 +233,9 @@ export function EditStoreItemsModal({ return createPortal(
{ + if (!isSubmitting) onClose() + }} >
@@ -263,6 +277,7 @@ export function EditStoreItemsModal({ ))} {t('storeItems.create.price')} {t('storeItems.create.stock')} + {t('storeItems.create.unlimited')}
@@ -300,7 +315,7 @@ export function EditStoreItemsModal({ className={rowStyles.compactInput} value={row.price} onChange={(e) => updateRow(row.id, { price: formatIrtInput(e.target.value) })} - placeholder="0" + placeholder={t('storeItems.price.contact')} disabled={isSubmitting} /> updateRow(row.id, { stock: e.target.value })} - disabled={isSubmitting} + disabled={isSubmitting || row.unlimitedStock} + placeholder={row.unlimitedStock ? '∞' : '0'} /> +
+ + updateRow(row.id, { unlimitedStock: e.target.checked }) + } + disabled={isSubmitting} + aria-label={t('storeItems.create.unlimited')} + /> +
+
+ +
)} diff --git a/apps/business/src/components/StoreItemCard.module.css b/apps/business/src/components/StoreItemCard.module.css index 1b59bf6..f8f93b5 100644 --- a/apps/business/src/components/StoreItemCard.module.css +++ b/apps/business/src/components/StoreItemCard.module.css @@ -52,7 +52,15 @@ .imagePlaceholder { width: 100%; height: 100%; - background: rgba(148, 163, 184, 0.08); + display: flex; + align-items: center; + justify-content: center; + color: rgba(148, 163, 184, 0.75); + background: linear-gradient( + 135deg, + rgba(148, 163, 184, 0.14) 0%, + rgba(148, 163, 184, 0.05) 100% + ); } .festivalBadge, diff --git a/apps/business/src/components/StoreItemCard.tsx b/apps/business/src/components/StoreItemCard.tsx index 06cab06..7f31fc8 100644 --- a/apps/business/src/components/StoreItemCard.tsx +++ b/apps/business/src/components/StoreItemCard.tsx @@ -1,5 +1,5 @@ -import { useId } from 'react' -import { Pencil, Percent, Sparkles, Trash2 } from 'lucide-react' +import { useEffect, useId, useState } from 'react' +import { ImageOff, Pencil, Percent, Sparkles, Trash2 } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import { @@ -80,20 +80,30 @@ export function StoreItemCard({ ? listing.productTitle : '' : listing.productNameFa + const imageSrc = listing.productImage?.trim() || '' + const [imageFailed, setImageFailed] = useState(false) + const showImage = Boolean(imageSrc) && !imageFailed + + useEffect(() => { + setImageFailed(false) + }, [imageSrc]) return (