Réserver une dégustation

Starter T-IN · guide des composants

Boutique

Une boutique sans paiement en ligne : le visiteur compose son panier dans son navigateur, envoie sa commande par le formulaire, et règle au retrait. Aucun backend, aucune donnée bancaire, aucun stock à tenir. Tout se règle dans le bloc `boutique` de src/data/site.ts et la collection `produits` — un site vitrine peut retirer l'ensemble en trois minutes (voir README).

CarteProduit

Un article : packshot carré sur aplat, rayon, nom, prix, et la liste des choix quand l'article en propose (« taille, coloris au choix »). Toute la carte est un seul lien — le choix des options se fait sur la fiche, jamais dans une grille, sinon deux cibles cliquables se disputent le même geste. Un article indisponible garde sa carte et reçoit un bandeau. Le libellé du rayon vient de lib/rayons.ts (ou de la prop rayons), l'aplat de boutique.fondPackshot (ou de la variable CSS --produit-fond). Props : produit, heading, loading, rayons, fond, labels.

Voir le code
---
import CarteProduit from '../components/CarteProduit.astro';
import { lireProduits } from '../lib/boutique';
const produits = await lireProduits();
---

<CarteProduit produit={produits[0]} heading="h3" loading="eager" />

<!-- autre vocabulaire de rayons, autre aplat -->
<CarteProduit produit={produit} rayons={MES_RAYONS} fond="var(--paper-soft)" />

GrilleBoutique — le catalogue marchand

Le pendant clair et marchand de GrilleProduits : celui-là met une gamme en scène sur fond de nuit, celui-ci sert à trouver un article et à l'acheter. Filtre par rayon en îlot — sans JavaScript toutes les cartes sont dans le HTML, la page reste complète et indexable ; avec, une vingtaine de lignes basculent hidden et aria-pressed, et un compteur role=status annonce le résultat. La barre disparaît d'elle-même sous deux rayons représentés. filtres={false} donne une simple grille : sélection d'accueil, bandeau « à voir aussi » en bas d'une fiche.

Voir le code
<GrilleBoutique produits={produits} />

<!-- simple grille, sans filtres, titres en h3 -->
<GrilleBoutique produits={autres} filtres={false} heading="h3" colonnes={4} />

<!-- vocabulaire et libellés sur mesure -->
<GrilleBoutique
  produits={produits}
  rayons={MES_RAYONS}
  labels={{ tout: 'Tout le domaine', filtrer: 'Filtrer par rayon',
            compteUn: '{n} article affiché', comptePlusieurs: '{n} articles affichés' }}
/>

AjoutPanier

Le formulaire d'une fiche produit : une liste par option déclarée dans le contenu (millésime, taille, coloris…), une quantité plafonnée par boutique.quantiteMax, un bouton. Les choix partent dans le panier déjà mis en phrase (« Millésime : 2022 ») : le stockage du navigateur ne contient que du texte et des quantités. La confirmation s'annonce en role=status avec un lien vers le panier. Sans JavaScript, pas de bouton qui ne fait rien : un renvoi vers le contact. Un article indisponible n'a pas de formulaire du tout. Props : produit, hrefPanier, hrefContact, labels.

Article disponible, avec une option et une quantité :

Article retiré de la vente (disponible: false) :

Cet article n’est plus disponible.

Voir le code
<AjoutPanier produit={produit} />

<!-- libellés sur mesure -->
<AjoutPanier
  produit={produit}
  labels={{ ajouter: 'Réserver cet article', quantite: 'Nombre' }}
/>

CompteurPanier

« Panier (3) », tenu à jour à chaque ajout, y compris depuis un autre onglet (événement storage). Rendu en lien sur les pages boutique ; avec lien={false} il ne rend rien de visible et se contente de mettre à jour les cibles déjà dans la page — posé dans Base.astro, il transforme le CTA de l'en-tête en panier (ajouter data-panier-cta="Panier" au lien de Header.astro et régler site.cta). Toute cible [data-panier-cta] reçoit « Libellé (n) » + un aria-label rédigé, tout [data-panier-compteur] le nombre seul. Le texte passe par textContent : jamais d'innerHTML avec une donnée du localStorage. Il vit dans son propre composant, et non dans le layout, pour qu'un site vitrine n'embarque pas le JavaScript du panier sur chacune de ses pages.

Panier

Ajoutez un article ci-dessus : le libellé se met à jour sans rechargement. Pastille seule : 0 article(s).

