import { UIEvent, useEffect, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { fetchProduct, formatPrice, ORIGIN_LABELS, ProductDetail as ProductDetailType, } from '../api' import EmptyState from '../components/EmptyState' import { IconExternal, IconLeaf } from '../components/Icons' import OriginBadge from '../components/OriginBadge' import ProductCard from '../components/ProductCard' import Skeleton from '../components/Skeleton' import StoreLogo from '../components/StoreLogo' const DESCRIPTION_CLAMP = 280 export default function ProductDetail() { const { uid } = useParams<{ uid: string }>() const [product, setProduct] = useState(null) const [error, setError] = useState(false) const [activeImage, setActiveImage] = useState(0) const [descExpanded, setDescExpanded] = useState(false) const trackRef = useRef(null) useEffect(() => { if (!uid) return const controller = new AbortController() setProduct(null) setError(false) setActiveImage(0) setDescExpanded(false) fetchProduct(uid, controller.signal) .then(setProduct) .catch((err: unknown) => { if (err instanceof DOMException && err.name === 'AbortError') return setError(true) }) window.scrollTo({ top: 0 }) return () => controller.abort() }, [uid]) function onTrackScroll(e: UIEvent) { const el = e.currentTarget if (el.clientWidth === 0) return const idx = Math.round(el.scrollLeft / el.clientWidth) if (idx !== activeImage) setActiveImage(idx) } function goToImage(i: number) { const el = trackRef.current if (!el) return el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }) setActiveImage(i) } if (error) { return (
Retour aux produits
) } if (!product) { return (
) } const images = product.images const hasDiscount = product.compare_at_price !== null && product.price !== null && product.compare_at_price > product.price const originLabel = ORIGIN_LABELS[product.origin_class] ?? '' const description = product.description ?? '' const descIsLong = description.length > DESCRIPTION_CLAMP return (
{images.length > 0 ? ( <>
{images.map((img, i) => (
))}
{images.length > 1 && ( <>
{images.map((img, i) => ( ))}
)} ) : (
)}
{!product.available && ( Non disponible )}

{product.title}

{product.price !== null ? ( {formatPrice(product.price)} {product.price_max !== null && product.price_max > product.price && ` – ${formatPrice(product.price_max)}`} ) : ( Prix affiché en boutique )} {hasDiscount && ( {formatPrice(product.compare_at_price)} )}
{description && (

{description}

{descIsLong && ( )}
)}
{product.vendor && (
Marque
{product.vendor}
)} {product.category && (
Catégorie
{product.category}
)} {product.product_type && (
Type
{product.product_type}
)} {originLabel && (
Origine
{originLabel}
)}
{product.tags.length > 0 && (
{product.tags.map((t) => ( {t} ))}
)} Voir chez {product.store_name}
Boutique
{product.store_name}

{[product.store_city, product.store_region] .filter(Boolean) .join(', ') || 'Québec'}

{product.related.length > 0 && (

Produits similaires

{product.related.map((p) => (
))}
)} {/* Mobile sticky CTA (safe-area aware) */}
{product.price !== null ? ( {formatPrice(product.price)} ) : ( Prix en boutique )}
Voir chez {product.store_name}
) } function GalleryImage({ src, alt, eager, }: { src: string alt: string eager?: boolean }) { const [failed, setFailed] = useState(false) if (failed) { return ( ) } return ( {alt} setFailed(true)} /> ) }