22
33import Image from "next/image" ;
44import { useState } from "react" ;
5- import { ExternalLink } from "lucide-react" ;
5+ import { ArrowUpRight } from "lucide-react" ;
66import type { Product } from "@/lib/types" ;
77import { RetailerIcon , retailerLabel } from "./retailer-icon" ;
88
@@ -11,54 +11,51 @@ interface ProductCardProps {
1111 eagerImage ?: boolean ;
1212}
1313
14+ // Compact, fully-clickable product tile built for a grid — scales cleanly
15+ // from a few products to dozens. The whole card links through the redirect
16+ // service (/go/{slug}) so clicks are logged and affiliate-tagged.
1417export function ProductCard ( { product, eagerImage = false } : ProductCardProps ) {
1518 const [ imgFailed , setImgFailed ] = useState ( false ) ;
1619 const imgSrc = imgFailed ? "/placeholder-product.svg" : product . imageUrl ;
17-
18- // Shop via the redirect service so the click is logged and the affiliate
19- // tag is applied. /go/{slug} is rewritten to shoplit-redirect (see
20- // next.config.mjs). Fall back to the raw URL if a slug is somehow missing.
2120 const shopHref = product . goSlug ? `/go/${ product . goSlug } ` : product . originalUrl ;
2221
2322 return (
24- < article className = "group rounded-xl overflow-hidden border border-rule bg-cream transition-shadow hover:shadow-md" >
25- < a
26- href = { shopHref }
27- target = "_blank"
28- rel = "noopener noreferrer sponsored"
29- aria-label = { `Shop ${ product . title } on ${ retailerLabel ( product . retailer ) } ` }
30- >
31- < div className = "relative aspect-square bg-paper" >
32- < Image
33- src = { imgSrc }
34- alt = { product . title }
35- fill
36- sizes = "(max-width: 768px) 100vw, 50vw"
37- priority = { eagerImage }
38- loading = { eagerImage ? "eager" : "lazy" }
39- className = "object-cover transition-transform duration-300 group-hover:scale-[1.02]"
40- unoptimized
41- onError = { ( ) => setImgFailed ( true ) }
42- />
43- </ div >
44- </ a >
45- < div className = "p-5 sm:p-6" >
46- < h3 className = "font-serif text-xl leading-tight mb-1" > { product . title } </ h3 >
23+ < a
24+ href = { shopHref }
25+ target = "_blank"
26+ rel = "noopener noreferrer sponsored"
27+ aria-label = { `Shop ${ product . title } on ${ retailerLabel ( product . retailer ) } ` }
28+ className = "group flex flex-col rounded-xl overflow-hidden border border-rule bg-cream transition-all hover:shadow-md hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
29+ >
30+ < div className = "relative aspect-square bg-paper" >
31+ < Image
32+ src = { imgSrc }
33+ alt = { product . title }
34+ fill
35+ sizes = "(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 300px"
36+ priority = { eagerImage }
37+ loading = { eagerImage ? "eager" : "lazy" }
38+ className = "object-cover transition-transform duration-300 group-hover:scale-[1.03]"
39+ unoptimized
40+ onError = { ( ) => setImgFailed ( true ) }
41+ />
42+ < span className = "absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-cream/90 backdrop-blur px-2 py-0.5 text-[10px] font-medium text-ink" >
43+ < RetailerIcon retailer = { product . retailer } size = { 11 } />
44+ { retailerLabel ( product . retailer ) }
45+ </ span >
46+ </ div >
47+ < div className = "flex flex-1 flex-col p-3 sm:p-4" >
48+ < h3 className = "font-serif text-sm sm:text-base leading-snug line-clamp-2 mb-1" > { product . title } </ h3 >
4749 { product . note && (
48- < p className = "italic text-sm text-muted mb-2 leading-relaxed " > “{ product . note } ”</ p >
50+ < p className = "italic text-xs text-muted line-clamp-1 mb-1.5 " > “{ product . note } ”</ p >
4951 ) }
50- { product . priceText && < p className = "text-sm text-muted mb-4" > { product . priceText } </ p > }
51- < a
52- href = { shopHref }
53- target = "_blank"
54- rel = "noopener noreferrer sponsored"
55- className = "inline-flex w-full items-center justify-center gap-2 rounded-full bg-accent text-cream font-medium py-3 px-4 transition-opacity hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-cream"
56- >
57- < RetailerIcon retailer = { product . retailer } size = { 16 } />
58- Shop on { retailerLabel ( product . retailer ) }
59- < ExternalLink size = { 14 } aria-hidden />
60- </ a >
52+ < div className = "mt-auto flex items-center justify-between pt-2" >
53+ < span className = "text-sm font-medium text-ink" > { product . priceText || " " } </ span >
54+ < span className = "inline-flex items-center gap-0.5 text-xs font-medium text-accent transition-all group-hover:gap-1.5" >
55+ Shop < ArrowUpRight size = { 14 } aria-hidden />
56+ </ span >
57+ </ div >
6158 </ div >
62- </ article >
59+ </ a >
6360 ) ;
6461}
0 commit comments