fix(recipes): overflow des préférences + régimes liés aux ingrédients
- Fix overflow horizontal sur /parametres/preferences : <fieldset> a un min-width: min-content par défaut du navigateur, ce qui empêchait la grille de l'IngredientPicker de wrapper (page entière poussée à ~3100px de large). Reset min-width: 0 sur .disliked-ingredients-field. - Recatégorise les laits/crèmes végétaux (coco, amande, avoine) de PRODUITS_LAITIERS_OEUFS vers LIQUIDES_BOISSONS — ce ne sont pas des produits laitiers. - Nouveau modèle IngredientDiet (many-to-many ingrédient <-> régime) : quels régimes (Végétarien, Végan, Pescétarien) chaque ingrédient respecte. Omnivore volontairement absent (trivial) et Sans gluten aussi (déjà dérivable de l'allergène Gluten existant). - reference-seed-data.ts : chaque groupe de catégorie porte un defaultDiets, avec dietNames en override pour les exceptions (fromages, viandes, poissons, sauces à base d'œuf/poisson...). - IngredientView.diets exposé par /reference/ingredients et /recipes, affiché via DietBadges dans IngredientPicker et IngredientRow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ea86a4a7f1
commit
5890c62462
10 changed files with 330 additions and 59 deletions
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ingredient_diet" (
|
||||||
|
"ingredient_id" INTEGER NOT NULL,
|
||||||
|
"diet_id" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ingredient_diet_pkey" PRIMARY KEY ("ingredient_id","diet_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ingredient_diet" ADD CONSTRAINT "ingredient_diet_ingredient_id_fkey" FOREIGN KEY ("ingredient_id") REFERENCES "ingredients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ingredient_diet" ADD CONSTRAINT "ingredient_diet_diet_id_fkey" FOREIGN KEY ("diet_id") REFERENCES "diet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
@ -44,6 +44,7 @@ model Diet {
|
||||||
|
|
||||||
users UserProfile[]
|
users UserProfile[]
|
||||||
recipes RecipeDiet[]
|
recipes RecipeDiet[]
|
||||||
|
ingredients IngredientDiet[]
|
||||||
|
|
||||||
@@map("diet")
|
@@map("diet")
|
||||||
}
|
}
|
||||||
|
|
@ -336,10 +337,35 @@ model Ingredient {
|
||||||
allergies IngredientAllergy[]
|
allergies IngredientAllergy[]
|
||||||
/// Profiles that personally dislike this ingredient — see {@link UserProfileDislikedIngredient}.
|
/// Profiles that personally dislike this ingredient — see {@link UserProfileDislikedIngredient}.
|
||||||
dislikedBy UserProfileDislikedIngredient[]
|
dislikedBy UserProfileDislikedIngredient[]
|
||||||
|
/// Diet regimes this ingredient is compatible with — see {@link IngredientDiet}.
|
||||||
|
diets IngredientDiet[]
|
||||||
|
|
||||||
@@map("ingredients")
|
@@map("ingredients")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explicit join table for the ingredients <-> diet regime association —
|
||||||
|
/// which regimes (Végétarien, Végan, Pescétarien…) this ingredient is safe
|
||||||
|
/// for, so the picker (apps/web's `IngredientPicker`/`IngredientRow`) can
|
||||||
|
/// flag e.g. an ingredient as vegan without the user having to open its
|
||||||
|
/// packaging. Seeded by category in `reference-seed-data.ts` (most
|
||||||
|
/// ingredients in a category share the same compatible regimes, with
|
||||||
|
/// per-item overrides for exceptions — meat cuts, dairy, seafood…), same as
|
||||||
|
/// `IngredientAllergy`. Deliberately omits `Omnivore` (every ingredient is
|
||||||
|
/// trivially compatible — storing it would be pure noise) and `Sans gluten`
|
||||||
|
/// (already fully derivable from whether `IngredientAllergy` links this
|
||||||
|
/// ingredient to the `Gluten` allergen — a second, hand-maintained source
|
||||||
|
/// for the same fact would only risk drifting out of sync with it).
|
||||||
|
model IngredientDiet {
|
||||||
|
ingredientId Int @map("ingredient_id")
|
||||||
|
dietId Int @map("diet_id")
|
||||||
|
|
||||||
|
ingredient Ingredient @relation(fields: [ingredientId], references: [id], onDelete: Cascade)
|
||||||
|
diet Diet @relation(fields: [dietId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([ingredientId, dietId])
|
||||||
|
@@map("ingredient_diet")
|
||||||
|
}
|
||||||
|
|
||||||
/// Explicit join table for the ingredients <-> allergy association — not in
|
/// Explicit join table for the ingredients <-> allergy association — not in
|
||||||
/// the original spec doc, added so the recipe catalog can surface which
|
/// the original spec doc, added so the recipe catalog can surface which
|
||||||
/// allergens an ingredient (and by extension a recipe) carries. Same shape
|
/// allergens an ingredient (and by extension a recipe) carries. Same shape
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,17 @@ interface IngredientSeed {
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
allergenNames: string[];
|
allergenNames: string[];
|
||||||
|
/**
|
||||||
|
* Diet regimes this ingredient is compatible with, overriding its group's
|
||||||
|
* `defaultDiets` below — only needed for the exceptions within a category
|
||||||
|
* (a dairy cheese inside an otherwise-vegan cuisine group, a meat-based
|
||||||
|
* stock inside an otherwise-plant condiments group…). 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
|
// A broad pantry list — the goal is to cover the large majority of what a
|
||||||
|
|
@ -52,9 +63,21 @@ interface IngredientSeed {
|
||||||
// alone isn't enough to actually *find* something. Each group's key is the
|
// 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
|
// single source of truth for that mapping; `INGREDIENTS` below just flattens
|
||||||
// it back to one array for the seeding loop.
|
// it back to one array for the seeding loop.
|
||||||
const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: IngredientSeed[] }> = [
|
//
|
||||||
|
// `defaultDiets` is the regime compatibility every item in the group shares
|
||||||
|
// unless it sets its own `dietNames` — most categories are diet-homogeneous
|
||||||
|
// (a vegetable is always vegan, a cut of meat never is), so this avoids
|
||||||
|
// repeating the same three diet names on hundreds of items; only the
|
||||||
|
// category's exceptions (a cheese inside "cuisine italienne", a fish sauce
|
||||||
|
// inside "cuisine asiatique"…) need a per-item override.
|
||||||
|
const INGREDIENT_GROUPS: Array<{
|
||||||
|
category: IngredientCategory;
|
||||||
|
defaultDiets: string[];
|
||||||
|
items: IngredientSeed[];
|
||||||
|
}> = [
|
||||||
{
|
{
|
||||||
category: "CEREALES_FECULENTS",
|
category: "CEREALES_FECULENTS",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Farine de blé", icon: "🌾", allergenNames: ["Gluten"] },
|
{ name: "Farine de blé", icon: "🌾", allergenNames: ["Gluten"] },
|
||||||
{ name: "Farine complète", icon: "🌾", allergenNames: ["Gluten"] },
|
{ name: "Farine complète", icon: "🌾", allergenNames: ["Gluten"] },
|
||||||
|
|
@ -87,6 +110,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "LEGUMINEUSES",
|
category: "LEGUMINEUSES",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Lentilles vertes", icon: "🫘", allergenNames: [] },
|
{ name: "Lentilles vertes", icon: "🫘", allergenNames: [] },
|
||||||
{ name: "Lentilles corail", icon: "🫘", allergenNames: [] },
|
{ name: "Lentilles corail", icon: "🫘", allergenNames: [] },
|
||||||
|
|
@ -101,6 +125,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "VIANDES_VOLAILLES",
|
category: "VIANDES_VOLAILLES",
|
||||||
|
defaultDiets: [],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Poulet", icon: "🍗", allergenNames: [] },
|
{ name: "Poulet", icon: "🍗", allergenNames: [] },
|
||||||
{ name: "Dinde", icon: "🍗", allergenNames: [] },
|
{ name: "Dinde", icon: "🍗", allergenNames: [] },
|
||||||
|
|
@ -126,6 +151,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "POISSONS_FRUITS_DE_MER",
|
category: "POISSONS_FRUITS_DE_MER",
|
||||||
|
defaultDiets: ["Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Saumon", icon: "🐟", allergenNames: ["Poissons"] },
|
{ name: "Saumon", icon: "🐟", allergenNames: ["Poissons"] },
|
||||||
{ name: "Thon", icon: "🐟", allergenNames: ["Poissons"] },
|
{ name: "Thon", icon: "🐟", allergenNames: ["Poissons"] },
|
||||||
|
|
@ -172,6 +198,11 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "PRODUITS_LAITIERS_OEUFS",
|
category: "PRODUITS_LAITIERS_OEUFS",
|
||||||
|
// Real dairy/egg products only — plant-based "milks" (coconut, almond,
|
||||||
|
// oat…) live under `LIQUIDES_BOISSONS` instead: they're not dairy, and
|
||||||
|
// grouping them here purely because the French name says "lait" was a
|
||||||
|
// categorization bug (they're vegan; nothing in this group is).
|
||||||
|
defaultDiets: ["Végétarien", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Œuf", icon: "🥚", allergenNames: ["Œufs"] },
|
{ name: "Œuf", icon: "🥚", allergenNames: ["Œufs"] },
|
||||||
{ name: "Lait", icon: "🥛", allergenNames: ["Lait"] },
|
{ name: "Lait", icon: "🥛", allergenNames: ["Lait"] },
|
||||||
|
|
@ -189,14 +220,11 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Fromage blanc", icon: "🥣", allergenNames: ["Lait"] },
|
{ name: "Fromage blanc", icon: "🥣", allergenNames: ["Lait"] },
|
||||||
{ name: "Mascarpone", icon: "🧀", allergenNames: ["Lait"] },
|
{ name: "Mascarpone", icon: "🧀", allergenNames: ["Lait"] },
|
||||||
{ name: "Yaourt", 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",
|
category: "LEGUMES",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Tomate", icon: "🍅", allergenNames: [] },
|
{ name: "Tomate", icon: "🍅", allergenNames: [] },
|
||||||
{ name: "Oignon", icon: "🧅", allergenNames: [] },
|
{ name: "Oignon", icon: "🧅", allergenNames: [] },
|
||||||
|
|
@ -240,6 +268,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "FRUITS",
|
category: "FRUITS",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Citron", icon: "🍋", allergenNames: [] },
|
{ name: "Citron", icon: "🍋", allergenNames: [] },
|
||||||
{ name: "Citron vert", icon: "🍋", allergenNames: [] },
|
{ name: "Citron vert", icon: "🍋", allergenNames: [] },
|
||||||
|
|
@ -273,6 +302,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "FRUITS_SECS_OLEAGINEUX",
|
category: "FRUITS_SECS_OLEAGINEUX",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Cacahuètes", icon: "🥜", allergenNames: ["Arachides"] },
|
{ name: "Cacahuètes", icon: "🥜", allergenNames: ["Arachides"] },
|
||||||
{ name: "Amandes", icon: "🌰", allergenNames: ["Fruits à coque"] },
|
{ name: "Amandes", icon: "🌰", allergenNames: ["Fruits à coque"] },
|
||||||
|
|
@ -293,6 +323,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "CONDIMENTS_SAUCES",
|
category: "CONDIMENTS_SAUCES",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Sel", icon: "🧂", allergenNames: [] },
|
{ name: "Sel", icon: "🧂", allergenNames: [] },
|
||||||
{ name: "Sucre", icon: "🍬", allergenNames: [] },
|
{ name: "Sucre", icon: "🍬", allergenNames: [] },
|
||||||
|
|
@ -307,20 +338,35 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Sauce soja", icon: "🍶", allergenNames: ["Soja"] },
|
{ name: "Sauce soja", icon: "🍶", allergenNames: ["Soja"] },
|
||||||
{ name: "Tofu", icon: "🧊", allergenNames: ["Soja"] },
|
{ name: "Tofu", icon: "🧊", allergenNames: ["Soja"] },
|
||||||
{ name: "Moutarde", icon: "🟡", allergenNames: ["Moutarde"] },
|
{ name: "Moutarde", icon: "🟡", allergenNames: ["Moutarde"] },
|
||||||
{ name: "Mayonnaise", icon: "🫙", allergenNames: ["Œufs"] },
|
{
|
||||||
|
name: "Mayonnaise",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Œufs"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Ketchup", icon: "🫙", allergenNames: [] },
|
{ name: "Ketchup", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Miel", icon: "🍯", allergenNames: [] },
|
{ name: "Miel", icon: "🍯", allergenNames: [], dietNames: ["Végétarien", "Pescétarien"] },
|
||||||
{ name: "Sirop d'érable", icon: "🍁", allergenNames: [] },
|
{ name: "Sirop d'érable", icon: "🍁", allergenNames: [] },
|
||||||
{ name: "Câpres", allergenNames: [] },
|
{ name: "Câpres", allergenNames: [] },
|
||||||
{ name: "Olives", icon: "🫒", allergenNames: [] },
|
{ name: "Olives", icon: "🫒", allergenNames: [] },
|
||||||
{ name: "Tabasco", icon: "🌶️", allergenNames: [] },
|
{ name: "Tabasco", icon: "🌶️", allergenNames: [] },
|
||||||
{ name: "Sauce Worcestershire", icon: "🫙", allergenNames: ["Poissons"] },
|
{
|
||||||
{ name: "Sauce nuoc-mâm", icon: "🫙", allergenNames: ["Poissons"] },
|
name: "Sauce Worcestershire",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauce nuoc-mâm",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Wasabi", allergenNames: [] },
|
{ name: "Wasabi", allergenNames: [] },
|
||||||
{ name: "Harissa", icon: "🌶️", allergenNames: [] },
|
{ name: "Harissa", icon: "🌶️", allergenNames: [] },
|
||||||
{ name: "Pâte de curry", icon: "🍛", allergenNames: [] },
|
{ name: "Pâte de curry", icon: "🍛", allergenNames: [] },
|
||||||
{ name: "Bouillon cube légumes", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon cube légumes", icon: "🫙", allergenNames: ["Céleri"] },
|
||||||
{ name: "Bouillon cube volaille", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon cube volaille", icon: "🫙", allergenNames: ["Céleri"], dietNames: [] },
|
||||||
{ name: "Concentré de tomate", icon: "🍅", allergenNames: [] },
|
{ name: "Concentré de tomate", icon: "🍅", allergenNames: [] },
|
||||||
{ name: "Coulis de tomate", icon: "🍅", allergenNames: [] },
|
{ name: "Coulis de tomate", icon: "🍅", allergenNames: [] },
|
||||||
{ name: "Tomates pelées (conserve)", icon: "🍅", allergenNames: [] },
|
{ name: "Tomates pelées (conserve)", icon: "🍅", allergenNames: [] },
|
||||||
|
|
@ -338,34 +384,81 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Moutarde de Dijon", icon: "🟡", allergenNames: ["Moutarde"] },
|
{ name: "Moutarde de Dijon", icon: "🟡", allergenNames: ["Moutarde"] },
|
||||||
{ name: "Moutarde à l'ancienne", icon: "🟡", allergenNames: ["Moutarde"] },
|
{ name: "Moutarde à l'ancienne", icon: "🟡", allergenNames: ["Moutarde"] },
|
||||||
{ name: "Sauce barbecue", icon: "🫙", allergenNames: [] },
|
{ name: "Sauce barbecue", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Sauce tartare", icon: "🫙", allergenNames: ["Œufs"] },
|
{
|
||||||
{ name: "Sauce cocktail", icon: "🫙", allergenNames: ["Œufs"] },
|
name: "Sauce tartare",
|
||||||
{ name: "Sauce béarnaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] },
|
icon: "🫙",
|
||||||
{ name: "Sauce hollandaise", icon: "🫙", allergenNames: ["Œufs", "Lait"] },
|
allergenNames: ["Œufs"],
|
||||||
{ name: "Sauce béchamel", icon: "🫙", allergenNames: ["Lait", "Gluten"] },
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauce cocktail",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Œufs"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauce béarnaise",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Œufs", "Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauce hollandaise",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Œufs", "Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauce béchamel",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Lait", "Gluten"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Sauce teriyaki", icon: "🫙", allergenNames: ["Soja"] },
|
{ name: "Sauce teriyaki", icon: "🫙", allergenNames: ["Soja"] },
|
||||||
{ name: "Sauce ponzu", icon: "🫙", allergenNames: ["Soja", "Poissons"] },
|
{
|
||||||
|
name: "Sauce ponzu",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Soja", "Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Chimichurri", icon: "🌿", allergenNames: [] },
|
{ name: "Chimichurri", icon: "🌿", allergenNames: [] },
|
||||||
{
|
{
|
||||||
name: "Pesto rouge (tomates séchées)",
|
name: "Pesto rouge (tomates séchées)",
|
||||||
icon: "🫙",
|
icon: "🫙",
|
||||||
allergenNames: ["Lait", "Fruits à coque"],
|
allergenNames: ["Lait", "Fruits à coque"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
},
|
},
|
||||||
{ name: "Tomates séchées", icon: "🍅", allergenNames: [] },
|
{ name: "Tomates séchées", icon: "🍅", allergenNames: [] },
|
||||||
{ name: "Fond de veau", icon: "🫙", allergenNames: [] },
|
{ name: "Fond de veau", icon: "🫙", allergenNames: [], dietNames: [] },
|
||||||
{ name: "Fond de volaille", icon: "🫙", allergenNames: [] },
|
{ name: "Fond de volaille", icon: "🫙", allergenNames: [], dietNames: [] },
|
||||||
{ name: "Bouillon cube bœuf", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon cube bœuf", icon: "🫙", allergenNames: ["Céleri"], dietNames: [] },
|
||||||
{ name: "Bouillon cube poisson", icon: "🫙", allergenNames: ["Poissons", "Céleri"] },
|
{
|
||||||
|
name: "Bouillon cube poisson",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Poissons", "Céleri"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Bouillon de légumes", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon de légumes", icon: "🫙", allergenNames: ["Céleri"] },
|
||||||
{ name: "Bouillon de volaille", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon de volaille", icon: "🫙", allergenNames: ["Céleri"], dietNames: [] },
|
||||||
{ name: "Bouillon de bœuf", icon: "🫙", allergenNames: ["Céleri"] },
|
{ name: "Bouillon de bœuf", icon: "🫙", allergenNames: ["Céleri"], dietNames: [] },
|
||||||
{ name: "Court-bouillon", icon: "🫙", allergenNames: [] },
|
{ name: "Court-bouillon", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Dashi (bouillon japonais)", icon: "🫙", allergenNames: ["Poissons"] },
|
{
|
||||||
{ name: "Bisque de crustacés", icon: "🫙", allergenNames: ["Crustacés"] },
|
name: "Dashi (bouillon japonais)",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bisque de crustacés",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Crustacés"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "EPICES_HERBES",
|
category: "EPICES_HERBES",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Basilic", icon: "🌿", allergenNames: [] },
|
{ name: "Basilic", icon: "🌿", allergenNames: [] },
|
||||||
{ name: "Persil", icon: "🌿", allergenNames: [] },
|
{ name: "Persil", icon: "🌿", allergenNames: [] },
|
||||||
|
|
@ -418,21 +511,36 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "SUCRE_PATISSERIE",
|
category: "SUCRE_PATISSERIE",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Sucre roux", icon: "🍬", allergenNames: [] },
|
{ name: "Sucre roux", icon: "🍬", allergenNames: [] },
|
||||||
{ name: "Sucre glace", icon: "🍬", allergenNames: [] },
|
{ name: "Sucre glace", icon: "🍬", allergenNames: [] },
|
||||||
{ name: "Cassonade", icon: "🍬", allergenNames: [] },
|
{ name: "Cassonade", icon: "🍬", allergenNames: [] },
|
||||||
{ name: "Chocolat noir", icon: "🍫", allergenNames: [] },
|
{ name: "Chocolat noir", icon: "🍫", allergenNames: [] },
|
||||||
{ name: "Chocolat au lait", icon: "🍫", allergenNames: ["Lait"] },
|
{
|
||||||
{ name: "Chocolat blanc", icon: "🍫", allergenNames: ["Lait"] },
|
name: "Chocolat au lait",
|
||||||
|
icon: "🍫",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chocolat blanc",
|
||||||
|
icon: "🍫",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Pépites de chocolat", icon: "🍫", allergenNames: [] },
|
{ name: "Pépites de chocolat", icon: "🍫", allergenNames: [] },
|
||||||
{ name: "Cacao en poudre", icon: "🍫", allergenNames: [] },
|
{ name: "Cacao en poudre", icon: "🍫", allergenNames: [] },
|
||||||
{ name: "Gélatine", icon: "🫙", allergenNames: [] },
|
// Animal collagen (bones/skin, usually pork or beef) — not
|
||||||
|
// vegetarian/vegan, and not reliably fish-derived either, so no
|
||||||
|
// pescetarian flag.
|
||||||
|
{ name: "Gélatine", icon: "🫙", allergenNames: [], dietNames: [] },
|
||||||
{ name: "Extrait de vanille", icon: "🌿", allergenNames: [] },
|
{ name: "Extrait de vanille", icon: "🌿", allergenNames: [] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "CUISINE_ITALIENNE",
|
category: "CUISINE_ITALIENNE",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Spaghetti", icon: "🍝", allergenNames: ["Gluten"] },
|
{ name: "Spaghetti", icon: "🍝", allergenNames: ["Gluten"] },
|
||||||
{ name: "Penne", icon: "🍝", allergenNames: ["Gluten"] },
|
{ name: "Penne", icon: "🍝", allergenNames: ["Gluten"] },
|
||||||
|
|
@ -440,15 +548,40 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Lasagnes (feuilles)", icon: "🍝", allergenNames: ["Gluten"] },
|
{ name: "Lasagnes (feuilles)", icon: "🍝", allergenNames: ["Gluten"] },
|
||||||
{ name: "Gnocchi", icon: "🍝", allergenNames: ["Gluten"] },
|
{ name: "Gnocchi", icon: "🍝", allergenNames: ["Gluten"] },
|
||||||
{ name: "Riz arborio", icon: "🍚", allergenNames: [] },
|
{ name: "Riz arborio", icon: "🍚", allergenNames: [] },
|
||||||
{ name: "Burrata", icon: "🧀", allergenNames: ["Lait"] },
|
{
|
||||||
{ name: "Ricotta", icon: "🧀", allergenNames: ["Lait"] },
|
name: "Burrata",
|
||||||
{ name: "Pecorino", icon: "🧀", allergenNames: ["Lait"] },
|
icon: "🧀",
|
||||||
{ name: "Gorgonzola", icon: "🧀", allergenNames: ["Lait"] },
|
allergenNames: ["Lait"],
|
||||||
{ name: "Prosciutto", icon: "🍖", allergenNames: [] },
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
{ name: "Pancetta", icon: "🥓", allergenNames: [] },
|
},
|
||||||
{ name: "Mortadelle", icon: "🍖", allergenNames: [] },
|
{
|
||||||
{ name: "Salami", icon: "🍖", allergenNames: [] },
|
name: "Ricotta",
|
||||||
{ name: "Pesto", icon: "🫙", allergenNames: ["Lait", "Fruits à coque"] },
|
icon: "🧀",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pecorino",
|
||||||
|
icon: "🧀",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gorgonzola",
|
||||||
|
icon: "🧀",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{ name: "Prosciutto", icon: "🍖", allergenNames: [], dietNames: [] },
|
||||||
|
{ name: "Pancetta", icon: "🥓", allergenNames: [], dietNames: [] },
|
||||||
|
{ name: "Mortadelle", icon: "🍖", allergenNames: [], dietNames: [] },
|
||||||
|
{ name: "Salami", icon: "🍖", allergenNames: [], dietNames: [] },
|
||||||
|
{
|
||||||
|
name: "Pesto",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Lait", "Fruits à coque"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Tomates cerises", icon: "🍅", allergenNames: [] },
|
{ name: "Tomates cerises", icon: "🍅", allergenNames: [] },
|
||||||
{ name: "Focaccia", icon: "🍞", allergenNames: ["Gluten"] },
|
{ name: "Focaccia", icon: "🍞", allergenNames: ["Gluten"] },
|
||||||
{ name: "Ciabatta", icon: "🍞", allergenNames: ["Gluten"] },
|
{ name: "Ciabatta", icon: "🍞", allergenNames: ["Gluten"] },
|
||||||
|
|
@ -456,8 +589,14 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "CUISINE_ASIATIQUE",
|
category: "CUISINE_ASIATIQUE",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Sauce huître", icon: "🫙", allergenNames: ["Mollusques"] },
|
{
|
||||||
|
name: "Sauce huître",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Mollusques"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Sauce hoisin", icon: "🫙", allergenNames: ["Soja"] },
|
{ name: "Sauce hoisin", icon: "🫙", allergenNames: ["Soja"] },
|
||||||
{ name: "Sauce sriracha", icon: "🫙", allergenNames: [] },
|
{ name: "Sauce sriracha", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Sauce sweet chili", icon: "🫙", allergenNames: [] },
|
{ name: "Sauce sweet chili", icon: "🫙", allergenNames: [] },
|
||||||
|
|
@ -496,14 +635,25 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Sucre de palme", icon: "🍬", allergenNames: [] },
|
{ name: "Sucre de palme", icon: "🍬", allergenNames: [] },
|
||||||
{ name: "Pousses de bambou", allergenNames: [] },
|
{ name: "Pousses de bambou", allergenNames: [] },
|
||||||
{ name: "Châtaignes d'eau", allergenNames: [] },
|
{ name: "Châtaignes d'eau", allergenNames: [] },
|
||||||
{ name: "Poisson séché", icon: "🐟", allergenNames: ["Poissons"] },
|
{
|
||||||
{ name: "Pâte de crevettes", icon: "🫙", allergenNames: ["Crustacés"] },
|
name: "Poisson séché",
|
||||||
|
icon: "🐟",
|
||||||
|
allergenNames: ["Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pâte de crevettes",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Crustacés"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Pâte de curry rouge (thaï)", icon: "🍛", allergenNames: [] },
|
{ name: "Pâte de curry rouge (thaï)", icon: "🍛", allergenNames: [] },
|
||||||
{ name: "Pâte de curry vert (thaï)", icon: "🍛", allergenNames: [] },
|
{ name: "Pâte de curry vert (thaï)", icon: "🍛", allergenNames: [] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "CUISINE_MEXICAINE",
|
category: "CUISINE_MEXICAINE",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Tortilla de maïs", icon: "🌮", allergenNames: [] },
|
{ name: "Tortilla de maïs", icon: "🌮", allergenNames: [] },
|
||||||
{ name: "Tortilla de blé", icon: "🌮", allergenNames: ["Gluten"] },
|
{ name: "Tortilla de blé", icon: "🌮", allergenNames: ["Gluten"] },
|
||||||
|
|
@ -513,11 +663,17 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Piment poblano", icon: "🌶️", allergenNames: [] },
|
{ name: "Piment poblano", icon: "🌶️", allergenNames: [] },
|
||||||
{ name: "Piment habanero", icon: "🌶️", allergenNames: [] },
|
{ name: "Piment habanero", icon: "🌶️", allergenNames: [] },
|
||||||
{ name: "Masa harina", icon: "🌽", allergenNames: [] },
|
{ name: "Masa harina", icon: "🌽", allergenNames: [] },
|
||||||
{ name: "Cheddar", icon: "🧀", allergenNames: ["Lait"] },
|
{
|
||||||
|
name: "Cheddar",
|
||||||
|
icon: "🧀",
|
||||||
|
allergenNames: ["Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "MAGHREB_MOYEN_ORIENT",
|
category: "MAGHREB_MOYEN_ORIENT",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Ras el hanout", icon: "🌿", allergenNames: [] },
|
{ name: "Ras el hanout", icon: "🌿", allergenNames: [] },
|
||||||
{ name: "Za'atar", icon: "🌿", allergenNames: ["Graines de sésame"] },
|
{ name: "Za'atar", icon: "🌿", allergenNames: ["Graines de sésame"] },
|
||||||
|
|
@ -530,15 +686,31 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
// seeded under `CEREALES_FECULENTS` above — this category covers the
|
// seeded under `CEREALES_FECULENTS` above — this category covers the
|
||||||
// shapes specifically reached for to build a sandwich, plus a few
|
// shapes specifically reached for to build a sandwich, plus a few
|
||||||
// international flatbreads not tied to one cuisine category above.
|
// international flatbreads not tied to one cuisine category above.
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Pain à burger", icon: "🍔", allergenNames: ["Gluten", "Lait", "Œufs"] },
|
{
|
||||||
{ name: "Pain brioché", icon: "🍞", allergenNames: ["Gluten", "Lait", "Œufs"] },
|
name: "Pain à burger",
|
||||||
|
icon: "🍔",
|
||||||
|
allergenNames: ["Gluten", "Lait", "Œufs"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pain brioché",
|
||||||
|
icon: "🍞",
|
||||||
|
allergenNames: ["Gluten", "Lait", "Œufs"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Pain à hot-dog", icon: "🌭", allergenNames: ["Gluten"] },
|
{ name: "Pain à hot-dog", icon: "🌭", allergenNames: ["Gluten"] },
|
||||||
{ name: "Pain pita", icon: "🫓", allergenNames: ["Gluten"] },
|
{ name: "Pain pita", icon: "🫓", allergenNames: ["Gluten"] },
|
||||||
{ name: "Pain bagel", icon: "🥯", allergenNames: ["Gluten"] },
|
{ name: "Pain bagel", icon: "🥯", allergenNames: ["Gluten"] },
|
||||||
{ name: "Naan", icon: "🫓", allergenNames: ["Gluten"] },
|
{ name: "Naan", icon: "🫓", allergenNames: ["Gluten"] },
|
||||||
{ name: "Pain wrap", icon: "🫓", allergenNames: ["Gluten"] },
|
{ name: "Pain wrap", icon: "🫓", allergenNames: ["Gluten"] },
|
||||||
{ name: "Pain viennois", icon: "🍞", allergenNames: ["Gluten", "Lait"] },
|
{
|
||||||
|
name: "Pain viennois",
|
||||||
|
icon: "🍞",
|
||||||
|
allergenNames: ["Gluten", "Lait"],
|
||||||
|
dietNames: ["Végétarien", "Pescétarien"],
|
||||||
|
},
|
||||||
{ name: "Pain de campagne", icon: "🍞", allergenNames: ["Gluten"] },
|
{ name: "Pain de campagne", icon: "🍞", allergenNames: ["Gluten"] },
|
||||||
{ name: "Pain aux céréales", icon: "🍞", allergenNames: ["Gluten"] },
|
{ name: "Pain aux céréales", icon: "🍞", allergenNames: ["Gluten"] },
|
||||||
{ name: "Petit pain", icon: "🍞", allergenNames: ["Gluten"] },
|
{ name: "Petit pain", icon: "🍞", allergenNames: ["Gluten"] },
|
||||||
|
|
@ -550,6 +722,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "EPICERIE_DIVERS",
|
category: "EPICERIE_DIVERS",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Bicarbonate de soude", icon: "🫙", allergenNames: [] },
|
{ name: "Bicarbonate de soude", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Fécule de pomme de terre", icon: "🫙", allergenNames: [] },
|
{ name: "Fécule de pomme de terre", icon: "🫙", allergenNames: [] },
|
||||||
|
|
@ -557,6 +730,7 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "LIQUIDES_BOISSONS",
|
category: "LIQUIDES_BOISSONS",
|
||||||
|
defaultDiets: ["Végétarien", "Végan", "Pescétarien"],
|
||||||
items: [
|
items: [
|
||||||
{ name: "Eau", icon: "💧", allergenNames: [] },
|
{ name: "Eau", icon: "💧", allergenNames: [] },
|
||||||
{ name: "Eau gazeuse", icon: "💧", allergenNames: [] },
|
{ name: "Eau gazeuse", icon: "💧", allergenNames: [] },
|
||||||
|
|
@ -581,13 +755,27 @@ const INGREDIENT_GROUPS: Array<{ category: IngredientCategory; items: Ingredient
|
||||||
{ name: "Whisky", icon: "🥃", allergenNames: [] },
|
{ name: "Whisky", icon: "🥃", allergenNames: [] },
|
||||||
{ name: "Vodka", icon: "🥃", allergenNames: [] },
|
{ name: "Vodka", icon: "🥃", allergenNames: [] },
|
||||||
{ name: "Sirop de sucre de canne", icon: "🫙", allergenNames: [] },
|
{ name: "Sirop de sucre de canne", icon: "🫙", allergenNames: [] },
|
||||||
{ name: "Fumet de poisson", icon: "🫙", allergenNames: ["Poissons"] },
|
{
|
||||||
|
name: "Fumet de poisson",
|
||||||
|
icon: "🫙",
|
||||||
|
allergenNames: ["Poissons"],
|
||||||
|
dietNames: ["Pescétarien"],
|
||||||
|
},
|
||||||
|
// Plant-based milk/cream substitutes — moved here from
|
||||||
|
// `PRODUITS_LAITIERS_OEUFS` (they're not dairy; see that group's
|
||||||
|
// comment) alongside the other drinkable/pourable liquids.
|
||||||
|
{ 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"] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const INGREDIENTS: Array<IngredientSeed & { category: IngredientCategory }> =
|
const INGREDIENTS: Array<IngredientSeed & { category: IngredientCategory; dietNames: string[] }> =
|
||||||
INGREDIENT_GROUPS.flatMap(({ category, items }) => items.map((item) => ({ ...item, category })));
|
INGREDIENT_GROUPS.flatMap(({ category, defaultDiets, items }) =>
|
||||||
|
items.map((item) => ({ ...item, category, dietNames: item.dietNames ?? defaultDiets })),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent
|
* Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent
|
||||||
|
|
@ -675,4 +863,23 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
await prisma.ingredientAllergy.createMany({ data: links, skipDuplicates: true });
|
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 dietIdByName = new Map(diets.map((d) => [d.name, d.id]));
|
||||||
|
|
||||||
|
const dietLinks: Array<{ ingredientId: number; dietId: number }> = [];
|
||||||
|
for (const { name, dietNames } of INGREDIENTS) {
|
||||||
|
const ingredientId = ingredientIdByName.get(name);
|
||||||
|
if (ingredientId === undefined) continue;
|
||||||
|
for (const dietName of dietNames) {
|
||||||
|
const dietId = dietIdByName.get(dietName);
|
||||||
|
if (dietId !== undefined) dietLinks.push({ ingredientId, dietId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dietLinks.length > 0) {
|
||||||
|
await prisma.ingredientDiet.createMany({ data: dietLinks, skipDuplicates: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ function recipeInclude(viewerId: number) {
|
||||||
ingredients: {
|
ingredients: {
|
||||||
include: {
|
include: {
|
||||||
ingredient: {
|
ingredient: {
|
||||||
include: { allergies: { include: { allergy: { include: { category: true } } } } },
|
include: {
|
||||||
|
allergies: { include: { allergy: { include: { category: true } } } },
|
||||||
|
diets: { include: { diet: true } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -30,10 +33,10 @@ function recipeInclude(viewerId: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecipeWithDetails = Prisma.RecipeGetPayload<{ include: ReturnType<typeof recipeInclude> }>;
|
type RecipeWithDetails = Prisma.RecipeGetPayload<{ include: ReturnType<typeof recipeInclude> }>;
|
||||||
type IngredientWithAllergies = RecipeWithDetails["ingredients"][number]["ingredient"];
|
type IngredientWithDetails = RecipeWithDetails["ingredients"][number]["ingredient"];
|
||||||
|
|
||||||
/** Shapes a Prisma `Ingredient` (with its `allergies` relation included) into the public {@link IngredientView} — same aplattening as `reference.service.ts`'s `getIngredients`. */
|
/** Shapes a Prisma `Ingredient` (with its `allergies`/`diets` relations included) into the public {@link IngredientView} — same aplattening as `reference.service.ts`'s `getIngredients`. */
|
||||||
function toIngredientView(ingredient: IngredientWithAllergies): IngredientView {
|
function toIngredientView(ingredient: IngredientWithDetails): IngredientView {
|
||||||
return {
|
return {
|
||||||
id: ingredient.id,
|
id: ingredient.id,
|
||||||
name: ingredient.name,
|
name: ingredient.name,
|
||||||
|
|
@ -44,6 +47,7 @@ function toIngredientView(ingredient: IngredientWithAllergies): IngredientView {
|
||||||
name: allergy.category.name,
|
name: allergy.category.name,
|
||||||
kind: allergy.category.kind,
|
kind: allergy.category.kind,
|
||||||
})),
|
})),
|
||||||
|
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, name: diet.name })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,16 @@ export async function getAllergies(): Promise<AllergyView[]> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All reference ingredients, alphabetically, each resolved to its allergens
|
* All reference ingredients, alphabetically, each resolved to its allergens
|
||||||
* (see `IngredientAllergy` in schema.prisma) — same aplattening approach as
|
* (see `IngredientAllergy` in schema.prisma) and compatible diet regimes
|
||||||
* {@link getAllergies}. Ingredients with no linked allergen come back with
|
* (see `IngredientDiet`) — same aplattening approach as {@link getAllergies}.
|
||||||
* `allergens: []`.
|
* Ingredients with no linked allergen/diet come back with `allergens: []`/
|
||||||
|
* `diets: []`.
|
||||||
*/
|
*/
|
||||||
export async function getIngredients(): Promise<IngredientView[]> {
|
export async function getIngredients(): Promise<IngredientView[]> {
|
||||||
const ingredients = await prisma.ingredient.findMany({
|
const ingredients = await prisma.ingredient.findMany({
|
||||||
include: {
|
include: {
|
||||||
allergies: { include: { allergy: { include: { category: true } } } },
|
allergies: { include: { allergy: { include: { category: true } } } },
|
||||||
|
diets: { include: { diet: true } },
|
||||||
},
|
},
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
});
|
});
|
||||||
|
|
@ -47,5 +49,6 @@ export async function getIngredients(): Promise<IngredientView[]> {
|
||||||
name: allergy.category.name,
|
name: allergy.category.name,
|
||||||
kind: allergy.category.kind,
|
kind: allergy.category.kind,
|
||||||
})),
|
})),
|
||||||
|
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, name: diet.name })),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ describe("Reference data", () => {
|
||||||
expect(res.status).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
expect(res.body.length).to.be.greaterThan(0);
|
expect(res.body.length).to.be.greaterThan(0);
|
||||||
expect(res.body.map((i: { name: string }) => i.name)).to.include("Tomate");
|
expect(res.body.map((i: { name: string }) => i.name)).to.include("Tomate");
|
||||||
expect(res.body[0]).to.have.keys(["id", "name", "icon", "category", "allergens"]);
|
expect(res.body[0]).to.have.keys(["id", "name", "icon", "category", "allergens", "diets"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves each ingredient's linked allergens, empty for one with none", async () => {
|
it("resolves each ingredient's linked allergens, empty for one with none", async () => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AllergenBadges } from "./AllergenBadges";
|
import { AllergenBadges } from "./AllergenBadges";
|
||||||
|
import { DietBadges } from "./DietBadges";
|
||||||
import "./recipes.scss";
|
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). */
|
/** "No category filter" — a UI-only pseudo-category, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES} for the real, closed set). */
|
||||||
|
|
@ -100,6 +101,7 @@ export function IngredientPicker({
|
||||||
</span>
|
</span>
|
||||||
<span className="ingredient-picker__card-name">{ingredient.name}</span>
|
<span className="ingredient-picker__card-name">{ingredient.name}</span>
|
||||||
<AllergenBadges allergens={ingredient.allergens} />
|
<AllergenBadges allergens={ingredient.allergens} />
|
||||||
|
<DietBadges diets={ingredient.diets} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { IngredientView } from "@batch-cooking/shared";
|
import type { IngredientView } from "@batch-cooking/shared";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AllergenBadges } from "./AllergenBadges";
|
import { AllergenBadges } from "./AllergenBadges";
|
||||||
|
import { DietBadges } from "./DietBadges";
|
||||||
import "./recipes.scss";
|
import "./recipes.scss";
|
||||||
|
|
||||||
/** 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. */
|
/** 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. */
|
||||||
|
|
@ -45,6 +46,7 @@ export function IngredientRow({
|
||||||
aria-label={t("recipes.form.unitLabel")}
|
aria-label={t("recipes.form.unitLabel")}
|
||||||
/>
|
/>
|
||||||
<AllergenBadges allergens={ingredient.allergens} />
|
<AllergenBadges allergens={ingredient.allergens} />
|
||||||
|
<DietBadges diets={ingredient.diets} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ingredient-row__remove"
|
className="ingredient-row__remove"
|
||||||
|
|
|
||||||
|
|
@ -889,6 +889,12 @@
|
||||||
// ingredient picker but without quantity/unit — see
|
// ingredient picker but without quantity/unit — see
|
||||||
// `features/profile/DislikedIngredientsField.tsx`.
|
// `features/profile/DislikedIngredientsField.tsx`.
|
||||||
.disliked-ingredients-field {
|
.disliked-ingredients-field {
|
||||||
|
// `<fieldset>` carries a browser-default `min-width: min-content` that
|
||||||
|
// ordinary block elements don't — it refuses to shrink for its content,
|
||||||
|
// so `.ingredient-picker__grid` below (a `repeat(auto-fill, …)` grid)
|
||||||
|
// never gets narrow enough to wrap and blows out the whole page's width
|
||||||
|
// instead. Reset it back to the normal shrinkable behavior.
|
||||||
|
min-width: 0;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: var(--space-md) 0 0;
|
margin: var(--space-md) 0 0;
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,15 @@ export type IngredientCategory = (typeof INGREDIENT_CATEGORIES)[number];
|
||||||
*
|
*
|
||||||
* `allergens` is resolved server-side from the `IngredientAllergy` join
|
* `allergens` is resolved server-side from the `IngredientAllergy` join
|
||||||
* table — empty for an ingredient that carries none of the 14 EU-regulated
|
* table — empty for an ingredient that carries none of the 14 EU-regulated
|
||||||
* allergens. Used by the recipe catalog (`apps/web`'s recipe form and detail
|
* allergens. `diets` is resolved from `IngredientDiet` the same way — the
|
||||||
* page) to pick ingredients and to surface which allergens a recipe
|
* regimes this ingredient is compatible with (e.g. `Végétarien`, `Végan`),
|
||||||
* contains, aggregated across its ingredients.
|
* so the picker can flag it without the user opening its packaging. Omits
|
||||||
|
* `Omnivore` (every ingredient qualifies, so it's never stored) and
|
||||||
|
* `Sans gluten` (already derivable from whether `allergens` contains
|
||||||
|
* `Gluten` — see `IngredientDiet` in schema.prisma). Used by the recipe
|
||||||
|
* catalog (`apps/web`'s recipe form and detail page) to pick ingredients and
|
||||||
|
* to surface which allergens/regimes a recipe contains, aggregated across
|
||||||
|
* its ingredients.
|
||||||
*/
|
*/
|
||||||
export interface IngredientView {
|
export interface IngredientView {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -82,4 +88,5 @@ export interface IngredientView {
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
category: IngredientCategory;
|
category: IngredientCategory;
|
||||||
allergens: AllergyView[];
|
allergens: AllergyView[];
|
||||||
|
diets: DietView[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue