Files
playX/src/features/goods/GoodsCategoryList.tsx
2026-04-10 09:27:11 +08:00

132 lines
5.6 KiB
TypeScript

import {useState} from 'react'
import {ChevronRight} from 'lucide-react'
import Button from '@/components/button'
import {HOME_CATEGORY_META_MAP} from '@/constant'
import type {ProductCategory, ProductItem} from '@/types'
type GoodsCategoryListProps = {
categories: ProductCategory[]
loading?: boolean
emptyText?: string
showMore?: boolean
onMoreClick?: (categoryId: ProductCategory['id']) => void
onRedeem: (product: ProductItem, categoryId: ProductCategory['id']) => void
}
function GoodsImage({
imageUrl,
}: {
imageUrl?: string
}) {
const [hasError, setHasError] = useState(false)
const showFallback = !imageUrl || hasError
return (
<div className="relative h-[116px] w-full overflow-hidden rounded-t-[10px] sm:h-[128px]">
{showFallback ? (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[radial-gradient(circle_at_top,rgba(250,106,0,0.18),transparent_58%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-[12px] text-center">
<div className="h-[30px] w-[30px] rounded-[10px] border border-white/10 bg-white/6"></div>
<div className="mt-[10px] text-[12px] tracking-[0.08em] text-white/42">NO IMAGE</div>
</div>
) : null}
{imageUrl ? (
<img
src={imageUrl}
alt=""
className={`h-full w-full object-cover ${showFallback ? 'hidden' : 'block'}`}
onError={() => setHasError(true)}
/>
) : null}
</div>
)
}
export function GoodsCategoryList({
categories,
loading = false,
emptyText = 'No goods available yet.',
showMore = false,
onMoreClick,
onRedeem,
}: GoodsCategoryListProps) {
if (loading) {
return (
<div className="pb-[24px] mt-[20px]">
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
Loading ...
</div>
</div>
)
}
if (!categories.length) {
return (
<div className="pb-[24px]">
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
{emptyText}
</div>
</div>
)
}
return (
<div className="pb-[24px]">
{categories.map((category) => {
const CategoryIcon = HOME_CATEGORY_META_MAP[category.id].icon
return (
<div key={category.id} className="mt-[20px]">
<div className="mb-[10px] flex items-center justify-between gap-[12px]">
<div className="flex min-w-0 items-center gap-[10px]">
<div
className="flex h-[36px] w-[36px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
<CategoryIcon className="h-[18px] w-[18px]" aria-hidden="true"/>
</div>
<div className="truncate text-[15px] font-semibold text-white">{category.name}</div>
</div>
{showMore && onMoreClick ? (
<button
type="button"
className="flex shrink-0 items-center gap-[3px] text-[12px] font-light text-[#FA6A00] underline cursor-pointer"
onClick={() => onMoreClick(category.id)}
>
more
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true"/>
</button>
) : null}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
{category.items.map((product) => (
<div
key={product.id}
className="liquid-glass-bg flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
>
<GoodsImage imageUrl={product.imageUrl}/>
<div
className="flex flex-1 flex-col items-start justify-between gap-[12px] p-[12px] sm:p-[14px]"
>
<div className="space-y-[4px]">
<div className="text-[16px] font-medium text-white">{product.title}</div>
<div className="text-[13px] text-white/52">{product.subtitle}</div>
</div>
<div className="text-[16px] font-semibold text-[#FA6A00]">{product.score}</div>
<Button
className="h-[36px] w-full text-[13px]"
onClick={() => onRedeem(product, category.id)}
>
{product.ctaLabel}
</Button>
</div>
</div>
))}
</div>
</div>
)
})}
</div>
)
}