diff --git a/apps/api/prisma/migrations/20260818081109_ingredient_category/migration.sql b/apps/api/prisma/migrations/20260818081109_ingredient_category/migration.sql new file mode 100644 index 0000000..f2674a0 --- /dev/null +++ b/apps/api/prisma/migrations/20260818081109_ingredient_category/migration.sql @@ -0,0 +1,6 @@ +-- CreateEnum +CREATE TYPE "IngredientCategory" AS ENUM ('CEREALES_FECULENTS', 'LEGUMINEUSES', 'VIANDES_VOLAILLES', 'POISSONS_FRUITS_DE_MER', 'PRODUITS_LAITIERS_OEUFS', 'LEGUMES', 'FRUITS', 'FRUITS_SECS_OLEAGINEUX', 'CONDIMENTS_SAUCES', 'EPICES_HERBES', 'SUCRE_PATISSERIE', 'CUISINE_ITALIENNE', 'CUISINE_ASIATIQUE', 'CUISINE_MEXICAINE', 'MAGHREB_MOYEN_ORIENT', 'PAINS_SANDWICHS', 'EPICERIE_DIVERS', 'LIQUIDES_BOISSONS'); + +-- AlterTable +ALTER TABLE "ingredients" ADD COLUMN "category" "IngredientCategory" NOT NULL DEFAULT 'EPICERIE_DIVERS'; + diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 8c4e87d..8ee4b93 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -292,11 +292,44 @@ model RecipeDiet { /// idempotent/safe to re-run, same reason as `Diet.name`/`Category.name`. /// Ingredients are reference data (like Diet/Allergy): seeded, never /// created/edited/deleted through the API. +/// Not in the original spec doc — coarse grouping (viandes, légumes, +/// épices...) so the ingredient picker (apps/web) can offer category +/// browsing, not just free-text search: with 400+ reference ingredients, +/// search alone doesn't scale to actually *finding* one. Mirrors +/// `reference-seed-data.ts`'s `INGREDIENT_GROUPS` keys exactly — that file +/// is the single source of truth for which ingredient belongs to which +/// category, this enum just gives it a type-safe column to live in. +/// `@default(EPICERIE_DIVERS)` exists only so this column can be added +/// `NOT NULL` to a table that may already have rows — the seed script +/// corrects every row's real category on the very next run, this default +/// is never the intended value for a real ingredient. +enum IngredientCategory { + CEREALES_FECULENTS + LEGUMINEUSES + VIANDES_VOLAILLES + POISSONS_FRUITS_DE_MER + PRODUITS_LAITIERS_OEUFS + LEGUMES + FRUITS + FRUITS_SECS_OLEAGINEUX + CONDIMENTS_SAUCES + EPICES_HERBES + SUCRE_PATISSERIE + CUISINE_ITALIENNE + CUISINE_ASIATIQUE + CUISINE_MEXICAINE + MAGHREB_MOYEN_ORIENT + PAINS_SANDWICHS + EPICERIE_DIVERS + LIQUIDES_BOISSONS +} + model Ingredient { - id Int @id @default(autoincrement()) - name String @unique + id Int @id @default(autoincrement()) + name String @unique icon String? - alternateRecipeId Int? @map("alternate_recipe") + category IngredientCategory @default(EPICERIE_DIVERS) + alternateRecipeId Int? @map("alternate_recipe") alternateRecipe Recipe? @relation("IngredientAlternateRecipe", fields: [alternateRecipeId], references: [id], onDelete: SetNull) recipes RecipeIngredient[] diff --git a/apps/api/src/db/reference-seed-data.ts b/apps/api/src/db/reference-seed-data.ts index 7e4dd7c..5459f82 100644 --- a/apps/api/src/db/reference-seed-data.ts +++ b/apps/api/src/db/reference-seed-data.ts @@ -1,4 +1,4 @@ -import type { AllergenKind, PrismaClient } from "@prisma/client"; +import type { AllergenKind, IngredientCategory, PrismaClient } from "@prisma/client"; // Short, optional-to-pick regime list — `UserProfile.dietId` stays // nullable, this is not meant to be exhaustive. @@ -28,6 +28,12 @@ const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [ { name: "Mollusques", kind: "ALLERGY" }, ]; +interface IngredientSeed { + name: string; + icon?: string; + allergenNames: 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 @@ -39,481 +45,550 @@ const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [ // covered by at least one ingredient here. `icon` is left unset (rather than // forcing a misleading emoji) for the handful of items with no good match in // the standard emoji set (e.g. `Radis`, `Asperge`). -const INGREDIENTS: Array<{ name: string; icon?: string; allergenNames: string[] }> = [ - // --- Céréales, farines & féculents --------------------------------------- - { name: "Farine de blé", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Farine complète", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Farine de maïs", icon: "🌽", allergenNames: [] }, - { name: "Farine de sarrasin", icon: "🌾", allergenNames: [] }, - { name: "Farine de riz", icon: "🍚", allergenNames: [] }, - { name: "Semoule", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Couscous", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Boulgour", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Polenta", icon: "🌽", allergenNames: [] }, - { name: "Quinoa", icon: "🌾", allergenNames: [] }, - { name: "Pâtes", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Pâtes complètes", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Riz", icon: "🍚", allergenNames: [] }, - { name: "Riz basmati", icon: "🍚", allergenNames: [] }, - { name: "Riz complet", icon: "🍚", allergenNames: [] }, - { name: "Flocons d'avoine", icon: "🌾", allergenNames: ["Gluten"] }, - { name: "Pomme de terre", icon: "🥔", allergenNames: [] }, - { name: "Patate douce", icon: "🍠", allergenNames: [] }, - { name: "Pain", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Pain de mie", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Pain complet", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Baguette", icon: "🥖", allergenNames: ["Gluten"] }, - { name: "Pain de seigle", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Chapelure", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Levure boulangère", icon: "🫙", allergenNames: [] }, - { name: "Levure chimique", icon: "🫙", allergenNames: [] }, - { name: "Maïzena", icon: "🫙", allergenNames: [] }, - - // --- Légumineuses --------------------------------------------------------- - { name: "Lentilles vertes", icon: "🫘", allergenNames: [] }, - { name: "Lentilles corail", icon: "🫘", allergenNames: [] }, - { name: "Pois chiches", icon: "🫘", allergenNames: [] }, - { name: "Haricots blancs", icon: "🫘", allergenNames: [] }, - { name: "Haricots rouges", icon: "🫘", allergenNames: [] }, - { name: "Haricots noirs", icon: "🫘", allergenNames: [] }, - { name: "Pois cassés", icon: "🫘", allergenNames: [] }, - { name: "Fèves", icon: "🫘", allergenNames: [] }, - { name: "Edamame", icon: "🫛", allergenNames: ["Soja"] }, - - // --- Viandes & volailles --------------------------------------------------- - { name: "Poulet", icon: "🍗", allergenNames: [] }, - { name: "Dinde", icon: "🍗", allergenNames: [] }, - { name: "Canard", icon: "🦆", allergenNames: [] }, - { name: "Magret de canard", icon: "🦆", allergenNames: [] }, - { name: "Lapin", icon: "🐇", allergenNames: [] }, - { name: "Bœuf haché", icon: "🥩", allergenNames: [] }, - { name: "Steak de bœuf", icon: "🥩", allergenNames: [] }, - { name: "Rôti de bœuf", icon: "🥩", allergenNames: [] }, - { name: "Escalope de veau", icon: "🥩", allergenNames: [] }, - { name: "Filet mignon de porc", icon: "🥩", allergenNames: [] }, - { name: "Côte de porc", icon: "🥩", allergenNames: [] }, - { name: "Agneau", icon: "🍖", allergenNames: [] }, - { name: "Gigot d'agneau", icon: "🍖", allergenNames: [] }, - { name: "Lardons", icon: "🥓", allergenNames: [] }, - { name: "Bacon", icon: "🥓", allergenNames: [] }, - { name: "Jambon blanc", icon: "🍖", allergenNames: [] }, - { name: "Jambon cru", icon: "🍖", allergenNames: [] }, - { name: "Saucisse", icon: "🌭", allergenNames: [] }, - { name: "Chorizo", icon: "🌭", allergenNames: [] }, - { name: "Merguez", icon: "🌭", allergenNames: [] }, - - // --- Poissons & fruits de mer ----------------------------------------------- - { name: "Saumon", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Thon", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Cabillaud", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Truite", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Sardine", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Anchois", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Merlan", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Surimi", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Bar (loup de mer)", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Dorade", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Sole", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Turbot", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Merlu", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Colin", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Lieu noir", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Églefin", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Maquereau", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Hareng", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Rouget", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Raie", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Lotte", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Flétan", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Espadon", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Carpe", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Brochet", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Perche", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Tilapia", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Panga", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Saumon fumé", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Crevettes", icon: "🍤", allergenNames: ["Crustacés"] }, - { name: "Langoustines", icon: "🍤", allergenNames: ["Crustacés"] }, - { name: "Homard", icon: "🦞", allergenNames: ["Crustacés"] }, - { name: "Crabe", icon: "🦀", allergenNames: ["Crustacés"] }, - { name: "Langouste", icon: "🦞", allergenNames: ["Crustacés"] }, - { name: "Moules", icon: "🦪", allergenNames: ["Mollusques"] }, - { name: "Huîtres", icon: "🦪", allergenNames: ["Mollusques"] }, - { name: "Saint-Jacques", icon: "🦪", allergenNames: ["Mollusques"] }, - { name: "Calamar", icon: "🦑", allergenNames: ["Mollusques"] }, - { name: "Poulpe", icon: "🐙", allergenNames: ["Mollusques"] }, - { name: "Palourdes", icon: "🦪", allergenNames: ["Mollusques"] }, - { name: "Bulots", icon: "🐚", allergenNames: ["Mollusques"] }, - - // --- Produits laitiers & œufs ----------------------------------------------- - { name: "Œuf", icon: "🥚", allergenNames: ["Œufs"] }, - { name: "Lait", icon: "🥛", allergenNames: ["Lait"] }, - { name: "Beurre", icon: "🧈", allergenNames: ["Lait"] }, - { name: "Crème fraîche", icon: "🥛", allergenNames: ["Lait"] }, - { name: "Crème liquide", icon: "🥛", allergenNames: ["Lait"] }, - { name: "Fromage", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Emmental", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Gruyère", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Parmesan", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Mozzarella", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Chèvre (fromage)", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Feta", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Comté", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Fromage blanc", icon: "🥣", allergenNames: ["Lait"] }, - { name: "Mascarpone", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Yaourt", icon: "🥣", allergenNames: ["Lait"] }, - { name: "Lait de coco", icon: "🥥", allergenNames: [] }, - { name: "Crème de coco", icon: "🥥", allergenNames: [] }, - { name: "Lait d'amande", icon: "🥛", allergenNames: ["Fruits à coque"] }, - { name: "Lait d'avoine", icon: "🥛", allergenNames: ["Gluten"] }, - - // --- Légumes ----------------------------------------------------------------- - { name: "Tomate", icon: "🍅", allergenNames: [] }, - { name: "Oignon", icon: "🧅", allergenNames: [] }, - { name: "Échalote", icon: "🧅", allergenNames: [] }, - { name: "Ail", icon: "🧄", allergenNames: [] }, - { name: "Carotte", icon: "🥕", allergenNames: [] }, - { name: "Courgette", icon: "🥒", allergenNames: [] }, - { name: "Concombre", icon: "🥒", allergenNames: [] }, - { name: "Cornichons", icon: "🥒", allergenNames: [] }, - { name: "Poivron", icon: "🫑", allergenNames: [] }, - { name: "Champignon", icon: "🍄", allergenNames: [] }, - { name: "Cèpes", icon: "🍄", allergenNames: [] }, - { name: "Aubergine", icon: "🍆", allergenNames: [] }, - { name: "Brocoli", icon: "🥦", allergenNames: [] }, - { name: "Chou-fleur", icon: "🥦", allergenNames: [] }, - { name: "Chou blanc", icon: "🥬", allergenNames: [] }, - { name: "Chou rouge", icon: "🥬", allergenNames: [] }, - { name: "Chou de Bruxelles", icon: "🥬", allergenNames: [] }, - { name: "Épinard", icon: "🥬", allergenNames: [] }, - { name: "Blette", icon: "🥬", allergenNames: [] }, - { name: "Salade", icon: "🥬", allergenNames: [] }, - { name: "Roquette", icon: "🥬", allergenNames: [] }, - { name: "Cresson", icon: "🥬", allergenNames: [] }, - { name: "Poireau", icon: "🥬", allergenNames: [] }, - { name: "Céleri", icon: "🥬", allergenNames: ["Céleri"] }, - { name: "Radis", allergenNames: [] }, - { name: "Betterave", allergenNames: [] }, - { name: "Navet", allergenNames: [] }, - { name: "Panais", allergenNames: [] }, - { name: "Haricot vert", icon: "🫛", allergenNames: [] }, - { name: "Petit pois", icon: "🫛", allergenNames: [] }, - { name: "Maïs", icon: "🌽", allergenNames: [] }, - { name: "Artichaut", allergenNames: [] }, - { name: "Fenouil", allergenNames: [] }, - { name: "Endive", allergenNames: [] }, - { name: "Potiron", icon: "🎃", allergenNames: [] }, - { name: "Butternut", icon: "🎃", allergenNames: [] }, - { name: "Asperge", allergenNames: [] }, - { name: "Avocat", icon: "🥑", allergenNames: [] }, - - // --- Fruits ------------------------------------------------------------------ - { name: "Citron", icon: "🍋", allergenNames: [] }, - { name: "Citron vert", icon: "🍋", allergenNames: [] }, - { name: "Pomme", icon: "🍎", allergenNames: [] }, - { name: "Poire", icon: "🍐", allergenNames: [] }, - { name: "Banane", icon: "🍌", allergenNames: [] }, - { name: "Orange", icon: "🍊", allergenNames: [] }, - { name: "Clémentine", icon: "🍊", allergenNames: [] }, - { name: "Pamplemousse", icon: "🍊", allergenNames: [] }, - { name: "Fraise", icon: "🍓", allergenNames: [] }, - { name: "Framboise", icon: "🍓", allergenNames: [] }, - { name: "Myrtille", icon: "🫐", allergenNames: [] }, - { name: "Mûre", icon: "🫐", allergenNames: [] }, - { name: "Cerise", icon: "🍒", allergenNames: [] }, - { name: "Abricot", icon: "🍑", allergenNames: [] }, - { name: "Pêche", icon: "🍑", allergenNames: [] }, - { name: "Prune", allergenNames: [] }, - { name: "Raisin", icon: "🍇", allergenNames: [] }, - { name: "Melon", icon: "🍈", allergenNames: [] }, - { name: "Pastèque", icon: "🍉", allergenNames: [] }, - { name: "Ananas", icon: "🍍", allergenNames: [] }, - { name: "Mangue", icon: "🥭", allergenNames: [] }, - { name: "Kiwi", icon: "🥝", allergenNames: [] }, - { name: "Figue", allergenNames: [] }, - { name: "Datte", allergenNames: [] }, - { name: "Litchi", allergenNames: [] }, - { name: "Grenade", allergenNames: [] }, - { name: "Rhubarbe", allergenNames: [] }, - { name: "Coing", allergenNames: [] }, - - // --- Fruits secs & oléagineux ------------------------------------------------ - { name: "Cacahuètes", icon: "🥜", allergenNames: ["Arachides"] }, - { name: "Amandes", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Noix", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Noisettes", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Noix de cajou", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Pistaches", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Noix de pécan", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Poudre d'amande", icon: "🌰", allergenNames: ["Fruits à coque"] }, - { name: "Pignons de pin", icon: "🌰", allergenNames: [] }, - { name: "Graines de tournesol", icon: "🌻", allergenNames: [] }, - { name: "Graines de courge", allergenNames: [] }, - { name: "Noix de coco râpée", icon: "🥥", allergenNames: [] }, - { name: "Raisins secs", icon: "🍇", allergenNames: ["Sulfites"] }, - { name: "Pruneaux", allergenNames: ["Sulfites"] }, - { name: "Abricots secs", icon: "🍑", allergenNames: ["Sulfites"] }, - - // --- Condiments, sauces & huiles ---------------------------------------------- - { name: "Sel", icon: "🧂", allergenNames: [] }, - { name: "Sucre", icon: "🍬", allergenNames: [] }, - { name: "Huile d'olive", icon: "🫒", allergenNames: [] }, - { name: "Huile de tournesol", icon: "🧴", allergenNames: [] }, - { name: "Huile de colza", icon: "🧴", allergenNames: [] }, - { name: "Huile de coco", icon: "🥥", allergenNames: [] }, - { name: "Huile de sésame", icon: "🧴", allergenNames: ["Graines de sésame"] }, - { name: "Vinaigre de cidre", icon: "🧴", allergenNames: [] }, - { name: "Vinaigre blanc", icon: "🧴", allergenNames: [] }, - { name: "Vinaigre balsamique", icon: "🧴", allergenNames: ["Sulfites"] }, - { name: "Sauce soja", icon: "🍶", allergenNames: ["Soja"] }, - { name: "Tofu", icon: "🧊", allergenNames: ["Soja"] }, - { name: "Moutarde", icon: "🟡", allergenNames: ["Moutarde"] }, - { name: "Mayonnaise", icon: "🫙", allergenNames: ["Œufs"] }, - { name: "Ketchup", icon: "🫙", allergenNames: [] }, - { name: "Miel", icon: "🍯", allergenNames: [] }, - { name: "Sirop d'érable", icon: "🍁", allergenNames: [] }, - { name: "Câpres", allergenNames: [] }, - { name: "Olives", icon: "🫒", allergenNames: [] }, - { name: "Tabasco", icon: "🌶️", allergenNames: [] }, - { name: "Sauce Worcestershire", icon: "🫙", allergenNames: ["Poissons"] }, - { name: "Sauce nuoc-mâm", icon: "🫙", allergenNames: ["Poissons"] }, - { name: "Wasabi", allergenNames: [] }, - { name: "Harissa", icon: "🌶️", allergenNames: [] }, - { name: "Pâte de curry", icon: "🍛", allergenNames: [] }, - { name: "Bouillon cube légumes", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Bouillon cube volaille", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Concentré de tomate", icon: "🍅", allergenNames: [] }, - { name: "Coulis de tomate", icon: "🍅", allergenNames: [] }, - { name: "Tomates pelées (conserve)", icon: "🍅", allergenNames: [] }, - { name: "Vin blanc (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, - { name: "Vin rouge (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de vin rouge", icon: "🧴", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de vin blanc", icon: "🧴", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de xérès", icon: "🧴", allergenNames: ["Sulfites"] }, - { name: "Huile de noix", icon: "🧴", allergenNames: ["Fruits à coque"] }, - { name: "Huile de noisette", icon: "🧴", allergenNames: ["Fruits à coque"] }, - { name: "Huile d'arachide", icon: "🧴", allergenNames: ["Arachides"] }, - { name: "Huile pimentée", icon: "🌶️", allergenNames: [] }, - { name: "Beurre de cacahuète", icon: "🥜", allergenNames: ["Arachides"] }, - { name: "Farine de lupin", icon: "🌱", allergenNames: ["Lupin"] }, - { name: "Moutarde de Dijon", icon: "🟡", allergenNames: ["Moutarde"] }, - { name: "Moutarde à l'ancienne", icon: "🟡", allergenNames: ["Moutarde"] }, - { name: "Sauce barbecue", icon: "🫙", allergenNames: [] }, - { name: "Sauce tartare", icon: "🫙", allergenNames: ["Œufs"] }, - { name: "Sauce cocktail", icon: "🫙", allergenNames: ["Œufs"] }, - { name: "Sauce béarnaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] }, - { name: "Sauce hollandaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] }, - { name: "Sauce béchamel", icon: "🫙", allergenNames: ["Lait", "Gluten"] }, - { name: "Sauce teriyaki", icon: "🫙", allergenNames: ["Soja"] }, - { name: "Sauce ponzu", icon: "🫙", allergenNames: ["Soja", "Poissons"] }, - { name: "Chimichurri", icon: "🌿", allergenNames: [] }, - { name: "Pesto rouge (tomates séchées)", icon: "🫙", allergenNames: ["Lait", "Fruits à coque"] }, - { name: "Tomates séchées", icon: "🍅", allergenNames: [] }, - { name: "Fond de veau", icon: "🫙", allergenNames: [] }, - { name: "Fond de volaille", icon: "🫙", allergenNames: [] }, - { name: "Bouillon cube bœuf", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Bouillon cube poisson", icon: "🫙", allergenNames: ["Poissons", "Céleri"] }, - { name: "Bouillon de légumes", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Bouillon de volaille", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Bouillon de bœuf", icon: "🫙", allergenNames: ["Céleri"] }, - { name: "Court-bouillon", icon: "🫙", allergenNames: [] }, - { name: "Dashi (bouillon japonais)", icon: "🫙", allergenNames: ["Poissons"] }, - { name: "Bisque de crustacés", icon: "🫙", allergenNames: ["Crustacés"] }, - - // --- Épices & herbes ----------------------------------------------------------- - { name: "Basilic", icon: "🌿", allergenNames: [] }, - { name: "Persil", icon: "🌿", allergenNames: [] }, - { name: "Thym", icon: "🌿", allergenNames: [] }, - { name: "Romarin", icon: "🌿", allergenNames: [] }, - { name: "Laurier", icon: "🌿", allergenNames: [] }, - { name: "Ciboulette", icon: "🌿", allergenNames: [] }, - { name: "Coriandre fraîche", icon: "🌿", allergenNames: [] }, - { name: "Menthe", icon: "🌿", allergenNames: [] }, - { name: "Origan", icon: "🌿", allergenNames: [] }, - { name: "Aneth", icon: "🌿", allergenNames: [] }, - { name: "Estragon", icon: "🌿", allergenNames: [] }, - { name: "Herbes de Provence", icon: "🌿", allergenNames: [] }, - { name: "Poivre noir", allergenNames: [] }, - { name: "Paprika", icon: "🌶️", allergenNames: [] }, - { name: "Piment d'Espelette", icon: "🌶️", allergenNames: [] }, - { name: "Piment de Cayenne", icon: "🌶️", allergenNames: [] }, - { name: "Cumin", icon: "🌿", allergenNames: [] }, - { name: "Curry (poudre)", icon: "🌿", allergenNames: [] }, - { name: "Curcuma", icon: "🌿", allergenNames: [] }, - { name: "Cannelle", icon: "🌿", allergenNames: [] }, - { name: "Gingembre", icon: "🫚", allergenNames: [] }, - { name: "Muscade", icon: "🌿", allergenNames: [] }, - { name: "Safran", icon: "🌿", allergenNames: [] }, - { name: "Clou de girofle", icon: "🌿", allergenNames: [] }, - { name: "Vanille (gousse)", icon: "🌿", allergenNames: [] }, - { name: "Poivre blanc", allergenNames: [] }, - { name: "Poivre rose", allergenNames: [] }, - { name: "Poivre du Sichuan", allergenNames: [] }, - { name: "Paprika fumé", icon: "🌶️", allergenNames: [] }, - { name: "Piment oiseau", icon: "🌶️", allergenNames: [] }, - { name: "Baies de genièvre", allergenNames: [] }, - { name: "Anis étoilé (badiane)", icon: "🌿", allergenNames: [] }, - { name: "Anis vert", icon: "🌿", allergenNames: [] }, - { name: "Graines de fenouil", icon: "🌿", allergenNames: [] }, - { name: "Sarriette", icon: "🌿", allergenNames: [] }, - { name: "Marjolaine", icon: "🌿", allergenNames: [] }, - { name: "Sauge", icon: "🌿", allergenNames: [] }, - { name: "Cerfeuil", icon: "🌿", allergenNames: [] }, - { name: "Sumac", allergenNames: [] }, - { name: "Nigelle", allergenNames: [] }, - { name: "Quatre épices", icon: "🌿", allergenNames: [] }, - { name: "Colombo (poudre)", icon: "🌿", allergenNames: [] }, - { name: "Baharat", icon: "🌿", allergenNames: [] }, - { name: "Raifort", allergenNames: [] }, - { name: "Sel aux herbes", icon: "🧂", allergenNames: [] }, - { name: "Sel de céleri", icon: "🧂", allergenNames: ["Céleri"] }, - { name: "Fleur de sel", icon: "🧂", allergenNames: [] }, - - // --- Sucre & pâtisserie --------------------------------------------------------- - { name: "Sucre roux", icon: "🍬", allergenNames: [] }, - { name: "Sucre glace", icon: "🍬", allergenNames: [] }, - { name: "Cassonade", icon: "🍬", allergenNames: [] }, - { name: "Chocolat noir", icon: "🍫", allergenNames: [] }, - { name: "Chocolat au lait", icon: "🍫", allergenNames: ["Lait"] }, - { name: "Chocolat blanc", icon: "🍫", allergenNames: ["Lait"] }, - { name: "Pépites de chocolat", icon: "🍫", allergenNames: [] }, - { name: "Cacao en poudre", icon: "🍫", allergenNames: [] }, - { name: "Gélatine", icon: "🫙", allergenNames: [] }, - { name: "Extrait de vanille", icon: "🌿", allergenNames: [] }, - - // --- Cuisine italienne --------------------------------------------------------- - { name: "Spaghetti", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Penne", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Tagliatelles", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Lasagnes (feuilles)", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Gnocchi", icon: "🍝", allergenNames: ["Gluten"] }, - { name: "Riz arborio", icon: "🍚", allergenNames: [] }, - { name: "Burrata", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Ricotta", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Pecorino", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Gorgonzola", icon: "🧀", allergenNames: ["Lait"] }, - { name: "Prosciutto", icon: "🍖", allergenNames: [] }, - { name: "Pancetta", icon: "🥓", allergenNames: [] }, - { name: "Mortadelle", icon: "🍖", allergenNames: [] }, - { name: "Salami", icon: "🍖", allergenNames: [] }, - { name: "Pesto", icon: "🫙", allergenNames: ["Lait", "Fruits à coque"] }, - { name: "Tomates cerises", icon: "🍅", allergenNames: [] }, - { name: "Focaccia", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Ciabatta", icon: "🍞", allergenNames: ["Gluten"] }, - - // --- Cuisine asiatique (chinoise, japonaise, thaïe, coréenne, indienne...) ------ - { name: "Sauce huître", icon: "🫙", allergenNames: ["Mollusques"] }, - { name: "Sauce hoisin", icon: "🫙", allergenNames: ["Soja"] }, - { name: "Sauce sriracha", icon: "🫙", allergenNames: [] }, - { name: "Sauce sweet chili", icon: "🫙", allergenNames: [] }, - { name: "Vinaigre de riz", icon: "🧴", allergenNames: [] }, - { name: "Mirin", icon: "🍶", allergenNames: [] }, - { name: "Saké (cuisine)", icon: "🍶", allergenNames: [] }, - { name: "Miso", icon: "🫙", allergenNames: ["Soja"] }, - { name: "Tofu soyeux", icon: "🧊", allergenNames: ["Soja"] }, - { name: "Graines de sésame", icon: "🌱", allergenNames: ["Graines de sésame"] }, - { name: "Nouilles de riz", icon: "🍜", allergenNames: [] }, - { name: "Nouilles udon", icon: "🍜", allergenNames: ["Gluten"] }, - { name: "Nouilles soba", icon: "🍜", allergenNames: ["Gluten"] }, - { name: "Nouilles chinoises", icon: "🍜", allergenNames: ["Gluten"] }, - { name: "Vermicelles de riz", icon: "🍜", allergenNames: [] }, - { name: "Vermicelles de soja", icon: "🍜", allergenNames: [] }, - { name: "Riz gluant", icon: "🍚", allergenNames: [] }, - { name: "Riz à sushi", icon: "🍚", allergenNames: [] }, - { name: "Riz jasmin", icon: "🍚", allergenNames: [] }, - { name: "Citronnelle", icon: "🌿", allergenNames: [] }, - { name: "Combava", icon: "🌿", allergenNames: [] }, - { name: "Pak-choï", icon: "🥬", allergenNames: [] }, - { name: "Germes de soja", icon: "🌱", allergenNames: ["Soja"] }, - { name: "Shiitake", icon: "🍄", allergenNames: [] }, - { name: "Champignons noirs", icon: "🍄", allergenNames: [] }, - { name: "Daikon", allergenNames: [] }, - { name: "Algue nori", icon: "🌿", allergenNames: [] }, - { name: "Algue wakamé", icon: "🌿", allergenNames: [] }, - { name: "Algue kombu", icon: "🌿", allergenNames: [] }, - { name: "Cinq épices", icon: "🌿", allergenNames: [] }, - { name: "Garam masala", icon: "🌿", allergenNames: [] }, - { name: "Graines de coriandre", icon: "🌿", allergenNames: [] }, - { name: "Cardamome", icon: "🌿", allergenNames: [] }, - { name: "Fenugrec", icon: "🌿", allergenNames: [] }, - { name: "Piment vert frais", icon: "🌶️", allergenNames: [] }, - { name: "Farine de tapioca", icon: "🌾", allergenNames: [] }, - { name: "Sucre de palme", icon: "🍬", allergenNames: [] }, - { name: "Pousses de bambou", allergenNames: [] }, - { name: "Châtaignes d'eau", allergenNames: [] }, - { name: "Poisson séché", icon: "🐟", allergenNames: ["Poissons"] }, - { name: "Pâte de crevettes", icon: "🫙", allergenNames: ["Crustacés"] }, - { name: "Pâte de curry rouge (thaï)", icon: "🍛", allergenNames: [] }, - { name: "Pâte de curry vert (thaï)", icon: "🍛", allergenNames: [] }, - - // --- Cuisine mexicaine ----------------------------------------------------------- - { name: "Tortilla de maïs", icon: "🌮", allergenNames: [] }, - { name: "Tortilla de blé", icon: "🌮", allergenNames: ["Gluten"] }, - { name: "Haricots pinto", icon: "🫘", allergenNames: [] }, - { name: "Piment jalapeño", icon: "🌶️", allergenNames: [] }, - { name: "Piment chipotle", icon: "🌶️", allergenNames: [] }, - { name: "Piment poblano", icon: "🌶️", allergenNames: [] }, - { name: "Piment habanero", icon: "🌶️", allergenNames: [] }, - { name: "Masa harina", icon: "🌽", allergenNames: [] }, - { name: "Cheddar", icon: "🧀", allergenNames: ["Lait"] }, - - // --- Maghreb & Moyen-Orient -------------------------------------------------------- - { name: "Ras el hanout", icon: "🌿", allergenNames: [] }, - { name: "Za'atar", icon: "🌿", allergenNames: ["Graines de sésame"] }, - { name: "Tahini", icon: "🫙", allergenNames: ["Graines de sésame"] }, - - // --- Pains & sandwichs --------------------------------------------------------------- - // `Pain`/`Pain de mie`/`Pain complet`/`Baguette`/`Pain de seigle` are - // seeded further up (Céréales, farines & féculents) — this section covers - // the shapes specifically reached for to build a sandwich, plus a few - // international flatbreads not tied to one cuisine section above. - { name: "Pain à burger", icon: "🍔", allergenNames: ["Gluten", "Lait", "Œufs"] }, - { name: "Pain brioché", icon: "🍞", allergenNames: ["Gluten", "Lait", "Œufs"] }, - { name: "Pain à hot-dog", icon: "🌭", allergenNames: ["Gluten"] }, - { name: "Pain pita", icon: "🫓", allergenNames: ["Gluten"] }, - { name: "Pain bagel", icon: "🥯", allergenNames: ["Gluten"] }, - { name: "Naan", icon: "🫓", allergenNames: ["Gluten"] }, - { name: "Pain wrap", icon: "🫓", allergenNames: ["Gluten"] }, - { name: "Pain viennois", icon: "🍞", allergenNames: ["Gluten", "Lait"] }, - { name: "Pain de campagne", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Pain aux céréales", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Petit pain", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Pain suédois", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Pain sans gluten", icon: "🍞", allergenNames: [] }, - { name: "Biscotte", icon: "🍞", allergenNames: ["Gluten"] }, - { name: "Croûtons", icon: "🍞", allergenNames: ["Gluten"] }, - - // --- Épicerie divers --------------------------------------------------------------- - { name: "Bicarbonate de soude", icon: "🫙", allergenNames: [] }, - { name: "Fécule de pomme de terre", icon: "🫙", allergenNames: [] }, - - // --- Liquides & boissons de cuisine ------------------------------------------- - { name: "Eau", icon: "💧", allergenNames: [] }, - { name: "Eau gazeuse", icon: "💧", allergenNames: [] }, - { name: "Eau de fleur d'oranger", icon: "💧", allergenNames: [] }, - { name: "Eau de rose", icon: "💧", allergenNames: [] }, - { name: "Jus de citron", icon: "🧃", allergenNames: [] }, - { name: "Jus de citron vert", icon: "🧃", allergenNames: [] }, - { name: "Jus d'orange", icon: "🧃", allergenNames: [] }, - { name: "Jus de pomme", icon: "🧃", allergenNames: [] }, - { name: "Jus de raisin", icon: "🧃", allergenNames: [] }, - { name: "Jus de tomate", icon: "🧃", allergenNames: [] }, - { name: "Jus de cranberry", icon: "🧃", allergenNames: [] }, - { name: "Café", icon: "☕", allergenNames: [] }, - { name: "Thé", icon: "🍵", allergenNames: [] }, - { name: "Bière (cuisine)", icon: "🍺", allergenNames: ["Gluten"] }, - { name: "Cidre (cuisine)", allergenNames: ["Sulfites"] }, - { name: "Champagne / vin pétillant (cuisine)", icon: "🥂", allergenNames: ["Sulfites"] }, - { name: "Porto (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, - { name: "Vin jaune (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, - { name: "Cognac", icon: "🥃", allergenNames: [] }, - { name: "Rhum", icon: "🥃", allergenNames: [] }, - { name: "Whisky", icon: "🥃", allergenNames: [] }, - { name: "Vodka", icon: "🥃", allergenNames: [] }, - { name: "Sirop de sucre de canne", icon: "🫙", allergenNames: [] }, - { name: "Fumet de poisson", icon: "🫙", allergenNames: ["Poissons"] }, +// +// Grouped by `category` (mirrors `IngredientCategory` in schema.prisma) so +// the picker UI (`apps/web`'s `IngredientPicker`) can offer category +// chips/browsing, not just free-text search — with 400+ ingredients, search +// alone isn't enough to actually *find* something. 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. +const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: IngredientSeed[] }> = [ + { + category: "CEREALES_FECULENTS", + items: [ + { name: "Farine de blé", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Farine complète", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Farine de maïs", icon: "🌽", allergenNames: [] }, + { name: "Farine de sarrasin", icon: "🌾", allergenNames: [] }, + { name: "Farine de riz", icon: "🍚", allergenNames: [] }, + { name: "Semoule", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Couscous", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Boulgour", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Polenta", icon: "🌽", allergenNames: [] }, + { name: "Quinoa", icon: "🌾", allergenNames: [] }, + { name: "Pâtes", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Pâtes complètes", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Riz", icon: "🍚", allergenNames: [] }, + { name: "Riz basmati", icon: "🍚", allergenNames: [] }, + { name: "Riz complet", icon: "🍚", allergenNames: [] }, + { name: "Flocons d'avoine", icon: "🌾", allergenNames: ["Gluten"] }, + { name: "Pomme de terre", icon: "🥔", allergenNames: [] }, + { name: "Patate douce", icon: "🍠", allergenNames: [] }, + { name: "Pain", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Pain de mie", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Pain complet", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Baguette", icon: "🥖", allergenNames: ["Gluten"] }, + { name: "Pain de seigle", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Chapelure", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Levure boulangère", icon: "🫙", allergenNames: [] }, + { name: "Levure chimique", icon: "🫙", allergenNames: [] }, + { name: "Maïzena", icon: "🫙", allergenNames: [] }, + ], + }, + { + category: "LEGUMINEUSES", + items: [ + { name: "Lentilles vertes", icon: "🫘", allergenNames: [] }, + { name: "Lentilles corail", icon: "🫘", allergenNames: [] }, + { name: "Pois chiches", icon: "🫘", allergenNames: [] }, + { name: "Haricots blancs", icon: "🫘", allergenNames: [] }, + { name: "Haricots rouges", icon: "🫘", allergenNames: [] }, + { name: "Haricots noirs", icon: "🫘", allergenNames: [] }, + { name: "Pois cassés", icon: "🫘", allergenNames: [] }, + { name: "Fèves", icon: "🫘", allergenNames: [] }, + { name: "Edamame", icon: "🫛", allergenNames: ["Soja"] }, + ], + }, + { + category: "VIANDES_VOLAILLES", + items: [ + { name: "Poulet", icon: "🍗", allergenNames: [] }, + { name: "Dinde", icon: "🍗", allergenNames: [] }, + { name: "Canard", icon: "🦆", allergenNames: [] }, + { name: "Magret de canard", icon: "🦆", allergenNames: [] }, + { name: "Lapin", icon: "🐇", allergenNames: [] }, + { name: "Bœuf haché", icon: "🥩", allergenNames: [] }, + { name: "Steak de bœuf", icon: "🥩", allergenNames: [] }, + { name: "Rôti de bœuf", icon: "🥩", allergenNames: [] }, + { name: "Escalope de veau", icon: "🥩", allergenNames: [] }, + { name: "Filet mignon de porc", icon: "🥩", allergenNames: [] }, + { name: "Côte de porc", icon: "🥩", allergenNames: [] }, + { name: "Agneau", icon: "🍖", allergenNames: [] }, + { name: "Gigot d'agneau", icon: "🍖", allergenNames: [] }, + { name: "Lardons", icon: "🥓", allergenNames: [] }, + { name: "Bacon", icon: "🥓", allergenNames: [] }, + { name: "Jambon blanc", icon: "🍖", allergenNames: [] }, + { name: "Jambon cru", icon: "🍖", allergenNames: [] }, + { name: "Saucisse", icon: "🌭", allergenNames: [] }, + { name: "Chorizo", icon: "🌭", allergenNames: [] }, + { name: "Merguez", icon: "🌭", allergenNames: [] }, + ], + }, + { + category: "POISSONS_FRUITS_DE_MER", + items: [ + { name: "Saumon", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Thon", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Cabillaud", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Truite", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Sardine", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Anchois", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Merlan", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Surimi", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Bar (loup de mer)", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Dorade", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Sole", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Turbot", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Merlu", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Colin", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Lieu noir", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Églefin", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Maquereau", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Hareng", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Rouget", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Raie", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Lotte", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Flétan", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Espadon", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Carpe", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Brochet", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Perche", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Tilapia", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Panga", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Saumon fumé", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Crevettes", icon: "🍤", allergenNames: ["Crustacés"] }, + { name: "Langoustines", icon: "🍤", allergenNames: ["Crustacés"] }, + { name: "Homard", icon: "🦞", allergenNames: ["Crustacés"] }, + { name: "Crabe", icon: "🦀", allergenNames: ["Crustacés"] }, + { name: "Langouste", icon: "🦞", allergenNames: ["Crustacés"] }, + { name: "Moules", icon: "🦪", allergenNames: ["Mollusques"] }, + { name: "Huîtres", icon: "🦪", allergenNames: ["Mollusques"] }, + { name: "Saint-Jacques", icon: "🦪", allergenNames: ["Mollusques"] }, + { name: "Calamar", icon: "🦑", allergenNames: ["Mollusques"] }, + { name: "Poulpe", icon: "🐙", allergenNames: ["Mollusques"] }, + { name: "Palourdes", icon: "🦪", allergenNames: ["Mollusques"] }, + { name: "Bulots", icon: "🐚", allergenNames: ["Mollusques"] }, + ], + }, + { + category: "PRODUITS_LAITIERS_OEUFS", + items: [ + { name: "Œuf", icon: "🥚", allergenNames: ["Œufs"] }, + { name: "Lait", icon: "🥛", allergenNames: ["Lait"] }, + { name: "Beurre", icon: "🧈", allergenNames: ["Lait"] }, + { name: "Crème fraîche", icon: "🥛", allergenNames: ["Lait"] }, + { name: "Crème liquide", icon: "🥛", allergenNames: ["Lait"] }, + { name: "Fromage", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Emmental", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Gruyère", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Parmesan", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Mozzarella", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Chèvre (fromage)", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Feta", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Comté", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Fromage blanc", icon: "🥣", allergenNames: ["Lait"] }, + { name: "Mascarpone", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Yaourt", icon: "🥣", allergenNames: ["Lait"] }, + { name: "Lait de coco", icon: "🥥", allergenNames: [] }, + { name: "Crème de coco", icon: "🥥", allergenNames: [] }, + { name: "Lait d'amande", icon: "🥛", allergenNames: ["Fruits à coque"] }, + { name: "Lait d'avoine", icon: "🥛", allergenNames: ["Gluten"] }, + ], + }, + { + category: "LEGUMES", + items: [ + { name: "Tomate", icon: "🍅", allergenNames: [] }, + { name: "Oignon", icon: "🧅", allergenNames: [] }, + { name: "Échalote", icon: "🧅", allergenNames: [] }, + { name: "Ail", icon: "🧄", allergenNames: [] }, + { name: "Carotte", icon: "🥕", allergenNames: [] }, + { name: "Courgette", icon: "🥒", allergenNames: [] }, + { name: "Concombre", icon: "🥒", allergenNames: [] }, + { name: "Cornichons", icon: "🥒", allergenNames: [] }, + { name: "Poivron", icon: "🫑", allergenNames: [] }, + { name: "Champignon", icon: "🍄", allergenNames: [] }, + { name: "Cèpes", icon: "🍄", allergenNames: [] }, + { name: "Aubergine", icon: "🍆", allergenNames: [] }, + { name: "Brocoli", icon: "🥦", allergenNames: [] }, + { name: "Chou-fleur", icon: "🥦", allergenNames: [] }, + { name: "Chou blanc", icon: "🥬", allergenNames: [] }, + { name: "Chou rouge", icon: "🥬", allergenNames: [] }, + { name: "Chou de Bruxelles", icon: "🥬", allergenNames: [] }, + { name: "Épinard", icon: "🥬", allergenNames: [] }, + { name: "Blette", icon: "🥬", allergenNames: [] }, + { name: "Salade", icon: "🥬", allergenNames: [] }, + { name: "Roquette", icon: "🥬", allergenNames: [] }, + { name: "Cresson", icon: "🥬", allergenNames: [] }, + { name: "Poireau", icon: "🥬", allergenNames: [] }, + { name: "Céleri", icon: "🥬", allergenNames: ["Céleri"] }, + { name: "Radis", allergenNames: [] }, + { name: "Betterave", allergenNames: [] }, + { name: "Navet", allergenNames: [] }, + { name: "Panais", allergenNames: [] }, + { name: "Haricot vert", icon: "🫛", allergenNames: [] }, + { name: "Petit pois", icon: "🫛", allergenNames: [] }, + { name: "Maïs", icon: "🌽", allergenNames: [] }, + { name: "Artichaut", allergenNames: [] }, + { name: "Fenouil", allergenNames: [] }, + { name: "Endive", allergenNames: [] }, + { name: "Potiron", icon: "🎃", allergenNames: [] }, + { name: "Butternut", icon: "🎃", allergenNames: [] }, + { name: "Asperge", allergenNames: [] }, + { name: "Avocat", icon: "🥑", allergenNames: [] }, + ], + }, + { + category: "FRUITS", + items: [ + { name: "Citron", icon: "🍋", allergenNames: [] }, + { name: "Citron vert", icon: "🍋", allergenNames: [] }, + { name: "Pomme", icon: "🍎", allergenNames: [] }, + { name: "Poire", icon: "🍐", allergenNames: [] }, + { name: "Banane", icon: "🍌", allergenNames: [] }, + { name: "Orange", icon: "🍊", allergenNames: [] }, + { name: "Clémentine", icon: "🍊", allergenNames: [] }, + { name: "Pamplemousse", icon: "🍊", allergenNames: [] }, + { name: "Fraise", icon: "🍓", allergenNames: [] }, + { name: "Framboise", icon: "🍓", allergenNames: [] }, + { name: "Myrtille", icon: "🫐", allergenNames: [] }, + { name: "Mûre", icon: "🫐", allergenNames: [] }, + { name: "Cerise", icon: "🍒", allergenNames: [] }, + { name: "Abricot", icon: "🍑", allergenNames: [] }, + { name: "Pêche", icon: "🍑", allergenNames: [] }, + { name: "Prune", allergenNames: [] }, + { name: "Raisin", icon: "🍇", allergenNames: [] }, + { name: "Melon", icon: "🍈", allergenNames: [] }, + { name: "Pastèque", icon: "🍉", allergenNames: [] }, + { name: "Ananas", icon: "🍍", allergenNames: [] }, + { name: "Mangue", icon: "🥭", allergenNames: [] }, + { name: "Kiwi", icon: "🥝", allergenNames: [] }, + { name: "Figue", allergenNames: [] }, + { name: "Datte", allergenNames: [] }, + { name: "Litchi", allergenNames: [] }, + { name: "Grenade", allergenNames: [] }, + { name: "Rhubarbe", allergenNames: [] }, + { name: "Coing", allergenNames: [] }, + ], + }, + { + category: "FRUITS_SECS_OLEAGINEUX", + items: [ + { name: "Cacahuètes", icon: "🥜", allergenNames: ["Arachides"] }, + { name: "Amandes", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Noix", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Noisettes", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Noix de cajou", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Pistaches", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Noix de pécan", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Poudre d'amande", icon: "🌰", allergenNames: ["Fruits à coque"] }, + { name: "Pignons de pin", icon: "🌰", allergenNames: [] }, + { name: "Graines de tournesol", icon: "🌻", allergenNames: [] }, + { name: "Graines de courge", allergenNames: [] }, + { name: "Noix de coco râpée", icon: "🥥", allergenNames: [] }, + { name: "Raisins secs", icon: "🍇", allergenNames: ["Sulfites"] }, + { name: "Pruneaux", allergenNames: ["Sulfites"] }, + { name: "Abricots secs", icon: "🍑", allergenNames: ["Sulfites"] }, + ], + }, + { + category: "CONDIMENTS_SAUCES", + items: [ + { name: "Sel", icon: "🧂", allergenNames: [] }, + { name: "Sucre", icon: "🍬", allergenNames: [] }, + { name: "Huile d'olive", icon: "🫒", allergenNames: [] }, + { name: "Huile de tournesol", icon: "🧴", allergenNames: [] }, + { name: "Huile de colza", icon: "🧴", allergenNames: [] }, + { name: "Huile de coco", icon: "🥥", allergenNames: [] }, + { name: "Huile de sésame", icon: "🧴", allergenNames: ["Graines de sésame"] }, + { name: "Vinaigre de cidre", icon: "🧴", allergenNames: [] }, + { name: "Vinaigre blanc", icon: "🧴", allergenNames: [] }, + { name: "Vinaigre balsamique", icon: "🧴", allergenNames: ["Sulfites"] }, + { name: "Sauce soja", icon: "🍶", allergenNames: ["Soja"] }, + { name: "Tofu", icon: "🧊", allergenNames: ["Soja"] }, + { name: "Moutarde", icon: "🟡", allergenNames: ["Moutarde"] }, + { name: "Mayonnaise", icon: "🫙", allergenNames: ["Œufs"] }, + { name: "Ketchup", icon: "🫙", allergenNames: [] }, + { name: "Miel", icon: "🍯", allergenNames: [] }, + { name: "Sirop d'érable", icon: "🍁", allergenNames: [] }, + { name: "Câpres", allergenNames: [] }, + { name: "Olives", icon: "🫒", allergenNames: [] }, + { name: "Tabasco", icon: "🌶️", allergenNames: [] }, + { name: "Sauce Worcestershire", icon: "🫙", allergenNames: ["Poissons"] }, + { name: "Sauce nuoc-mâm", icon: "🫙", allergenNames: ["Poissons"] }, + { name: "Wasabi", allergenNames: [] }, + { name: "Harissa", icon: "🌶️", allergenNames: [] }, + { name: "Pâte de curry", icon: "🍛", allergenNames: [] }, + { name: "Bouillon cube légumes", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Bouillon cube volaille", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Concentré de tomate", icon: "🍅", allergenNames: [] }, + { name: "Coulis de tomate", icon: "🍅", allergenNames: [] }, + { name: "Tomates pelées (conserve)", icon: "🍅", allergenNames: [] }, + { name: "Vin blanc (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, + { name: "Vin rouge (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, + { name: "Vinaigre de vin rouge", icon: "🧴", allergenNames: ["Sulfites"] }, + { name: "Vinaigre de vin blanc", icon: "🧴", allergenNames: ["Sulfites"] }, + { name: "Vinaigre de xérès", icon: "🧴", allergenNames: ["Sulfites"] }, + { name: "Huile de noix", icon: "🧴", allergenNames: ["Fruits à coque"] }, + { name: "Huile de noisette", icon: "🧴", allergenNames: ["Fruits à coque"] }, + { name: "Huile d'arachide", icon: "🧴", allergenNames: ["Arachides"] }, + { name: "Huile pimentée", icon: "🌶️", allergenNames: [] }, + { name: "Beurre de cacahuète", icon: "🥜", allergenNames: ["Arachides"] }, + { name: "Farine de lupin", icon: "🌱", allergenNames: ["Lupin"] }, + { name: "Moutarde de Dijon", icon: "🟡", allergenNames: ["Moutarde"] }, + { name: "Moutarde à l'ancienne", icon: "🟡", allergenNames: ["Moutarde"] }, + { name: "Sauce barbecue", icon: "🫙", allergenNames: [] }, + { name: "Sauce tartare", icon: "🫙", allergenNames: ["Œufs"] }, + { name: "Sauce cocktail", icon: "🫙", allergenNames: ["Œufs"] }, + { name: "Sauce béarnaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] }, + { name: "Sauce hollandaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] }, + { name: "Sauce béchamel", icon: "🫙", allergenNames: ["Lait", "Gluten"] }, + { name: "Sauce teriyaki", icon: "🫙", allergenNames: ["Soja"] }, + { name: "Sauce ponzu", icon: "🫙", allergenNames: ["Soja", "Poissons"] }, + { name: "Chimichurri", icon: "🌿", allergenNames: [] }, + { + name: "Pesto rouge (tomates séchées)", + icon: "🫙", + allergenNames: ["Lait", "Fruits à coque"], + }, + { name: "Tomates séchées", icon: "🍅", allergenNames: [] }, + { name: "Fond de veau", icon: "🫙", allergenNames: [] }, + { name: "Fond de volaille", icon: "🫙", allergenNames: [] }, + { name: "Bouillon cube bœuf", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Bouillon cube poisson", icon: "🫙", allergenNames: ["Poissons", "Céleri"] }, + { name: "Bouillon de légumes", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Bouillon de volaille", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Bouillon de bœuf", icon: "🫙", allergenNames: ["Céleri"] }, + { name: "Court-bouillon", icon: "🫙", allergenNames: [] }, + { name: "Dashi (bouillon japonais)", icon: "🫙", allergenNames: ["Poissons"] }, + { name: "Bisque de crustacés", icon: "🫙", allergenNames: ["Crustacés"] }, + ], + }, + { + category: "EPICES_HERBES", + items: [ + { name: "Basilic", icon: "🌿", allergenNames: [] }, + { name: "Persil", icon: "🌿", allergenNames: [] }, + { name: "Thym", icon: "🌿", allergenNames: [] }, + { name: "Romarin", icon: "🌿", allergenNames: [] }, + { name: "Laurier", icon: "🌿", allergenNames: [] }, + { name: "Ciboulette", icon: "🌿", allergenNames: [] }, + { name: "Coriandre fraîche", icon: "🌿", allergenNames: [] }, + { name: "Menthe", icon: "🌿", allergenNames: [] }, + { name: "Origan", icon: "🌿", allergenNames: [] }, + { name: "Aneth", icon: "🌿", allergenNames: [] }, + { name: "Estragon", icon: "🌿", allergenNames: [] }, + { name: "Herbes de Provence", icon: "🌿", allergenNames: [] }, + { name: "Poivre noir", allergenNames: [] }, + { name: "Paprika", icon: "🌶️", allergenNames: [] }, + { name: "Piment d'Espelette", icon: "🌶️", allergenNames: [] }, + { name: "Piment de Cayenne", icon: "🌶️", allergenNames: [] }, + { name: "Cumin", icon: "🌿", allergenNames: [] }, + { name: "Curry (poudre)", icon: "🌿", allergenNames: [] }, + { name: "Curcuma", icon: "🌿", allergenNames: [] }, + { name: "Cannelle", icon: "🌿", allergenNames: [] }, + { name: "Gingembre", icon: "🫚", allergenNames: [] }, + { name: "Muscade", icon: "🌿", allergenNames: [] }, + { name: "Safran", icon: "🌿", allergenNames: [] }, + { name: "Clou de girofle", icon: "🌿", allergenNames: [] }, + { name: "Vanille (gousse)", icon: "🌿", allergenNames: [] }, + { name: "Poivre blanc", allergenNames: [] }, + { name: "Poivre rose", allergenNames: [] }, + { name: "Poivre du Sichuan", allergenNames: [] }, + { name: "Paprika fumé", icon: "🌶️", allergenNames: [] }, + { name: "Piment oiseau", icon: "🌶️", allergenNames: [] }, + { name: "Baies de genièvre", allergenNames: [] }, + { name: "Anis étoilé (badiane)", icon: "🌿", allergenNames: [] }, + { name: "Anis vert", icon: "🌿", allergenNames: [] }, + { name: "Graines de fenouil", icon: "🌿", allergenNames: [] }, + { name: "Sarriette", icon: "🌿", allergenNames: [] }, + { name: "Marjolaine", icon: "🌿", allergenNames: [] }, + { name: "Sauge", icon: "🌿", allergenNames: [] }, + { name: "Cerfeuil", icon: "🌿", allergenNames: [] }, + { name: "Sumac", allergenNames: [] }, + { name: "Nigelle", allergenNames: [] }, + { name: "Quatre épices", icon: "🌿", allergenNames: [] }, + { name: "Colombo (poudre)", icon: "🌿", allergenNames: [] }, + { name: "Baharat", icon: "🌿", allergenNames: [] }, + { name: "Raifort", allergenNames: [] }, + { name: "Sel aux herbes", icon: "🧂", allergenNames: [] }, + { name: "Sel de céleri", icon: "🧂", allergenNames: ["Céleri"] }, + { name: "Fleur de sel", icon: "🧂", allergenNames: [] }, + ], + }, + { + category: "SUCRE_PATISSERIE", + items: [ + { name: "Sucre roux", icon: "🍬", allergenNames: [] }, + { name: "Sucre glace", icon: "🍬", allergenNames: [] }, + { name: "Cassonade", icon: "🍬", allergenNames: [] }, + { name: "Chocolat noir", icon: "🍫", allergenNames: [] }, + { name: "Chocolat au lait", icon: "🍫", allergenNames: ["Lait"] }, + { name: "Chocolat blanc", icon: "🍫", allergenNames: ["Lait"] }, + { name: "Pépites de chocolat", icon: "🍫", allergenNames: [] }, + { name: "Cacao en poudre", icon: "🍫", allergenNames: [] }, + { name: "Gélatine", icon: "🫙", allergenNames: [] }, + { name: "Extrait de vanille", icon: "🌿", allergenNames: [] }, + ], + }, + { + category: "CUISINE_ITALIENNE", + items: [ + { name: "Spaghetti", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Penne", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Tagliatelles", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Lasagnes (feuilles)", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Gnocchi", icon: "🍝", allergenNames: ["Gluten"] }, + { name: "Riz arborio", icon: "🍚", allergenNames: [] }, + { name: "Burrata", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Ricotta", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Pecorino", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Gorgonzola", icon: "🧀", allergenNames: ["Lait"] }, + { name: "Prosciutto", icon: "🍖", allergenNames: [] }, + { name: "Pancetta", icon: "🥓", allergenNames: [] }, + { name: "Mortadelle", icon: "🍖", allergenNames: [] }, + { name: "Salami", icon: "🍖", allergenNames: [] }, + { name: "Pesto", icon: "🫙", allergenNames: ["Lait", "Fruits à coque"] }, + { name: "Tomates cerises", icon: "🍅", allergenNames: [] }, + { name: "Focaccia", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Ciabatta", icon: "🍞", allergenNames: ["Gluten"] }, + ], + }, + { + category: "CUISINE_ASIATIQUE", + items: [ + { name: "Sauce huître", icon: "🫙", allergenNames: ["Mollusques"] }, + { name: "Sauce hoisin", icon: "🫙", allergenNames: ["Soja"] }, + { name: "Sauce sriracha", icon: "🫙", allergenNames: [] }, + { name: "Sauce sweet chili", icon: "🫙", allergenNames: [] }, + { name: "Vinaigre de riz", icon: "🧴", allergenNames: [] }, + { name: "Mirin", icon: "🍶", allergenNames: [] }, + { name: "Saké (cuisine)", icon: "🍶", allergenNames: [] }, + { name: "Miso", icon: "🫙", allergenNames: ["Soja"] }, + { name: "Tofu soyeux", icon: "🧊", allergenNames: ["Soja"] }, + { name: "Graines de sésame", icon: "🌱", allergenNames: ["Graines de sésame"] }, + { name: "Nouilles de riz", icon: "🍜", allergenNames: [] }, + { name: "Nouilles udon", icon: "🍜", allergenNames: ["Gluten"] }, + { name: "Nouilles soba", icon: "🍜", allergenNames: ["Gluten"] }, + { name: "Nouilles chinoises", icon: "🍜", allergenNames: ["Gluten"] }, + { name: "Vermicelles de riz", icon: "🍜", allergenNames: [] }, + { name: "Vermicelles de soja", icon: "🍜", allergenNames: [] }, + { name: "Riz gluant", icon: "🍚", allergenNames: [] }, + { name: "Riz à sushi", icon: "🍚", allergenNames: [] }, + { name: "Riz jasmin", icon: "🍚", allergenNames: [] }, + { name: "Citronnelle", icon: "🌿", allergenNames: [] }, + { name: "Combava", icon: "🌿", allergenNames: [] }, + { name: "Pak-choï", icon: "🥬", allergenNames: [] }, + { name: "Germes de soja", icon: "🌱", allergenNames: ["Soja"] }, + { name: "Shiitake", icon: "🍄", allergenNames: [] }, + { name: "Champignons noirs", icon: "🍄", allergenNames: [] }, + { name: "Daikon", allergenNames: [] }, + { name: "Algue nori", icon: "🌿", allergenNames: [] }, + { name: "Algue wakamé", icon: "🌿", allergenNames: [] }, + { name: "Algue kombu", icon: "🌿", allergenNames: [] }, + { name: "Cinq épices", icon: "🌿", allergenNames: [] }, + { name: "Garam masala", icon: "🌿", allergenNames: [] }, + { name: "Graines de coriandre", icon: "🌿", allergenNames: [] }, + { name: "Cardamome", icon: "🌿", allergenNames: [] }, + { name: "Fenugrec", icon: "🌿", allergenNames: [] }, + { name: "Piment vert frais", icon: "🌶️", allergenNames: [] }, + { name: "Farine de tapioca", icon: "🌾", allergenNames: [] }, + { name: "Sucre de palme", icon: "🍬", allergenNames: [] }, + { name: "Pousses de bambou", allergenNames: [] }, + { name: "Châtaignes d'eau", allergenNames: [] }, + { name: "Poisson séché", icon: "🐟", allergenNames: ["Poissons"] }, + { name: "Pâte de crevettes", icon: "🫙", allergenNames: ["Crustacés"] }, + { name: "Pâte de curry rouge (thaï)", icon: "🍛", allergenNames: [] }, + { name: "Pâte de curry vert (thaï)", icon: "🍛", allergenNames: [] }, + ], + }, + { + category: "CUISINE_MEXICAINE", + items: [ + { name: "Tortilla de maïs", icon: "🌮", allergenNames: [] }, + { name: "Tortilla de blé", icon: "🌮", allergenNames: ["Gluten"] }, + { name: "Haricots pinto", icon: "🫘", allergenNames: [] }, + { name: "Piment jalapeño", icon: "🌶️", allergenNames: [] }, + { name: "Piment chipotle", icon: "🌶️", allergenNames: [] }, + { name: "Piment poblano", icon: "🌶️", allergenNames: [] }, + { name: "Piment habanero", icon: "🌶️", allergenNames: [] }, + { name: "Masa harina", icon: "🌽", allergenNames: [] }, + { name: "Cheddar", icon: "🧀", allergenNames: ["Lait"] }, + ], + }, + { + category: "MAGHREB_MOYEN_ORIENT", + items: [ + { name: "Ras el hanout", icon: "🌿", allergenNames: [] }, + { name: "Za'atar", icon: "🌿", allergenNames: ["Graines de sésame"] }, + { name: "Tahini", icon: "🫙", allergenNames: ["Graines de sésame"] }, + ], + }, + { + category: "PAINS_SANDWICHS", + // `Pain`/`Pain de mie`/`Pain complet`/`Baguette`/`Pain de seigle` are + // seeded under `CEREALES_FECULENTS` above — this category covers the + // shapes specifically reached for to build a sandwich, plus a few + // international flatbreads not tied to one cuisine category above. + items: [ + { name: "Pain à burger", icon: "🍔", allergenNames: ["Gluten", "Lait", "Œufs"] }, + { name: "Pain brioché", icon: "🍞", allergenNames: ["Gluten", "Lait", "Œufs"] }, + { name: "Pain à hot-dog", icon: "🌭", allergenNames: ["Gluten"] }, + { name: "Pain pita", icon: "🫓", allergenNames: ["Gluten"] }, + { name: "Pain bagel", icon: "🥯", allergenNames: ["Gluten"] }, + { name: "Naan", icon: "🫓", allergenNames: ["Gluten"] }, + { name: "Pain wrap", icon: "🫓", allergenNames: ["Gluten"] }, + { name: "Pain viennois", icon: "🍞", allergenNames: ["Gluten", "Lait"] }, + { name: "Pain de campagne", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Pain aux céréales", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Petit pain", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Pain suédois", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Pain sans gluten", icon: "🍞", allergenNames: [] }, + { name: "Biscotte", icon: "🍞", allergenNames: ["Gluten"] }, + { name: "Croûtons", icon: "🍞", allergenNames: ["Gluten"] }, + ], + }, + { + category: "EPICERIE_DIVERS", + items: [ + { name: "Bicarbonate de soude", icon: "🫙", allergenNames: [] }, + { name: "Fécule de pomme de terre", icon: "🫙", allergenNames: [] }, + ], + }, + { + category: "LIQUIDES_BOISSONS", + items: [ + { name: "Eau", icon: "💧", allergenNames: [] }, + { name: "Eau gazeuse", icon: "💧", allergenNames: [] }, + { name: "Eau de fleur d'oranger", icon: "💧", allergenNames: [] }, + { name: "Eau de rose", icon: "💧", allergenNames: [] }, + { name: "Jus de citron", icon: "🧃", allergenNames: [] }, + { name: "Jus de citron vert", icon: "🧃", allergenNames: [] }, + { name: "Jus d'orange", icon: "🧃", allergenNames: [] }, + { name: "Jus de pomme", icon: "🧃", allergenNames: [] }, + { name: "Jus de raisin", icon: "🧃", allergenNames: [] }, + { name: "Jus de tomate", icon: "🧃", allergenNames: [] }, + { name: "Jus de cranberry", icon: "🧃", allergenNames: [] }, + { name: "Café", icon: "☕", allergenNames: [] }, + { name: "Thé", icon: "🍵", allergenNames: [] }, + { name: "Bière (cuisine)", icon: "🍺", allergenNames: ["Gluten"] }, + { name: "Cidre (cuisine)", allergenNames: ["Sulfites"] }, + { name: "Champagne / vin pétillant (cuisine)", icon: "🥂", allergenNames: ["Sulfites"] }, + { name: "Porto (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, + { name: "Vin jaune (cuisine)", icon: "🍷", allergenNames: ["Sulfites"] }, + { name: "Cognac", icon: "🥃", allergenNames: [] }, + { name: "Rhum", icon: "🥃", allergenNames: [] }, + { name: "Whisky", icon: "🥃", allergenNames: [] }, + { name: "Vodka", icon: "🥃", allergenNames: [] }, + { name: "Sirop de sucre de canne", icon: "🫙", allergenNames: [] }, + { name: "Fumet de poisson", icon: "🫙", allergenNames: ["Poissons"] }, + ], + }, ]; +const INGREDIENTS: Array = + INGREDIENT_GROUPS.flatMap(({ category, items }) => items.map((item) => ({ ...item, category }))); + /** * Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent * (safe to call against a database that already has this data — upserts by @@ -549,29 +624,29 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { // 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` only for the rows where it 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 icons were edited in - // code since the last deploy, never the full list. + // reconcile `icon`/`category` only for the rows where either 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 existingIngredients = await prisma.ingredient.findMany({ where: { name: { in: INGREDIENTS.map((i) => i.name) } }, - select: { id: true, name: true, icon: true }, + select: { id: true, name: true, icon: true, category: true }, }); const existingByName = new Map(existingIngredients.map((i) => [i.name, i])); const missingIngredients = INGREDIENTS.filter((i) => !existingByName.has(i.name)); if (missingIngredients.length > 0) { await prisma.ingredient.createMany({ - data: missingIngredients.map(({ name, icon }) => ({ name, icon })), + data: missingIngredients.map(({ name, icon, category }) => ({ name, icon, category })), }); } - const changedIcons = INGREDIENTS.filter((i) => { + const changed = INGREDIENTS.filter((i) => { const existing = existingByName.get(i.name); - return existing && existing.icon !== (i.icon ?? null); + return existing && (existing.icon !== (i.icon ?? null) || existing.category !== i.category); }); - for (const { name, icon } of changedIcons) { - await prisma.ingredient.update({ where: { name }, data: { icon } }); + for (const { name, icon, category } of changed) { + await prisma.ingredient.update({ where: { name }, data: { icon, category } }); } // Re-resolve every ingredient's id (existing + just-created) and every diff --git a/apps/api/src/modules/recipe/recipe.service.ts b/apps/api/src/modules/recipe/recipe.service.ts index cccca0e..76f13ba 100644 --- a/apps/api/src/modules/recipe/recipe.service.ts +++ b/apps/api/src/modules/recipe/recipe.service.ts @@ -38,6 +38,7 @@ function toIngredientView(ingredient: IngredientWithAllergies): IngredientView { id: ingredient.id, name: ingredient.name, icon: ingredient.icon, + category: ingredient.category, allergens: ingredient.allergies.map(({ allergy }) => ({ id: allergy.id, name: allergy.category.name, diff --git a/apps/api/src/modules/reference/reference.service.ts b/apps/api/src/modules/reference/reference.service.ts index 5440f20..0936b2a 100644 --- a/apps/api/src/modules/reference/reference.service.ts +++ b/apps/api/src/modules/reference/reference.service.ts @@ -41,6 +41,7 @@ export async function getIngredients(): Promise { id: ingredient.id, name: ingredient.name, icon: ingredient.icon, + category: ingredient.category, allergens: ingredient.allergies.map(({ allergy }) => ({ id: allergy.id, name: allergy.category.name, diff --git a/apps/api/test/reference.test.ts b/apps/api/test/reference.test.ts index 700f60e..ff1f51e 100644 --- a/apps/api/test/reference.test.ts +++ b/apps/api/test/reference.test.ts @@ -54,7 +54,7 @@ describe("Reference data", () => { expect(res.status).to.equal(200); expect(res.body.length).to.be.greaterThan(0); expect(res.body.map((i: { name: string }) => i.name)).to.include("Tomate"); - expect(res.body[0]).to.have.keys(["id", "name", "icon", "allergens"]); + expect(res.body[0]).to.have.keys(["id", "name", "icon", "category", "allergens"]); }); it("resolves each ingredient's linked allergens, empty for one with none", async () => { diff --git a/apps/web/src/features/profile/DislikedIngredientsField.tsx b/apps/web/src/features/profile/DislikedIngredientsField.tsx index 6dcb2a0..9299065 100644 --- a/apps/web/src/features/profile/DislikedIngredientsField.tsx +++ b/apps/web/src/features/profile/DislikedIngredientsField.tsx @@ -1,14 +1,15 @@ import type { IngredientView } from "@batch-cooking/shared"; import { useTranslation } from "react-i18next"; -// Reuses `IngredientAutocomplete` verbatim (built for the recipe form's -// ingredient picker, features/recipes/) rather than a second search field — -// same reference ingredient list, same "type to filter" behavior, just -// without the recipe form's quantity/unit per line. Its own recipes.scss -// import already covers `.ingredient-autocomplete`; this file's explicit -// import below only adds `.disliked-ingredients-field__*` (see recipes.scss -// — colocated there since it's the same "reference ingredient picker" -// visual family, even though this field lives in the profile feature). -import { IngredientAutocomplete } from "../recipes/IngredientAutocomplete"; +// Reuses `IngredientPicker` verbatim (built for the recipe form's +// ingredient picker, features/recipes/) rather than a second search+browse +// field — same reference ingredient list, same category/search browsing, +// just without the recipe form's quantity/unit per line. Its own +// recipes.scss import already covers `.ingredient-picker`; this file's +// explicit import below only adds `.disliked-ingredients-field__*` (see +// recipes.scss — colocated there since it's the same "reference ingredient +// picker" visual family, even though this field lives in the profile +// feature). +import { IngredientPicker } from "../recipes/IngredientPicker"; import "../recipes/recipes.scss"; import "./profile-forms.scss"; @@ -62,7 +63,7 @@ export function DislikedIngredientsField({ ))} )} - + ); } diff --git a/apps/web/src/features/recipes/IngredientAutocomplete.tsx b/apps/web/src/features/recipes/IngredientAutocomplete.tsx deleted file mode 100644 index 2a473b2..0000000 --- a/apps/web/src/features/recipes/IngredientAutocomplete.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { IngredientView } from "@batch-cooking/shared"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { AllergenBadges } from "./AllergenBadges"; -import "./recipes.scss"; - -/** Caps how many matches are shown at once — the reference list is small, but an unbounded dropdown would still be unwieldy for a broad query like a single letter. */ -const MAX_SUGGESTIONS = 8; - -/** - * Text field over the static reference ingredient list (`GET - * /reference/ingredients`) — receives the full list as a prop rather than - * fetching it itself, same rationale as `AllergySelect`/`DietSelect`, and - * filters it client-side as the user types: it's small, non-administrable - * reference data, no dedicated search endpoint needed. `excludeIds` (the - * ingredients already on the recipe) keeps the same one from being added - * twice. - */ -export function IngredientAutocomplete({ - ingredients, - excludeIds, - onSelect, -}: { - ingredients: IngredientView[]; - excludeIds: number[]; - onSelect: (ingredient: IngredientView) => void; -}) { - const { t } = useTranslation(); - const [query, setQuery] = useState(""); - - const normalizedQuery = query.trim().toLowerCase(); - const suggestions = - normalizedQuery.length === 0 - ? [] - : ingredients - .filter( - (ingredient) => - !excludeIds.includes(ingredient.id) && - ingredient.name.toLowerCase().includes(normalizedQuery), - ) - .slice(0, MAX_SUGGESTIONS); - - function handleSelect(ingredient: IngredientView) { - onSelect(ingredient); - setQuery(""); - } - - return ( -
- setQuery(e.target.value)} - placeholder={t("recipes.form.addIngredientPlaceholder")} - /> - {suggestions.length > 0 && ( -
    - {suggestions.map((ingredient) => ( -
  • - -
  • - ))} -
- )} -
- ); -} diff --git a/apps/web/src/features/recipes/IngredientPicker.tsx b/apps/web/src/features/recipes/IngredientPicker.tsx new file mode 100644 index 0000000..f1750c5 --- /dev/null +++ b/apps/web/src/features/recipes/IngredientPicker.tsx @@ -0,0 +1,109 @@ +import { + INGREDIENT_CATEGORIES, + type IngredientCategory, + type IngredientView, +} from "@batch-cooking/shared"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AllergenBadges } from "./AllergenBadges"; +import "./recipes.scss"; + +/** "No category filter" — a UI-only pseudo-category, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES} for the real, closed set). */ +const ALL_CATEGORIES = "ALL" as const; + +/** + * Browsable ingredient picker — category chips + search + a card grid, + * replacing the earlier `IngredientAutocomplete` (a plain type-to-filter + * dropdown). With 400+ reference ingredients, search alone doesn't scale to + * actually *finding* one — category browsing is the main fix; search still + * narrows within (or across) categories for when the name is already known. + * + * Receives `ingredients` as a prop rather than fetching them itself — same + * rationale as `AllergySelect`/`DietSelect`. `excludeIds` (already-selected + * ingredients — a recipe's ingredient list, or a profile's disliked list) + * keeps the same one from being added twice. + */ +export function IngredientPicker({ + ingredients, + excludeIds, + onSelect, +}: { + ingredients: IngredientView[]; + excludeIds: number[]; + onSelect: (ingredient: IngredientView) => void; +}) { + const { t } = useTranslation(); + const [category, setCategory] = useState( + ALL_CATEGORIES, + ); + const [query, setQuery] = useState(""); + + const normalizedQuery = query.trim().toLowerCase(); + const visible = ingredients.filter((ingredient) => { + if (excludeIds.includes(ingredient.id)) return false; + if (category !== ALL_CATEGORIES && ingredient.category !== category) return false; + if (normalizedQuery.length > 0 && !ingredient.name.toLowerCase().includes(normalizedQuery)) { + return false; + } + return true; + }); + + function handleSelect(ingredient: IngredientView) { + onSelect(ingredient); + setQuery(""); + } + + return ( +
+
+ setQuery(e.target.value)} + placeholder={t("recipes.form.searchIngredientPlaceholder")} + /> +
+ +
+ + {INGREDIENT_CATEGORIES.map((c) => ( + + ))} +
+ + {visible.length === 0 ? ( +

{t("recipes.form.noIngredientFound")}

+ ) : ( +
+ {visible.map((ingredient) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/features/recipes/IngredientRow.tsx b/apps/web/src/features/recipes/IngredientRow.tsx index 758d513..310f359 100644 --- a/apps/web/src/features/recipes/IngredientRow.tsx +++ b/apps/web/src/features/recipes/IngredientRow.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { AllergenBadges } from "./AllergenBadges"; import "./recipes.scss"; -/** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientAutocomplete`) plus its quantity/unit for this recipe. Quantity/unit are kept as raw strings while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input. */ +/** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientPicker`) plus its quantity/unit for this recipe. Quantity/unit are kept as raw strings while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input. */ export function IngredientRow({ ingredient, quantity, diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index f3847e7..58c2a3f 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -140,11 +140,17 @@ flex-shrink: 0; display: flex; gap: var(--space-xs); + // Never lets a tab overflow the page (which would force the whole body + // to scroll horizontally, see global.scss's rule against that) — scrolls + // within itself instead once the tabs (including the disabled "Sources" + // placeholder) don't all fit, same pattern as the sidebar's own nav. + overflow-x: auto; border-bottom: 1px solid var(--color-border); margin-bottom: var(--space-md); &__tab { appearance: none; + flex-shrink: 0; display: flex; align-items: center; gap: 0.4rem; @@ -661,62 +667,109 @@ } } -// --- Ingredient autocomplete (recipe form + disliked-ingredients field) ----- -.ingredient-autocomplete { - position: relative; +// --- Ingredient picker (recipe form + disliked-ingredients field) ---------- +// Always-visible category chips + search + card grid — replaces the earlier +// type-to-filter dropdown (`.ingredient-autocomplete`, now gone), which +// stopped scaling once the reference list passed a couple hundred items. +// The category row uses the same "scroll, don't crush" pattern as the app +// sidebar nav (see AppLayout.scss's mobile breakpoint): chips never shrink +// below a tappable size, the row scrolls horizontally instead. +.ingredient-picker { + display: flex; + flex-direction: column; + gap: var(--space-sm); + padding: var(--space-sm); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-base); - > input { - width: 100%; - max-width: 24rem; - padding: var(--space-sm); - font-family: var(--font-body); - font-size: var(--font-size-base); - border: 1px solid var(--color-border); - border-radius: var(--radius-base); - background: var(--color-surface); - color: var(--color-text); - } - - &__suggestions { - position: absolute; - z-index: 1; - top: 100%; - left: 0; - width: 100%; - max-width: 24rem; - max-height: 16rem; - overflow-y: auto; - margin: var(--space-xs) 0 0; - padding: var(--space-xs); - list-style: none; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-base); - box-shadow: var(--shadow-md); - - button { - display: flex; - align-items: center; - gap: var(--space-sm); + &__search { + input { width: 100%; - padding: var(--space-xs) var(--space-sm); + padding: var(--space-sm); font-family: var(--font-body); - font-size: var(--font-size-sm); - text-align: left; - border: none; + font-size: var(--font-size-base); + border: 1px solid var(--color-border); border-radius: var(--radius-base); - background: none; + background: var(--color-surface-alt); color: var(--color-text); - cursor: pointer; - - &:hover { - background: var(--color-surface-alt); - } } } - &__name { - margin-right: auto; + &__categories { + display: flex; + gap: var(--space-xs); + overflow-x: auto; + padding-bottom: var(--space-xs); + } + + &__category { + flex-shrink: 0; + appearance: none; + padding: var(--space-xs) var(--space-sm); + font-family: var(--font-body); + font-size: var(--font-size-sm); + white-space: nowrap; + cursor: pointer; + border: 1px solid var(--color-border); + border-radius: var(--radius-pill); + background: var(--color-surface-alt); + color: var(--color-text-muted); + + &:hover { + border-color: var(--color-primary); + color: var(--color-text); + } + + &.active { + border-color: var(--color-primary); + background: var(--color-primary); + color: #fff; + } + } + + &__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); + gap: var(--space-xs); + max-height: 18rem; + overflow-y: auto; + padding: 2px; + } + + &__card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-xs); + padding: var(--space-sm); + text-align: center; + cursor: pointer; + border: 1px solid var(--color-border); + border-radius: var(--radius-base); + background: var(--color-surface-alt); + color: var(--color-text); + + &:hover { + border-color: var(--color-primary); + background: var(--color-surface); + } + } + + &__card-icon { + font-size: 1.5rem; + line-height: 1; + } + + &__card-name { + font-size: var(--font-size-sm); + } + + &__empty { + padding: var(--space-md); + font-size: var(--font-size-sm); + color: var(--color-text-muted); + text-align: center; } } diff --git a/apps/web/src/layouts/AppLayout.scss b/apps/web/src/layouts/AppLayout.scss index 6cb886f..0468431 100644 --- a/apps/web/src/layouts/AppLayout.scss +++ b/apps/web/src/layouts/AppLayout.scss @@ -305,6 +305,17 @@ // viewport (relevant early given this app is meant to be embedded via // Capacitor later, see the root README) — collapse it into a horizontal // top bar instead of a side rail. +// +// The primary nav (Planning/Recettes/Liste de courses) must stay fully +// legible and tappable at any width — it's the app's main navigation, not +// optional chrome. Without `&__nav { min-width: 0 }` + `a { flex-shrink: 0 +// }` below, `&__settings`/`&__footer`'s own natural (non-shrinking) width +// silently crushed it down to ~16px unlabeled slivers on a narrow phone +// (measured on a 375px viewport) — invisible labels, no real tap target. +// The fix: `__settings`/`__footer` collapse to icon-only instead (same +// look as the desktop rail's `.collapsed` state), freeing width for the +// nav, which falls back to horizontal scroll (`overflow-x: auto`) rather +// than shrinking if it still doesn't fit. @media (max-width: 640px) { .app-layout { flex-direction: column; @@ -326,15 +337,75 @@ padding: 0; } + // Nothing to collapse into on a horizontal bar — there's no rail. + &__collapse-toggle { + display: none; + } + &__nav { + flex: 1 1 auto; + min-width: 0; flex-direction: row; overflow-x: auto; + + a { + flex-shrink: 0; + } } &__settings, &__footer { + position: relative; + flex: none; padding-top: 0; border-top: none; } + + &__settings-toggle, + &__account-toggle { + padding-left: var(--space-xs); + padding-right: var(--space-xs); + } + + &__settings-toggle-left .label, + &__account-toggle .label, + &__settings-toggle .chevron { + display: none; + } + + // Both reveals become floating panels anchored under their icon-only + // toggle — on the desktop rail one is inline (settings, stacks fine in + // a column) and the other already floats (account); neither can stay + // in normal flow on this horizontal bar without breaking the row. + // Selector order matters: `&__settings-nav` also carries the plain + // `&__nav` class (for the link styling), so this must come after it to + // win on `flex-direction`/`overflow-x`. + &__settings-nav { + position: absolute; + top: calc(100% + var(--space-xs)); + right: 0; + flex-direction: column; + width: 14rem; + padding: var(--space-xs); + overflow-x: visible; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-base); + box-shadow: var(--shadow-md); + z-index: 10; + } + + // Desktop opens this *upward* (`bottom: calc(100% + ...)`, see the base + // rule above) because the footer sits at the bottom of a tall rail — + // on this horizontal top bar the footer is near `y: 0`, so "upward" + // pushed the menu entirely off-screen above the viewport. Flip it to + // open downward here instead. + &__account-menu { + top: calc(100% + var(--space-xs)); + bottom: auto; + left: auto; + right: 0; + width: 12rem; + } } } diff --git a/apps/web/src/locales/fr/translation.json b/apps/web/src/locales/fr/translation.json index 6276b8f..4daf56d 100644 --- a/apps/web/src/locales/fr/translation.json +++ b/apps/web/src/locales/fr/translation.json @@ -165,7 +165,29 @@ "PUBLIC": "Tout le monde" }, "dietsLabel": "Régime(s) associé(s)", - "addIngredientPlaceholder": "Ajouter un ingrédient…", + "searchIngredientPlaceholder": "Rechercher un ingrédient…", + "allCategories": "Tout", + "noIngredientFound": "Aucun ingrédient trouvé.", + "category": { + "CEREALES_FECULENTS": "Céréales & féculents", + "LEGUMINEUSES": "Légumineuses", + "VIANDES_VOLAILLES": "Viandes & volailles", + "POISSONS_FRUITS_DE_MER": "Poissons & fruits de mer", + "PRODUITS_LAITIERS_OEUFS": "Produits laitiers & œufs", + "LEGUMES": "Légumes", + "FRUITS": "Fruits", + "FRUITS_SECS_OLEAGINEUX": "Fruits secs & oléagineux", + "CONDIMENTS_SAUCES": "Condiments & sauces", + "EPICES_HERBES": "Épices & herbes", + "SUCRE_PATISSERIE": "Sucre & pâtisserie", + "CUISINE_ITALIENNE": "Cuisine italienne", + "CUISINE_ASIATIQUE": "Cuisine asiatique", + "CUISINE_MEXICAINE": "Cuisine mexicaine", + "MAGHREB_MOYEN_ORIENT": "Maghreb & Moyen-Orient", + "PAINS_SANDWICHS": "Pains & sandwichs", + "EPICERIE_DIVERS": "Épicerie & divers", + "LIQUIDES_BOISSONS": "Liquides & boissons" + }, "quantityLabel": "Quantité", "unitLabel": "Unité", "unitPlaceholder": "g, ml, unité…", diff --git a/apps/web/src/pages/RecipeFormPage.tsx b/apps/web/src/pages/RecipeFormPage.tsx index 9f346b8..4385b97 100644 --- a/apps/web/src/pages/RecipeFormPage.tsx +++ b/apps/web/src/pages/RecipeFormPage.tsx @@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { ApiError, apiClient } from "../api/client"; import { DietTagSelect } from "../features/recipes/DietTagSelect"; -import { IngredientAutocomplete } from "../features/recipes/IngredientAutocomplete"; +import { IngredientPicker } from "../features/recipes/IngredientPicker"; import { IngredientRow } from "../features/recipes/IngredientRow"; import { type StepDraft, StepListEditor } from "../features/recipes/StepListEditor"; import "../features/recipes/recipes.scss"; @@ -251,7 +251,7 @@ export function RecipeFormPage() { /> ))} -