* chore(web): session de polish global — version, checkbox, danger zone, icônes
- Affiche le numéro de version (package.json, injecté via Vite) en bas de
la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
IngredientPicker, UserPreferencesPage) en composants partagés
CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
restent en SVG custom (pas d'équivalents fins côté lucide).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).
* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits
- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
base (pas de migration, pas de mapping par ingrédient — cf. le
commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
wrapper Icon existant (stroke) — les deux stylent au même endroit via
CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
nouvelle entrée de nav "Crédits" (icône lucide Info).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).
* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n
- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
`name` (le libellé français, utilisé comme clé unique) devient `key`, un
slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
Le libellé lui-même déménage entièrement côté client, dans
apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
IngredientCategory/IngredientSubcategory. Migration Prisma
(rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
et composants web mis à jour en conséquence.
- apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
(regénère le fichier de traduction depuis reference-seed-data.ts).
- 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.
Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(api): les uids du catalogue sont en anglais, pas des slugs français
reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().
- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
(label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
437 ingrédients ; getEnglishKey() lève une erreur explicite si un
nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
ingrédient de reference-seed-data.ts a une entrée, et que les clés
anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
remappe les lignes déjà seedées avec un slug français (par la migration
précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
maintenant indexé par clé anglaise ("vegetarian", "eggs",
"ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
base migrée. Vérifié aussi en direct via GET /reference/diets et
/reference/allergies.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient
Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.
- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
n'a besoin d'être unique que le temps de la session de rendu, jamais
envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
ingrédient (recherche, sélection, exclusion du picker une fois
sélectionné, retrait), la création et l'édition d'une recette, et un
test de non-régression dédié qui supprime crypto.randomUUID avant le
chargement de la page (comme le ferait un vrai contexte non sécurisé)
pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
plus.
Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome
- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
key manquant, ça affichait littéralement "catalog.diets.undefined" au
lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
{key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
au lieu du message "Cette recette n'existe pas." — bug dans mon propre
test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
avaient quelques soucis de formatage/tri d'imports (des sed multi-
fichiers, pas d'édition via l'outil habituel) — corrigés par
`biome check --write`.
Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1014 lines
43 KiB
TypeScript
1014 lines
43 KiB
TypeScript
import type {
|
|
AllergenKind,
|
|
IngredientCategory,
|
|
IngredientIcon,
|
|
IngredientSubcategory,
|
|
PrismaClient,
|
|
} from "@prisma/client";
|
|
import { getEnglishKey } from "./catalog-en-keys.js";
|
|
|
|
// Short, optional-to-pick regime list — `UserProfile.dietId` stays
|
|
// nullable, this is not meant to be exhaustive. Exported for
|
|
// `apps/api/scripts/generate-catalog-i18n.ts` (one-off, regenerates
|
|
// `apps/web`'s `locales/fr/translation.json` `catalog.diets` section from
|
|
// this same list).
|
|
export const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans gluten"];
|
|
|
|
// The 14 allergens EU Regulation 1169/2011 (Annex II) requires food
|
|
// businesses to declare — a standard, defensible reference list rather than
|
|
// an invented one. Split into ALLERGY (classic IgE-mediated immune
|
|
// reaction) vs INTOLERANCE (non-immune — gluten sensitivity, sulfite
|
|
// sensitivity) per the product decision discussed in chat: only Gluten and
|
|
// Sulfites are commonly-recognized intolerances among the 14; the rest are
|
|
// true allergens.
|
|
export const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [
|
|
{ name: "Gluten", kind: "INTOLERANCE" },
|
|
{ name: "Crustacés", kind: "ALLERGY" },
|
|
{ name: "Œufs", kind: "ALLERGY" },
|
|
{ name: "Poissons", kind: "ALLERGY" },
|
|
{ name: "Arachides", kind: "ALLERGY" },
|
|
{ name: "Soja", kind: "ALLERGY" },
|
|
{ name: "Lait", kind: "ALLERGY" },
|
|
{ name: "Fruits à coque", kind: "ALLERGY" },
|
|
{ name: "Céleri", kind: "ALLERGY" },
|
|
{ name: "Moutarde", kind: "ALLERGY" },
|
|
{ name: "Graines de sésame", kind: "ALLERGY" },
|
|
{ name: "Sulfites", kind: "INTOLERANCE" },
|
|
{ name: "Lupin", kind: "ALLERGY" },
|
|
{ name: "Mollusques", kind: "ALLERGY" },
|
|
];
|
|
|
|
interface IngredientSeed {
|
|
name: string;
|
|
/**
|
|
* Generic pictogram type, overriding its group's `defaultIcon` below —
|
|
* only needed for the exceptions within a subcategory (a wedge of cheese
|
|
* inside an otherwise-milk "produits laitiers" group, a stockpot inside
|
|
* an otherwise-flour "bases" group…). See `IngredientIcon` in
|
|
* schema.prisma and `apps/web`'s `features/recipes/ingredient-icons.tsx`
|
|
* for the actual pictograms — this was a free-text emoji field until the
|
|
* product decision recorded in chat replaced it with this small, shared
|
|
* vocabulary.
|
|
*/
|
|
icon?: IngredientIcon;
|
|
allergenNames: string[];
|
|
/**
|
|
* Diet regimes this ingredient is compatible with, overriding its group's
|
|
* `defaultDiets` below — only needed for the exceptions within a
|
|
* subcategory (a fish-based stock inside an otherwise-vegan "bases"
|
|
* group, a butter-based dough inside an otherwise-vegan "pâtes à
|
|
* cuire"…). References `DIETS` by name, same as `allergenNames`
|
|
* references `ALLERGENS`. Deliberately never includes `"Omnivore"`
|
|
* (trivial, every ingredient qualifies) or `"Sans gluten"` (derived from
|
|
* `allergenNames` instead — see `IngredientDiet` in schema.prisma for
|
|
* why).
|
|
*/
|
|
dietNames?: string[];
|
|
}
|
|
|
|
// A broad pantry list — the goal is to cover the large majority of what a
|
|
// home cook reaches for (viandes, poissons, légumes, fruits, féculents,
|
|
// condiments, épices...), not just enough to exercise the recipe catalog in
|
|
// tests. Ingredients are reference data (see `Ingredient` in schema.prisma:
|
|
// `name` is `@unique`, there's no create/update/delete endpoint), so this is
|
|
// meant to already be comprehensive at first deploy rather than grown
|
|
// piecemeal as recipes need more of it. `allergenNames` reference
|
|
// `ALLERGENS` above by name — every one of the 14 EU-regulated allergens is
|
|
// covered by at least one ingredient here.
|
|
//
|
|
// Grouped by (`category`, `subcategory`) — mirrors `IngredientCategory`/
|
|
// `IngredientSubcategory` in schema.prisma, a supermarket-aisle taxonomy
|
|
// ("rayons") reworked from an earlier, less intuitive scheme that mixed
|
|
// cuisine-of-origin buckets ("cuisine italienne") in with aisle-style ones
|
|
// ("légumes") — an ingredient's category used to depend on which angle you
|
|
// thought of first. The picker UI (`apps/web`'s `IngredientPicker`) uses
|
|
// this to offer category browsing, not just free-text search — with 400+
|
|
// ingredients, search alone doesn't scale to actually *finding* something,
|
|
// and a single flat list of 7 aisles alone wouldn't either (some aisles
|
|
// would be 100+ items deep). Each group's key is the single source of
|
|
// truth for that mapping; `INGREDIENTS` below just flattens it back to one
|
|
// array for the seeding loop.
|
|
//
|
|
// `defaultDiets`/`defaultIcon` are what every item in the group shares
|
|
// unless it sets its own `dietNames`/`icon` — most groups are homogeneous
|
|
// on both counts (a vegetable is always vegan and always looks like a
|
|
// vegetable; a cut of meat never is and never does), so this avoids
|
|
// repeating the same values on hundreds of items; only a group's
|
|
// exceptions (a fish-based stock inside "bases", a cheese inside "produits
|
|
// laitiers"…) need a per-item override.
|
|
export const INGREDIENT_GROUPS: Array<{
|
|
category: IngredientCategory;
|
|
subcategory: IngredientSubcategory;
|
|
defaultDiets: string[];
|
|
defaultIcon: IngredientIcon;
|
|
items: IngredientSeed[];
|
|
}> = [
|
|
// =========================================================================
|
|
// Produits frais
|
|
// =========================================================================
|
|
{
|
|
category: "PRODUITS_FRAIS",
|
|
subcategory: "LEGUMES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "VEGETABLE",
|
|
items: [
|
|
{ name: "Tomate", allergenNames: [] },
|
|
{ name: "Oignon", allergenNames: [] },
|
|
{ name: "Échalote", allergenNames: [] },
|
|
{ name: "Ail", allergenNames: [] },
|
|
{ name: "Carotte", allergenNames: [] },
|
|
{ name: "Courgette", allergenNames: [] },
|
|
{ name: "Concombre", allergenNames: [] },
|
|
{ name: "Cornichons", allergenNames: [] },
|
|
{ name: "Poivron", allergenNames: [] },
|
|
{ name: "Champignon", allergenNames: [] },
|
|
{ name: "Cèpes", allergenNames: [] },
|
|
{ name: "Aubergine", allergenNames: [] },
|
|
{ name: "Brocoli", allergenNames: [] },
|
|
{ name: "Chou-fleur", allergenNames: [] },
|
|
{ name: "Chou blanc", allergenNames: [] },
|
|
{ name: "Chou rouge", allergenNames: [] },
|
|
{ name: "Chou de Bruxelles", allergenNames: [] },
|
|
{ name: "Épinard", allergenNames: [] },
|
|
{ name: "Blette", allergenNames: [] },
|
|
{ name: "Salade", allergenNames: [] },
|
|
{ name: "Roquette", allergenNames: [] },
|
|
{ name: "Cresson", allergenNames: [] },
|
|
{ name: "Poireau", allergenNames: [] },
|
|
{ name: "Céleri", allergenNames: ["Céleri"] },
|
|
{ name: "Radis", allergenNames: [] },
|
|
{ name: "Betterave", allergenNames: [] },
|
|
{ name: "Navet", allergenNames: [] },
|
|
{ name: "Panais", allergenNames: [] },
|
|
{ name: "Haricot vert", allergenNames: [] },
|
|
{ name: "Petit pois", allergenNames: [] },
|
|
{ name: "Maïs", allergenNames: [] },
|
|
{ name: "Artichaut", allergenNames: [] },
|
|
{ name: "Fenouil", allergenNames: [] },
|
|
{ name: "Endive", allergenNames: [] },
|
|
{ name: "Potiron", allergenNames: [] },
|
|
{ name: "Butternut", allergenNames: [] },
|
|
{ name: "Asperge", allergenNames: [] },
|
|
{ name: "Avocat", allergenNames: [] },
|
|
{ name: "Pomme de terre", allergenNames: [] },
|
|
{ name: "Patate douce", allergenNames: [] },
|
|
{ name: "Tomates cerises", allergenNames: [] },
|
|
{ name: "Pak-choï", allergenNames: [] },
|
|
{ name: "Germes de soja", allergenNames: ["Soja"] },
|
|
{ name: "Shiitake", allergenNames: [] },
|
|
{ name: "Daikon", allergenNames: [] },
|
|
{ name: "Piment vert frais", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "PRODUITS_FRAIS",
|
|
subcategory: "FRUITS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "FRUIT",
|
|
items: [
|
|
{ name: "Citron", allergenNames: [] },
|
|
{ name: "Citron vert", allergenNames: [] },
|
|
{ name: "Pomme", allergenNames: [] },
|
|
{ name: "Poire", allergenNames: [] },
|
|
{ name: "Banane", allergenNames: [] },
|
|
{ name: "Orange", allergenNames: [] },
|
|
{ name: "Clémentine", allergenNames: [] },
|
|
{ name: "Pamplemousse", allergenNames: [] },
|
|
{ name: "Fraise", allergenNames: [] },
|
|
{ name: "Framboise", allergenNames: [] },
|
|
{ name: "Myrtille", allergenNames: [] },
|
|
{ name: "Mûre", allergenNames: [] },
|
|
{ name: "Cerise", allergenNames: [] },
|
|
{ name: "Abricot", allergenNames: [] },
|
|
{ name: "Pêche", allergenNames: [] },
|
|
{ name: "Prune", allergenNames: [] },
|
|
{ name: "Raisin", allergenNames: [] },
|
|
{ name: "Melon", allergenNames: [] },
|
|
{ name: "Pastèque", allergenNames: [] },
|
|
{ name: "Ananas", allergenNames: [] },
|
|
{ name: "Mangue", allergenNames: [] },
|
|
{ name: "Kiwi", allergenNames: [] },
|
|
{ name: "Figue", allergenNames: [] },
|
|
{ name: "Datte", allergenNames: [] },
|
|
{ name: "Litchi", allergenNames: [] },
|
|
{ name: "Grenade", allergenNames: [] },
|
|
{ name: "Rhubarbe", allergenNames: [] },
|
|
{ name: "Coing", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "PRODUITS_FRAIS",
|
|
subcategory: "HERBES_FRAICHES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "HERB",
|
|
items: [
|
|
{ name: "Basilic", allergenNames: [] },
|
|
{ name: "Persil", allergenNames: [] },
|
|
{ name: "Thym", allergenNames: [] },
|
|
{ name: "Romarin", allergenNames: [] },
|
|
{ name: "Laurier", allergenNames: [] },
|
|
{ name: "Ciboulette", allergenNames: [] },
|
|
{ name: "Coriandre fraîche", allergenNames: [] },
|
|
{ name: "Menthe", allergenNames: [] },
|
|
{ name: "Origan", allergenNames: [] },
|
|
{ name: "Aneth", allergenNames: [] },
|
|
{ name: "Estragon", allergenNames: [] },
|
|
{ name: "Sarriette", allergenNames: [] },
|
|
{ name: "Marjolaine", allergenNames: [] },
|
|
{ name: "Sauge", allergenNames: [] },
|
|
{ name: "Cerfeuil", allergenNames: [] },
|
|
{ name: "Gingembre", allergenNames: [] },
|
|
{ name: "Citronnelle", allergenNames: [] },
|
|
{ name: "Combava", allergenNames: [] },
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Boucherie & poissonnerie
|
|
// =========================================================================
|
|
{
|
|
category: "BOUCHERIE_POISSONNERIE",
|
|
subcategory: "VIANDES",
|
|
defaultDiets: [],
|
|
defaultIcon: "MEAT",
|
|
items: [
|
|
{ name: "Lapin", allergenNames: [] },
|
|
{ name: "Bœuf haché", allergenNames: [] },
|
|
{ name: "Steak de bœuf", allergenNames: [] },
|
|
{ name: "Rôti de bœuf", allergenNames: [] },
|
|
{ name: "Escalope de veau", allergenNames: [] },
|
|
{ name: "Filet mignon de porc", allergenNames: [] },
|
|
{ name: "Côte de porc", allergenNames: [] },
|
|
{ name: "Agneau", allergenNames: [] },
|
|
{ name: "Gigot d'agneau", allergenNames: [] },
|
|
{ name: "Lardons", allergenNames: [] },
|
|
{ name: "Bacon", allergenNames: [] },
|
|
{ name: "Jambon blanc", allergenNames: [] },
|
|
{ name: "Jambon cru", allergenNames: [] },
|
|
{ name: "Saucisse", allergenNames: [] },
|
|
{ name: "Chorizo", allergenNames: [] },
|
|
{ name: "Merguez", allergenNames: [] },
|
|
{ name: "Prosciutto", allergenNames: [] },
|
|
{ name: "Pancetta", allergenNames: [] },
|
|
{ name: "Mortadelle", allergenNames: [] },
|
|
{ name: "Salami", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "BOUCHERIE_POISSONNERIE",
|
|
subcategory: "VOLAILLES",
|
|
defaultDiets: [],
|
|
defaultIcon: "POULTRY",
|
|
items: [
|
|
{ name: "Poulet", allergenNames: [] },
|
|
{ name: "Dinde", allergenNames: [] },
|
|
{ name: "Canard", allergenNames: [] },
|
|
{ name: "Magret de canard", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "BOUCHERIE_POISSONNERIE",
|
|
subcategory: "POISSONS",
|
|
defaultDiets: ["Pescétarien"],
|
|
defaultIcon: "FISH",
|
|
items: [
|
|
{ name: "Saumon", allergenNames: ["Poissons"] },
|
|
{ name: "Thon", allergenNames: ["Poissons"] },
|
|
{ name: "Cabillaud", allergenNames: ["Poissons"] },
|
|
{ name: "Truite", allergenNames: ["Poissons"] },
|
|
{ name: "Sardine", allergenNames: ["Poissons"] },
|
|
{ name: "Anchois", allergenNames: ["Poissons"] },
|
|
{ name: "Merlan", allergenNames: ["Poissons"] },
|
|
{ name: "Surimi", allergenNames: ["Poissons"] },
|
|
{ name: "Bar (loup de mer)", allergenNames: ["Poissons"] },
|
|
{ name: "Dorade", allergenNames: ["Poissons"] },
|
|
{ name: "Sole", allergenNames: ["Poissons"] },
|
|
{ name: "Turbot", allergenNames: ["Poissons"] },
|
|
{ name: "Merlu", allergenNames: ["Poissons"] },
|
|
{ name: "Colin", allergenNames: ["Poissons"] },
|
|
{ name: "Lieu noir", allergenNames: ["Poissons"] },
|
|
{ name: "Églefin", allergenNames: ["Poissons"] },
|
|
{ name: "Maquereau", allergenNames: ["Poissons"] },
|
|
{ name: "Hareng", allergenNames: ["Poissons"] },
|
|
{ name: "Rouget", allergenNames: ["Poissons"] },
|
|
{ name: "Raie", allergenNames: ["Poissons"] },
|
|
{ name: "Lotte", allergenNames: ["Poissons"] },
|
|
{ name: "Flétan", allergenNames: ["Poissons"] },
|
|
{ name: "Espadon", allergenNames: ["Poissons"] },
|
|
{ name: "Carpe", allergenNames: ["Poissons"] },
|
|
{ name: "Brochet", allergenNames: ["Poissons"] },
|
|
{ name: "Perche", allergenNames: ["Poissons"] },
|
|
{ name: "Tilapia", allergenNames: ["Poissons"] },
|
|
{ name: "Panga", allergenNames: ["Poissons"] },
|
|
{ name: "Saumon fumé", allergenNames: ["Poissons"] },
|
|
{ name: "Poisson séché", allergenNames: ["Poissons"] },
|
|
],
|
|
},
|
|
{
|
|
category: "BOUCHERIE_POISSONNERIE",
|
|
subcategory: "CRUSTACES_FRUITS_DE_MER",
|
|
defaultDiets: ["Pescétarien"],
|
|
defaultIcon: "SHELLFISH",
|
|
items: [
|
|
{ name: "Crevettes", allergenNames: ["Crustacés"] },
|
|
{ name: "Langoustines", allergenNames: ["Crustacés"] },
|
|
{ name: "Homard", allergenNames: ["Crustacés"] },
|
|
{ name: "Crabe", allergenNames: ["Crustacés"] },
|
|
{ name: "Langouste", allergenNames: ["Crustacés"] },
|
|
{ name: "Moules", allergenNames: ["Mollusques"] },
|
|
{ name: "Huîtres", allergenNames: ["Mollusques"] },
|
|
{ name: "Saint-Jacques", allergenNames: ["Mollusques"] },
|
|
{ name: "Calamar", allergenNames: ["Mollusques"] },
|
|
{ name: "Poulpe", allergenNames: ["Mollusques"] },
|
|
{ name: "Palourdes", allergenNames: ["Mollusques"] },
|
|
{ name: "Bulots", allergenNames: ["Mollusques"] },
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Épicerie sèche
|
|
// =========================================================================
|
|
{
|
|
category: "EPICERIE_SECHE",
|
|
subcategory: "FECULENTS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "GRAIN",
|
|
items: [
|
|
{ name: "Semoule", allergenNames: ["Gluten"] },
|
|
{ name: "Couscous", allergenNames: ["Gluten"] },
|
|
{ name: "Boulgour", allergenNames: ["Gluten"] },
|
|
{ name: "Polenta", allergenNames: [] },
|
|
{ name: "Quinoa", allergenNames: [] },
|
|
{ name: "Pâtes", allergenNames: ["Gluten"] },
|
|
{ name: "Pâtes complètes", allergenNames: ["Gluten"] },
|
|
{ name: "Riz", allergenNames: [] },
|
|
{ name: "Riz basmati", allergenNames: [] },
|
|
{ name: "Riz complet", allergenNames: [] },
|
|
{ name: "Flocons d'avoine", allergenNames: ["Gluten"] },
|
|
{ name: "Spaghetti", allergenNames: ["Gluten"] },
|
|
{ name: "Penne", allergenNames: ["Gluten"] },
|
|
{ name: "Tagliatelles", allergenNames: ["Gluten"] },
|
|
{ name: "Lasagnes (feuilles)", allergenNames: ["Gluten"] },
|
|
{ name: "Gnocchi", allergenNames: ["Gluten"] },
|
|
{ name: "Riz arborio", allergenNames: [] },
|
|
{ name: "Nouilles de riz", allergenNames: [] },
|
|
{ name: "Nouilles udon", allergenNames: ["Gluten"] },
|
|
{ name: "Nouilles soba", allergenNames: ["Gluten"] },
|
|
{ name: "Nouilles chinoises", allergenNames: ["Gluten"] },
|
|
{ name: "Vermicelles de riz", allergenNames: [] },
|
|
{ name: "Vermicelles de soja", allergenNames: [] },
|
|
{ name: "Riz gluant", allergenNames: [] },
|
|
{ name: "Riz à sushi", allergenNames: [] },
|
|
{ name: "Riz jasmin", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "EPICERIE_SECHE",
|
|
subcategory: "LEGUMINEUSES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "LEGUME",
|
|
items: [
|
|
{ name: "Lentilles vertes", allergenNames: [] },
|
|
{ name: "Lentilles corail", allergenNames: [] },
|
|
{ name: "Pois chiches", allergenNames: [] },
|
|
{ name: "Haricots blancs", allergenNames: [] },
|
|
{ name: "Haricots rouges", allergenNames: [] },
|
|
{ name: "Haricots noirs", allergenNames: [] },
|
|
{ name: "Pois cassés", allergenNames: [] },
|
|
{ name: "Fèves", allergenNames: [] },
|
|
{ name: "Edamame", allergenNames: ["Soja"] },
|
|
{ name: "Haricots pinto", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "EPICERIE_SECHE",
|
|
subcategory: "GRAINES_FRUITS_SECS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "NUT_SEED",
|
|
items: [
|
|
{ name: "Cacahuètes", allergenNames: ["Arachides"] },
|
|
{ name: "Amandes", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Noix", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Noisettes", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Noix de cajou", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Pistaches", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Noix de pécan", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Poudre d'amande", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Pignons de pin", allergenNames: [] },
|
|
{ name: "Graines de tournesol", allergenNames: [] },
|
|
{ name: "Graines de courge", allergenNames: [] },
|
|
{ name: "Noix de coco râpée", allergenNames: [] },
|
|
{ name: "Raisins secs", allergenNames: ["Sulfites"] },
|
|
{ name: "Pruneaux", allergenNames: ["Sulfites"] },
|
|
{ name: "Abricots secs", allergenNames: ["Sulfites"] },
|
|
{ name: "Graines de sésame", allergenNames: ["Graines de sésame"] },
|
|
],
|
|
},
|
|
{
|
|
category: "EPICERIE_SECHE",
|
|
subcategory: "AUTRES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "JAR",
|
|
items: [
|
|
{ name: "Champignons noirs", allergenNames: [] },
|
|
{ name: "Algue nori", allergenNames: [] },
|
|
{ name: "Algue wakamé", allergenNames: [] },
|
|
{ name: "Algue kombu", allergenNames: [] },
|
|
{ name: "Pousses de bambou", allergenNames: [] },
|
|
{ name: "Châtaignes d'eau", allergenNames: [] },
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Boulangerie
|
|
// =========================================================================
|
|
{
|
|
category: "BOULANGERIE",
|
|
subcategory: "PAINS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "BREAD",
|
|
items: [
|
|
{ name: "Pain", allergenNames: ["Gluten"] },
|
|
{ name: "Pain de mie", allergenNames: ["Gluten"] },
|
|
{ name: "Pain complet", allergenNames: ["Gluten"] },
|
|
{ name: "Baguette", allergenNames: ["Gluten"] },
|
|
{ name: "Pain de seigle", allergenNames: ["Gluten"] },
|
|
{ name: "Chapelure", allergenNames: ["Gluten"] },
|
|
{
|
|
name: "Pain à burger",
|
|
allergenNames: ["Gluten", "Lait", "Œufs"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Pain brioché",
|
|
allergenNames: ["Gluten", "Lait", "Œufs"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Pain à hot-dog", allergenNames: ["Gluten"] },
|
|
{ name: "Pain pita", allergenNames: ["Gluten"] },
|
|
{ name: "Pain bagel", allergenNames: ["Gluten"] },
|
|
{ name: "Naan", allergenNames: ["Gluten"] },
|
|
{ name: "Pain wrap", allergenNames: ["Gluten"] },
|
|
{
|
|
name: "Pain viennois",
|
|
allergenNames: ["Gluten", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Pain de campagne", allergenNames: ["Gluten"] },
|
|
{ name: "Pain aux céréales", allergenNames: ["Gluten"] },
|
|
{ name: "Petit pain", allergenNames: ["Gluten"] },
|
|
{ name: "Pain suédois", allergenNames: ["Gluten"] },
|
|
{ name: "Pain sans gluten", allergenNames: [] },
|
|
{ name: "Biscotte", allergenNames: ["Gluten"] },
|
|
{ name: "Croûtons", allergenNames: ["Gluten"] },
|
|
{ name: "Focaccia", allergenNames: ["Gluten"] },
|
|
{ name: "Ciabatta", allergenNames: ["Gluten"] },
|
|
{ name: "Tortilla de maïs", allergenNames: [] },
|
|
{ name: "Tortilla de blé", allergenNames: ["Gluten"] },
|
|
],
|
|
},
|
|
{
|
|
category: "BOULANGERIE",
|
|
subcategory: "PATES_A_CUIRE",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "DOUGH",
|
|
items: [
|
|
{
|
|
name: "Pâte feuilletée",
|
|
allergenNames: ["Gluten", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Pâte brisée",
|
|
allergenNames: ["Gluten", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Pâte à pizza", allergenNames: ["Gluten"] },
|
|
{
|
|
name: "Pâte à tarte sablée",
|
|
allergenNames: ["Gluten", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Crémerie & fromage
|
|
// =========================================================================
|
|
{
|
|
category: "CREMERIE_FROMAGE",
|
|
subcategory: "PRODUITS_LAITIERS",
|
|
defaultDiets: ["Végétarien", "Pescétarien"],
|
|
defaultIcon: "MILK",
|
|
items: [
|
|
{ name: "Lait", allergenNames: ["Lait"] },
|
|
{ name: "Beurre", allergenNames: ["Lait"] },
|
|
{ name: "Crème fraîche", allergenNames: ["Lait"] },
|
|
{ name: "Crème liquide", allergenNames: ["Lait"] },
|
|
{ name: "Fromage", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Emmental", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Gruyère", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Parmesan", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Mozzarella", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Chèvre (fromage)", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Feta", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Comté", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Fromage blanc", allergenNames: ["Lait"] },
|
|
{ name: "Mascarpone", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Yaourt", allergenNames: ["Lait"] },
|
|
{ name: "Burrata", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Ricotta", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Pecorino", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Gorgonzola", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
{ name: "Cheddar", icon: "CHEESE", allergenNames: ["Lait"] },
|
|
],
|
|
},
|
|
{
|
|
category: "CREMERIE_FROMAGE",
|
|
subcategory: "OEUFS",
|
|
defaultDiets: ["Végétarien", "Pescétarien"],
|
|
defaultIcon: "EGG",
|
|
items: [{ name: "Œuf", allergenNames: ["Œufs"] }],
|
|
},
|
|
{
|
|
category: "CREMERIE_FROMAGE",
|
|
subcategory: "ALTERNATIVES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "SPROUT",
|
|
items: [
|
|
{ name: "Lait de coco", icon: "MILK", allergenNames: [] },
|
|
{ name: "Crème de coco", icon: "MILK", allergenNames: [] },
|
|
{ name: "Lait d'amande", icon: "MILK", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Lait d'avoine", icon: "MILK", allergenNames: ["Gluten"] },
|
|
{ name: "Tofu", allergenNames: ["Soja"] },
|
|
{ name: "Tofu soyeux", allergenNames: ["Soja"] },
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Condiments & épices
|
|
// =========================================================================
|
|
{
|
|
category: "CONDIMENTS_EPICES",
|
|
subcategory: "EPICES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "SPICE",
|
|
items: [
|
|
{ name: "Herbes de Provence", allergenNames: [] },
|
|
{ name: "Poivre noir", allergenNames: [] },
|
|
{ name: "Paprika", allergenNames: [] },
|
|
{ name: "Piment d'Espelette", allergenNames: [] },
|
|
{ name: "Piment de Cayenne", allergenNames: [] },
|
|
{ name: "Cumin", allergenNames: [] },
|
|
{ name: "Curry (poudre)", allergenNames: [] },
|
|
{ name: "Curcuma", allergenNames: [] },
|
|
{ name: "Cannelle", allergenNames: [] },
|
|
{ name: "Muscade", allergenNames: [] },
|
|
{ name: "Safran", allergenNames: [] },
|
|
{ name: "Clou de girofle", allergenNames: [] },
|
|
{ name: "Vanille (gousse)", allergenNames: [] },
|
|
{ name: "Poivre blanc", allergenNames: [] },
|
|
{ name: "Poivre rose", allergenNames: [] },
|
|
{ name: "Poivre du Sichuan", allergenNames: [] },
|
|
{ name: "Paprika fumé", allergenNames: [] },
|
|
{ name: "Piment oiseau", allergenNames: [] },
|
|
{ name: "Baies de genièvre", allergenNames: [] },
|
|
{ name: "Anis étoilé (badiane)", allergenNames: [] },
|
|
{ name: "Anis vert", allergenNames: [] },
|
|
{ name: "Graines de fenouil", allergenNames: [] },
|
|
{ name: "Sumac", allergenNames: [] },
|
|
{ name: "Nigelle", allergenNames: [] },
|
|
{ name: "Quatre épices", allergenNames: [] },
|
|
{ name: "Colombo (poudre)", allergenNames: [] },
|
|
{ name: "Baharat", allergenNames: [] },
|
|
{ name: "Raifort", allergenNames: [] },
|
|
{ name: "Sel aux herbes", allergenNames: [] },
|
|
{ name: "Sel de céleri", allergenNames: ["Céleri"] },
|
|
{ name: "Fleur de sel", allergenNames: [] },
|
|
{ name: "Sel", allergenNames: [] },
|
|
{ name: "Cinq épices", allergenNames: [] },
|
|
{ name: "Garam masala", allergenNames: [] },
|
|
{ name: "Graines de coriandre", allergenNames: [] },
|
|
{ name: "Cardamome", allergenNames: [] },
|
|
{ name: "Fenugrec", allergenNames: [] },
|
|
{ name: "Piment jalapeño", allergenNames: [] },
|
|
{ name: "Piment chipotle", allergenNames: [] },
|
|
{ name: "Piment poblano", allergenNames: [] },
|
|
{ name: "Piment habanero", allergenNames: [] },
|
|
{ name: "Ras el hanout", allergenNames: [] },
|
|
{ name: "Za'atar", allergenNames: ["Graines de sésame"] },
|
|
],
|
|
},
|
|
{
|
|
category: "CONDIMENTS_EPICES",
|
|
subcategory: "SAUCES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "JAR",
|
|
items: [
|
|
{ name: "Sauce soja", allergenNames: ["Soja"] },
|
|
{ name: "Moutarde", allergenNames: ["Moutarde"] },
|
|
{
|
|
name: "Mayonnaise",
|
|
allergenNames: ["Œufs"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Ketchup", allergenNames: [] },
|
|
{ name: "Tabasco", allergenNames: [] },
|
|
{
|
|
name: "Sauce Worcestershire",
|
|
allergenNames: ["Poissons"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce nuoc-mâm",
|
|
allergenNames: ["Poissons"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Wasabi", allergenNames: [] },
|
|
{ name: "Harissa", allergenNames: [] },
|
|
{ name: "Pâte de curry", allergenNames: [] },
|
|
{ name: "Beurre de cacahuète", allergenNames: ["Arachides"] },
|
|
{ name: "Moutarde de Dijon", allergenNames: ["Moutarde"] },
|
|
{ name: "Moutarde à l'ancienne", allergenNames: ["Moutarde"] },
|
|
{ name: "Sauce barbecue", allergenNames: [] },
|
|
{
|
|
name: "Sauce tartare",
|
|
allergenNames: ["Œufs"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce cocktail",
|
|
allergenNames: ["Œufs"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce béarnaise",
|
|
allergenNames: ["Œufs", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce hollandaise",
|
|
allergenNames: ["Œufs", "Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce béchamel",
|
|
allergenNames: ["Lait", "Gluten"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Sauce teriyaki", allergenNames: ["Soja"] },
|
|
{
|
|
name: "Sauce ponzu",
|
|
allergenNames: ["Soja", "Poissons"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Chimichurri", allergenNames: [] },
|
|
{
|
|
name: "Pesto rouge (tomates séchées)",
|
|
allergenNames: ["Lait", "Fruits à coque"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Pesto",
|
|
allergenNames: ["Lait", "Fruits à coque"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Sauce huître",
|
|
allergenNames: ["Mollusques"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Sauce hoisin", allergenNames: ["Soja"] },
|
|
{ name: "Sauce sriracha", allergenNames: [] },
|
|
{ name: "Sauce sweet chili", allergenNames: [] },
|
|
{ name: "Miso", allergenNames: ["Soja"] },
|
|
{
|
|
name: "Pâte de crevettes",
|
|
allergenNames: ["Crustacés"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Pâte de curry rouge (thaï)", allergenNames: [] },
|
|
{ name: "Pâte de curry vert (thaï)", allergenNames: [] },
|
|
{ name: "Tahini", allergenNames: ["Graines de sésame"] },
|
|
],
|
|
},
|
|
{
|
|
category: "CONDIMENTS_EPICES",
|
|
subcategory: "ASSAISONNEMENTS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "BOTTLE",
|
|
items: [
|
|
{ name: "Huile d'olive", allergenNames: [] },
|
|
{ name: "Huile de tournesol", allergenNames: [] },
|
|
{ name: "Huile de colza", allergenNames: [] },
|
|
{ name: "Huile de coco", allergenNames: [] },
|
|
{ name: "Huile de sésame", allergenNames: ["Graines de sésame"] },
|
|
{ name: "Vinaigre de cidre", allergenNames: [] },
|
|
{ name: "Vinaigre blanc", allergenNames: [] },
|
|
{ name: "Vinaigre balsamique", allergenNames: ["Sulfites"] },
|
|
{ name: "Câpres", icon: "JAR", allergenNames: [] },
|
|
{ name: "Olives", icon: "JAR", allergenNames: [] },
|
|
{ name: "Vin blanc (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] },
|
|
{ name: "Vin rouge (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] },
|
|
{ name: "Vinaigre de vin rouge", allergenNames: ["Sulfites"] },
|
|
{ name: "Vinaigre de vin blanc", allergenNames: ["Sulfites"] },
|
|
{ name: "Vinaigre de xérès", allergenNames: ["Sulfites"] },
|
|
{ name: "Huile de noix", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Huile de noisette", allergenNames: ["Fruits à coque"] },
|
|
{ name: "Huile d'arachide", allergenNames: ["Arachides"] },
|
|
{ name: "Huile pimentée", allergenNames: [] },
|
|
{ name: "Vinaigre de riz", allergenNames: [] },
|
|
{ name: "Mirin", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Saké (cuisine)", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de citron", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de citron vert", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus d'orange", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de pomme", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de raisin", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de tomate", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Jus de cranberry", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Café", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Thé", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Bière (cuisine)", icon: "DRINK", allergenNames: ["Gluten"] },
|
|
{ name: "Cidre (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] },
|
|
{
|
|
name: "Champagne / vin pétillant (cuisine)",
|
|
icon: "DRINK",
|
|
allergenNames: ["Sulfites"],
|
|
},
|
|
{ name: "Porto (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] },
|
|
{ name: "Vin jaune (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] },
|
|
{ name: "Cognac", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Rhum", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Whisky", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Vodka", icon: "DRINK", allergenNames: [] },
|
|
],
|
|
},
|
|
// =========================================================================
|
|
// Aides culinaires
|
|
// =========================================================================
|
|
{
|
|
category: "AIDES_CULINAIRES",
|
|
subcategory: "BASES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "GRAIN",
|
|
items: [
|
|
{ name: "Farine de blé", allergenNames: ["Gluten"] },
|
|
{ name: "Farine complète", allergenNames: ["Gluten"] },
|
|
{ name: "Farine de maïs", allergenNames: [] },
|
|
{ name: "Farine de sarrasin", allergenNames: [] },
|
|
{ name: "Farine de riz", allergenNames: [] },
|
|
{ name: "Bouillon cube légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] },
|
|
{
|
|
name: "Bouillon cube volaille",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Céleri"],
|
|
dietNames: [],
|
|
},
|
|
{ name: "Concentré de tomate", icon: "JAR", allergenNames: [] },
|
|
{ name: "Coulis de tomate", icon: "JAR", allergenNames: [] },
|
|
{ name: "Tomates pelées (conserve)", icon: "JAR", allergenNames: [] },
|
|
{ name: "Tomates séchées", icon: "JAR", allergenNames: [] },
|
|
{ name: "Fond de veau", icon: "STOCK_POT", allergenNames: [], dietNames: [] },
|
|
{ name: "Fond de volaille", icon: "STOCK_POT", allergenNames: [], dietNames: [] },
|
|
{
|
|
name: "Bouillon cube bœuf",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Céleri"],
|
|
dietNames: [],
|
|
},
|
|
{
|
|
name: "Bouillon cube poisson",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Poissons", "Céleri"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Bouillon de légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] },
|
|
{
|
|
name: "Bouillon de volaille",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Céleri"],
|
|
dietNames: [],
|
|
},
|
|
{ name: "Bouillon de bœuf", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [] },
|
|
{ name: "Court-bouillon", icon: "STOCK_POT", allergenNames: [] },
|
|
{
|
|
name: "Dashi (bouillon japonais)",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Poissons"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{
|
|
name: "Bisque de crustacés",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Crustacés"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
{ name: "Farine de tapioca", allergenNames: [] },
|
|
{ name: "Masa harina", allergenNames: [] },
|
|
{ name: "Eau", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Eau gazeuse", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Eau de fleur d'oranger", icon: "DRINK", allergenNames: [] },
|
|
{ name: "Eau de rose", icon: "DRINK", allergenNames: [] },
|
|
{
|
|
name: "Fumet de poisson",
|
|
icon: "STOCK_POT",
|
|
allergenNames: ["Poissons"],
|
|
dietNames: ["Pescétarien"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
category: "AIDES_CULINAIRES",
|
|
subcategory: "EPAISSISSANTS",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "JAR",
|
|
items: [
|
|
{ name: "Levure boulangère", allergenNames: [] },
|
|
{ name: "Levure chimique", allergenNames: [] },
|
|
{ name: "Maïzena", allergenNames: [] },
|
|
{ name: "Farine de lupin", allergenNames: ["Lupin"] },
|
|
// Animal collagen (bones/skin, usually pork or beef) — not
|
|
// vegetarian/vegan, and not reliably fish-derived either, so no
|
|
// pescetarian flag.
|
|
{ name: "Gélatine", allergenNames: [], dietNames: [] },
|
|
{ name: "Bicarbonate de soude", allergenNames: [] },
|
|
{ name: "Fécule de pomme de terre", allergenNames: [] },
|
|
],
|
|
},
|
|
{
|
|
category: "AIDES_CULINAIRES",
|
|
subcategory: "SUCRES",
|
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
|
defaultIcon: "SUGAR",
|
|
items: [
|
|
{ name: "Sucre", allergenNames: [] },
|
|
{ name: "Miel", allergenNames: [], dietNames: ["Végétarien", "Pescétarien"] },
|
|
{ name: "Sirop d'érable", allergenNames: [] },
|
|
{ name: "Sucre roux", allergenNames: [] },
|
|
{ name: "Sucre glace", allergenNames: [] },
|
|
{ name: "Cassonade", allergenNames: [] },
|
|
{ name: "Chocolat noir", allergenNames: [] },
|
|
{
|
|
name: "Chocolat au lait",
|
|
allergenNames: ["Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{
|
|
name: "Chocolat blanc",
|
|
allergenNames: ["Lait"],
|
|
dietNames: ["Végétarien", "Pescétarien"],
|
|
},
|
|
{ name: "Pépites de chocolat", allergenNames: [] },
|
|
{ name: "Cacao en poudre", allergenNames: [] },
|
|
{ name: "Extrait de vanille", allergenNames: [] },
|
|
{ name: "Sucre de palme", allergenNames: [] },
|
|
{ name: "Sirop de sucre de canne", allergenNames: [] },
|
|
],
|
|
},
|
|
];
|
|
|
|
const INGREDIENTS: Array<
|
|
Omit<IngredientSeed, "icon" | "dietNames"> & {
|
|
category: IngredientCategory;
|
|
subcategory: IngredientSubcategory;
|
|
icon: IngredientIcon;
|
|
dietNames: string[];
|
|
}
|
|
> = INGREDIENT_GROUPS.flatMap(({ category, subcategory, defaultDiets, defaultIcon, items }) =>
|
|
items.map((item) => ({
|
|
...item,
|
|
category,
|
|
subcategory,
|
|
icon: item.icon ?? defaultIcon,
|
|
dietNames: item.dietNames ?? defaultDiets,
|
|
})),
|
|
);
|
|
|
|
/**
|
|
* Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent
|
|
* (safe to call against a database that already has this data — upserts by
|
|
* `key`, all `@unique`) — used both by `prisma/seed.ts` (the CLI entry
|
|
* point, `prisma db seed`) and by `test-support/reset-db.ts` (so every
|
|
* test starts from the same realistic reference data the real app seeds,
|
|
* not an empty table).
|
|
*
|
|
* Every `name` below (`DIETS`, `ALLERGENS`, `INGREDIENT_GROUPS`) is an
|
|
* *authoring* label, never written to the database or seen by a client —
|
|
* {@link getEnglishKey} resolves each row's real, stable, English `key`
|
|
* from it (see `catalog-en-keys.ts` for why the key must be English even
|
|
* though this file's labels are French). The database only ever stores
|
|
* that key; the French label itself lives in `apps/web`'s
|
|
* `locales/fr/translation.json` (`catalog.*`
|
|
* namespace, kept in sync by `scripts/generate-catalog-i18n.ts`).
|
|
*/
|
|
export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
|
|
for (const name of DIETS) {
|
|
const key = getEnglishKey(name);
|
|
await prisma.diet.upsert({ where: { key }, update: {}, create: { key } });
|
|
}
|
|
|
|
// `Allergy` itself carries no `key` — it's the selectable instance of a
|
|
// keyed `Category` (see schema.prisma) — so seeding an allergen means one
|
|
// Category (upserted by key) plus exactly one Allergy row under it,
|
|
// created only the first time. `update: { kind }` (not `{}`) — a reseed
|
|
// must correct `kind` on an already-existing category if the
|
|
// classification above ever changes, not just skip it.
|
|
for (const { name, kind } of ALLERGENS) {
|
|
const key = getEnglishKey(name);
|
|
const category = await prisma.category.upsert({
|
|
where: { key },
|
|
update: { kind },
|
|
create: { key, kind },
|
|
});
|
|
const existing = await prisma.allergy.findFirst({ where: { categoryId: category.id } });
|
|
if (!existing) {
|
|
await prisma.allergy.create({ data: { categoryId: category.id } });
|
|
}
|
|
}
|
|
|
|
// Ingredients: bulk, not one upsert per row (`INGREDIENTS` is a few
|
|
// hundred entries long, and `seedReferenceData` re-runs on every single
|
|
// test's `resetDatabase()` — a per-row round trip made the whole suite
|
|
// measurably slower). Bulk-create whatever's missing in one query, then
|
|
// reconcile `icon`/`category`/`subcategory` only for the rows where any
|
|
// of them actually changed — on a freshly-truncated table (the common
|
|
// test-suite case) that's zero updates, on a real re-deploy it's however
|
|
// many rows were edited in code since the last deploy, never the full
|
|
// list.
|
|
const ingredientKeys = INGREDIENTS.map((i) => getEnglishKey(i.name));
|
|
const existingIngredients = await prisma.ingredient.findMany({
|
|
where: { key: { in: ingredientKeys } },
|
|
select: { id: true, key: true, icon: true, category: true, subcategory: true },
|
|
});
|
|
const existingByKey = new Map(existingIngredients.map((i) => [i.key, i]));
|
|
|
|
const missingIngredients = INGREDIENTS.filter((i) => !existingByKey.has(getEnglishKey(i.name)));
|
|
if (missingIngredients.length > 0) {
|
|
await prisma.ingredient.createMany({
|
|
data: missingIngredients.map(({ name, icon, category, subcategory }) => ({
|
|
key: getEnglishKey(name),
|
|
icon,
|
|
category,
|
|
subcategory,
|
|
})),
|
|
});
|
|
}
|
|
|
|
const changed = INGREDIENTS.filter((i) => {
|
|
const existing = existingByKey.get(getEnglishKey(i.name));
|
|
return (
|
|
existing &&
|
|
(existing.icon !== i.icon ||
|
|
existing.category !== i.category ||
|
|
existing.subcategory !== i.subcategory)
|
|
);
|
|
});
|
|
for (const { name, icon, category, subcategory } of changed) {
|
|
await prisma.ingredient.update({
|
|
where: { key: getEnglishKey(name) },
|
|
data: { icon, category, subcategory },
|
|
});
|
|
}
|
|
|
|
// Re-resolve every ingredient's id (existing + just-created) and every
|
|
// allergy's id (by its category key) once, then link them in a single
|
|
// bulk insert — same "re-derived every time, not upserted per link"
|
|
// reasoning as before for `IngredientAllergy` (it has no natural per-row
|
|
// identity to upsert against), just batched instead of looped.
|
|
const allIngredients = await prisma.ingredient.findMany({
|
|
where: { key: { in: ingredientKeys } },
|
|
select: { id: true, key: true },
|
|
});
|
|
const ingredientIdByKey = new Map(allIngredients.map((i) => [i.key, i.id]));
|
|
|
|
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
|
const allergyIdByCategoryKey = new Map(allergies.map((a) => [a.category.key, a.id]));
|
|
|
|
const links: Array<{ ingredientId: number; allergyId: number }> = [];
|
|
for (const { name, allergenNames } of INGREDIENTS) {
|
|
const ingredientId = ingredientIdByKey.get(getEnglishKey(name));
|
|
if (ingredientId === undefined) continue;
|
|
for (const allergenName of allergenNames) {
|
|
const allergyId = allergyIdByCategoryKey.get(getEnglishKey(allergenName));
|
|
if (allergyId !== undefined) links.push({ ingredientId, allergyId });
|
|
}
|
|
}
|
|
if (links.length > 0) {
|
|
await prisma.ingredientAllergy.createMany({ data: links, skipDuplicates: true });
|
|
}
|
|
|
|
// Same bulk-insert approach as the allergy links above, resolved against
|
|
// `dietNames` (item override, falling back to its group's `defaultDiets`
|
|
// in the `INGREDIENTS` flatten step) instead of `allergenNames`.
|
|
const diets = await prisma.diet.findMany();
|
|
const dietIdByKey = new Map(diets.map((d) => [d.key, d.id]));
|
|
|
|
const dietLinks: Array<{ ingredientId: number; dietId: number }> = [];
|
|
for (const { name, dietNames } of INGREDIENTS) {
|
|
const ingredientId = ingredientIdByKey.get(getEnglishKey(name));
|
|
if (ingredientId === undefined) continue;
|
|
for (const dietName of dietNames) {
|
|
const dietId = dietIdByKey.get(getEnglishKey(dietName));
|
|
if (dietId !== undefined) dietLinks.push({ ingredientId, dietId });
|
|
}
|
|
}
|
|
if (dietLinks.length > 0) {
|
|
await prisma.ingredientDiet.createMany({ data: dietLinks, skipDuplicates: true });
|
|
}
|
|
}
|