Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
1// ---------------------------------------------------------------------------2// Fabri-Ka — Agrégateur de produits fabriqués au Québec3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/ProductFacts.tsx : « Le produit » — grille icône/valeur (marque,5// catégorie, type, origine, poids, dimensions, SKU), fiche technique6// (attributs) en rangées, description tronquée, informations additionnelles7// (matériaux, entretien…) en accordéons, étiquettes.8// ---------------------------------------------------------------------------9import { ReactNode, useState } from 'react'10import { CATEGORY_LABELS, ProductDetail, STORE_KIND_LABELS } from '../api'11import { Ico } from '../components/KaIcons'12import { Accordion, MoreButton, SectionCard } from './ui'13import { origineLabel } from './synthese'1415export default function ProductFacts({ p }: { p: ProductDetail }) {16 const [open, setOpen] = useState(false)17 const det = p.details ?? {}18 const facts: { ico: ReactNode; v: string; l: string }[] = []19 if (p.vendor) facts.push({ ico: <Ico name="tag" size={17} />, v: p.vendor, l: 'Marque' })20 if (p.category) facts.push({ ico: <Ico name="layers" size={17} />, v: CATEGORY_LABELS[p.category] ?? p.category, l: 'Catégorie' })21 if (p.product_type) facts.push({ ico: <Ico name="store" size={17} />, v: p.product_type, l: 'Type' })22 if (origineLabel(p)) facts.push({ ico: <Ico name="pin" size={17} />, v: origineLabel(p), l: `Origine (classe ${p.origin_class})` })23 const poids = det.formatted_weight || (det.weight ? String(det.weight) : det.weight_grams ? `${det.weight_grams} g` : null)24 if (poids) facts.push({ ico: <Ico name="scale" size={17} />, v: poids, l: 'Poids' })25 const dims = det.formatted_dimensions || (det.dimensions ? [det.dimensions.length, det.dimensions.width, det.dimensions.height].filter(Boolean).join(' × ') : '')26 if (dims) facts.push({ ico: <Ico name="ruler" size={17} />, v: dims, l: 'Dimensions' })27 if (det.sku) facts.push({ ico: <Ico name="tag" size={17} />, v: det.sku, l: 'SKU' })28 if (det.model) facts.push({ ico: <Ico name="hammer" size={17} />, v: det.model, l: 'Modèle' })2930 const rows: [string, string][] = []31 for (const a of det.attributes ?? []) if (a.name && a.terms?.length) rows.push([a.name, a.terms.join(', ')])32 if (det.gtin) rows.push(['Code (GTIN/MPN)', det.gtin])33 if (p.store_kind && STORE_KIND_LABELS[p.store_kind]) rows.push(['Type de boutique', STORE_KIND_LABELS[p.store_kind]])34 if (det.categories?.length) rows.push(['Rayons chez la boutique', det.categories.slice(0, 6).join(' · ')])35 if (det.published_at) rows.push(['Publié par la boutique', new Date(det.published_at).toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' })])3637 const texte = (p.description || '').trim()38 const long = texte.length > 42039 const infos = det.additional_info ?? []4041 return (42 <SectionCard id="produit" title="Le produit">43 {facts.length > 0 && (44 <div className="fb-facts" role="list">45 {facts.map((x) => (46 <div className="fb-fact" role="listitem" key={x.l}>47 <span className="fb-fact-ico" aria-hidden="true">{x.ico}</span>48 <span className="fb-fact-txt"><div className="fb-fact-v" title={x.v}>{x.v}</div><div className="fb-fact-l">{x.l}</div></span>49 </div>50 ))}51 </div>52 )}53 {rows.length > 0 && (54 <div className="fb-facts-rows">55 {rows.map(([k, v]) => <div className="fb-kv" key={k}><span className="k">{k}</span><span className="v">{v}</span></div>)}56 </div>57 )}58 <h3 className="fb-card-sub fb-subtitle" id="description">Description</h3>59 {texte ? (60 <>61 <div className={`fb-desc ${long && !open ? 'clamped' : ''}`}><div className="fb-desc-body"><p>{texte}</p></div></div>62 {long && <MoreButton onClick={() => setOpen(!open)} expanded={open}>{open ? 'Réduire' : 'Lire la suite'}</MoreButton>}63 </>64 ) : <p className="fb-fine" style={{ marginTop: 0 }}>La boutique ne fournit pas de description pour ce produit.</p>}65 {infos.length > 0 && (66 <div style={{ marginTop: 10 }}>67 {infos.map((a) => <Accordion key={a.title} title={a.title} small><p style={{ whiteSpace: 'pre-line' }}>{a.description}</p></Accordion>)}68 </div>69 )}70 {p.tags.length > 0 && (71 <div className="fb-tags" style={{ marginTop: 12 }}>72 {p.tags.slice(0, 16).map((t) => <span className="fb-tag n" key={t}>{t}</span>)}73 </div>74 )}75 </SectionCard>76 )77}78