Voir le code
<!-- sur une page boutique : un lien visible -->
<CompteurPanier />

<!-- dans Base.astro : îlot silencieux qui tient l'en-tête à jour -->
<CompteurPanier lien={false} />

<!-- une pastille ailleurs dans la page -->
<span data-panier-compteur>0</span>

PanierVue

Le contenu du panier : lignes modifiables, sous-totaux, total, passage à la commande, et l'état vide rédigé. Le panier ne retient que { id, options, qte } — noms, prix, vignettes et disponibilités sont relus dans le catalogue du build, déposé par <Catalogue /> (inclus par le composant) : un prix modifié à la main dans le stockage du navigateur n'a aucun effet, et un article retiré de la vente disparaît du panier au premier rechargement. Les lignes sont clonées d'un <template> et remplies par textContent, jamais par innerHTML. La page /panier/ n'est plus qu'un appel à ce composant.

Voir le code
---
import PanierVue from '../components/PanierVue.astro';
---

<Base title="Votre panier" description="…" noindex>
  <PageHeader eyebrow="La boutique" title="Votre panier" />
  <section class="container"><PanierVue /></section>
</Base>

<!-- liens et libellés sur mesure -->
<PanierVue hrefBoutique="/la-cave/" note={null} labels={{ commander: 'Réserver' }} />

CommandeForm

La commande sans paiement en ligne : coordonnées, mode de retrait, récapitulatif, et c'est tout. Deux modes comme ContactForm, selon site.formEndpoint : POST amélioré en fetch avec écran de remerciement et panier vidé, ou repli mailto: qui ouvre la commande déjà rédigée dans la messagerie du visiteur (le panier est alors conservé — on ne vide pas un panier dont on ne sait pas si le courriel est parti). Le récapitulatif envoyé est recomposé depuis le catalogue du build, jamais depuis le localStorage. Pot-de-miel anti-spam. La case de majorité n'apparaît que si le panier contient un article marqué alcool. Modes de retrait, mention de paiement, lien de CGV et mention d'alcool viennent du bloc boutique ou des props.

Voir le code
---
import CommandeForm from '../components/CommandeForm.astro';
---

<CommandeForm />

<!-- retraits, CGV et objet du courriel sur mesure -->
<CommandeForm
  retraits={['Retrait à la boutique', 'Retrait sur le marché du samedi']}
  cgv={{ href: '/conditions-de-vente/', label: 'conditions de vente' }}
  sujet="Nouvelle commande — boutique"
/>

// src/data/site.ts
formEndpoint: 'https://formspree.io/f/xxxxxxx',

Les données : collection, rayons, réglages

Rien de marchand n'est codé dans un composant. Un article est un fichier Markdown de src/content/produits/ ; les rayons sont un objet de src/lib/rayons.ts dont le schéma tire son enum (un rayon inexistant casse le build, pas la page) ; tout le reste — clé du panier, devise, modes de retrait, mention de paiement, CGV, mention d'alcool — vit dans le bloc boutique de src/data/site.ts. Le champ images accepte les deux écritures d'Img.astro : chemin public/ (les placeholders du starter) ou chemin relatif vers src/assets/, optimisé au build.

Clé du panier
trois-pierres:panier
Racine des fiches
/boutique/<id>/
Devise · locale
EUR · fr-FR
Quantité maximale
20
Aplat des packshots
#ffffff
Modes de retrait
Retrait au caveau, du mardi au samedi · Retrait sur un salon ou un marché, sur rendez-vous
Paiement
Aucun paiement en ligne : vous réglez au moment du retrait.
Articles au catalogue
4
Voir le code
// src/lib/rayons.ts — le seul endroit où les rayons sont nommés
export const RAYONS = {
  vins: 'Les vins',
  coffrets: 'Coffrets',
  'art-de-la-table': 'Art de la table',
} as const;

---
# src/content/produits/les-trois-pierres.md
nom: Meursault « Les Trois Pierres »
rayon: vins
prix: 24
accroche: Le chardonnay de haut de coteau, dix mois de fût.
images:
  - src: ../../assets/produits/trois-pierres.jpg   # ou /images/packshot-1.svg
    alt: Bouteille de la cuvée Les Trois Pierres
options:
  - nom: Millésime
    valeurs: ['2022', '2021']
details:
  - Élevage 10 mois en fût
alcool: true
ordre: 10
disponible: true
---
Le corps du fichier est le texte long de la fiche.
Guide