// --------------------------------------------------------------------------- // Fabri-Ka — Agrégateur de produits fabriqués au Québec // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/ProductHero.tsx : héro de la fiche — boutique (logo) · lieu, prix + // prix barré + fourchette, capsules (origine, solde, dispo), titre (H1) + // résumé, galerie (balayage natif, compteur, plein écran, vignettes ≥ 768), // actions (favoris · partager · Voir chez la boutique). Ordre DOM = visuel. // --------------------------------------------------------------------------- import { useEffect, useRef, useState } from 'react' import { ProductDetail, formatPrice } from '../api' import { Ico, IcoHeart } from '../components/KaIcons' import OriginBadge from '../components/OriginBadge' import StoreLogo from '../components/StoreLogo' import { ligneResume, rabais } from './synthese' function Lightbox({ images, start, titre, onClose }: { images: string[]; start: number; titre: string; onClose: () => void }) { const [idx, setIdx] = useState(start) const track = useRef(null) useEffect(() => { track.current?.scrollTo({ left: start * track.current.clientWidth }) document.documentElement.classList.add('ka-scroll-lock') const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() if (e.key === 'ArrowRight') go(1) if (e.key === 'ArrowLeft') go(-1) } window.addEventListener('keydown', onKey) return () => { document.documentElement.classList.remove('ka-scroll-lock'); window.removeEventListener('keydown', onKey) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const go = (d: number) => { const el = track.current; if (!el) return const i = Math.max(0, Math.min(images.length - 1, Math.round(el.scrollLeft / el.clientWidth) + d)) el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }) } return (
{idx + 1} / {images.length}
{ const el = track.current; if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth)) }}> {images.map((u, i) => (
{`${titre}
))}
{idx > 0 && } {idx < images.length - 1 && }
) } export function ProductGallery({ images, titre }: { images: string[]; titre: string }) { const [idx, setIdx] = useState(0) const [zoom, setZoom] = useState(false) const [dead, setDead] = useState>(new Set()) const track = useRef(null) const alive = images.filter((u) => !dead.has(u)) const cur = Math.min(idx, Math.max(0, alive.length - 1)) const goto = (i: number) => track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: 'smooth' }) if (alive.length === 0) return
Aucune image fournie par la boutique
return ( <>
{ const el = track.current; if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth)) }}> {alive.map((u, i) => ( {`${titre} setDead((d) => new Set(d).add(u))} onClick={() => setZoom(true)} /> ))}
{alive.length > 1 && {cur + 1} / {alive.length}} {cur > 0 && } {cur < alive.length - 1 && }
{alive.length > 1 && (
{alive.slice(0, 12).map((u, i) => ( ))}
)} {zoom && setZoom(false)} />} ) } export default function ProductHero({ p, fav, onFav, onShare, actionsRef }: { p: ProductDetail; fav: boolean; onFav: () => void; onShare: () => void; actionsRef: React.RefObject }) { const r = rabais(p) const resume = ligneResume(p) return (
{p.store_name}{p.store_city || p.store_region ? ` — ${[p.store_city, p.store_region].filter(Boolean).join(', ')}` : ''}
{p.price != null ? ( <> {formatPrice(p.price, p.currency)} {p.price_max != null && p.price_max > p.price && à {formatPrice(p.price_max, p.currency)}} {r && {formatPrice(r.avant, p.currency)}} ) : ( {p.price_on_request ? 'Sur devis' : 'Prix affiché en boutique'} )}
{r && Solde−{r.pct} %} {!p.available && Non disponible} {p.currency && p.currency !== 'CAD' && Devise {p.currency}} {p.ka_reco && Recommandé pour vous}

{p.title} {resume.length > 0 && {resume.join(' · ')}}

) }