Merchants can mark items as used on create/edit, see a used badge on cards, and filter the store list for used products. Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
6.7 KiB
TypeScript
219 lines
6.7 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.isUsed && (
|
|
<span className={styles.usedBadge}>{t('storeItems.card.used')}</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>
|
|
)
|
|
}
|