Files
dashboards/apps/business/src/components/StoreItemCard.tsx
T
Alireza HassaniandCursor 77080a7a4b Improve website settings and special-item pick UX.
Tighten sitemap/settings controls, debounce product search to 10 results, keep catalog-only specials visible after add, and use frosted glass on link/category modals.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 20:38:58 +03:30

216 lines
6.5 KiB
TypeScript

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 {
formatVariantCount,
type StoreProductListing,
} from '../utils/storeProductGroups'
import { StoreItemPrice } from './StoreItemPrice'
import { Tooltip } from './Tooltip'
import styles from './StoreItemCard.module.css'
interface StoreItemCardProps {
listing: StoreProductListing
onOpen: (listing: StoreProductListing) => void
onEdit: (listing: StoreProductListing) => void
onDiscount: (listing: StoreProductListing) => void
onFestival: (listing: StoreProductListing) => void
onRemove: (listing: StoreProductListing) => void
onAddToCart: (listing: StoreProductListing) => void
removeTooltip?: string
}
function GradientPlusIcon({ gradientId }: { gradientId: string }) {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={styles.addToCartIcon}
>
<defs>
<linearGradient
id={gradientId}
x1="4"
y1="4"
x2="20"
y2="20"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#22c55e" />
<stop offset="0.5" stopColor="var(--primary)" />
<stop offset="1" stopColor="#a855f7" />
</linearGradient>
</defs>
<path
d="M12 5v14M5 12h14"
stroke={`url(#${gradientId})`}
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function StoreItemCard({
listing,
onOpen,
onEdit,
onDiscount,
onFestival,
onRemove,
onAddToCart,
removeTooltip,
}: StoreItemCardProps) {
const t = useT()
const { locale } = useLocale()
const isFa = locale === 'fa'
const plusGradientId = `add-cart-gradient-${useId().replace(/:/g, '')}`
const removeLabel = removeTooltip ?? t('storeItems.card.remove')
const primaryName = isFa
? listing.productNameFa || listing.productTitle
: listing.productTitle
const secondaryName = isFa
? listing.productNameFa
? listing.productTitle
: ''
: listing.productNameFa
const imageSrc = listing.productImage?.trim() || ''
const [imageFailed, setImageFailed] = useState(false)
const showImage = Boolean(imageSrc) && !imageFailed
useEffect(() => {
setImageFailed(false)
}, [imageSrc])
return (
<article className={styles.card}>
<button
type="button"
className={styles.clickable}
onClick={() => {
if (listing.variantCount > 0) onOpen(listing)
}}
disabled={listing.variantCount === 0}
>
<div className={styles.imageWrap}>
{showImage ? (
<img
src={imageSrc}
alt={primaryName}
className={styles.image}
loading="lazy"
onError={() => setImageFailed(true)}
/>
) : (
<div className={styles.imagePlaceholder} aria-hidden="true">
<ImageOff size={28} strokeWidth={1.5} />
</div>
)}
{listing.showFestival && (
<span className={styles.festivalBadge}>{t('storeItems.card.festival')}</span>
)}
{listing.productTotalStock > 0 && (
<span className={styles.stockBadge}>
{t('storeItems.card.inStock', { count: listing.productTotalStock })}
</span>
)}
</div>
<div
className={`${styles.body}${isFa ? '' : ` ${styles.bodyLtr}`}`}
dir={isFa ? 'rtl' : 'ltr'}
>
{isFa ? (
<>
<h3 className={styles.nameFa}>{primaryName}</h3>
{secondaryName ? <p className={styles.nameEn}>{secondaryName}</p> : null}
</>
) : (
<>
<h3 className={styles.nameEnPrimary}>{primaryName}</h3>
{secondaryName ? (
<p className={styles.nameFaSecondary}>{secondaryName}</p>
) : null}
</>
)}
{listing.variantCount > 0 ? (
<p className={styles.variantLabel}>
{formatVariantCount(listing.variantCount, locale)}
</p>
) : null}
<StoreItemPrice
price={listing.displayPrice}
discountedPrice={listing.displayDiscountedPrice}
/>
</div>
</button>
<div className={styles.controls} onClick={(e) => e.stopPropagation()}>
<div className={styles.controlsLeft}>
{listing.variantCount > 0 ? (
<>
<Tooltip label={t('storeItems.card.edit')}>
<button
type="button"
onClick={() => onEdit(listing)}
aria-label={t('storeItems.card.edit')}
>
<Pencil size={16} />
</button>
</Tooltip>
<Tooltip label={t('storeItems.card.discount')}>
<button
type="button"
onClick={() => onDiscount(listing)}
aria-label={t('storeItems.card.discount')}
>
<Percent size={16} />
</button>
</Tooltip>
<Tooltip label={t('storeItems.card.festivalPoints')}>
<button
type="button"
className={listing.showFestival ? styles.festivalActive : undefined}
onClick={() => onFestival(listing)}
aria-label={t('storeItems.card.festivalPoints')}
>
<Sparkles size={16} />
</button>
</Tooltip>
</>
) : null}
<Tooltip label={removeLabel}>
<button
type="button"
className={styles.danger}
onClick={() => onRemove(listing)}
aria-label={removeLabel}
>
<Trash2 size={16} />
</button>
</Tooltip>
</div>
{listing.variantCount > 0 ? (
<Tooltip label={t('storeItems.card.addToCart')}>
<button
type="button"
className={styles.addToCartBtn}
onClick={() => onAddToCart(listing)}
aria-label={t('storeItems.card.addToCart')}
>
<GradientPlusIcon gradientId={plusGradientId} />
</button>
</Tooltip>
) : null}
</div>
</article>
)
}