SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
5 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%
5.5 KB · 89 lines tsx
Raw Blame History
1// ---------------------------------------------------------------------------2// Fabri-Ka — Agrégateur de produits fabriqués au Québec3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/PriceCard.tsx : « Prix et déclinaisons » — KPI (prix, prix barré ou5//   fourchette, disponibilité), options (tailles, couleurs, formats) en puces,6//   liste des déclinaisons avec prix et rupture (8 + « Voir les N »), note7//   moyenne, méthodologie. Toutes les données viennent de la boutique.8// ---------------------------------------------------------------------------9import { useState } from 'react'10import { ProductDetail, ProductOption, formatPrice } from '../api'11import { Ico } from '../components/KaIcons'12import { rabais, variantsOf } from './synthese'13import { Accordion, MoreButton, SectionCard, StatTile, NBSP } from './ui'1415const optName = (o: ProductOption) => o.name || o.title || ''16const optValues = (o: ProductOption) => (o.values || o.selections || []).filter(Boolean)1718export default function PriceCard({ p }: { p: ProductDetail }) {19  const [all, setAll] = useState(false)20  const det = p.details ?? {}21  const r = rabais(p)22  const options = (det.options ?? []).filter((o) => optName(o) && optValues(o).length > 0)23  const variants = variantsOf(p)24  const LIMIT = 825  const dispo = !p.available ? 'Non disponible' : det.low_stock_remaining ? `Plus que ${det.low_stock_remaining}` : det.is_on_backorder ? 'Précommande' : det.inventory_quantity != null && det.inventory_quantity > 0 ? `${det.inventory_quantity} en stock` : det.availability_text || 'Disponible'26  const rating = det.average_rating27  return (28    <SectionCard id="prix" title="Prix et déclinaisons" icon={<Ico name="scale" size={18} />}>29      <div className="fb-kpis cols-3">30        <StatTile accent value={p.price != null ? formatPrice(p.price, p.currency) : (p.price_on_request ? 'Sur devis' : '—')} label={p.price != null ? (r ? 'Prix en solde' : 'Prix en boutique') : 'Prix non publié en ligne'} />31        {r ? <StatTile value={formatPrice(r.avant, p.currency)} label={`Prix régulier · −${r.pct}${NBSP}%`} />32          : p.price_max != null && p.price != null && p.price_max > p.price ? <StatTile value={`${formatPrice(p.price, p.currency)} – ${formatPrice(p.price_max, p.currency)}`} label="Fourchette selon la déclinaison" />33          : <StatTile value={variants.length > 1 ? variants.length : 1} label={variants.length > 1 ? 'Déclinaisons' : 'Déclinaison unique'} />}34        <StatTile value={dispo} label="Disponibilité (boutique)" />35      </div>36      {rating != null && rating > 0 && (37        <p className="fb-note good" style={{ marginTop: 12 }}>38          <b>Note {rating.toFixed(1).replace('.', ',')} / 5</b>{det.review_count ? ` — ${det.review_count} avis publiés sur la boutique` : ' — note publiée par la boutique'}39        </p>40      )}41      {options.length > 0 && (42        <>43          <h3 className="fb-card-sub fb-subtitle">Options</h3>44          {options.map((o) => (45            <div key={optName(o)} style={{ marginBottom: 10 }}>46              <div className="fb-fine" style={{ margin: '0 0 6px', fontWeight: 600, color: 'var(--fb-text-2)' }}>{optName(o)}</div>47              <div className="fb-tags">48                {optValues(o).slice(0, 18).map((v) => <span className="fb-tag n" key={v}>{v}</span>)}49                {optValues(o).length > 18 && <span className="fb-tag n">+{optValues(o).length - 18}</span>}50              </div>51            </div>52          ))}53        </>54      )}55      {variants.length > 1 && (56        <>57          <h3 className="fb-card-sub fb-subtitle">Déclinaisons <small>· {variants.length}{det.variations && det.variations > variants.length ? ` sur ${det.variations}` : ''}</small></h3>58          <ul className="fb-list">59            {(all ? variants : variants.slice(0, LIMIT)).map((v, i) => {60              const vr = v.compare_at_price != null && v.price != null && v.compare_at_price > v.price && v.compare_at_price <= v.price * 1061              return (62                <li className="fb-item" key={`${v.title || v.sku}-${i}`}>63                  <div className="fb-item-main">64                    <div className="fb-item-t">{v.title || v.sku}</div>65                    <div className="fb-item-s">{[v.sku && v.sku !== v.title ? `SKU ${v.sku}` : null, v.grams ? `${v.grams} g` : null, v.available === false ? 'épuisé' : null].filter(Boolean).join(' · ')}</div>66                  </div>67                  <div className="fb-item-r">68                    <div className="fb-item-v">{v.price != null ? formatPrice(v.price, p.currency) : '—'}</div>69                    {vr && <div className="fb-item-m" style={{ textDecoration: 'line-through' }}>{formatPrice(v.compare_at_price!, p.currency)}</div>}70                  </div>71                </li>72              )73            })}74          </ul>75          {variants.length > LIMIT && <MoreButton onClick={() => setAll(!all)} expanded={all}>{all ? 'Réduire' : `Voir les ${variants.length} déclinaisons`}</MoreButton>}76        </>77      )}78      <Accordion title="Méthodologie" small>79        <p>80          Prix, prix barré, options, déclinaisons et disponibilité sont ceux publiés par la boutique (Shopify, WooCommerce,81          Wix, Squarespace…) lors de la dernière synchronisation Fabri-Ka. Un prix barré supérieur à dix fois le prix est82          ignoré (saisie en cents chez certains marchands). Le prix final, les taxes et la livraison se confirment sur la83          boutique.84        </p>85      </Accordion>86    </SectionCard>87  )88}89