Merge pull request #29 from kyuno053/feat/ingredient-reproducible-flag
feat(recipes): flag les ingrédients faisables maison + suggestion de recherche
This commit is contained in:
commit
3fd55c4f85
24 changed files with 1733 additions and 2060 deletions
|
|
@ -0,0 +1,9 @@
|
|||
-- Replaces the never-wired-up `Ingredient.alternateRecipeId` FK (zero
|
||||
-- usage anywhere outside schema.prisma — confirmed by repo-wide grep)
|
||||
-- with a plain boolean flag: whether this ingredient is reasonably
|
||||
-- makeable at home. Product decision: no ingredient↔recipe linking in
|
||||
-- the database — the recipe form only nudges the author toward the
|
||||
-- recipe catalog's own search, pre-filled with the ingredient's name.
|
||||
ALTER TABLE "ingredients" DROP CONSTRAINT "ingredients_alternate_recipe_fkey";
|
||||
ALTER TABLE "ingredients" DROP COLUMN "alternate_recipe";
|
||||
ALTER TABLE "ingredients" ADD COLUMN "reproducible" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
-- Replaces every Diet/Category(allergen)/Ingredient `key` with a
|
||||
-- directly-authored English camelCase uid (no more French label +
|
||||
-- separate catalog-en-keys.ts lookup table — see reference-seed-data.ts's
|
||||
-- module doc comment). Auto-generated once by
|
||||
-- scripts/gen-camel-uid-migration.ts — do not re-run, do not hand-edit.
|
||||
-- Same shape as 20260818193000_catalog_keys_to_english/migration.sql.
|
||||
|
||||
UPDATE "diet" SET "key" = 'glutenFree' WHERE "key" = 'gluten_free';
|
||||
UPDATE "category" SET "key" = 'treeNuts' WHERE "key" = 'tree_nuts';
|
||||
UPDATE "category" SET "key" = 'sesameSeeds' WHERE "key" = 'sesame_seeds';
|
||||
-- "sesame_seeds" is also an Ingredient key (the ingredient "Graines de
|
||||
-- sésame" carries an allergen of the same name on itself) — hand-added,
|
||||
-- the generator's first pass missed this dual-table case (see its
|
||||
-- updated `if`/`if`/`if` — not `if`/`else if` — comment for why).
|
||||
UPDATE "ingredients" SET "key" = 'sesameSeeds' WHERE "key" = 'sesame_seeds';
|
||||
UPDATE "ingredients" SET "key" = 'bellPepper' WHERE "key" = 'bell_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'whiteCabbage' WHERE "key" = 'white_cabbage';
|
||||
UPDATE "ingredients" SET "key" = 'redCabbage' WHERE "key" = 'red_cabbage';
|
||||
UPDATE "ingredients" SET "key" = 'brusselsSprouts' WHERE "key" = 'brussels_sprouts';
|
||||
UPDATE "ingredients" SET "key" = 'swissChard' WHERE "key" = 'swiss_chard';
|
||||
UPDATE "ingredients" SET "key" = 'greenBean' WHERE "key" = 'green_bean';
|
||||
UPDATE "ingredients" SET "key" = 'butternutSquash' WHERE "key" = 'butternut_squash';
|
||||
UPDATE "ingredients" SET "key" = 'sweetPotato' WHERE "key" = 'sweet_potato';
|
||||
UPDATE "ingredients" SET "key" = 'cherryTomato' WHERE "key" = 'cherry_tomato';
|
||||
UPDATE "ingredients" SET "key" = 'bokChoy' WHERE "key" = 'bok_choy';
|
||||
UPDATE "ingredients" SET "key" = 'soybeanSprouts' WHERE "key" = 'soybean_sprouts';
|
||||
UPDATE "ingredients" SET "key" = 'freshGreenChili' WHERE "key" = 'fresh_green_chili';
|
||||
UPDATE "ingredients" SET "key" = 'napaCabbage' WHERE "key" = 'napa_cabbage';
|
||||
UPDATE "ingredients" SET "key" = 'springOnion' WHERE "key" = 'spring_onion';
|
||||
UPDATE "ingredients" SET "key" = 'redKuriSquash' WHERE "key" = 'red_kuri_squash';
|
||||
UPDATE "ingredients" SET "key" = 'lambsLettuce' WHERE "key" = 'lambs_lettuce';
|
||||
UPDATE "ingredients" SET "key" = 'bayLeaf' WHERE "key" = 'bay_leaf';
|
||||
UPDATE "ingredients" SET "key" = 'freshCilantro' WHERE "key" = 'fresh_cilantro';
|
||||
UPDATE "ingredients" SET "key" = 'kaffirLime' WHERE "key" = 'kaffir_lime';
|
||||
UPDATE "ingredients" SET "key" = 'groundBeef' WHERE "key" = 'ground_beef';
|
||||
UPDATE "ingredients" SET "key" = 'beefSteak' WHERE "key" = 'beef_steak';
|
||||
UPDATE "ingredients" SET "key" = 'beefRoast' WHERE "key" = 'beef_roast';
|
||||
UPDATE "ingredients" SET "key" = 'vealCutlet' WHERE "key" = 'veal_cutlet';
|
||||
UPDATE "ingredients" SET "key" = 'porkTenderloin' WHERE "key" = 'pork_tenderloin';
|
||||
UPDATE "ingredients" SET "key" = 'porkChop' WHERE "key" = 'pork_chop';
|
||||
UPDATE "ingredients" SET "key" = 'legOfLamb' WHERE "key" = 'leg_of_lamb';
|
||||
UPDATE "ingredients" SET "key" = 'baconLardons' WHERE "key" = 'bacon_lardons';
|
||||
UPDATE "ingredients" SET "key" = 'curedHam' WHERE "key" = 'cured_ham';
|
||||
UPDATE "ingredients" SET "key" = 'whitePudding' WHERE "key" = 'white_pudding';
|
||||
UPDATE "ingredients" SET "key" = 'blackPudding' WHERE "key" = 'black_pudding';
|
||||
UPDATE "ingredients" SET "key" = 'dryCuredSausage' WHERE "key" = 'dry_cured_sausage';
|
||||
UPDATE "ingredients" SET "key" = 'bayonneHam' WHERE "key" = 'bayonne_ham';
|
||||
UPDATE "ingredients" SET "key" = 'rosetteSausage' WHERE "key" = 'rosette_sausage';
|
||||
UPDATE "ingredients" SET "key" = 'vealLiver' WHERE "key" = 'veal_liver';
|
||||
UPDATE "ingredients" SET "key" = 'vealKidneys' WHERE "key" = 'veal_kidneys';
|
||||
UPDATE "ingredients" SET "key" = 'vealBrain' WHERE "key" = 'veal_brain';
|
||||
UPDATE "ingredients" SET "key" = 'vealSweetbread' WHERE "key" = 'veal_sweetbread';
|
||||
UPDATE "ingredients" SET "key" = 'beefTongue' WHERE "key" = 'beef_tongue';
|
||||
UPDATE "ingredients" SET "key" = 'roeDeer' WHERE "key" = 'roe_deer';
|
||||
UPDATE "ingredients" SET "key" = 'wildBoar' WHERE "key" = 'wild_boar';
|
||||
UPDATE "ingredients" SET "key" = 'horseMeat' WHERE "key" = 'horse_meat';
|
||||
UPDATE "ingredients" SET "key" = 'beefHeart' WHERE "key" = 'beef_heart';
|
||||
UPDATE "ingredients" SET "key" = 'foieGras' WHERE "key" = 'foie_gras';
|
||||
UPDATE "ingredients" SET "key" = 'beefMuzzle' WHERE "key" = 'beef_muzzle';
|
||||
UPDATE "ingredients" SET "key" = 'grisonsDriedBeef' WHERE "key" = 'grisons_dried_beef';
|
||||
UPDATE "ingredients" SET "key" = 'duckBreast' WHERE "key" = 'duck_breast';
|
||||
UPDATE "ingredients" SET "key" = 'guineaFowl' WHERE "key" = 'guinea_fowl';
|
||||
UPDATE "ingredients" SET "key" = 'poultryLiver' WHERE "key" = 'poultry_liver';
|
||||
UPDATE "ingredients" SET "key" = 'seaBass' WHERE "key" = 'sea_bass';
|
||||
UPDATE "ingredients" SET "key" = 'seaBream' WHERE "key" = 'sea_bream';
|
||||
UPDATE "ingredients" SET "key" = 'redMullet' WHERE "key" = 'red_mullet';
|
||||
UPDATE "ingredients" SET "key" = 'smokedSalmon' WHERE "key" = 'smoked_salmon';
|
||||
UPDATE "ingredients" SET "key" = 'driedFish' WHERE "key" = 'dried_fish';
|
||||
UPDATE "ingredients" SET "key" = 'saltCod' WHERE "key" = 'salt_cod';
|
||||
UPDATE "ingredients" SET "key" = 'lemonSole' WHERE "key" = 'lemon_sole';
|
||||
UPDATE "ingredients" SET "key" = 'spinyLobster' WHERE "key" = 'spiny_lobster';
|
||||
UPDATE "ingredients" SET "key" = 'spiderCrab' WHERE "key" = 'spider_crab';
|
||||
UPDATE "ingredients" SET "key" = 'greyShrimp' WHERE "key" = 'grey_shrimp';
|
||||
UPDATE "ingredients" SET "key" = 'wholeWheatPasta' WHERE "key" = 'whole_wheat_pasta';
|
||||
UPDATE "ingredients" SET "key" = 'basmatiRice' WHERE "key" = 'basmati_rice';
|
||||
UPDATE "ingredients" SET "key" = 'brownRice' WHERE "key" = 'brown_rice';
|
||||
UPDATE "ingredients" SET "key" = 'lasagnaSheets' WHERE "key" = 'lasagna_sheets';
|
||||
UPDATE "ingredients" SET "key" = 'arborioRice' WHERE "key" = 'arborio_rice';
|
||||
UPDATE "ingredients" SET "key" = 'riceNoodles' WHERE "key" = 'rice_noodles';
|
||||
UPDATE "ingredients" SET "key" = 'udonNoodles' WHERE "key" = 'udon_noodles';
|
||||
UPDATE "ingredients" SET "key" = 'sobaNoodles' WHERE "key" = 'soba_noodles';
|
||||
UPDATE "ingredients" SET "key" = 'chineseNoodles' WHERE "key" = 'chinese_noodles';
|
||||
UPDATE "ingredients" SET "key" = 'riceVermicelli' WHERE "key" = 'rice_vermicelli';
|
||||
UPDATE "ingredients" SET "key" = 'soyVermicelli' WHERE "key" = 'soy_vermicelli';
|
||||
UPDATE "ingredients" SET "key" = 'stickyRice' WHERE "key" = 'sticky_rice';
|
||||
UPDATE "ingredients" SET "key" = 'sushiRice' WHERE "key" = 'sushi_rice';
|
||||
UPDATE "ingredients" SET "key" = 'jasmineRice' WHERE "key" = 'jasmine_rice';
|
||||
UPDATE "ingredients" SET "key" = 'greenLentils' WHERE "key" = 'green_lentils';
|
||||
UPDATE "ingredients" SET "key" = 'redLentils' WHERE "key" = 'red_lentils';
|
||||
UPDATE "ingredients" SET "key" = 'whiteBeans' WHERE "key" = 'white_beans';
|
||||
UPDATE "ingredients" SET "key" = 'kidneyBeans' WHERE "key" = 'kidney_beans';
|
||||
UPDATE "ingredients" SET "key" = 'blackBeans' WHERE "key" = 'black_beans';
|
||||
UPDATE "ingredients" SET "key" = 'splitPeas' WHERE "key" = 'split_peas';
|
||||
UPDATE "ingredients" SET "key" = 'favaBeans' WHERE "key" = 'fava_beans';
|
||||
UPDATE "ingredients" SET "key" = 'pintoBeans' WHERE "key" = 'pinto_beans';
|
||||
UPDATE "ingredients" SET "key" = 'flageoletBeans' WHERE "key" = 'flageolet_beans';
|
||||
UPDATE "ingredients" SET "key" = 'goldenLentils' WHERE "key" = 'golden_lentils';
|
||||
UPDATE "ingredients" SET "key" = 'peanutsShelled' WHERE "key" = 'peanuts_shelled';
|
||||
UPDATE "ingredients" SET "key" = 'almondPowder' WHERE "key" = 'almond_powder';
|
||||
UPDATE "ingredients" SET "key" = 'pineNuts' WHERE "key" = 'pine_nuts';
|
||||
UPDATE "ingredients" SET "key" = 'sunflowerSeeds' WHERE "key" = 'sunflower_seeds';
|
||||
UPDATE "ingredients" SET "key" = 'pumpkinSeeds' WHERE "key" = 'pumpkin_seeds';
|
||||
UPDATE "ingredients" SET "key" = 'shreddedCoconut' WHERE "key" = 'shredded_coconut';
|
||||
UPDATE "ingredients" SET "key" = 'driedApricots' WHERE "key" = 'dried_apricots';
|
||||
UPDATE "ingredients" SET "key" = 'blackMushrooms' WHERE "key" = 'black_mushrooms';
|
||||
UPDATE "ingredients" SET "key" = 'noriSeaweed' WHERE "key" = 'nori_seaweed';
|
||||
UPDATE "ingredients" SET "key" = 'wakameSeaweed' WHERE "key" = 'wakame_seaweed';
|
||||
UPDATE "ingredients" SET "key" = 'kombuSeaweed' WHERE "key" = 'kombu_seaweed';
|
||||
UPDATE "ingredients" SET "key" = 'bambooShoots' WHERE "key" = 'bamboo_shoots';
|
||||
UPDATE "ingredients" SET "key" = 'waterChestnuts' WHERE "key" = 'water_chestnuts';
|
||||
UPDATE "ingredients" SET "key" = 'sandwichBread' WHERE "key" = 'sandwich_bread';
|
||||
UPDATE "ingredients" SET "key" = 'wholeWheatBread' WHERE "key" = 'whole_wheat_bread';
|
||||
UPDATE "ingredients" SET "key" = 'ryeBread' WHERE "key" = 'rye_bread';
|
||||
UPDATE "ingredients" SET "key" = 'burgerBun' WHERE "key" = 'burger_bun';
|
||||
UPDATE "ingredients" SET "key" = 'briocheBun' WHERE "key" = 'brioche_bun';
|
||||
UPDATE "ingredients" SET "key" = 'hotDogBun' WHERE "key" = 'hot_dog_bun';
|
||||
UPDATE "ingredients" SET "key" = 'pitaBread' WHERE "key" = 'pita_bread';
|
||||
UPDATE "ingredients" SET "key" = 'wrapBread' WHERE "key" = 'wrap_bread';
|
||||
UPDATE "ingredients" SET "key" = 'vienneseBread' WHERE "key" = 'viennese_bread';
|
||||
UPDATE "ingredients" SET "key" = 'countryBread' WHERE "key" = 'country_bread';
|
||||
UPDATE "ingredients" SET "key" = 'multigrainBread' WHERE "key" = 'multigrain_bread';
|
||||
UPDATE "ingredients" SET "key" = 'breadRoll' WHERE "key" = 'bread_roll';
|
||||
UPDATE "ingredients" SET "key" = 'swedishBread' WHERE "key" = 'swedish_bread';
|
||||
UPDATE "ingredients" SET "key" = 'glutenFreeBread' WHERE "key" = 'gluten_free_bread';
|
||||
UPDATE "ingredients" SET "key" = 'cornTortilla' WHERE "key" = 'corn_tortilla';
|
||||
UPDATE "ingredients" SET "key" = 'wheatTortilla' WHERE "key" = 'wheat_tortilla';
|
||||
UPDATE "ingredients" SET "key" = 'puffPastry' WHERE "key" = 'puff_pastry';
|
||||
UPDATE "ingredients" SET "key" = 'shortcrustPastry' WHERE "key" = 'shortcrust_pastry';
|
||||
UPDATE "ingredients" SET "key" = 'pizzaDough' WHERE "key" = 'pizza_dough';
|
||||
UPDATE "ingredients" SET "key" = 'sweetShortcrustPastry' WHERE "key" = 'sweet_shortcrust_pastry';
|
||||
UPDATE "ingredients" SET "key" = 'cremeFraiche' WHERE "key" = 'creme_fraiche';
|
||||
UPDATE "ingredients" SET "key" = 'liquidCream' WHERE "key" = 'liquid_cream';
|
||||
UPDATE "ingredients" SET "key" = 'goatCheese' WHERE "key" = 'goat_cheese';
|
||||
UPDATE "ingredients" SET "key" = 'fromageBlanc' WHERE "key" = 'fromage_blanc';
|
||||
UPDATE "ingredients" SET "key" = 'saintNectaire' WHERE "key" = 'saint_nectaire';
|
||||
UPDATE "ingredients" SET "key" = 'blueCheese' WHERE "key" = 'blue_cheese';
|
||||
UPDATE "ingredients" SET "key" = 'pontLeveque' WHERE "key" = 'pont_leveque';
|
||||
UPDATE "ingredients" SET "key" = 'racletteCheese' WHERE "key" = 'raclette_cheese';
|
||||
UPDATE "ingredients" SET "key" = 'fourmeDAmbert' WHERE "key" = 'fourme_d_ambert';
|
||||
UPDATE "ingredients" SET "key" = 'ossauIraty' WHERE "key" = 'ossau_iraty';
|
||||
UPDATE "ingredients" SET "key" = 'saintMarcellin' WHERE "key" = 'saint_marcellin';
|
||||
UPDATE "ingredients" SET "key" = 'crottinDeChavignol' WHERE "key" = 'crottin_de_chavignol';
|
||||
UPDATE "ingredients" SET "key" = 'abondanceCheese' WHERE "key" = 'abondance_cheese';
|
||||
UPDATE "ingredients" SET "key" = 'carreDeLEst' WHERE "key" = 'carre_de_l_est';
|
||||
UPDATE "ingredients" SET "key" = 'montDor' WHERE "key" = 'mont_dor';
|
||||
UPDATE "ingredients" SET "key" = 'greekYogurt' WHERE "key" = 'greek_yogurt';
|
||||
UPDATE "ingredients" SET "key" = 'coconutMilk' WHERE "key" = 'coconut_milk';
|
||||
UPDATE "ingredients" SET "key" = 'coconutCream' WHERE "key" = 'coconut_cream';
|
||||
UPDATE "ingredients" SET "key" = 'almondMilk' WHERE "key" = 'almond_milk';
|
||||
UPDATE "ingredients" SET "key" = 'oatMilk' WHERE "key" = 'oat_milk';
|
||||
UPDATE "ingredients" SET "key" = 'silkenTofu' WHERE "key" = 'silken_tofu';
|
||||
UPDATE "ingredients" SET "key" = 'herbesDeProvence' WHERE "key" = 'herbes_de_provence';
|
||||
UPDATE "ingredients" SET "key" = 'blackPepper' WHERE "key" = 'black_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'espelettePepper' WHERE "key" = 'espelette_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'cayennePepper' WHERE "key" = 'cayenne_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'curryPowder' WHERE "key" = 'curry_powder';
|
||||
UPDATE "ingredients" SET "key" = 'vanillaBean' WHERE "key" = 'vanilla_bean';
|
||||
UPDATE "ingredients" SET "key" = 'whitePepper' WHERE "key" = 'white_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'pinkPepper' WHERE "key" = 'pink_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'sichuanPepper' WHERE "key" = 'sichuan_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'smokedPaprika' WHERE "key" = 'smoked_paprika';
|
||||
UPDATE "ingredients" SET "key" = 'birdEyeChili' WHERE "key" = 'bird_eye_chili';
|
||||
UPDATE "ingredients" SET "key" = 'juniperBerries' WHERE "key" = 'juniper_berries';
|
||||
UPDATE "ingredients" SET "key" = 'starAnise' WHERE "key" = 'star_anise';
|
||||
UPDATE "ingredients" SET "key" = 'greenAnise' WHERE "key" = 'green_anise';
|
||||
UPDATE "ingredients" SET "key" = 'fennelSeeds' WHERE "key" = 'fennel_seeds';
|
||||
UPDATE "ingredients" SET "key" = 'colomboPowder' WHERE "key" = 'colombo_powder';
|
||||
UPDATE "ingredients" SET "key" = 'herbSalt' WHERE "key" = 'herb_salt';
|
||||
UPDATE "ingredients" SET "key" = 'celerySalt' WHERE "key" = 'celery_salt';
|
||||
UPDATE "ingredients" SET "key" = 'fleurDeSel' WHERE "key" = 'fleur_de_sel';
|
||||
UPDATE "ingredients" SET "key" = 'fiveSpice' WHERE "key" = 'five_spice';
|
||||
UPDATE "ingredients" SET "key" = 'garamMasala' WHERE "key" = 'garam_masala';
|
||||
UPDATE "ingredients" SET "key" = 'corianderSeeds' WHERE "key" = 'coriander_seeds';
|
||||
UPDATE "ingredients" SET "key" = 'poblanoPepper' WHERE "key" = 'poblano_pepper';
|
||||
UPDATE "ingredients" SET "key" = 'rasElHanout' WHERE "key" = 'ras_el_hanout';
|
||||
UPDATE "ingredients" SET "key" = 'soySauce' WHERE "key" = 'soy_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'worcestershireSauce' WHERE "key" = 'worcestershire_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'fishSauce' WHERE "key" = 'fish_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'curryPaste' WHERE "key" = 'curry_paste';
|
||||
UPDATE "ingredients" SET "key" = 'peanutButter' WHERE "key" = 'peanut_butter';
|
||||
UPDATE "ingredients" SET "key" = 'dijonMustard' WHERE "key" = 'dijon_mustard';
|
||||
UPDATE "ingredients" SET "key" = 'wholegrainMustard' WHERE "key" = 'wholegrain_mustard';
|
||||
UPDATE "ingredients" SET "key" = 'barbecueSauce' WHERE "key" = 'barbecue_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'tartarSauce' WHERE "key" = 'tartar_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'cocktailSauce' WHERE "key" = 'cocktail_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'bearnaiseSauce' WHERE "key" = 'bearnaise_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'hollandaiseSauce' WHERE "key" = 'hollandaise_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'bechamelSauce' WHERE "key" = 'bechamel_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'teriyakiSauce' WHERE "key" = 'teriyaki_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'ponzuSauce' WHERE "key" = 'ponzu_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'redPesto' WHERE "key" = 'red_pesto';
|
||||
UPDATE "ingredients" SET "key" = 'oysterSauce' WHERE "key" = 'oyster_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'hoisinSauce' WHERE "key" = 'hoisin_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'sweetChiliSauce' WHERE "key" = 'sweet_chili_sauce';
|
||||
UPDATE "ingredients" SET "key" = 'shrimpPaste' WHERE "key" = 'shrimp_paste';
|
||||
UPDATE "ingredients" SET "key" = 'redCurryPaste' WHERE "key" = 'red_curry_paste';
|
||||
UPDATE "ingredients" SET "key" = 'greenCurryPaste' WHERE "key" = 'green_curry_paste';
|
||||
UPDATE "ingredients" SET "key" = 'oliveOil' WHERE "key" = 'olive_oil';
|
||||
UPDATE "ingredients" SET "key" = 'sunflowerOil' WHERE "key" = 'sunflower_oil';
|
||||
UPDATE "ingredients" SET "key" = 'rapeseedOil' WHERE "key" = 'rapeseed_oil';
|
||||
UPDATE "ingredients" SET "key" = 'coconutOil' WHERE "key" = 'coconut_oil';
|
||||
UPDATE "ingredients" SET "key" = 'sesameOil' WHERE "key" = 'sesame_oil';
|
||||
UPDATE "ingredients" SET "key" = 'ciderVinegar' WHERE "key" = 'cider_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'whiteVinegar' WHERE "key" = 'white_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'balsamicVinegar' WHERE "key" = 'balsamic_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'blackOlives' WHERE "key" = 'black_olives';
|
||||
UPDATE "ingredients" SET "key" = 'greenOlives' WHERE "key" = 'green_olives';
|
||||
UPDATE "ingredients" SET "key" = 'whiteWine' WHERE "key" = 'white_wine';
|
||||
UPDATE "ingredients" SET "key" = 'redWine' WHERE "key" = 'red_wine';
|
||||
UPDATE "ingredients" SET "key" = 'roseWine' WHERE "key" = 'rose_wine';
|
||||
UPDATE "ingredients" SET "key" = 'redWineVinegar' WHERE "key" = 'red_wine_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'whiteWineVinegar' WHERE "key" = 'white_wine_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'sherryVinegar' WHERE "key" = 'sherry_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'walnutOil' WHERE "key" = 'walnut_oil';
|
||||
UPDATE "ingredients" SET "key" = 'hazelnutOil' WHERE "key" = 'hazelnut_oil';
|
||||
UPDATE "ingredients" SET "key" = 'peanutOil' WHERE "key" = 'peanut_oil';
|
||||
UPDATE "ingredients" SET "key" = 'chiliOil' WHERE "key" = 'chili_oil';
|
||||
UPDATE "ingredients" SET "key" = 'riceVinegar' WHERE "key" = 'rice_vinegar';
|
||||
UPDATE "ingredients" SET "key" = 'cornOil' WHERE "key" = 'corn_oil';
|
||||
UPDATE "ingredients" SET "key" = 'grapeseedOil' WHERE "key" = 'grapeseed_oil';
|
||||
UPDATE "ingredients" SET "key" = 'soybeanOil' WHERE "key" = 'soybean_oil';
|
||||
UPDATE "ingredients" SET "key" = 'palmOil' WHERE "key" = 'palm_oil';
|
||||
UPDATE "ingredients" SET "key" = 'lemonJuice' WHERE "key" = 'lemon_juice';
|
||||
UPDATE "ingredients" SET "key" = 'limeJuice' WHERE "key" = 'lime_juice';
|
||||
UPDATE "ingredients" SET "key" = 'orangeJuice' WHERE "key" = 'orange_juice';
|
||||
UPDATE "ingredients" SET "key" = 'appleJuice' WHERE "key" = 'apple_juice';
|
||||
UPDATE "ingredients" SET "key" = 'grapeJuice' WHERE "key" = 'grape_juice';
|
||||
UPDATE "ingredients" SET "key" = 'tomatoJuice' WHERE "key" = 'tomato_juice';
|
||||
UPDATE "ingredients" SET "key" = 'cranberryJuice' WHERE "key" = 'cranberry_juice';
|
||||
UPDATE "ingredients" SET "key" = 'portWine' WHERE "key" = 'port_wine';
|
||||
UPDATE "ingredients" SET "key" = 'vinJaune' WHERE "key" = 'vin_jaune';
|
||||
UPDATE "ingredients" SET "key" = 'wheatFlour' WHERE "key" = 'wheat_flour';
|
||||
UPDATE "ingredients" SET "key" = 'wholeWheatFlour' WHERE "key" = 'whole_wheat_flour';
|
||||
UPDATE "ingredients" SET "key" = 'cornFlour' WHERE "key" = 'corn_flour';
|
||||
UPDATE "ingredients" SET "key" = 'buckwheatFlour' WHERE "key" = 'buckwheat_flour';
|
||||
UPDATE "ingredients" SET "key" = 'riceFlour' WHERE "key" = 'rice_flour';
|
||||
UPDATE "ingredients" SET "key" = 'vegetableStockCube' WHERE "key" = 'vegetable_stock_cube';
|
||||
UPDATE "ingredients" SET "key" = 'chickenStockCube' WHERE "key" = 'chicken_stock_cube';
|
||||
UPDATE "ingredients" SET "key" = 'tomatoPaste' WHERE "key" = 'tomato_paste';
|
||||
UPDATE "ingredients" SET "key" = 'tomatoCoulis' WHERE "key" = 'tomato_coulis';
|
||||
UPDATE "ingredients" SET "key" = 'cannedPeeledTomatoes' WHERE "key" = 'canned_peeled_tomatoes';
|
||||
UPDATE "ingredients" SET "key" = 'sunDriedTomatoes' WHERE "key" = 'sun_dried_tomatoes';
|
||||
UPDATE "ingredients" SET "key" = 'vealStock' WHERE "key" = 'veal_stock';
|
||||
UPDATE "ingredients" SET "key" = 'chickenStock' WHERE "key" = 'chicken_stock';
|
||||
UPDATE "ingredients" SET "key" = 'beefStockCube' WHERE "key" = 'beef_stock_cube';
|
||||
UPDATE "ingredients" SET "key" = 'fishStockCube' WHERE "key" = 'fish_stock_cube';
|
||||
UPDATE "ingredients" SET "key" = 'vegetableBroth' WHERE "key" = 'vegetable_broth';
|
||||
UPDATE "ingredients" SET "key" = 'chickenBroth' WHERE "key" = 'chicken_broth';
|
||||
UPDATE "ingredients" SET "key" = 'beefBroth' WHERE "key" = 'beef_broth';
|
||||
UPDATE "ingredients" SET "key" = 'courtBouillon' WHERE "key" = 'court_bouillon';
|
||||
UPDATE "ingredients" SET "key" = 'shellfishBisque' WHERE "key" = 'shellfish_bisque';
|
||||
UPDATE "ingredients" SET "key" = 'tapiocaFlour' WHERE "key" = 'tapioca_flour';
|
||||
UPDATE "ingredients" SET "key" = 'masaHarina' WHERE "key" = 'masa_harina';
|
||||
UPDATE "ingredients" SET "key" = 'sparklingWater' WHERE "key" = 'sparkling_water';
|
||||
UPDATE "ingredients" SET "key" = 'orangeBlossomWater' WHERE "key" = 'orange_blossom_water';
|
||||
UPDATE "ingredients" SET "key" = 'roseWater' WHERE "key" = 'rose_water';
|
||||
UPDATE "ingredients" SET "key" = 'fishFumet' WHERE "key" = 'fish_fumet';
|
||||
UPDATE "ingredients" SET "key" = 'bakersYeast' WHERE "key" = 'bakers_yeast';
|
||||
UPDATE "ingredients" SET "key" = 'bakingPowder' WHERE "key" = 'baking_powder';
|
||||
UPDATE "ingredients" SET "key" = 'lupinFlour' WHERE "key" = 'lupin_flour';
|
||||
UPDATE "ingredients" SET "key" = 'bakingSoda' WHERE "key" = 'baking_soda';
|
||||
UPDATE "ingredients" SET "key" = 'potatoStarch' WHERE "key" = 'potato_starch';
|
||||
UPDATE "ingredients" SET "key" = 'mapleSyrup' WHERE "key" = 'maple_syrup';
|
||||
UPDATE "ingredients" SET "key" = 'brownSugar' WHERE "key" = 'brown_sugar';
|
||||
UPDATE "ingredients" SET "key" = 'powderedSugar' WHERE "key" = 'powdered_sugar';
|
||||
UPDATE "ingredients" SET "key" = 'demeraraSugar' WHERE "key" = 'demerara_sugar';
|
||||
UPDATE "ingredients" SET "key" = 'darkChocolate' WHERE "key" = 'dark_chocolate';
|
||||
UPDATE "ingredients" SET "key" = 'milkChocolate' WHERE "key" = 'milk_chocolate';
|
||||
UPDATE "ingredients" SET "key" = 'whiteChocolate' WHERE "key" = 'white_chocolate';
|
||||
UPDATE "ingredients" SET "key" = 'chocolateChips' WHERE "key" = 'chocolate_chips';
|
||||
UPDATE "ingredients" SET "key" = 'cocoaPowder' WHERE "key" = 'cocoa_powder';
|
||||
UPDATE "ingredients" SET "key" = 'vanillaExtract' WHERE "key" = 'vanilla_extract';
|
||||
UPDATE "ingredients" SET "key" = 'palmSugar' WHERE "key" = 'palm_sugar';
|
||||
UPDATE "ingredients" SET "key" = 'caneSyrup' WHERE "key" = 'cane_syrup';
|
||||
|
||||
-- IngredientCategory/IngredientSubcategory enum rename — same
|
||||
-- add-with-default-then-swap approach as
|
||||
-- 20260818113250_ingredient_taxonomy_rework/migration.sql: the old and
|
||||
-- new enums share no values, so a direct cast isn't possible. Existing
|
||||
-- rows land on the placeholder default; seedReferenceData() (runs on
|
||||
-- every container start, see apps/api/Dockerfile) corrects every row's
|
||||
-- real category/subcategory immediately after.
|
||||
CREATE TYPE "IngredientCategory_new" AS ENUM ('freshProduce', 'meatAndSeafood', 'dryGoods', 'bakery', 'dairyAndCheese', 'condimentsAndSpices', 'cookingEssentials');
|
||||
CREATE TYPE "IngredientSubcategory_new" AS ENUM ('vegetables', 'fruits', 'freshHerbs', 'meats', 'poultry', 'fish', 'shellfish', 'starches', 'legumes', 'nutsAndSeeds', 'other', 'breads', 'rawDough', 'dairy', 'eggs', 'plantBasedAlternatives', 'spices', 'sauces', 'seasonings', 'bases', 'thickeners', 'sugars');
|
||||
ALTER TABLE "ingredients" ADD COLUMN "category_new" "IngredientCategory_new" NOT NULL DEFAULT 'dryGoods';
|
||||
ALTER TABLE "ingredients" ADD COLUMN "subcategory_new" "IngredientSubcategory_new" NOT NULL DEFAULT 'other';
|
||||
ALTER TABLE "ingredients" DROP COLUMN "category";
|
||||
ALTER TABLE "ingredients" DROP COLUMN "subcategory";
|
||||
ALTER TABLE "ingredients" RENAME COLUMN "category_new" TO "category";
|
||||
ALTER TABLE "ingredients" RENAME COLUMN "subcategory_new" TO "subcategory";
|
||||
DROP TYPE "IngredientCategory";
|
||||
DROP TYPE "IngredientSubcategory";
|
||||
ALTER TYPE "IngredientCategory_new" RENAME TO "IngredientCategory";
|
||||
ALTER TYPE "IngredientSubcategory_new" RENAME TO "IngredientSubcategory";
|
||||
|
|
@ -37,11 +37,11 @@ model House {
|
|||
/// `key` is `@unique` — not in the original spec doc, added so the seed
|
||||
/// script (prisma/seed.ts) can `upsert` by key and stay idempotent/safe to
|
||||
/// re-run, and so two reference rows can never silently duplicate the same
|
||||
/// regime. A stable slug (e.g. `"vegetarien"`), not the display label —
|
||||
/// the label itself lives in `apps/web`'s `locales/fr/translation.json`
|
||||
/// under `catalog.diets.<key>` (see `reference-seed-data.ts`'s `DIETS` and
|
||||
/// `utils/slugify.ts`), so it can be edited/translated without ever
|
||||
/// touching this column or the rows that reference it by id.
|
||||
/// regime. A stable English camelCase uid (e.g. `"vegetarian"`), not the
|
||||
/// display label — the label itself lives in `apps/web`'s
|
||||
/// `locales/fr/translation.json` under `catalog.diets.<key>` (see
|
||||
/// `reference-seed-data.ts`'s `DIETS`), so it can be edited/translated
|
||||
/// without ever touching this column or the rows that reference it by id.
|
||||
model Diet {
|
||||
id Int @id @default(autoincrement())
|
||||
key String @unique
|
||||
|
|
@ -259,8 +259,6 @@ model Recipe {
|
|||
planningItems PlanningItem[]
|
||||
favoritedBy RecipeFavorite[]
|
||||
diets RecipeDiet[]
|
||||
/// Ingredients for which this recipe is offered as a make-it-yourself alternative.
|
||||
alternateFor Ingredient[] @relation("IngredientAlternateRecipe")
|
||||
|
||||
@@map("recipe")
|
||||
}
|
||||
|
|
@ -315,30 +313,28 @@ model RecipeDiet {
|
|||
/// `reference-seed-data.ts`'s `INGREDIENT_GROUPS` keys exactly — that file
|
||||
/// is the single source of truth for which ingredient belongs to which
|
||||
/// (category, subcategory) pair, these enums just give it type-safe
|
||||
/// columns to live in. `@default(EPICERIE_SECHE)` exists only so this
|
||||
/// columns to live in. `@default(dryGoods)` 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 {
|
||||
/// 🥦 Légumes, fruits, herbes fraîches.
|
||||
PRODUITS_FRAIS
|
||||
/// 🥩 Viandes, volailles, poissons, crustacés & fruits de mer.
|
||||
BOUCHERIE_POISSONNERIE
|
||||
/// 🥫 Féculents, légumineuses, graines & fruits secs, et le reste des
|
||||
/// produits secs/en conserve qui ne rentre dans aucune autre case
|
||||
/// (algues séchées, champignons séchés…).
|
||||
EPICERIE_SECHE
|
||||
/// 🍞 Pains et pâtes à cuire (crues, à enfourner).
|
||||
BOULANGERIE
|
||||
/// 🧈 Produits laitiers, œufs, alternatives végétales (laits végétaux,
|
||||
/// tofu…).
|
||||
CREMERIE_FROMAGE
|
||||
/// 🧂 Épices, sauces, assaisonnements (huiles, vinaigres, alcools de
|
||||
/// cuisine…).
|
||||
CONDIMENTS_EPICES
|
||||
/// 🍳 Bases de préparation (farines, bouillons, eau), épaississants
|
||||
/// (levures, fécules, gélatine), sucres.
|
||||
AIDES_CULINAIRES
|
||||
/// 🥦 Vegetables, fruits, fresh herbs.
|
||||
freshProduce
|
||||
/// 🥩 Meats, poultry, fish, shellfish & seafood.
|
||||
meatAndSeafood
|
||||
/// 🥫 Starches, legumes, nuts & seeds, and the rest of the dry/tinned
|
||||
/// goods that don't fit any other bucket (dried seaweed, dried
|
||||
/// mushrooms…).
|
||||
dryGoods
|
||||
/// 🍞 Breads and raw dough (uncooked, ready to bake).
|
||||
bakery
|
||||
/// 🧈 Dairy, eggs, plant-based alternatives (plant milks, tofu…).
|
||||
dairyAndCheese
|
||||
/// 🧂 Spices, sauces, seasonings (oils, vinegars, cooking alcohols…).
|
||||
condimentsAndSpices
|
||||
/// 🍳 Prep bases (flours, stocks, water), thickeners (yeasts, starches,
|
||||
/// gelatin), sugars.
|
||||
cookingEssentials
|
||||
}
|
||||
|
||||
/// Finer-grained rack within one {@link IngredientCategory} aisle — see
|
||||
|
|
@ -347,50 +343,50 @@ enum IngredientCategory {
|
|||
/// `reference-seed-data.ts`'s `INGREDIENT_GROUPS`, not enforced at the
|
||||
/// database level — Postgres enums can't express that relationship, same
|
||||
/// tradeoff already accepted for `IngredientCategory` itself).
|
||||
/// `@default(AUTRES)` — same NOT-NULL-migration-safety-net reasoning as
|
||||
/// `@default(other)` — same NOT-NULL-migration-safety-net reasoning as
|
||||
/// `IngredientCategory`'s default, never the intended value for a real row.
|
||||
enum IngredientSubcategory {
|
||||
// --- Produits frais ------------------------------------------------------
|
||||
LEGUMES
|
||||
FRUITS
|
||||
HERBES_FRAICHES
|
||||
// --- Boucherie & poissonnerie ---------------------------------------------
|
||||
VIANDES
|
||||
VOLAILLES
|
||||
POISSONS
|
||||
CRUSTACES_FRUITS_DE_MER
|
||||
// --- Épicerie sèche --------------------------------------------------------
|
||||
FECULENTS
|
||||
LEGUMINEUSES
|
||||
GRAINES_FRUITS_SECS
|
||||
// --- freshProduce ----------------------------------------------------------
|
||||
vegetables
|
||||
fruits
|
||||
freshHerbs
|
||||
// --- meatAndSeafood ----------------------------------------------------------
|
||||
meats
|
||||
poultry
|
||||
fish
|
||||
shellfish
|
||||
// --- dryGoods ----------------------------------------------------------------
|
||||
starches
|
||||
legumes
|
||||
nutsAndSeeds
|
||||
/// Catch-all for dried/tinned pantry items that don't fit the three
|
||||
/// subcategories above — dried seaweed, dried mushrooms, tinned bamboo
|
||||
/// shoots/water chestnuts…
|
||||
AUTRES
|
||||
// --- Boulangerie -------------------------------------------------------
|
||||
PAINS
|
||||
other
|
||||
// --- bakery --------------------------------------------------------------
|
||||
breads
|
||||
/// Raw, uncooked doughs meant to be baked (puff pastry, shortcrust…) —
|
||||
/// distinct from `PAINS` (already-baked bread).
|
||||
PATES_A_CUIRE
|
||||
// --- Crémerie & fromage --------------------------------------------------
|
||||
PRODUITS_LAITIERS
|
||||
OEUFS
|
||||
/// distinct from `breads` (already-baked bread).
|
||||
rawDough
|
||||
// --- dairyAndCheese ------------------------------------------------------
|
||||
dairy
|
||||
eggs
|
||||
/// Plant-based dairy/meat substitutes — coconut/almond/oat "milk", tofu.
|
||||
ALTERNATIVES
|
||||
// --- Condiments & épices -------------------------------------------------
|
||||
EPICES
|
||||
SAUCES
|
||||
plantBasedAlternatives
|
||||
// --- condimentsAndSpices ---------------------------------------------------
|
||||
spices
|
||||
sauces
|
||||
/// Oils, vinegars, citrus juices, cooking alcohols/wines — liquids that
|
||||
/// season rather than form the base of a dish.
|
||||
ASSAISONNEMENTS
|
||||
// --- Aides culinaires ----------------------------------------------------
|
||||
seasonings
|
||||
// --- cookingEssentials -----------------------------------------------------
|
||||
/// Flours, stocks/broths, canned tomato bases, water — the literal base
|
||||
/// a recipe is built on.
|
||||
BASES
|
||||
bases
|
||||
/// Leavening/gelling/thickening agents — yeast, baking soda, cornstarch,
|
||||
/// gelatin.
|
||||
EPAISSISSANTS
|
||||
SUCRES
|
||||
thickeners
|
||||
sugars
|
||||
}
|
||||
|
||||
/// Generic pictogram *type* for an ingredient — not in the original spec
|
||||
|
|
@ -432,20 +428,29 @@ enum IngredientIcon {
|
|||
}
|
||||
|
||||
model Ingredient {
|
||||
id Int @id @default(autoincrement())
|
||||
key String @unique
|
||||
icon IngredientIcon @default(JAR)
|
||||
category IngredientCategory @default(EPICERIE_SECHE)
|
||||
subcategory IngredientSubcategory @default(AUTRES)
|
||||
alternateRecipeId Int? @map("alternate_recipe")
|
||||
id Int @id @default(autoincrement())
|
||||
key String @unique
|
||||
icon IngredientIcon @default(JAR)
|
||||
category IngredientCategory @default(dryGoods)
|
||||
subcategory IngredientSubcategory @default(other)
|
||||
/// Whether this ingredient is reasonably makeable at home (a burger bun,
|
||||
/// a béchamel) rather than something you'd only ever buy (a raw
|
||||
/// vegetable, a specific cut of meat) — surfaced in the recipe form as a
|
||||
/// badge/link nudging the author to go check the recipe catalog for a
|
||||
/// "make it yourself" recipe (see `apps/web`'s `IngredientRow`/
|
||||
/// `IngredientPicker`). Deliberately just a flag, not a link to a
|
||||
/// specific recipe — replaces an earlier, never-wired-up
|
||||
/// `alternateRecipeId` FK (product decision discussed in chat: no
|
||||
/// ingredient↔recipe linking in the database, the UI only pre-fills the
|
||||
/// catalog's own search with this ingredient's name).
|
||||
reproducible Boolean @default(false)
|
||||
|
||||
alternateRecipe Recipe? @relation("IngredientAlternateRecipe", fields: [alternateRecipeId], references: [id], onDelete: SetNull)
|
||||
recipes RecipeIngredient[]
|
||||
allergies IngredientAllergy[]
|
||||
recipes RecipeIngredient[]
|
||||
allergies IngredientAllergy[]
|
||||
/// 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[]
|
||||
diets IngredientDiet[]
|
||||
|
||||
@@map("ingredients")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* One-off generator, run by hand whenever the catalog's reference data
|
||||
* changes (a new ingredient/diet/allergen added to
|
||||
* `db/reference-seed-data.ts`, or an English key corrected in
|
||||
* `catalog-en-keys.ts`): regenerates
|
||||
* `apps/web/src/locales/fr/translation.json`'s
|
||||
* `catalog.{diets,allergens,ingredients}` sections (English key -> French
|
||||
* label), merged in without touching the rest of the file.
|
||||
*
|
||||
* Doesn't touch the database — a brand new diet/allergen/ingredient is
|
||||
* created fresh by `seedReferenceData`'s normal create path (see
|
||||
* `reference-seed-data.ts`), no backfill needed. Renaming an *existing*
|
||||
* item's English key in `catalog-en-keys.ts` does need a one-off migration
|
||||
* (`UPDATE ... SET key = ...`, keyed by the *old* key value) written by
|
||||
* hand for that occasion — see
|
||||
* `prisma/migrations/20260818193000_catalog_keys_to_english/` for the shape
|
||||
* one looks like.
|
||||
*
|
||||
* Never imported by the app itself — a dev-time tool, run via
|
||||
* `tsx scripts/generate-catalog-i18n.ts`.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||
import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
|
||||
|
||||
const here = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
function toKeyLabelMap(labels: string[]): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const label of labels) {
|
||||
map[getEnglishKey(label)] = label;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const diets = toKeyLabelMap(DIETS);
|
||||
const allergens = toKeyLabelMap(ALLERGENS.map((a) => a.name));
|
||||
const ingredients = toKeyLabelMap(INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)));
|
||||
|
||||
console.log(
|
||||
`diets: ${Object.keys(diets).length}, allergens: ${Object.keys(allergens).length}, ingredients: ${Object.keys(ingredients).length}`,
|
||||
);
|
||||
|
||||
const localePath = `${here}../../web/src/locales/fr/translation.json`;
|
||||
const locale = JSON.parse(readFileSync(localePath, "utf8"));
|
||||
locale.catalog = { diets, allergens, ingredients };
|
||||
writeFileSync(localePath, `${JSON.stringify(locale, null, 2)}\n`);
|
||||
console.log(`wrote ${localePath}`);
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||
/**
|
||||
* One-off validation, run by hand: checks that `catalog-en-keys.ts` has an
|
||||
* entry for every diet/allergen/ingredient currently in
|
||||
* `reference-seed-data.ts`, and that the resulting English keys are unique
|
||||
* within each table. Not part of the app or the seed itself — just a
|
||||
* pre-flight check while building/editing the dictionary by hand.
|
||||
*/
|
||||
import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
|
||||
|
||||
function check(label: string, names: string[]) {
|
||||
const keys = new Map<string, string>();
|
||||
const missing: string[] = [];
|
||||
const duplicates: string[] = [];
|
||||
for (const name of names) {
|
||||
let key: string;
|
||||
try {
|
||||
key = getEnglishKey(name);
|
||||
} catch {
|
||||
missing.push(name);
|
||||
continue;
|
||||
}
|
||||
const existing = keys.get(key);
|
||||
if (existing !== undefined && existing !== name) {
|
||||
duplicates.push(`"${existing}" and "${name}" both map to "${key}"`);
|
||||
}
|
||||
keys.set(key, name);
|
||||
}
|
||||
console.log(`${label}: ${names.length} names, ${keys.size} unique keys`);
|
||||
if (missing.length > 0) {
|
||||
console.log(` MISSING (${missing.length}):`, missing);
|
||||
}
|
||||
if (duplicates.length > 0) {
|
||||
console.log(` DUPLICATES (${duplicates.length}):`, duplicates);
|
||||
}
|
||||
}
|
||||
|
||||
check("Diets", DIETS);
|
||||
check(
|
||||
"Allergens",
|
||||
ALLERGENS.map((a) => a.name),
|
||||
);
|
||||
check(
|
||||
"Ingredients",
|
||||
INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)),
|
||||
);
|
||||
|
|
@ -1,666 +0,0 @@
|
|||
/**
|
||||
* English key for every catalog reference label — `Diet`/`Category`
|
||||
* (allergens)/`Ingredient` rows must carry a stable, storage-safe `key`
|
||||
* that is itself English, independent of whatever language the *seed's*
|
||||
* authoring label (`DIETS`/`ALLERGENS`/`INGREDIENT_GROUPS` in
|
||||
* `reference-seed-data.ts`, currently French) happens to be in — a French
|
||||
* `key` would tie the identifier to the one language it's meant to be
|
||||
* decoupled from (see `utils/slugify.ts`'s doc comment and `apps/web`'s
|
||||
* `locales/fr/translation.json` `catalog.*` namespace, which resolves the
|
||||
* *display* label from this same key).
|
||||
*
|
||||
* Hand-assigned (not machine-translated) — an English label is chosen once
|
||||
* and never changes, exactly like the key it produces (via {@link
|
||||
* slugify}). Keyed by the exact French authoring label so
|
||||
* `reference-seed-data.ts` and `scripts/generate-catalog-i18n.ts` can look
|
||||
* a row's key up by the same string they already have in hand.
|
||||
*
|
||||
* `getEnglishKey` throws on a missing entry rather than falling back to
|
||||
* slugifying the French label — a silently-French key defeats the point,
|
||||
* so a newly-added diet/allergen/ingredient must get an entry here before
|
||||
* it can seed.
|
||||
*/
|
||||
import { slugify } from "../utils/slugify.js";
|
||||
|
||||
const DIET_KEYS: Record<string, string> = {
|
||||
Omnivore: "omnivore",
|
||||
Végétarien: "vegetarian",
|
||||
Végan: "vegan",
|
||||
Pescétarien: "pescatarian",
|
||||
"Sans gluten": "gluten_free",
|
||||
};
|
||||
|
||||
const ALLERGEN_KEYS: Record<string, string> = {
|
||||
Gluten: "gluten",
|
||||
Crustacés: "crustaceans",
|
||||
Œufs: "eggs",
|
||||
Poissons: "fish",
|
||||
Arachides: "peanuts",
|
||||
Soja: "soy",
|
||||
Lait: "milk",
|
||||
"Fruits à coque": "tree_nuts",
|
||||
Céleri: "celery",
|
||||
Moutarde: "mustard",
|
||||
"Graines de sésame": "sesame_seeds",
|
||||
Sulfites: "sulfites",
|
||||
Lupin: "lupin",
|
||||
Mollusques: "molluscs",
|
||||
};
|
||||
|
||||
// Grouped by (category, subcategory), mirroring `INGREDIENT_GROUPS` in
|
||||
// reference-seed-data.ts, purely so a translator can find/check an entry
|
||||
// against its source group — this is one flat lookup table at runtime.
|
||||
const INGREDIENT_KEYS: Record<string, string> = {
|
||||
// --- Produits frais / Légumes ---------------------------------------
|
||||
Tomate: "tomato",
|
||||
Oignon: "onion",
|
||||
Échalote: "shallot",
|
||||
Ail: "garlic",
|
||||
Carotte: "carrot",
|
||||
Courgette: "zucchini",
|
||||
Concombre: "cucumber",
|
||||
Cornichons: "gherkins",
|
||||
Poivron: "bell_pepper",
|
||||
Champignon: "mushroom",
|
||||
Cèpes: "porcini",
|
||||
Aubergine: "eggplant",
|
||||
Brocoli: "broccoli",
|
||||
"Chou-fleur": "cauliflower",
|
||||
"Chou blanc": "white_cabbage",
|
||||
"Chou rouge": "red_cabbage",
|
||||
"Chou de Bruxelles": "brussels_sprouts",
|
||||
Épinard: "spinach",
|
||||
Blette: "swiss_chard",
|
||||
Salade: "lettuce",
|
||||
Roquette: "arugula",
|
||||
Cresson: "watercress",
|
||||
Poireau: "leek",
|
||||
Radis: "radish",
|
||||
Betterave: "beetroot",
|
||||
Navet: "turnip",
|
||||
Panais: "parsnip",
|
||||
"Haricot vert": "green_bean",
|
||||
"Petit pois": "pea",
|
||||
Maïs: "corn",
|
||||
Artichaut: "artichoke",
|
||||
Fenouil: "fennel",
|
||||
Endive: "endive",
|
||||
Potiron: "pumpkin",
|
||||
Butternut: "butternut_squash",
|
||||
Asperge: "asparagus",
|
||||
Avocat: "avocado",
|
||||
"Pomme de terre": "potato",
|
||||
"Patate douce": "sweet_potato",
|
||||
"Tomates cerises": "cherry_tomato",
|
||||
"Pak-choï": "bok_choy",
|
||||
"Germes de soja": "soybean_sprouts",
|
||||
Shiitake: "shiitake",
|
||||
Daikon: "daikon",
|
||||
"Piment vert frais": "fresh_green_chili",
|
||||
Cardon: "cardoon",
|
||||
"Chicorée rouge": "radicchio",
|
||||
"Chou romanesco": "romanesco",
|
||||
"Chou-rave": "kohlrabi",
|
||||
"Chou chinois": "napa_cabbage",
|
||||
"Céleri-rave": "celeriac",
|
||||
Gombo: "okra",
|
||||
"Oignon nouveau": "spring_onion",
|
||||
Potimarron: "red_kuri_squash",
|
||||
Rutabaga: "rutabaga",
|
||||
Salicorne: "samphire",
|
||||
Salsifis: "salsify",
|
||||
Mâche: "lambs_lettuce",
|
||||
Scarole: "escarole",
|
||||
|
||||
// --- Produits frais / Fruits -----------------------------------------
|
||||
Citron: "lemon",
|
||||
"Citron vert": "lime",
|
||||
Pomme: "apple",
|
||||
Poire: "pear",
|
||||
Banane: "banana",
|
||||
Orange: "orange",
|
||||
Clémentine: "clementine",
|
||||
Pamplemousse: "grapefruit",
|
||||
Fraise: "strawberry",
|
||||
Framboise: "raspberry",
|
||||
Myrtille: "blueberry",
|
||||
Mûre: "blackberry",
|
||||
Cerise: "cherry",
|
||||
Abricot: "apricot",
|
||||
Pêche: "peach",
|
||||
Prune: "plum",
|
||||
Raisin: "grape",
|
||||
Melon: "melon",
|
||||
Pastèque: "watermelon",
|
||||
Ananas: "pineapple",
|
||||
Mangue: "mango",
|
||||
Kiwi: "kiwi",
|
||||
Figue: "fig",
|
||||
Datte: "date",
|
||||
Litchi: "lychee",
|
||||
Grenade: "pomegranate",
|
||||
Rhubarbe: "rhubarb",
|
||||
Coing: "quince",
|
||||
Cassis: "blackcurrant",
|
||||
Canneberge: "cranberry",
|
||||
Groseille: "redcurrant",
|
||||
Kaki: "persimmon",
|
||||
Nectarine: "nectarine",
|
||||
Tamarin: "tamarind",
|
||||
|
||||
// --- Produits frais / Herbes fraîches ---------------------------------
|
||||
Basilic: "basil",
|
||||
Persil: "parsley",
|
||||
Thym: "thyme",
|
||||
Romarin: "rosemary",
|
||||
Laurier: "bay_leaf",
|
||||
Ciboulette: "chives",
|
||||
"Coriandre fraîche": "fresh_cilantro",
|
||||
Menthe: "mint",
|
||||
Origan: "oregano",
|
||||
Aneth: "dill",
|
||||
Estragon: "tarragon",
|
||||
Sarriette: "savory",
|
||||
Marjolaine: "marjoram",
|
||||
Sauge: "sage",
|
||||
Cerfeuil: "chervil",
|
||||
Gingembre: "ginger",
|
||||
Citronnelle: "lemongrass",
|
||||
Combava: "kaffir_lime",
|
||||
|
||||
// --- Boucherie & poissonnerie / Viandes -------------------------------
|
||||
Lapin: "rabbit",
|
||||
"Bœuf haché": "ground_beef",
|
||||
"Steak de bœuf": "beef_steak",
|
||||
"Rôti de bœuf": "beef_roast",
|
||||
"Escalope de veau": "veal_cutlet",
|
||||
"Filet mignon de porc": "pork_tenderloin",
|
||||
"Côte de porc": "pork_chop",
|
||||
Agneau: "lamb",
|
||||
"Gigot d'agneau": "leg_of_lamb",
|
||||
Lardons: "bacon_lardons",
|
||||
Bacon: "bacon",
|
||||
"Jambon blanc": "ham",
|
||||
"Jambon cru": "cured_ham",
|
||||
Saucisse: "sausage",
|
||||
Chorizo: "chorizo",
|
||||
Merguez: "merguez",
|
||||
Prosciutto: "prosciutto",
|
||||
Pancetta: "pancetta",
|
||||
Mortadelle: "mortadella",
|
||||
Salami: "salami",
|
||||
Andouille: "andouille",
|
||||
Andouillette: "andouillette",
|
||||
"Boudin blanc": "white_pudding",
|
||||
"Boudin noir": "black_pudding",
|
||||
Cervelas: "cervelat",
|
||||
Rillettes: "rillettes",
|
||||
"Saucisson sec": "dry_cured_sausage",
|
||||
"Jambon de Bayonne": "bayonne_ham",
|
||||
Coppa: "coppa",
|
||||
"Rosette (saucisson)": "rosette_sausage",
|
||||
"Foie de veau": "veal_liver",
|
||||
"Rognons de veau": "veal_kidneys",
|
||||
"Cervelle de veau": "veal_brain",
|
||||
"Ris de veau": "veal_sweetbread",
|
||||
"Langue de bœuf": "beef_tongue",
|
||||
Tripes: "tripe",
|
||||
Cerf: "venison",
|
||||
Chevreuil: "roe_deer",
|
||||
Sanglier: "wild_boar",
|
||||
Cheval: "horse_meat",
|
||||
"Cœur de bœuf": "beef_heart",
|
||||
"Foie gras": "foie_gras",
|
||||
"Museau de bœuf": "beef_muzzle",
|
||||
"Viande des Grisons": "grisons_dried_beef",
|
||||
|
||||
// --- Boucherie & poissonnerie / Volailles -----------------------------
|
||||
Poulet: "chicken",
|
||||
Dinde: "turkey",
|
||||
Canard: "duck",
|
||||
"Magret de canard": "duck_breast",
|
||||
Caille: "quail",
|
||||
Pintade: "guinea_fowl",
|
||||
Oie: "goose",
|
||||
"Foie de volaille": "poultry_liver",
|
||||
Chapon: "capon",
|
||||
Pigeon: "pigeon",
|
||||
Faisan: "pheasant",
|
||||
|
||||
// --- Boucherie & poissonnerie / Poissons -------------------------------
|
||||
Saumon: "salmon",
|
||||
Thon: "tuna",
|
||||
Cabillaud: "cod",
|
||||
Truite: "trout",
|
||||
Sardine: "sardine",
|
||||
Anchois: "anchovy",
|
||||
Merlan: "whiting",
|
||||
Surimi: "surimi",
|
||||
"Bar (loup de mer)": "sea_bass",
|
||||
Dorade: "sea_bream",
|
||||
Sole: "sole",
|
||||
Turbot: "turbot",
|
||||
Merlu: "hake",
|
||||
Colin: "pollock",
|
||||
"Lieu noir": "saithe",
|
||||
Églefin: "haddock",
|
||||
Maquereau: "mackerel",
|
||||
Hareng: "herring",
|
||||
Rouget: "red_mullet",
|
||||
Raie: "skate",
|
||||
Lotte: "monkfish",
|
||||
Flétan: "halibut",
|
||||
Espadon: "swordfish",
|
||||
Carpe: "carp",
|
||||
Brochet: "pike",
|
||||
Perche: "perch",
|
||||
Tilapia: "tilapia",
|
||||
Panga: "pangasius",
|
||||
"Saumon fumé": "smoked_salmon",
|
||||
"Poisson séché": "dried_fish",
|
||||
Anguille: "eel",
|
||||
"Carrelet (ou plie)": "plaice",
|
||||
Morue: "salt_cod",
|
||||
Limande: "lemon_sole",
|
||||
Rascasse: "scorpionfish",
|
||||
|
||||
// --- Boucherie & poissonnerie / Crustacés & fruits de mer -------------
|
||||
Crevettes: "shrimp",
|
||||
Langoustines: "langoustine",
|
||||
Homard: "lobster",
|
||||
Crabe: "crab",
|
||||
Langouste: "spiny_lobster",
|
||||
Moules: "mussels",
|
||||
Huîtres: "oysters",
|
||||
"Saint-Jacques": "scallops",
|
||||
Calamar: "squid",
|
||||
Poulpe: "octopus",
|
||||
Palourdes: "clams",
|
||||
Bulots: "whelks",
|
||||
"Araignée de mer": "spider_crab",
|
||||
Bigorneau: "periwinkle",
|
||||
Écrevisse: "crayfish",
|
||||
"Crevette grise": "grey_shrimp",
|
||||
Coque: "cockle",
|
||||
Escargot: "snail",
|
||||
Seiche: "cuttlefish",
|
||||
|
||||
// --- Épicerie sèche / Féculents ----------------------------------------
|
||||
Semoule: "semolina",
|
||||
Couscous: "couscous",
|
||||
Boulgour: "bulgur",
|
||||
Polenta: "polenta",
|
||||
Quinoa: "quinoa",
|
||||
Pâtes: "pasta",
|
||||
"Pâtes complètes": "whole_wheat_pasta",
|
||||
Riz: "rice",
|
||||
"Riz basmati": "basmati_rice",
|
||||
"Riz complet": "brown_rice",
|
||||
"Flocons d'avoine": "oats",
|
||||
Spaghetti: "spaghetti",
|
||||
Penne: "penne",
|
||||
Tagliatelles: "tagliatelle",
|
||||
"Lasagnes (feuilles)": "lasagna_sheets",
|
||||
Gnocchi: "gnocchi",
|
||||
"Riz arborio": "arborio_rice",
|
||||
"Nouilles de riz": "rice_noodles",
|
||||
"Nouilles udon": "udon_noodles",
|
||||
"Nouilles soba": "soba_noodles",
|
||||
"Nouilles chinoises": "chinese_noodles",
|
||||
"Vermicelles de riz": "rice_vermicelli",
|
||||
"Vermicelles de soja": "soy_vermicelli",
|
||||
"Riz gluant": "sticky_rice",
|
||||
"Riz à sushi": "sushi_rice",
|
||||
"Riz jasmin": "jasmine_rice",
|
||||
|
||||
// --- Épicerie sèche / Légumineuses --------------------------------------
|
||||
"Lentilles vertes": "green_lentils",
|
||||
"Lentilles corail": "red_lentils",
|
||||
"Pois chiches": "chickpeas",
|
||||
"Haricots blancs": "white_beans",
|
||||
"Haricots rouges": "kidney_beans",
|
||||
"Haricots noirs": "black_beans",
|
||||
"Pois cassés": "split_peas",
|
||||
Fèves: "fava_beans",
|
||||
Edamame: "edamame",
|
||||
"Haricots pinto": "pinto_beans",
|
||||
"Haricots flageolets": "flageolet_beans",
|
||||
"Lentilles blondes": "golden_lentils",
|
||||
|
||||
// --- Épicerie sèche / Graines & fruits secs ----------------------------
|
||||
Cacahuètes: "peanuts_shelled",
|
||||
Amandes: "almonds",
|
||||
Noix: "walnuts",
|
||||
Noisettes: "hazelnuts",
|
||||
"Noix de cajou": "cashews",
|
||||
Pistaches: "pistachios",
|
||||
"Noix de pécan": "pecans",
|
||||
"Poudre d'amande": "almond_powder",
|
||||
"Pignons de pin": "pine_nuts",
|
||||
"Graines de tournesol": "sunflower_seeds",
|
||||
"Graines de courge": "pumpkin_seeds",
|
||||
"Noix de coco râpée": "shredded_coconut",
|
||||
"Raisins secs": "raisins",
|
||||
Pruneaux: "prunes",
|
||||
"Abricots secs": "dried_apricots",
|
||||
|
||||
// --- Épicerie sèche / Autres --------------------------------------------
|
||||
"Champignons noirs": "black_mushrooms",
|
||||
"Algue nori": "nori_seaweed",
|
||||
"Algue wakamé": "wakame_seaweed",
|
||||
"Algue kombu": "kombu_seaweed",
|
||||
"Pousses de bambou": "bamboo_shoots",
|
||||
"Châtaignes d'eau": "water_chestnuts",
|
||||
|
||||
// --- Boulangerie / Pains -------------------------------------------------
|
||||
Pain: "bread",
|
||||
"Pain de mie": "sandwich_bread",
|
||||
"Pain complet": "whole_wheat_bread",
|
||||
Baguette: "baguette",
|
||||
"Pain de seigle": "rye_bread",
|
||||
Chapelure: "breadcrumbs",
|
||||
"Pain à burger": "burger_bun",
|
||||
"Pain brioché": "brioche_bun",
|
||||
"Pain à hot-dog": "hot_dog_bun",
|
||||
"Pain pita": "pita_bread",
|
||||
"Pain bagel": "bagel",
|
||||
Naan: "naan",
|
||||
"Pain wrap": "wrap_bread",
|
||||
"Pain viennois": "viennese_bread",
|
||||
"Pain de campagne": "country_bread",
|
||||
"Pain aux céréales": "multigrain_bread",
|
||||
"Petit pain": "bread_roll",
|
||||
"Pain suédois": "swedish_bread",
|
||||
"Pain sans gluten": "gluten_free_bread",
|
||||
Biscotte: "rusk",
|
||||
Croûtons: "croutons",
|
||||
Focaccia: "focaccia",
|
||||
Ciabatta: "ciabatta",
|
||||
"Tortilla de maïs": "corn_tortilla",
|
||||
"Tortilla de blé": "wheat_tortilla",
|
||||
Gressin: "breadstick",
|
||||
|
||||
// --- Boulangerie / Pâtes à cuire -----------------------------------------
|
||||
"Pâte feuilletée": "puff_pastry",
|
||||
"Pâte brisée": "shortcrust_pastry",
|
||||
"Pâte à pizza": "pizza_dough",
|
||||
"Pâte à tarte sablée": "sweet_shortcrust_pastry",
|
||||
|
||||
// --- Crémerie & fromage / Produits laitiers ------------------------------
|
||||
Lait: "milk",
|
||||
Beurre: "butter",
|
||||
"Crème fraîche": "creme_fraiche",
|
||||
"Crème liquide": "liquid_cream",
|
||||
Fromage: "cheese",
|
||||
Emmental: "emmental",
|
||||
Gruyère: "gruyere",
|
||||
Parmesan: "parmesan",
|
||||
Mozzarella: "mozzarella",
|
||||
"Chèvre (fromage)": "goat_cheese",
|
||||
Feta: "feta",
|
||||
Comté: "comte",
|
||||
"Fromage blanc": "fromage_blanc",
|
||||
Mascarpone: "mascarpone",
|
||||
Yaourt: "yogurt",
|
||||
Burrata: "burrata",
|
||||
Ricotta: "ricotta",
|
||||
Pecorino: "pecorino",
|
||||
Gorgonzola: "gorgonzola",
|
||||
Cheddar: "cheddar",
|
||||
Brie: "brie",
|
||||
Camembert: "camembert",
|
||||
Roquefort: "roquefort",
|
||||
Munster: "munster",
|
||||
Reblochon: "reblochon",
|
||||
Cantal: "cantal",
|
||||
Beaufort: "beaufort",
|
||||
"Saint-Nectaire": "saint_nectaire",
|
||||
"Bleu (fromage)": "blue_cheese",
|
||||
Cancoillotte: "cancoillotte",
|
||||
Tomme: "tomme",
|
||||
Époisses: "epoisses",
|
||||
Chaource: "chaource",
|
||||
Livarot: "livarot",
|
||||
"Pont-l'Évêque": "pont_leveque",
|
||||
Morbier: "morbier",
|
||||
"Raclette (fromage)": "raclette_cheese",
|
||||
"Fourme d'Ambert": "fourme_d_ambert",
|
||||
Salers: "salers",
|
||||
"Ossau-Iraty": "ossau_iraty",
|
||||
Vacherin: "vacherin",
|
||||
"Saint-Marcellin": "saint_marcellin",
|
||||
Neufchâtel: "neufchatel",
|
||||
"Crottin de Chavignol": "crottin_de_chavignol",
|
||||
Abondance: "abondance_cheese",
|
||||
"Carré de l'Est": "carre_de_l_est",
|
||||
Edam: "edam",
|
||||
Gouda: "gouda",
|
||||
Mimolette: "mimolette",
|
||||
Maroilles: "maroilles",
|
||||
"Mont d'or": "mont_dor",
|
||||
Kéfir: "kefir",
|
||||
"Yaourt à la grecque": "greek_yogurt",
|
||||
|
||||
// --- Crémerie & fromage / Œufs -------------------------------------------
|
||||
Œuf: "egg",
|
||||
|
||||
// --- Crémerie & fromage / Alternatives ------------------------------------
|
||||
"Lait de coco": "coconut_milk",
|
||||
"Crème de coco": "coconut_cream",
|
||||
"Lait d'amande": "almond_milk",
|
||||
"Lait d'avoine": "oat_milk",
|
||||
Tofu: "tofu",
|
||||
"Tofu soyeux": "silken_tofu",
|
||||
|
||||
// --- Condiments & épices / Épices -----------------------------------------
|
||||
"Herbes de Provence": "herbes_de_provence",
|
||||
"Poivre noir": "black_pepper",
|
||||
Paprika: "paprika",
|
||||
"Piment d'Espelette": "espelette_pepper",
|
||||
"Piment de Cayenne": "cayenne_pepper",
|
||||
Cumin: "cumin",
|
||||
"Curry (poudre)": "curry_powder",
|
||||
Curcuma: "turmeric",
|
||||
Cannelle: "cinnamon",
|
||||
Muscade: "nutmeg",
|
||||
Safran: "saffron",
|
||||
"Clou de girofle": "clove",
|
||||
"Vanille (gousse)": "vanilla_bean",
|
||||
"Poivre blanc": "white_pepper",
|
||||
"Poivre rose": "pink_pepper",
|
||||
"Poivre du Sichuan": "sichuan_pepper",
|
||||
"Paprika fumé": "smoked_paprika",
|
||||
"Piment oiseau": "bird_eye_chili",
|
||||
"Baies de genièvre": "juniper_berries",
|
||||
"Anis étoilé (badiane)": "star_anise",
|
||||
"Anis vert": "green_anise",
|
||||
"Graines de fenouil": "fennel_seeds",
|
||||
Sumac: "sumac",
|
||||
Nigelle: "nigella",
|
||||
"Quatre épices": "allspice",
|
||||
"Colombo (poudre)": "colombo_powder",
|
||||
Baharat: "baharat",
|
||||
Raifort: "horseradish",
|
||||
"Sel aux herbes": "herb_salt",
|
||||
"Sel de céleri": "celery_salt",
|
||||
"Fleur de sel": "fleur_de_sel",
|
||||
Sel: "salt",
|
||||
"Cinq épices": "five_spice",
|
||||
"Garam masala": "garam_masala",
|
||||
"Graines de coriandre": "coriander_seeds",
|
||||
Cardamome: "cardamom",
|
||||
Fenugrec: "fenugreek",
|
||||
"Piment jalapeño": "jalapeno",
|
||||
"Piment chipotle": "chipotle",
|
||||
"Piment poblano": "poblano_pepper",
|
||||
"Piment habanero": "habanero",
|
||||
"Ras el hanout": "ras_el_hanout",
|
||||
"Za'atar": "zaatar",
|
||||
|
||||
// --- Condiments & épices / Sauces -------------------------------------------
|
||||
"Sauce soja": "soy_sauce",
|
||||
Moutarde: "mustard",
|
||||
Mayonnaise: "mayonnaise",
|
||||
Ketchup: "ketchup",
|
||||
Tabasco: "tabasco",
|
||||
"Sauce Worcestershire": "worcestershire_sauce",
|
||||
"Sauce nuoc-mâm": "fish_sauce",
|
||||
Wasabi: "wasabi",
|
||||
Harissa: "harissa",
|
||||
"Pâte de curry": "curry_paste",
|
||||
"Beurre de cacahuète": "peanut_butter",
|
||||
"Moutarde de Dijon": "dijon_mustard",
|
||||
"Moutarde à l'ancienne": "wholegrain_mustard",
|
||||
"Sauce barbecue": "barbecue_sauce",
|
||||
"Sauce tartare": "tartar_sauce",
|
||||
"Sauce cocktail": "cocktail_sauce",
|
||||
"Sauce béarnaise": "bearnaise_sauce",
|
||||
"Sauce hollandaise": "hollandaise_sauce",
|
||||
"Sauce béchamel": "bechamel_sauce",
|
||||
"Sauce teriyaki": "teriyaki_sauce",
|
||||
"Sauce ponzu": "ponzu_sauce",
|
||||
Chimichurri: "chimichurri",
|
||||
"Pesto rouge (tomates séchées)": "red_pesto",
|
||||
Pesto: "pesto",
|
||||
"Sauce huître": "oyster_sauce",
|
||||
"Sauce hoisin": "hoisin_sauce",
|
||||
"Sauce sriracha": "sriracha",
|
||||
"Sauce sweet chili": "sweet_chili_sauce",
|
||||
Miso: "miso",
|
||||
"Pâte de crevettes": "shrimp_paste",
|
||||
"Pâte de curry rouge (thaï)": "red_curry_paste",
|
||||
"Pâte de curry vert (thaï)": "green_curry_paste",
|
||||
Tahini: "tahini",
|
||||
Aïoli: "aioli",
|
||||
"Sauce vinaigrette": "vinaigrette",
|
||||
Houmous: "hummus",
|
||||
|
||||
// --- Condiments & épices / Assaisonnements -----------------------------------
|
||||
"Huile d'olive": "olive_oil",
|
||||
"Huile de tournesol": "sunflower_oil",
|
||||
"Huile de colza": "rapeseed_oil",
|
||||
"Huile de coco": "coconut_oil",
|
||||
"Huile de sésame": "sesame_oil",
|
||||
"Vinaigre de cidre": "cider_vinegar",
|
||||
"Vinaigre blanc": "white_vinegar",
|
||||
"Vinaigre balsamique": "balsamic_vinegar",
|
||||
Câpres: "capers",
|
||||
Olives: "olives",
|
||||
"Olives noires": "black_olives",
|
||||
"Olives vertes": "green_olives",
|
||||
"Vin blanc (cuisine)": "white_wine",
|
||||
"Vin rouge (cuisine)": "red_wine",
|
||||
"Vin rosé (cuisine)": "rose_wine",
|
||||
"Vinaigre de vin rouge": "red_wine_vinegar",
|
||||
"Vinaigre de vin blanc": "white_wine_vinegar",
|
||||
"Vinaigre de xérès": "sherry_vinegar",
|
||||
"Huile de noix": "walnut_oil",
|
||||
"Huile de noisette": "hazelnut_oil",
|
||||
"Huile d'arachide": "peanut_oil",
|
||||
"Huile pimentée": "chili_oil",
|
||||
"Huile de maïs": "corn_oil",
|
||||
"Huile de pépins de raisin": "grapeseed_oil",
|
||||
"Huile de soja": "soybean_oil",
|
||||
"Huile de palme": "palm_oil",
|
||||
"Vinaigre de riz": "rice_vinegar",
|
||||
Mirin: "mirin",
|
||||
"Saké (cuisine)": "sake",
|
||||
"Jus de citron": "lemon_juice",
|
||||
"Jus de citron vert": "lime_juice",
|
||||
"Jus d'orange": "orange_juice",
|
||||
"Jus de pomme": "apple_juice",
|
||||
"Jus de raisin": "grape_juice",
|
||||
"Jus de tomate": "tomato_juice",
|
||||
"Jus de cranberry": "cranberry_juice",
|
||||
Café: "coffee",
|
||||
Thé: "tea",
|
||||
"Bière (cuisine)": "beer",
|
||||
"Cidre (cuisine)": "cider",
|
||||
"Champagne / vin pétillant (cuisine)": "champagne",
|
||||
"Porto (cuisine)": "port_wine",
|
||||
"Vin jaune (cuisine)": "vin_jaune",
|
||||
Cognac: "cognac",
|
||||
Rhum: "rum",
|
||||
Whisky: "whisky",
|
||||
Vodka: "vodka",
|
||||
|
||||
// --- Aides culinaires / Bases -------------------------------------------
|
||||
"Farine de blé": "wheat_flour",
|
||||
"Farine complète": "whole_wheat_flour",
|
||||
"Farine de maïs": "corn_flour",
|
||||
"Farine de sarrasin": "buckwheat_flour",
|
||||
"Farine de riz": "rice_flour",
|
||||
"Bouillon cube légumes": "vegetable_stock_cube",
|
||||
"Bouillon cube volaille": "chicken_stock_cube",
|
||||
"Concentré de tomate": "tomato_paste",
|
||||
"Coulis de tomate": "tomato_coulis",
|
||||
"Tomates pelées (conserve)": "canned_peeled_tomatoes",
|
||||
"Tomates séchées": "sun_dried_tomatoes",
|
||||
"Fond de veau": "veal_stock",
|
||||
"Fond de volaille": "chicken_stock",
|
||||
"Bouillon cube bœuf": "beef_stock_cube",
|
||||
"Bouillon cube poisson": "fish_stock_cube",
|
||||
"Bouillon de légumes": "vegetable_broth",
|
||||
"Bouillon de volaille": "chicken_broth",
|
||||
"Bouillon de bœuf": "beef_broth",
|
||||
"Court-bouillon": "court_bouillon",
|
||||
"Dashi (bouillon japonais)": "dashi",
|
||||
"Bisque de crustacés": "shellfish_bisque",
|
||||
"Farine de tapioca": "tapioca_flour",
|
||||
"Masa harina": "masa_harina",
|
||||
Eau: "water",
|
||||
"Eau gazeuse": "sparkling_water",
|
||||
"Eau de fleur d'oranger": "orange_blossom_water",
|
||||
"Eau de rose": "rose_water",
|
||||
"Fumet de poisson": "fish_fumet",
|
||||
|
||||
// --- Aides culinaires / Épaississants -------------------------------------
|
||||
"Levure boulangère": "bakers_yeast",
|
||||
"Levure chimique": "baking_powder",
|
||||
Maïzena: "cornstarch",
|
||||
"Farine de lupin": "lupin_flour",
|
||||
Gélatine: "gelatin",
|
||||
"Bicarbonate de soude": "baking_soda",
|
||||
"Fécule de pomme de terre": "potato_starch",
|
||||
|
||||
// --- Aides culinaires / Sucres ---------------------------------------------
|
||||
Sucre: "sugar",
|
||||
Miel: "honey",
|
||||
"Sirop d'érable": "maple_syrup",
|
||||
"Sucre roux": "brown_sugar",
|
||||
"Sucre glace": "powdered_sugar",
|
||||
Cassonade: "demerara_sugar",
|
||||
"Chocolat noir": "dark_chocolate",
|
||||
"Chocolat au lait": "milk_chocolate",
|
||||
"Chocolat blanc": "white_chocolate",
|
||||
"Pépites de chocolat": "chocolate_chips",
|
||||
"Cacao en poudre": "cocoa_powder",
|
||||
"Extrait de vanille": "vanilla_extract",
|
||||
"Sucre de palme": "palm_sugar",
|
||||
"Sirop de sucre de canne": "cane_syrup",
|
||||
};
|
||||
|
||||
const ALL_KEYS: Record<string, string> = {
|
||||
...DIET_KEYS,
|
||||
...ALLERGEN_KEYS,
|
||||
...INGREDIENT_KEYS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a seed-time French authoring label to its English `key` —
|
||||
* throws if it's missing an entry above rather than silently falling back
|
||||
* to a French slug, since a new diet/allergen/ingredient needs a
|
||||
* deliberately-chosen English key before it can seed at all.
|
||||
* `slugify` still runs over the result so a stray character/casing slip in
|
||||
* the table above can't produce a key that doesn't match the
|
||||
* `[a-z0-9_]`-only shape every other key has.
|
||||
*/
|
||||
export function getEnglishKey(frenchLabel: string): string {
|
||||
const english = ALL_KEYS[frenchLabel];
|
||||
if (english === undefined) {
|
||||
throw new Error(
|
||||
`No English key registered for "${frenchLabel}" — add one to catalog-en-keys.ts`,
|
||||
);
|
||||
}
|
||||
return slugify(english);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -43,6 +43,7 @@ function toIngredientView(ingredient: IngredientWithDetails): IngredientView {
|
|||
icon: ingredient.icon,
|
||||
category: ingredient.category,
|
||||
subcategory: ingredient.subcategory,
|
||||
reproducible: ingredient.reproducible,
|
||||
allergens: ingredient.allergies.map(({ allergy }) => ({
|
||||
id: allergy.id,
|
||||
key: allergy.category.key,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export async function getIngredients(): Promise<IngredientView[]> {
|
|||
icon: ingredient.icon,
|
||||
category: ingredient.category,
|
||||
subcategory: ingredient.subcategory,
|
||||
reproducible: ingredient.reproducible,
|
||||
allergens: ingredient.allergies.map(({ allergy }) => ({
|
||||
id: allergy.id,
|
||||
key: allergy.category.key,
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Deterministic slug for a French reference-data label — used as the
|
||||
* stable, storage-safe `key` for `Diet`/`Category`/`Ingredient` rows (see
|
||||
* `db/reference-seed-data.ts`), decoupled from the display label so the
|
||||
* label itself can live in `apps/web`'s `locales/fr/translation.json`
|
||||
* (`catalog.*` namespace) instead of the database. A row's `key` is derived
|
||||
* from its seed-time French name once and then never changes — renaming the
|
||||
* *label* later (a translation fix, a rewording) never touches the key, the
|
||||
* FK-referencing rows, or any code that looks a row up by key.
|
||||
*
|
||||
* Handles the two French ligatures NFD decomposition doesn't touch (`œ`,
|
||||
* `æ` aren't accented letters, they're distinct glyphs) explicitly, then
|
||||
* strips every other accent via NFD decomposition + Unicode "Mark" removal
|
||||
* (`\p{M}`, every combining diacritic NFD can produce), then collapses
|
||||
* whatever isn't `[a-z0-9]` into single underscores.
|
||||
*/
|
||||
export function slugify(label: string): string {
|
||||
return label
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe")
|
||||
.replace(/æ/g, "ae")
|
||||
.normalize("NFD")
|
||||
.replace(/\p{M}/gu, "")
|
||||
.replace(/[^a-z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import { faker } from "@faker-js/faker";
|
|||
import { expect } from "chai";
|
||||
import request from "supertest";
|
||||
import { createApp } from "../src/app.js";
|
||||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||
import { prisma } from "../src/db/prisma.js";
|
||||
import { resetDatabase } from "../test-support/reset-db.js";
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ describe("Profile", () => {
|
|||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const diet = await prisma.diet.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Végétarien") },
|
||||
where: { key: "vegetarian" },
|
||||
});
|
||||
|
||||
const res = await agent.patch("/profile/diet").send({ dietId: diet.id });
|
||||
|
|
@ -53,7 +52,7 @@ describe("Profile", () => {
|
|||
it("clears the regime when dietId is null", async () => {
|
||||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey("Végan") } });
|
||||
const diet = await prisma.diet.findFirstOrThrow({ where: { key: "vegan" } });
|
||||
await agent.patch("/profile/diet").send({ dietId: diet.id });
|
||||
|
||||
const res = await agent.patch("/profile/diet").send({ dietId: null });
|
||||
|
|
@ -86,8 +85,8 @@ describe("Profile", () => {
|
|||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
||||
const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides"));
|
||||
const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten"));
|
||||
const peanuts = allergies.find((a) => a.category.key === "peanuts");
|
||||
const gluten = allergies.find((a) => a.category.key === "gluten");
|
||||
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
|
||||
|
||||
const initial = await agent.get("/profile/allergies");
|
||||
|
|
@ -107,8 +106,8 @@ describe("Profile", () => {
|
|||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
||||
const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides"));
|
||||
const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten"));
|
||||
const peanuts = allergies.find((a) => a.category.key === "peanuts");
|
||||
const gluten = allergies.find((a) => a.category.key === "gluten");
|
||||
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
|
||||
|
||||
await agent.patch("/profile/allergies").send({ allergyIds: [peanuts.id] });
|
||||
|
|
@ -144,10 +143,10 @@ describe("Profile", () => {
|
|||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const tomate = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Tomate") },
|
||||
where: { key: "tomato" },
|
||||
});
|
||||
const oignon = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Oignon") },
|
||||
where: { key: "onion" },
|
||||
});
|
||||
|
||||
const initial = await agent.get("/profile/disliked-ingredients");
|
||||
|
|
@ -167,10 +166,10 @@ describe("Profile", () => {
|
|||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const tomate = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Tomate") },
|
||||
where: { key: "tomato" },
|
||||
});
|
||||
const oignon = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Oignon") },
|
||||
where: { key: "onion" },
|
||||
});
|
||||
|
||||
await agent
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { faker } from "@faker-js/faker";
|
|||
import { expect } from "chai";
|
||||
import request from "supertest";
|
||||
import { createApp } from "../src/app.js";
|
||||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||
import { prisma } from "../src/db/prisma.js";
|
||||
import { resetDatabase } from "../test-support/reset-db.js";
|
||||
|
||||
|
|
@ -20,11 +19,9 @@ function buildSignupPayload(): SignupInput {
|
|||
};
|
||||
}
|
||||
|
||||
/** Resolves a reference ingredient's id by its `reference-seed-data.ts` French name (slugified to match its `key`). */
|
||||
async function ingredientId(name: string): Promise<number> {
|
||||
const ingredient = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey(name) },
|
||||
});
|
||||
/** Resolves a reference ingredient's id by its `reference-seed-data.ts` uid (also its DB `key`). */
|
||||
async function ingredientId(key: string): Promise<number> {
|
||||
const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key } });
|
||||
return ingredient.id;
|
||||
}
|
||||
|
||||
|
|
@ -164,10 +161,10 @@ describe("Recipes", () => {
|
|||
describe("POST /recipes", () => {
|
||||
it("creates a recipe with its ingredients, ordered steps and diet tags", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const oeuf = await ingredientId("Œuf");
|
||||
const tomate = await ingredientId("tomato");
|
||||
const oeuf = await ingredientId("egg");
|
||||
const vegetarien = await prisma.diet.findFirstOrThrow({
|
||||
where: { key: getEnglishKey("Végétarien") },
|
||||
where: { key: "vegetarian" },
|
||||
});
|
||||
|
||||
const res = await agent.post("/recipes").send({
|
||||
|
|
@ -188,18 +185,14 @@ describe("Recipes", () => {
|
|||
res.body.steps.map((s: { description: string; order: number }) => s.order),
|
||||
).to.deep.equal([0, 1]);
|
||||
// Allergens aggregated across ingredients — "Œuf" carries "Œufs".
|
||||
expect(res.body.allergens.map((a: { key: string }) => a.key)).to.include(
|
||||
getEnglishKey("Œufs"),
|
||||
);
|
||||
expect(res.body.diets.map((d: { key: string }) => d.key)).to.deep.equal([
|
||||
getEnglishKey("Végétarien"),
|
||||
]);
|
||||
expect(res.body.allergens.map((a: { key: string }) => a.key)).to.include("eggs");
|
||||
expect(res.body.diets.map((d: { key: string }) => d.key)).to.deep.equal(["vegetarian"]);
|
||||
});
|
||||
|
||||
it("defaults to PERSONAL visibility, and stamps the author's current household", async () => {
|
||||
const { agent } = await signup();
|
||||
const houseRes = await agent.post("/house").send({ name: "Chez moi" });
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
|
||||
const res = await agent.post("/recipes").send({
|
||||
name: "Test",
|
||||
|
|
@ -239,7 +232,7 @@ describe("Recipes", () => {
|
|||
|
||||
it("rejects an unknown dietId with 404 DIET_NOT_FOUND", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
|
||||
const res = await agent.post("/recipes").send({
|
||||
name: "Test",
|
||||
|
|
@ -276,7 +269,7 @@ describe("Recipes", () => {
|
|||
|
||||
it("returns the full recipe detail", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
const created = await agent.post("/recipes").send({
|
||||
name: "Salade",
|
||||
dietIds: [],
|
||||
|
|
@ -288,7 +281,7 @@ describe("Recipes", () => {
|
|||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.body.name).to.equal("Salade");
|
||||
expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Tomate"));
|
||||
expect(res.body.ingredients[0].ingredient.key).to.equal("tomato");
|
||||
expect(res.body.isFavorite).to.equal(false);
|
||||
});
|
||||
|
||||
|
|
@ -352,8 +345,8 @@ describe("Recipes", () => {
|
|||
describe("PATCH /recipes/:id", () => {
|
||||
it("replaces the recipe's whole content", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const oignon = await ingredientId("Oignon");
|
||||
const tomate = await ingredientId("tomato");
|
||||
const oignon = await ingredientId("onion");
|
||||
const created = await agent.post("/recipes").send({
|
||||
name: "Salade",
|
||||
dietIds: [],
|
||||
|
|
@ -373,13 +366,13 @@ describe("Recipes", () => {
|
|||
expect(res.body.name).to.equal("Salade composée");
|
||||
expect(res.body.visibility).to.equal("PUBLIC");
|
||||
expect(res.body.ingredients).to.have.length(1);
|
||||
expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Oignon"));
|
||||
expect(res.body.ingredients[0].ingredient.key).to.equal("onion");
|
||||
expect(res.body.steps).to.have.length(2);
|
||||
});
|
||||
|
||||
it("returns 404 RECIPE_NOT_FOUND for an unknown id", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
|
||||
const res = await agent.patch("/recipes/999999").send({
|
||||
name: "Test",
|
||||
|
|
@ -394,7 +387,7 @@ describe("Recipes", () => {
|
|||
|
||||
it("rejects an unknown dietId with 404 DIET_NOT_FOUND", async () => {
|
||||
const { agent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
const created = await agent.post("/recipes").send({
|
||||
name: "Salade",
|
||||
dietIds: [],
|
||||
|
|
@ -416,7 +409,7 @@ describe("Recipes", () => {
|
|||
it("rejects an edit from anyone other than the recipe's author with 403 NOT_RECIPE_AUTHOR", async () => {
|
||||
const { agent, profileId } = await signup();
|
||||
const { agent: otherAgent } = await signup();
|
||||
const tomate = await ingredientId("Tomate");
|
||||
const tomate = await ingredientId("tomato");
|
||||
const recipe = await prisma.recipe.create({
|
||||
data: { name: "Publique", authorId: profileId, visibility: "PUBLIC" },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { expect } from "chai";
|
||||
import request from "supertest";
|
||||
import { createApp } from "../src/app.js";
|
||||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||
import { prisma } from "../src/db/prisma.js";
|
||||
import { resetDatabase } from "../test-support/reset-db.js";
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ describe("Reference data", () => {
|
|||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.body).to.have.length(5);
|
||||
expect(res.body.map((d: { key: string }) => d.key)).to.include(getEnglishKey("Végétarien"));
|
||||
expect(res.body.map((d: { key: string }) => d.key)).to.include("vegetarian");
|
||||
expect(res.body[0]).to.have.keys(["id", "key"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -33,18 +32,17 @@ describe("Reference data", () => {
|
|||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.body).to.have.length(14);
|
||||
expect(res.body.map((a: { key: string }) => a.key)).to.include(getEnglishKey("Arachides"));
|
||||
expect(res.body.map((a: { key: string }) => a.key)).to.include("peanuts");
|
||||
expect(res.body[0]).to.have.keys(["id", "key", "kind"]);
|
||||
});
|
||||
|
||||
it("classifies Gluten and Sulfites as intolerances, the rest as allergies", async () => {
|
||||
const res = await request(app).get("/reference/allergies");
|
||||
|
||||
const byKey = (name: string) =>
|
||||
res.body.find((a: { key: string }) => a.key === getEnglishKey(name));
|
||||
expect(byKey("Gluten").kind).to.equal("INTOLERANCE");
|
||||
expect(byKey("Sulfites").kind).to.equal("INTOLERANCE");
|
||||
expect(byKey("Arachides").kind).to.equal("ALLERGY");
|
||||
const byKey = (key: string) => res.body.find((a: { key: string }) => a.key === key);
|
||||
expect(byKey("gluten").kind).to.equal("INTOLERANCE");
|
||||
expect(byKey("sulfites").kind).to.equal("INTOLERANCE");
|
||||
expect(byKey("peanuts").kind).to.equal("ALLERGY");
|
||||
expect(res.body.filter((a: { kind: string }) => a.kind === "INTOLERANCE")).to.have.length(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -55,13 +53,14 @@ describe("Reference data", () => {
|
|||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.body.length).to.be.greaterThan(0);
|
||||
expect(res.body.map((i: { key: string }) => i.key)).to.include(getEnglishKey("Tomate"));
|
||||
expect(res.body.map((i: { key: string }) => i.key)).to.include("tomato");
|
||||
expect(res.body[0]).to.have.keys([
|
||||
"id",
|
||||
"key",
|
||||
"icon",
|
||||
"category",
|
||||
"subcategory",
|
||||
"reproducible",
|
||||
"allergens",
|
||||
"diets",
|
||||
]);
|
||||
|
|
@ -70,12 +69,9 @@ describe("Reference data", () => {
|
|||
it("resolves each ingredient's linked allergens, empty for one with none", async () => {
|
||||
const res = await request(app).get("/reference/ingredients");
|
||||
|
||||
const byKey = (name: string) =>
|
||||
res.body.find((i: { key: string }) => i.key === getEnglishKey(name));
|
||||
expect(byKey("Œuf").allergens.map((a: { key: string }) => a.key)).to.include(
|
||||
getEnglishKey("Œufs"),
|
||||
);
|
||||
expect(byKey("Tomate").allergens).to.deep.equal([]);
|
||||
const byKey = (key: string) => res.body.find((i: { key: string }) => i.key === key);
|
||||
expect(byKey("egg").allergens.map((a: { key: string }) => a.key)).to.include("eggs");
|
||||
expect(byKey("tomato").allergens).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const tomato = {
|
|||
id: 1,
|
||||
key: "tomato",
|
||||
icon: "VEGETABLE",
|
||||
category: "PRODUITS_FRAIS",
|
||||
subcategory: "LEGUMES",
|
||||
category: "freshProduce",
|
||||
subcategory: "vegetables",
|
||||
allergens: [],
|
||||
diets: [{ id: 2, key: "vegetarian" }],
|
||||
};
|
||||
|
|
@ -13,8 +13,8 @@ const egg = {
|
|||
id: 2,
|
||||
key: "egg",
|
||||
icon: "EGG",
|
||||
category: "CREMERIE_FROMAGE",
|
||||
subcategory: "OEUFS",
|
||||
category: "dairyAndCheese",
|
||||
subcategory: "eggs",
|
||||
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
|
||||
diets: [],
|
||||
};
|
||||
|
|
@ -22,8 +22,8 @@ const carrot = {
|
|||
id: 3,
|
||||
key: "carrot",
|
||||
icon: "VEGETABLE",
|
||||
category: "PRODUITS_FRAIS",
|
||||
subcategory: "LEGUMES",
|
||||
category: "freshProduce",
|
||||
subcategory: "vegetables",
|
||||
allergens: [],
|
||||
diets: [{ id: 2, key: "vegetarian" }],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ const omeletteDetail = {
|
|||
id: 10,
|
||||
key: "egg",
|
||||
icon: "EGG",
|
||||
category: "CREMERIE_FROMAGE",
|
||||
subcategory: "OEUFS",
|
||||
category: "dairyAndCheese",
|
||||
subcategory: "eggs",
|
||||
allergens: [oeufs],
|
||||
diets: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ const omeletteDetail = {
|
|||
id: 10,
|
||||
key: "egg",
|
||||
icon: "EGG",
|
||||
category: "CREMERIE_FROMAGE",
|
||||
subcategory: "OEUFS",
|
||||
category: "dairyAndCheese",
|
||||
subcategory: "eggs",
|
||||
allergens: [oeufs],
|
||||
diets: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { CheckboxOption } from "../../components/ui/Checkbox";
|
|||
import { SettingsIcon } from "../../layouts/nav-icons";
|
||||
import { AllergenBadges } from "./AllergenBadges";
|
||||
import { DietBadges } from "./DietBadges";
|
||||
import { ReproducibleBadge } from "./ReproducibleBadge";
|
||||
import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons";
|
||||
import "./recipes.scss";
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ export function IngredientPicker({
|
|||
// badges per card too noisy while just browsing/searching by name.
|
||||
const [showAllergens, setShowAllergens] = useState(true);
|
||||
const [showDiets, setShowDiets] = useState(true);
|
||||
const [showReproducible, setShowReproducible] = useState(true);
|
||||
const [isDisplayMenuOpen, setIsDisplayMenuOpen] = useState(false);
|
||||
|
||||
function selectCategory(next: IngredientCategory | typeof ALL) {
|
||||
|
|
@ -116,6 +118,9 @@ export function IngredientPicker({
|
|||
<CheckboxOption checked={showAllergens} onChange={setShowAllergens}>
|
||||
{t("recipes.form.showAllergensLabel")}
|
||||
</CheckboxOption>
|
||||
<CheckboxOption checked={showReproducible} onChange={setShowReproducible}>
|
||||
{t("recipes.form.showReproducibleLabel")}
|
||||
</CheckboxOption>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -184,6 +189,7 @@ export function IngredientPicker({
|
|||
</span>
|
||||
{showAllergens && <AllergenBadges allergens={ingredient.allergens} />}
|
||||
{showDiets && <DietBadges diets={ingredient.diets} />}
|
||||
{showReproducible && <ReproducibleBadge reproducible={ingredient.reproducible} />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { IngredientView } from "@batch-cooking/shared";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { AllergenBadges } from "./AllergenBadges";
|
||||
import { DietBadges } from "./DietBadges";
|
||||
import { ReproducibleBadge } from "./ReproducibleBadge";
|
||||
import { IngredientTypeIcon } from "./ingredient-icons";
|
||||
import "./recipes.scss";
|
||||
|
||||
|
|
@ -48,6 +49,10 @@ export function IngredientRow({
|
|||
/>
|
||||
<AllergenBadges allergens={ingredient.allergens} />
|
||||
<DietBadges diets={ingredient.diets} />
|
||||
<ReproducibleBadge
|
||||
reproducible={ingredient.reproducible}
|
||||
searchLabel={t(`catalog.ingredients.${ingredient.key}`)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ingredient-row__remove"
|
||||
|
|
|
|||
49
apps/web/src/features/recipes/ReproducibleBadge.tsx
Normal file
49
apps/web/src/features/recipes/ReproducibleBadge.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import "./recipes.scss";
|
||||
|
||||
/**
|
||||
* Single "faisable maison" pill for an ingredient flagged
|
||||
* `IngredientView.reproducible` — renders nothing otherwise, same "mount
|
||||
* unconditionally" convention as `AllergenBadges`/`DietBadges`, just for
|
||||
* one boolean rather than a list.
|
||||
*
|
||||
* Two shapes depending on where it's used:
|
||||
* - `IngredientPicker`'s card grid: plain, non-interactive pill (no
|
||||
* `searchLabel`).
|
||||
* - `IngredientRow` (an ingredient already added to the recipe being
|
||||
* built): pass `searchLabel` (its translated display name) to render it
|
||||
* as a link opening the recipe catalog's own search, pre-filled with
|
||||
* that name, in a new tab — a plain `<a>`, not a router `<Link>`, so the
|
||||
* in-progress recipe form (no draft persistence, see `RecipeFormPage`)
|
||||
* is never at risk of being navigated away from.
|
||||
*/
|
||||
export function ReproducibleBadge({
|
||||
reproducible,
|
||||
searchLabel,
|
||||
}: {
|
||||
reproducible: boolean;
|
||||
searchLabel?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!reproducible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = t("recipes.form.reproducibleBadge");
|
||||
|
||||
if (searchLabel) {
|
||||
return (
|
||||
<a
|
||||
className="reproducible-badge reproducible-badge--link"
|
||||
href={`/recettes?search=${encodeURIComponent(searchLabel)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={t("recipes.form.reproducibleBadgeHint", { name: searchLabel })}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="reproducible-badge">{label}</span>;
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ function FilledIcon({ children }: { children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
/** Carrot (foodiconpack.com, CC BY 4.0 — see the credits page) — root vegetables, leafy greens, and produce generally (`PRODUITS_FRAIS`/`LEGUMES`). */
|
||||
/** Carrot (foodiconpack.com, CC BY 4.0 — see the credits page) — root vegetables, leafy greens, and produce generally (`freshProduce`/`vegetables`). */
|
||||
export function VegetableIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -57,7 +57,7 @@ export function VegetableIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Apple (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`FRUITS`. */
|
||||
/** Apple (foodiconpack.com, CC BY 4.0) — `freshProduce`/`fruits`. */
|
||||
export function FruitIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -69,7 +69,7 @@ export function FruitIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Basil sprig (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`HERBES_FRAICHES`. */
|
||||
/** Basil sprig (foodiconpack.com, CC BY 4.0) — `freshProduce`/`freshHerbs`. */
|
||||
export function HerbIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -78,7 +78,7 @@ export function HerbIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Cut of beef (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VIANDES`. */
|
||||
/** Cut of beef (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`meats`. */
|
||||
export function MeatIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -90,7 +90,7 @@ export function MeatIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Chicken (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VOLAILLES`. */
|
||||
/** Chicken (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`poultry`. */
|
||||
export function PoultryIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -101,7 +101,7 @@ export function PoultryIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Salmon (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`POISSONS`. */
|
||||
/** Salmon (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`fish`. */
|
||||
export function FishIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -113,7 +113,7 @@ export function FishIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Shrimp (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`CRUSTACES_FRUITS_DE_MER`. */
|
||||
/** Shrimp (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`shellfish`. */
|
||||
export function ShellfishIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -123,7 +123,7 @@ export function ShellfishIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Bowl of rice (foodiconpack.com, CC BY 4.0) — grains, pasta, rice, flour (`EPICERIE_SECHE`/`FECULENTS`, and flours under `AIDES_CULINAIRES`/`BASES`). */
|
||||
/** Bowl of rice (foodiconpack.com, CC BY 4.0) — grains, pasta, rice, flour (`dryGoods`/`starches`, and flours under `cookingEssentials`/`bases`). */
|
||||
export function GrainIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -142,7 +142,7 @@ export function GrainIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Chickpeas (foodiconpack.com, CC BY 4.0) — `EPICERIE_SECHE`/`LEGUMINEUSES`. */
|
||||
/** Chickpeas (foodiconpack.com, CC BY 4.0) — `dryGoods`/`legumes`. */
|
||||
export function LegumeIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -158,7 +158,7 @@ export function LegumeIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Almonds (foodiconpack.com, CC BY 4.0) — nuts, seeds, dried fruit (`EPICERIE_SECHE`/`GRAINES_FRUITS_SECS`). */
|
||||
/** Almonds (foodiconpack.com, CC BY 4.0) — nuts, seeds, dried fruit (`dryGoods`/`nutsAndSeeds`). */
|
||||
export function NutSeedIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -174,7 +174,7 @@ export function NutSeedIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** A loaf, scored on top — `BOULANGERIE`/`PAINS`. */
|
||||
/** A loaf, scored on top — `bakery`/`breads`. */
|
||||
export function BreadIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
|
|
@ -184,7 +184,7 @@ export function BreadIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Rolling pin — raw, uncooked pastry (`BOULANGERIE`/`PATES_A_CUIRE`). */
|
||||
/** Rolling pin — raw, uncooked pastry (`bakery`/`rawDough`). */
|
||||
export function DoughIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
|
|
@ -195,7 +195,7 @@ export function DoughIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Milk carton (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (non-cheese items). */
|
||||
/** Milk carton (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`dairy` (non-cheese items). */
|
||||
export function MilkIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -206,7 +206,7 @@ export function MilkIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Wedge of cheddar (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (cheese items). */
|
||||
/** Wedge of cheddar (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`dairy` (cheese items). */
|
||||
export function CheeseIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -225,7 +225,7 @@ export function CheeseIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Eggs (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`OEUFS`. */
|
||||
/** Eggs (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`eggs`. */
|
||||
export function EggIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -238,7 +238,7 @@ export function EggIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** A seedling — plant-based dairy/meat alternatives (`CREMERIE_FROMAGE`/`ALTERNATIVES`). */
|
||||
/** A seedling — plant-based dairy/meat alternatives (`dairyAndCheese`/`plantBasedAlternatives`). */
|
||||
export function SproutIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
|
|
@ -249,7 +249,7 @@ export function SproutIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Cinnamon sticks (foodiconpack.com, CC BY 4.0) — dried spices/herbs (`CONDIMENTS_EPICES`/`EPICES`). */
|
||||
/** Cinnamon sticks (foodiconpack.com, CC BY 4.0) — dried spices/herbs (`condimentsAndSpices`/`spices`). */
|
||||
export function SpiceIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -262,7 +262,7 @@ export function SpiceIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Honey jar (foodiconpack.com, CC BY 4.0) — sauces, pickles, tinned/preserved goods (`CONDIMENTS_EPICES`/`SAUCES` and the `EPICERIE_SECHE`/`AUTRES` catch-all). */
|
||||
/** Honey jar (foodiconpack.com, CC BY 4.0) — sauces, pickles, tinned/preserved goods (`condimentsAndSpices`/`sauces` and the `dryGoods`/`other` catch-all). */
|
||||
export function JarIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -272,7 +272,7 @@ export function JarIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Oil bottle (foodiconpack.com, CC BY 4.0) — oils, vinegars (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`, the pourable subset). */
|
||||
/** Oil bottle (foodiconpack.com, CC BY 4.0) — oils, vinegars (`condimentsAndSpices`/`seasonings`, the pourable subset). */
|
||||
export function BottleIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -283,7 +283,7 @@ export function BottleIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Glass (foodiconpack.com, CC BY 4.0) — juices, coffee/tea, cooking alcohols, water (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`'s drinkable subset). */
|
||||
/** Glass (foodiconpack.com, CC BY 4.0) — juices, coffee/tea, cooking alcohols, water (`condimentsAndSpices`/`seasonings`'s drinkable subset). */
|
||||
export function DrinkIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -295,7 +295,7 @@ export function DrinkIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Stockpot (foodiconpack.com, CC BY 4.0) — broths, stocks, water bases (`AIDES_CULINAIRES`/`BASES`'s liquid-base subset). */
|
||||
/** Stockpot (foodiconpack.com, CC BY 4.0) — broths, stocks, water bases (`cookingEssentials`/`bases`'s liquid-base subset). */
|
||||
export function StockPotIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -304,7 +304,7 @@ export function StockPotIcon() {
|
|||
);
|
||||
}
|
||||
|
||||
/** Sugar (foodiconpack.com, CC BY 4.0) — `AIDES_CULINAIRES`/`SUCRES`. */
|
||||
/** Sugar (foodiconpack.com, CC BY 4.0) — `cookingEssentials`/`sugars`. */
|
||||
export function SugarIcon() {
|
||||
return (
|
||||
<FilledIcon>
|
||||
|
|
@ -355,13 +355,13 @@ export function IngredientTypeIcon({ icon }: { icon: IngredientIconType }) {
|
|||
* derived fact.
|
||||
*/
|
||||
export const CATEGORY_ICON: Record<IngredientCategory, IngredientIconType> = {
|
||||
PRODUITS_FRAIS: "VEGETABLE",
|
||||
BOUCHERIE_POISSONNERIE: "MEAT",
|
||||
EPICERIE_SECHE: "GRAIN",
|
||||
BOULANGERIE: "BREAD",
|
||||
CREMERIE_FROMAGE: "CHEESE",
|
||||
CONDIMENTS_EPICES: "SPICE",
|
||||
AIDES_CULINAIRES: "STOCK_POT",
|
||||
freshProduce: "VEGETABLE",
|
||||
meatAndSeafood: "MEAT",
|
||||
dryGoods: "GRAIN",
|
||||
bakery: "BREAD",
|
||||
dairyAndCheese: "CHEESE",
|
||||
condimentsAndSpices: "SPICE",
|
||||
cookingEssentials: "STOCK_POT",
|
||||
};
|
||||
|
||||
/** Renders {@link CATEGORY_ICON}'s pictogram for one category — used by the category chip row. */
|
||||
|
|
@ -373,33 +373,33 @@ export function CategoryIcon({ category }: { category: IngredientCategory }) {
|
|||
* One representative {@link IngredientIconType} per {@link IngredientSubcategory}
|
||||
* rack, for `IngredientPicker`'s second-tier subcategory chips. Most map
|
||||
* 1:1 onto their subcategory's dominant shape; a couple of heterogeneous
|
||||
* subcategories (`ASSAISONNEMENTS` mixes oils with juices and coffee,
|
||||
* `BASES` mixes flour with stock and canned tomato) get one illustrative
|
||||
* subcategories (`seasonings` mixes oils with juices and coffee,
|
||||
* `bases` mixes flour with stock and canned tomato) get one illustrative
|
||||
* pick rather than a derived fact, same reasoning as {@link CATEGORY_ICON}.
|
||||
*/
|
||||
export const SUBCATEGORY_ICON: Record<IngredientSubcategory, IngredientIconType> = {
|
||||
LEGUMES: "VEGETABLE",
|
||||
FRUITS: "FRUIT",
|
||||
HERBES_FRAICHES: "HERB",
|
||||
VIANDES: "MEAT",
|
||||
VOLAILLES: "POULTRY",
|
||||
POISSONS: "FISH",
|
||||
CRUSTACES_FRUITS_DE_MER: "SHELLFISH",
|
||||
FECULENTS: "GRAIN",
|
||||
LEGUMINEUSES: "LEGUME",
|
||||
GRAINES_FRUITS_SECS: "NUT_SEED",
|
||||
AUTRES: "JAR",
|
||||
PAINS: "BREAD",
|
||||
PATES_A_CUIRE: "DOUGH",
|
||||
PRODUITS_LAITIERS: "MILK",
|
||||
OEUFS: "EGG",
|
||||
ALTERNATIVES: "SPROUT",
|
||||
EPICES: "SPICE",
|
||||
SAUCES: "JAR",
|
||||
ASSAISONNEMENTS: "BOTTLE",
|
||||
BASES: "STOCK_POT",
|
||||
EPAISSISSANTS: "JAR",
|
||||
SUCRES: "SUGAR",
|
||||
vegetables: "VEGETABLE",
|
||||
fruits: "FRUIT",
|
||||
freshHerbs: "HERB",
|
||||
meats: "MEAT",
|
||||
poultry: "POULTRY",
|
||||
fish: "FISH",
|
||||
shellfish: "SHELLFISH",
|
||||
starches: "GRAIN",
|
||||
legumes: "LEGUME",
|
||||
nutsAndSeeds: "NUT_SEED",
|
||||
other: "JAR",
|
||||
breads: "BREAD",
|
||||
rawDough: "DOUGH",
|
||||
dairy: "MILK",
|
||||
eggs: "EGG",
|
||||
plantBasedAlternatives: "SPROUT",
|
||||
spices: "SPICE",
|
||||
sauces: "JAR",
|
||||
seasonings: "BOTTLE",
|
||||
bases: "STOCK_POT",
|
||||
thickeners: "JAR",
|
||||
sugars: "SUGAR",
|
||||
};
|
||||
|
||||
/** Renders {@link SUBCATEGORY_ICON}'s pictogram for one subcategory — used by the subcategory chip row. */
|
||||
|
|
|
|||
|
|
@ -55,6 +55,34 @@
|
|||
border: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
// A fourth, distinct pill language for `ReproducibleBadge` — neither a
|
||||
// warning (allergen) nor a classification (diet) nor a negative (disliked):
|
||||
// a positive nudge ("this could be homemade"), so it borrows
|
||||
// --color-primary (the same tinted-fill treatment as `.is-selected` in
|
||||
// global.scss) rather than any of the three above. `margin-top` matches its
|
||||
// siblings' `.allergen-badges`/`.diet-badges` list wrappers even though this
|
||||
// is a lone element, not a list, so it lines up the same way when it's the
|
||||
// only badge present.
|
||||
.reproducible-badge {
|
||||
display: inline-block;
|
||||
margin: var(--space-sm) 0 0;
|
||||
padding: 0.15rem 0.6rem;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
|
||||
.reproducible-badge--link {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--color-primary) 22%, var(--color-surface));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Catalog page -------------------------------------------------------------
|
||||
// `.app-content` (AppLayout.scss) already stretches to the full viewport
|
||||
// height — same reasoning as `planning-page.scss`. `.recipes-page` fills
|
||||
|
|
|
|||
|
|
@ -185,41 +185,44 @@
|
|||
"displayOptions": "Options d'affichage",
|
||||
"showDietsLabel": "Régimes alimentaires",
|
||||
"showAllergensLabel": "Allergènes",
|
||||
"showReproducibleLabel": "Faisable maison",
|
||||
"reproducibleBadge": "Faisable maison",
|
||||
"reproducibleBadgeHint": "Chercher une recette pour {{name}}",
|
||||
"allCategories": "Tout",
|
||||
"allSubcategories": "Tout",
|
||||
"noIngredientFound": "Aucun ingrédient trouvé.",
|
||||
"category": {
|
||||
"PRODUITS_FRAIS": "Produits frais",
|
||||
"BOUCHERIE_POISSONNERIE": "Boucherie & poissonnerie",
|
||||
"EPICERIE_SECHE": "Épicerie sèche",
|
||||
"BOULANGERIE": "Boulangerie",
|
||||
"CREMERIE_FROMAGE": "Crémerie & fromage",
|
||||
"CONDIMENTS_EPICES": "Condiments & épices",
|
||||
"AIDES_CULINAIRES": "Aides culinaires"
|
||||
"freshProduce": "Produits frais",
|
||||
"meatAndSeafood": "Boucherie & poissonnerie",
|
||||
"dryGoods": "Épicerie sèche",
|
||||
"bakery": "Boulangerie",
|
||||
"dairyAndCheese": "Crémerie & fromage",
|
||||
"condimentsAndSpices": "Condiments & épices",
|
||||
"cookingEssentials": "Aides culinaires"
|
||||
},
|
||||
"subcategory": {
|
||||
"LEGUMES": "Légumes",
|
||||
"FRUITS": "Fruits",
|
||||
"HERBES_FRAICHES": "Herbes fraîches",
|
||||
"VIANDES": "Viandes",
|
||||
"VOLAILLES": "Volailles",
|
||||
"POISSONS": "Poissons",
|
||||
"CRUSTACES_FRUITS_DE_MER": "Crustacés & fruits de mer",
|
||||
"FECULENTS": "Féculents",
|
||||
"LEGUMINEUSES": "Légumineuses",
|
||||
"GRAINES_FRUITS_SECS": "Graines & fruits secs",
|
||||
"AUTRES": "Autres",
|
||||
"PAINS": "Pains",
|
||||
"PATES_A_CUIRE": "Pâtes à cuire",
|
||||
"PRODUITS_LAITIERS": "Produits laitiers",
|
||||
"OEUFS": "Œufs",
|
||||
"ALTERNATIVES": "Alternatives végétales",
|
||||
"EPICES": "Épices",
|
||||
"SAUCES": "Sauces",
|
||||
"ASSAISONNEMENTS": "Assaisonnements",
|
||||
"BASES": "Bases",
|
||||
"EPAISSISSANTS": "Épaississants",
|
||||
"SUCRES": "Sucres"
|
||||
"vegetables": "Légumes",
|
||||
"fruits": "Fruits",
|
||||
"freshHerbs": "Herbes fraîches",
|
||||
"meats": "Viandes",
|
||||
"poultry": "Volailles",
|
||||
"fish": "Poissons",
|
||||
"shellfish": "Crustacés & fruits de mer",
|
||||
"starches": "Féculents",
|
||||
"legumes": "Légumineuses",
|
||||
"nutsAndSeeds": "Graines & fruits secs",
|
||||
"other": "Autres",
|
||||
"breads": "Pains",
|
||||
"rawDough": "Pâtes à cuire",
|
||||
"dairy": "Produits laitiers",
|
||||
"eggs": "Œufs",
|
||||
"plantBasedAlternatives": "Alternatives végétales",
|
||||
"spices": "Épices",
|
||||
"sauces": "Sauces",
|
||||
"seasonings": "Assaisonnements",
|
||||
"bases": "Bases",
|
||||
"thickeners": "Épaississants",
|
||||
"sugars": "Sucres"
|
||||
},
|
||||
"quantityLabel": "Quantité",
|
||||
"unitLabel": "Unité",
|
||||
|
|
@ -319,7 +322,7 @@
|
|||
"vegetarian": "Végétarien",
|
||||
"vegan": "Végan",
|
||||
"pescatarian": "Pescétarien",
|
||||
"gluten_free": "Sans gluten"
|
||||
"glutenFree": "Sans gluten"
|
||||
},
|
||||
"allergens": {
|
||||
"gluten": "Gluten",
|
||||
|
|
@ -329,10 +332,10 @@
|
|||
"peanuts": "Arachides",
|
||||
"soy": "Soja",
|
||||
"milk": "Lait",
|
||||
"tree_nuts": "Fruits à coque",
|
||||
"treeNuts": "Fruits à coque",
|
||||
"celery": "Céleri",
|
||||
"mustard": "Moutarde",
|
||||
"sesame_seeds": "Graines de sésame",
|
||||
"sesameSeeds": "Graines de sésame",
|
||||
"sulfites": "Sulfites",
|
||||
"lupin": "Lupin",
|
||||
"molluscs": "Mollusques"
|
||||
|
|
@ -346,17 +349,17 @@
|
|||
"zucchini": "Courgette",
|
||||
"cucumber": "Concombre",
|
||||
"gherkins": "Cornichons",
|
||||
"bell_pepper": "Poivron",
|
||||
"bellPepper": "Poivron",
|
||||
"mushroom": "Champignon",
|
||||
"porcini": "Cèpes",
|
||||
"eggplant": "Aubergine",
|
||||
"broccoli": "Brocoli",
|
||||
"cauliflower": "Chou-fleur",
|
||||
"white_cabbage": "Chou blanc",
|
||||
"red_cabbage": "Chou rouge",
|
||||
"brussels_sprouts": "Chou de Bruxelles",
|
||||
"whiteCabbage": "Chou blanc",
|
||||
"redCabbage": "Chou rouge",
|
||||
"brusselsSprouts": "Chou de Bruxelles",
|
||||
"spinach": "Épinard",
|
||||
"swiss_chard": "Blette",
|
||||
"swissChard": "Blette",
|
||||
"lettuce": "Salade",
|
||||
"arugula": "Roquette",
|
||||
"watercress": "Cresson",
|
||||
|
|
@ -366,37 +369,37 @@
|
|||
"beetroot": "Betterave",
|
||||
"turnip": "Navet",
|
||||
"parsnip": "Panais",
|
||||
"green_bean": "Haricot vert",
|
||||
"greenBean": "Haricot vert",
|
||||
"pea": "Petit pois",
|
||||
"corn": "Maïs",
|
||||
"artichoke": "Artichaut",
|
||||
"fennel": "Fenouil",
|
||||
"endive": "Endive",
|
||||
"pumpkin": "Potiron",
|
||||
"butternut_squash": "Butternut",
|
||||
"butternutSquash": "Butternut",
|
||||
"asparagus": "Asperge",
|
||||
"avocado": "Avocat",
|
||||
"potato": "Pomme de terre",
|
||||
"sweet_potato": "Patate douce",
|
||||
"cherry_tomato": "Tomates cerises",
|
||||
"bok_choy": "Pak-choï",
|
||||
"soybean_sprouts": "Germes de soja",
|
||||
"sweetPotato": "Patate douce",
|
||||
"cherryTomato": "Tomates cerises",
|
||||
"bokChoy": "Pak-choï",
|
||||
"soybeanSprouts": "Germes de soja",
|
||||
"shiitake": "Shiitake",
|
||||
"daikon": "Daikon",
|
||||
"fresh_green_chili": "Piment vert frais",
|
||||
"freshGreenChili": "Piment vert frais",
|
||||
"cardoon": "Cardon",
|
||||
"radicchio": "Chicorée rouge",
|
||||
"romanesco": "Chou romanesco",
|
||||
"kohlrabi": "Chou-rave",
|
||||
"napa_cabbage": "Chou chinois",
|
||||
"napaCabbage": "Chou chinois",
|
||||
"celeriac": "Céleri-rave",
|
||||
"okra": "Gombo",
|
||||
"spring_onion": "Oignon nouveau",
|
||||
"red_kuri_squash": "Potimarron",
|
||||
"springOnion": "Oignon nouveau",
|
||||
"redKuriSquash": "Potimarron",
|
||||
"rutabaga": "Rutabaga",
|
||||
"samphire": "Salicorne",
|
||||
"salsify": "Salsifis",
|
||||
"lambs_lettuce": "Mâche",
|
||||
"lambsLettuce": "Mâche",
|
||||
"escarole": "Scarole",
|
||||
"lemon": "Citron",
|
||||
"lime": "Citron vert",
|
||||
|
|
@ -436,9 +439,9 @@
|
|||
"parsley": "Persil",
|
||||
"thyme": "Thym",
|
||||
"rosemary": "Romarin",
|
||||
"bay_leaf": "Laurier",
|
||||
"bayLeaf": "Laurier",
|
||||
"chives": "Ciboulette",
|
||||
"fresh_cilantro": "Coriandre fraîche",
|
||||
"freshCilantro": "Coriandre fraîche",
|
||||
"mint": "Menthe",
|
||||
"oregano": "Origan",
|
||||
"dill": "Aneth",
|
||||
|
|
@ -449,20 +452,20 @@
|
|||
"chervil": "Cerfeuil",
|
||||
"ginger": "Gingembre",
|
||||
"lemongrass": "Citronnelle",
|
||||
"kaffir_lime": "Combava",
|
||||
"kaffirLime": "Combava",
|
||||
"rabbit": "Lapin",
|
||||
"ground_beef": "Bœuf haché",
|
||||
"beef_steak": "Steak de bœuf",
|
||||
"beef_roast": "Rôti de bœuf",
|
||||
"veal_cutlet": "Escalope de veau",
|
||||
"pork_tenderloin": "Filet mignon de porc",
|
||||
"pork_chop": "Côte de porc",
|
||||
"groundBeef": "Bœuf haché",
|
||||
"beefSteak": "Steak de bœuf",
|
||||
"beefRoast": "Rôti de bœuf",
|
||||
"vealCutlet": "Escalope de veau",
|
||||
"porkTenderloin": "Filet mignon de porc",
|
||||
"porkChop": "Côte de porc",
|
||||
"lamb": "Agneau",
|
||||
"leg_of_lamb": "Gigot d'agneau",
|
||||
"bacon_lardons": "Lardons",
|
||||
"legOfLamb": "Gigot d'agneau",
|
||||
"baconLardons": "Lardons",
|
||||
"bacon": "Bacon",
|
||||
"ham": "Jambon blanc",
|
||||
"cured_ham": "Jambon cru",
|
||||
"curedHam": "Jambon cru",
|
||||
"sausage": "Saucisse",
|
||||
"chorizo": "Chorizo",
|
||||
"merguez": "Merguez",
|
||||
|
|
@ -472,36 +475,36 @@
|
|||
"salami": "Salami",
|
||||
"andouille": "Andouille",
|
||||
"andouillette": "Andouillette",
|
||||
"white_pudding": "Boudin blanc",
|
||||
"black_pudding": "Boudin noir",
|
||||
"whitePudding": "Boudin blanc",
|
||||
"blackPudding": "Boudin noir",
|
||||
"cervelat": "Cervelas",
|
||||
"rillettes": "Rillettes",
|
||||
"dry_cured_sausage": "Saucisson sec",
|
||||
"bayonne_ham": "Jambon de Bayonne",
|
||||
"dryCuredSausage": "Saucisson sec",
|
||||
"bayonneHam": "Jambon de Bayonne",
|
||||
"coppa": "Coppa",
|
||||
"rosette_sausage": "Rosette (saucisson)",
|
||||
"veal_liver": "Foie de veau",
|
||||
"veal_kidneys": "Rognons de veau",
|
||||
"veal_brain": "Cervelle de veau",
|
||||
"veal_sweetbread": "Ris de veau",
|
||||
"beef_tongue": "Langue de bœuf",
|
||||
"rosetteSausage": "Rosette (saucisson)",
|
||||
"vealLiver": "Foie de veau",
|
||||
"vealKidneys": "Rognons de veau",
|
||||
"vealBrain": "Cervelle de veau",
|
||||
"vealSweetbread": "Ris de veau",
|
||||
"beefTongue": "Langue de bœuf",
|
||||
"tripe": "Tripes",
|
||||
"venison": "Cerf",
|
||||
"roe_deer": "Chevreuil",
|
||||
"wild_boar": "Sanglier",
|
||||
"horse_meat": "Cheval",
|
||||
"beef_heart": "Cœur de bœuf",
|
||||
"foie_gras": "Foie gras",
|
||||
"beef_muzzle": "Museau de bœuf",
|
||||
"grisons_dried_beef": "Viande des Grisons",
|
||||
"roeDeer": "Chevreuil",
|
||||
"wildBoar": "Sanglier",
|
||||
"horseMeat": "Cheval",
|
||||
"beefHeart": "Cœur de bœuf",
|
||||
"foieGras": "Foie gras",
|
||||
"beefMuzzle": "Museau de bœuf",
|
||||
"grisonsDriedBeef": "Viande des Grisons",
|
||||
"chicken": "Poulet",
|
||||
"turkey": "Dinde",
|
||||
"duck": "Canard",
|
||||
"duck_breast": "Magret de canard",
|
||||
"duckBreast": "Magret de canard",
|
||||
"quail": "Caille",
|
||||
"guinea_fowl": "Pintade",
|
||||
"guineaFowl": "Pintade",
|
||||
"goose": "Oie",
|
||||
"poultry_liver": "Foie de volaille",
|
||||
"poultryLiver": "Foie de volaille",
|
||||
"capon": "Chapon",
|
||||
"pigeon": "Pigeon",
|
||||
"pheasant": "Faisan",
|
||||
|
|
@ -513,8 +516,8 @@
|
|||
"anchovy": "Anchois",
|
||||
"whiting": "Merlan",
|
||||
"surimi": "Surimi",
|
||||
"sea_bass": "Bar (loup de mer)",
|
||||
"sea_bream": "Dorade",
|
||||
"seaBass": "Bar (loup de mer)",
|
||||
"seaBream": "Dorade",
|
||||
"sole": "Sole",
|
||||
"turbot": "Turbot",
|
||||
"hake": "Merlu",
|
||||
|
|
@ -523,7 +526,7 @@
|
|||
"haddock": "Églefin",
|
||||
"mackerel": "Maquereau",
|
||||
"herring": "Hareng",
|
||||
"red_mullet": "Rouget",
|
||||
"redMullet": "Rouget",
|
||||
"skate": "Raie",
|
||||
"monkfish": "Lotte",
|
||||
"halibut": "Flétan",
|
||||
|
|
@ -533,18 +536,18 @@
|
|||
"perch": "Perche",
|
||||
"tilapia": "Tilapia",
|
||||
"pangasius": "Panga",
|
||||
"smoked_salmon": "Saumon fumé",
|
||||
"dried_fish": "Poisson séché",
|
||||
"smokedSalmon": "Saumon fumé",
|
||||
"driedFish": "Poisson séché",
|
||||
"eel": "Anguille",
|
||||
"plaice": "Carrelet (ou plie)",
|
||||
"salt_cod": "Morue",
|
||||
"lemon_sole": "Limande",
|
||||
"saltCod": "Morue",
|
||||
"lemonSole": "Limande",
|
||||
"scorpionfish": "Rascasse",
|
||||
"shrimp": "Crevettes",
|
||||
"langoustine": "Langoustines",
|
||||
"lobster": "Homard",
|
||||
"crab": "Crabe",
|
||||
"spiny_lobster": "Langouste",
|
||||
"spinyLobster": "Langouste",
|
||||
"mussels": "Moules",
|
||||
"oysters": "Huîtres",
|
||||
"scallops": "Saint-Jacques",
|
||||
|
|
@ -552,10 +555,10 @@
|
|||
"octopus": "Poulpe",
|
||||
"clams": "Palourdes",
|
||||
"whelks": "Bulots",
|
||||
"spider_crab": "Araignée de mer",
|
||||
"spiderCrab": "Araignée de mer",
|
||||
"periwinkle": "Bigorneau",
|
||||
"crayfish": "Écrevisse",
|
||||
"grey_shrimp": "Crevette grise",
|
||||
"greyShrimp": "Crevette grise",
|
||||
"cockle": "Coque",
|
||||
"snail": "Escargot",
|
||||
"cuttlefish": "Seiche",
|
||||
|
|
@ -565,103 +568,103 @@
|
|||
"polenta": "Polenta",
|
||||
"quinoa": "Quinoa",
|
||||
"pasta": "Pâtes",
|
||||
"whole_wheat_pasta": "Pâtes complètes",
|
||||
"wholeWheatPasta": "Pâtes complètes",
|
||||
"rice": "Riz",
|
||||
"basmati_rice": "Riz basmati",
|
||||
"brown_rice": "Riz complet",
|
||||
"basmatiRice": "Riz basmati",
|
||||
"brownRice": "Riz complet",
|
||||
"oats": "Flocons d'avoine",
|
||||
"spaghetti": "Spaghetti",
|
||||
"penne": "Penne",
|
||||
"tagliatelle": "Tagliatelles",
|
||||
"lasagna_sheets": "Lasagnes (feuilles)",
|
||||
"lasagnaSheets": "Lasagnes (feuilles)",
|
||||
"gnocchi": "Gnocchi",
|
||||
"arborio_rice": "Riz arborio",
|
||||
"rice_noodles": "Nouilles de riz",
|
||||
"udon_noodles": "Nouilles udon",
|
||||
"soba_noodles": "Nouilles soba",
|
||||
"chinese_noodles": "Nouilles chinoises",
|
||||
"rice_vermicelli": "Vermicelles de riz",
|
||||
"soy_vermicelli": "Vermicelles de soja",
|
||||
"sticky_rice": "Riz gluant",
|
||||
"sushi_rice": "Riz à sushi",
|
||||
"jasmine_rice": "Riz jasmin",
|
||||
"green_lentils": "Lentilles vertes",
|
||||
"red_lentils": "Lentilles corail",
|
||||
"arborioRice": "Riz arborio",
|
||||
"riceNoodles": "Nouilles de riz",
|
||||
"udonNoodles": "Nouilles udon",
|
||||
"sobaNoodles": "Nouilles soba",
|
||||
"chineseNoodles": "Nouilles chinoises",
|
||||
"riceVermicelli": "Vermicelles de riz",
|
||||
"soyVermicelli": "Vermicelles de soja",
|
||||
"stickyRice": "Riz gluant",
|
||||
"sushiRice": "Riz à sushi",
|
||||
"jasmineRice": "Riz jasmin",
|
||||
"greenLentils": "Lentilles vertes",
|
||||
"redLentils": "Lentilles corail",
|
||||
"chickpeas": "Pois chiches",
|
||||
"white_beans": "Haricots blancs",
|
||||
"kidney_beans": "Haricots rouges",
|
||||
"black_beans": "Haricots noirs",
|
||||
"split_peas": "Pois cassés",
|
||||
"fava_beans": "Fèves",
|
||||
"whiteBeans": "Haricots blancs",
|
||||
"kidneyBeans": "Haricots rouges",
|
||||
"blackBeans": "Haricots noirs",
|
||||
"splitPeas": "Pois cassés",
|
||||
"favaBeans": "Fèves",
|
||||
"edamame": "Edamame",
|
||||
"pinto_beans": "Haricots pinto",
|
||||
"flageolet_beans": "Haricots flageolets",
|
||||
"golden_lentils": "Lentilles blondes",
|
||||
"peanuts_shelled": "Cacahuètes",
|
||||
"pintoBeans": "Haricots pinto",
|
||||
"flageoletBeans": "Haricots flageolets",
|
||||
"goldenLentils": "Lentilles blondes",
|
||||
"peanutsShelled": "Cacahuètes",
|
||||
"almonds": "Amandes",
|
||||
"walnuts": "Noix",
|
||||
"hazelnuts": "Noisettes",
|
||||
"cashews": "Noix de cajou",
|
||||
"pistachios": "Pistaches",
|
||||
"pecans": "Noix de pécan",
|
||||
"almond_powder": "Poudre d'amande",
|
||||
"pine_nuts": "Pignons de pin",
|
||||
"sunflower_seeds": "Graines de tournesol",
|
||||
"pumpkin_seeds": "Graines de courge",
|
||||
"shredded_coconut": "Noix de coco râpée",
|
||||
"almondPowder": "Poudre d'amande",
|
||||
"pineNuts": "Pignons de pin",
|
||||
"sunflowerSeeds": "Graines de tournesol",
|
||||
"pumpkinSeeds": "Graines de courge",
|
||||
"shreddedCoconut": "Noix de coco râpée",
|
||||
"raisins": "Raisins secs",
|
||||
"prunes": "Pruneaux",
|
||||
"dried_apricots": "Abricots secs",
|
||||
"sesame_seeds": "Graines de sésame",
|
||||
"black_mushrooms": "Champignons noirs",
|
||||
"nori_seaweed": "Algue nori",
|
||||
"wakame_seaweed": "Algue wakamé",
|
||||
"kombu_seaweed": "Algue kombu",
|
||||
"bamboo_shoots": "Pousses de bambou",
|
||||
"water_chestnuts": "Châtaignes d'eau",
|
||||
"driedApricots": "Abricots secs",
|
||||
"sesameSeeds": "Graines de sésame",
|
||||
"blackMushrooms": "Champignons noirs",
|
||||
"noriSeaweed": "Algue nori",
|
||||
"wakameSeaweed": "Algue wakamé",
|
||||
"kombuSeaweed": "Algue kombu",
|
||||
"bambooShoots": "Pousses de bambou",
|
||||
"waterChestnuts": "Châtaignes d'eau",
|
||||
"bread": "Pain",
|
||||
"sandwich_bread": "Pain de mie",
|
||||
"whole_wheat_bread": "Pain complet",
|
||||
"sandwichBread": "Pain de mie",
|
||||
"wholeWheatBread": "Pain complet",
|
||||
"baguette": "Baguette",
|
||||
"rye_bread": "Pain de seigle",
|
||||
"ryeBread": "Pain de seigle",
|
||||
"breadcrumbs": "Chapelure",
|
||||
"burger_bun": "Pain à burger",
|
||||
"brioche_bun": "Pain brioché",
|
||||
"hot_dog_bun": "Pain à hot-dog",
|
||||
"pita_bread": "Pain pita",
|
||||
"burgerBun": "Pain à burger",
|
||||
"briocheBun": "Pain brioché",
|
||||
"hotDogBun": "Pain à hot-dog",
|
||||
"pitaBread": "Pain pita",
|
||||
"bagel": "Pain bagel",
|
||||
"naan": "Naan",
|
||||
"wrap_bread": "Pain wrap",
|
||||
"viennese_bread": "Pain viennois",
|
||||
"country_bread": "Pain de campagne",
|
||||
"multigrain_bread": "Pain aux céréales",
|
||||
"bread_roll": "Petit pain",
|
||||
"swedish_bread": "Pain suédois",
|
||||
"gluten_free_bread": "Pain sans gluten",
|
||||
"wrapBread": "Pain wrap",
|
||||
"vienneseBread": "Pain viennois",
|
||||
"countryBread": "Pain de campagne",
|
||||
"multigrainBread": "Pain aux céréales",
|
||||
"breadRoll": "Petit pain",
|
||||
"swedishBread": "Pain suédois",
|
||||
"glutenFreeBread": "Pain sans gluten",
|
||||
"rusk": "Biscotte",
|
||||
"croutons": "Croûtons",
|
||||
"focaccia": "Focaccia",
|
||||
"ciabatta": "Ciabatta",
|
||||
"corn_tortilla": "Tortilla de maïs",
|
||||
"wheat_tortilla": "Tortilla de blé",
|
||||
"cornTortilla": "Tortilla de maïs",
|
||||
"wheatTortilla": "Tortilla de blé",
|
||||
"breadstick": "Gressin",
|
||||
"puff_pastry": "Pâte feuilletée",
|
||||
"shortcrust_pastry": "Pâte brisée",
|
||||
"pizza_dough": "Pâte à pizza",
|
||||
"sweet_shortcrust_pastry": "Pâte à tarte sablée",
|
||||
"puffPastry": "Pâte feuilletée",
|
||||
"shortcrustPastry": "Pâte brisée",
|
||||
"pizzaDough": "Pâte à pizza",
|
||||
"sweetShortcrustPastry": "Pâte à tarte sablée",
|
||||
"milk": "Lait",
|
||||
"butter": "Beurre",
|
||||
"creme_fraiche": "Crème fraîche",
|
||||
"liquid_cream": "Crème liquide",
|
||||
"cremeFraiche": "Crème fraîche",
|
||||
"liquidCream": "Crème liquide",
|
||||
"cheese": "Fromage",
|
||||
"emmental": "Emmental",
|
||||
"gruyere": "Gruyère",
|
||||
"parmesan": "Parmesan",
|
||||
"mozzarella": "Mozzarella",
|
||||
"goat_cheese": "Chèvre (fromage)",
|
||||
"goatCheese": "Chèvre (fromage)",
|
||||
"feta": "Feta",
|
||||
"comte": "Comté",
|
||||
"fromage_blanc": "Fromage blanc",
|
||||
"fromageBlanc": "Fromage blanc",
|
||||
"mascarpone": "Mascarpone",
|
||||
"yogurt": "Yaourt",
|
||||
"burrata": "Burrata",
|
||||
|
|
@ -676,214 +679,214 @@
|
|||
"reblochon": "Reblochon",
|
||||
"cantal": "Cantal",
|
||||
"beaufort": "Beaufort",
|
||||
"saint_nectaire": "Saint-Nectaire",
|
||||
"blue_cheese": "Bleu (fromage)",
|
||||
"saintNectaire": "Saint-Nectaire",
|
||||
"blueCheese": "Bleu (fromage)",
|
||||
"cancoillotte": "Cancoillotte",
|
||||
"tomme": "Tomme",
|
||||
"epoisses": "Époisses",
|
||||
"chaource": "Chaource",
|
||||
"livarot": "Livarot",
|
||||
"pont_leveque": "Pont-l'Évêque",
|
||||
"pontLeveque": "Pont-l'Évêque",
|
||||
"morbier": "Morbier",
|
||||
"raclette_cheese": "Raclette (fromage)",
|
||||
"fourme_d_ambert": "Fourme d'Ambert",
|
||||
"racletteCheese": "Raclette (fromage)",
|
||||
"fourmeDAmbert": "Fourme d'Ambert",
|
||||
"salers": "Salers",
|
||||
"ossau_iraty": "Ossau-Iraty",
|
||||
"ossauIraty": "Ossau-Iraty",
|
||||
"vacherin": "Vacherin",
|
||||
"saint_marcellin": "Saint-Marcellin",
|
||||
"saintMarcellin": "Saint-Marcellin",
|
||||
"neufchatel": "Neufchâtel",
|
||||
"crottin_de_chavignol": "Crottin de Chavignol",
|
||||
"abondance_cheese": "Abondance",
|
||||
"carre_de_l_est": "Carré de l'Est",
|
||||
"crottinDeChavignol": "Crottin de Chavignol",
|
||||
"abondanceCheese": "Abondance",
|
||||
"carreDeLEst": "Carré de l'Est",
|
||||
"edam": "Edam",
|
||||
"gouda": "Gouda",
|
||||
"mimolette": "Mimolette",
|
||||
"maroilles": "Maroilles",
|
||||
"mont_dor": "Mont d'or",
|
||||
"montDor": "Mont d'or",
|
||||
"kefir": "Kéfir",
|
||||
"greek_yogurt": "Yaourt à la grecque",
|
||||
"greekYogurt": "Yaourt à la grecque",
|
||||
"egg": "Œuf",
|
||||
"coconut_milk": "Lait de coco",
|
||||
"coconut_cream": "Crème de coco",
|
||||
"almond_milk": "Lait d'amande",
|
||||
"oat_milk": "Lait d'avoine",
|
||||
"coconutMilk": "Lait de coco",
|
||||
"coconutCream": "Crème de coco",
|
||||
"almondMilk": "Lait d'amande",
|
||||
"oatMilk": "Lait d'avoine",
|
||||
"tofu": "Tofu",
|
||||
"silken_tofu": "Tofu soyeux",
|
||||
"herbes_de_provence": "Herbes de Provence",
|
||||
"black_pepper": "Poivre noir",
|
||||
"silkenTofu": "Tofu soyeux",
|
||||
"herbesDeProvence": "Herbes de Provence",
|
||||
"blackPepper": "Poivre noir",
|
||||
"paprika": "Paprika",
|
||||
"espelette_pepper": "Piment d'Espelette",
|
||||
"cayenne_pepper": "Piment de Cayenne",
|
||||
"espelettePepper": "Piment d'Espelette",
|
||||
"cayennePepper": "Piment de Cayenne",
|
||||
"cumin": "Cumin",
|
||||
"curry_powder": "Curry (poudre)",
|
||||
"curryPowder": "Curry (poudre)",
|
||||
"turmeric": "Curcuma",
|
||||
"cinnamon": "Cannelle",
|
||||
"nutmeg": "Muscade",
|
||||
"saffron": "Safran",
|
||||
"clove": "Clou de girofle",
|
||||
"vanilla_bean": "Vanille (gousse)",
|
||||
"white_pepper": "Poivre blanc",
|
||||
"pink_pepper": "Poivre rose",
|
||||
"sichuan_pepper": "Poivre du Sichuan",
|
||||
"smoked_paprika": "Paprika fumé",
|
||||
"bird_eye_chili": "Piment oiseau",
|
||||
"juniper_berries": "Baies de genièvre",
|
||||
"star_anise": "Anis étoilé (badiane)",
|
||||
"green_anise": "Anis vert",
|
||||
"fennel_seeds": "Graines de fenouil",
|
||||
"vanillaBean": "Vanille (gousse)",
|
||||
"whitePepper": "Poivre blanc",
|
||||
"pinkPepper": "Poivre rose",
|
||||
"sichuanPepper": "Poivre du Sichuan",
|
||||
"smokedPaprika": "Paprika fumé",
|
||||
"birdEyeChili": "Piment oiseau",
|
||||
"juniperBerries": "Baies de genièvre",
|
||||
"starAnise": "Anis étoilé (badiane)",
|
||||
"greenAnise": "Anis vert",
|
||||
"fennelSeeds": "Graines de fenouil",
|
||||
"sumac": "Sumac",
|
||||
"nigella": "Nigelle",
|
||||
"allspice": "Quatre épices",
|
||||
"colombo_powder": "Colombo (poudre)",
|
||||
"colomboPowder": "Colombo (poudre)",
|
||||
"baharat": "Baharat",
|
||||
"horseradish": "Raifort",
|
||||
"herb_salt": "Sel aux herbes",
|
||||
"celery_salt": "Sel de céleri",
|
||||
"fleur_de_sel": "Fleur de sel",
|
||||
"herbSalt": "Sel aux herbes",
|
||||
"celerySalt": "Sel de céleri",
|
||||
"fleurDeSel": "Fleur de sel",
|
||||
"salt": "Sel",
|
||||
"five_spice": "Cinq épices",
|
||||
"garam_masala": "Garam masala",
|
||||
"coriander_seeds": "Graines de coriandre",
|
||||
"fiveSpice": "Cinq épices",
|
||||
"garamMasala": "Garam masala",
|
||||
"corianderSeeds": "Graines de coriandre",
|
||||
"cardamom": "Cardamome",
|
||||
"fenugreek": "Fenugrec",
|
||||
"jalapeno": "Piment jalapeño",
|
||||
"chipotle": "Piment chipotle",
|
||||
"poblano_pepper": "Piment poblano",
|
||||
"poblanoPepper": "Piment poblano",
|
||||
"habanero": "Piment habanero",
|
||||
"ras_el_hanout": "Ras el hanout",
|
||||
"rasElHanout": "Ras el hanout",
|
||||
"zaatar": "Za'atar",
|
||||
"soy_sauce": "Sauce soja",
|
||||
"soySauce": "Sauce soja",
|
||||
"mustard": "Moutarde",
|
||||
"mayonnaise": "Mayonnaise",
|
||||
"ketchup": "Ketchup",
|
||||
"tabasco": "Tabasco",
|
||||
"worcestershire_sauce": "Sauce Worcestershire",
|
||||
"fish_sauce": "Sauce nuoc-mâm",
|
||||
"worcestershireSauce": "Sauce Worcestershire",
|
||||
"fishSauce": "Sauce nuoc-mâm",
|
||||
"wasabi": "Wasabi",
|
||||
"harissa": "Harissa",
|
||||
"curry_paste": "Pâte de curry",
|
||||
"peanut_butter": "Beurre de cacahuète",
|
||||
"dijon_mustard": "Moutarde de Dijon",
|
||||
"wholegrain_mustard": "Moutarde à l'ancienne",
|
||||
"barbecue_sauce": "Sauce barbecue",
|
||||
"tartar_sauce": "Sauce tartare",
|
||||
"cocktail_sauce": "Sauce cocktail",
|
||||
"bearnaise_sauce": "Sauce béarnaise",
|
||||
"hollandaise_sauce": "Sauce hollandaise",
|
||||
"bechamel_sauce": "Sauce béchamel",
|
||||
"teriyaki_sauce": "Sauce teriyaki",
|
||||
"ponzu_sauce": "Sauce ponzu",
|
||||
"curryPaste": "Pâte de curry",
|
||||
"peanutButter": "Beurre de cacahuète",
|
||||
"dijonMustard": "Moutarde de Dijon",
|
||||
"wholegrainMustard": "Moutarde à l'ancienne",
|
||||
"barbecueSauce": "Sauce barbecue",
|
||||
"tartarSauce": "Sauce tartare",
|
||||
"cocktailSauce": "Sauce cocktail",
|
||||
"bearnaiseSauce": "Sauce béarnaise",
|
||||
"hollandaiseSauce": "Sauce hollandaise",
|
||||
"bechamelSauce": "Sauce béchamel",
|
||||
"teriyakiSauce": "Sauce teriyaki",
|
||||
"ponzuSauce": "Sauce ponzu",
|
||||
"chimichurri": "Chimichurri",
|
||||
"red_pesto": "Pesto rouge (tomates séchées)",
|
||||
"redPesto": "Pesto rouge (tomates séchées)",
|
||||
"pesto": "Pesto",
|
||||
"oyster_sauce": "Sauce huître",
|
||||
"hoisin_sauce": "Sauce hoisin",
|
||||
"oysterSauce": "Sauce huître",
|
||||
"hoisinSauce": "Sauce hoisin",
|
||||
"sriracha": "Sauce sriracha",
|
||||
"sweet_chili_sauce": "Sauce sweet chili",
|
||||
"sweetChiliSauce": "Sauce sweet chili",
|
||||
"miso": "Miso",
|
||||
"shrimp_paste": "Pâte de crevettes",
|
||||
"red_curry_paste": "Pâte de curry rouge (thaï)",
|
||||
"green_curry_paste": "Pâte de curry vert (thaï)",
|
||||
"shrimpPaste": "Pâte de crevettes",
|
||||
"redCurryPaste": "Pâte de curry rouge (thaï)",
|
||||
"greenCurryPaste": "Pâte de curry vert (thaï)",
|
||||
"tahini": "Tahini",
|
||||
"aioli": "Aïoli",
|
||||
"vinaigrette": "Sauce vinaigrette",
|
||||
"hummus": "Houmous",
|
||||
"olive_oil": "Huile d'olive",
|
||||
"sunflower_oil": "Huile de tournesol",
|
||||
"rapeseed_oil": "Huile de colza",
|
||||
"coconut_oil": "Huile de coco",
|
||||
"sesame_oil": "Huile de sésame",
|
||||
"cider_vinegar": "Vinaigre de cidre",
|
||||
"white_vinegar": "Vinaigre blanc",
|
||||
"balsamic_vinegar": "Vinaigre balsamique",
|
||||
"oliveOil": "Huile d'olive",
|
||||
"sunflowerOil": "Huile de tournesol",
|
||||
"rapeseedOil": "Huile de colza",
|
||||
"coconutOil": "Huile de coco",
|
||||
"sesameOil": "Huile de sésame",
|
||||
"ciderVinegar": "Vinaigre de cidre",
|
||||
"whiteVinegar": "Vinaigre blanc",
|
||||
"balsamicVinegar": "Vinaigre balsamique",
|
||||
"capers": "Câpres",
|
||||
"olives": "Olives",
|
||||
"black_olives": "Olives noires",
|
||||
"green_olives": "Olives vertes",
|
||||
"white_wine": "Vin blanc (cuisine)",
|
||||
"red_wine": "Vin rouge (cuisine)",
|
||||
"rose_wine": "Vin rosé (cuisine)",
|
||||
"red_wine_vinegar": "Vinaigre de vin rouge",
|
||||
"white_wine_vinegar": "Vinaigre de vin blanc",
|
||||
"sherry_vinegar": "Vinaigre de xérès",
|
||||
"walnut_oil": "Huile de noix",
|
||||
"hazelnut_oil": "Huile de noisette",
|
||||
"peanut_oil": "Huile d'arachide",
|
||||
"chili_oil": "Huile pimentée",
|
||||
"rice_vinegar": "Vinaigre de riz",
|
||||
"corn_oil": "Huile de maïs",
|
||||
"grapeseed_oil": "Huile de pépins de raisin",
|
||||
"soybean_oil": "Huile de soja",
|
||||
"palm_oil": "Huile de palme",
|
||||
"blackOlives": "Olives noires",
|
||||
"greenOlives": "Olives vertes",
|
||||
"whiteWine": "Vin blanc (cuisine)",
|
||||
"redWine": "Vin rouge (cuisine)",
|
||||
"roseWine": "Vin rosé (cuisine)",
|
||||
"redWineVinegar": "Vinaigre de vin rouge",
|
||||
"whiteWineVinegar": "Vinaigre de vin blanc",
|
||||
"sherryVinegar": "Vinaigre de xérès",
|
||||
"walnutOil": "Huile de noix",
|
||||
"hazelnutOil": "Huile de noisette",
|
||||
"peanutOil": "Huile d'arachide",
|
||||
"chiliOil": "Huile pimentée",
|
||||
"riceVinegar": "Vinaigre de riz",
|
||||
"cornOil": "Huile de maïs",
|
||||
"grapeseedOil": "Huile de pépins de raisin",
|
||||
"soybeanOil": "Huile de soja",
|
||||
"palmOil": "Huile de palme",
|
||||
"mirin": "Mirin",
|
||||
"sake": "Saké (cuisine)",
|
||||
"lemon_juice": "Jus de citron",
|
||||
"lime_juice": "Jus de citron vert",
|
||||
"orange_juice": "Jus d'orange",
|
||||
"apple_juice": "Jus de pomme",
|
||||
"grape_juice": "Jus de raisin",
|
||||
"tomato_juice": "Jus de tomate",
|
||||
"cranberry_juice": "Jus de cranberry",
|
||||
"lemonJuice": "Jus de citron",
|
||||
"limeJuice": "Jus de citron vert",
|
||||
"orangeJuice": "Jus d'orange",
|
||||
"appleJuice": "Jus de pomme",
|
||||
"grapeJuice": "Jus de raisin",
|
||||
"tomatoJuice": "Jus de tomate",
|
||||
"cranberryJuice": "Jus de cranberry",
|
||||
"coffee": "Café",
|
||||
"tea": "Thé",
|
||||
"beer": "Bière (cuisine)",
|
||||
"cider": "Cidre (cuisine)",
|
||||
"champagne": "Champagne / vin pétillant (cuisine)",
|
||||
"port_wine": "Porto (cuisine)",
|
||||
"vin_jaune": "Vin jaune (cuisine)",
|
||||
"portWine": "Porto (cuisine)",
|
||||
"vinJaune": "Vin jaune (cuisine)",
|
||||
"cognac": "Cognac",
|
||||
"rum": "Rhum",
|
||||
"whisky": "Whisky",
|
||||
"vodka": "Vodka",
|
||||
"wheat_flour": "Farine de blé",
|
||||
"whole_wheat_flour": "Farine complète",
|
||||
"corn_flour": "Farine de maïs",
|
||||
"buckwheat_flour": "Farine de sarrasin",
|
||||
"rice_flour": "Farine de riz",
|
||||
"vegetable_stock_cube": "Bouillon cube légumes",
|
||||
"chicken_stock_cube": "Bouillon cube volaille",
|
||||
"tomato_paste": "Concentré de tomate",
|
||||
"tomato_coulis": "Coulis de tomate",
|
||||
"canned_peeled_tomatoes": "Tomates pelées (conserve)",
|
||||
"sun_dried_tomatoes": "Tomates séchées",
|
||||
"veal_stock": "Fond de veau",
|
||||
"chicken_stock": "Fond de volaille",
|
||||
"beef_stock_cube": "Bouillon cube bœuf",
|
||||
"fish_stock_cube": "Bouillon cube poisson",
|
||||
"vegetable_broth": "Bouillon de légumes",
|
||||
"chicken_broth": "Bouillon de volaille",
|
||||
"beef_broth": "Bouillon de bœuf",
|
||||
"court_bouillon": "Court-bouillon",
|
||||
"wheatFlour": "Farine de blé",
|
||||
"wholeWheatFlour": "Farine complète",
|
||||
"cornFlour": "Farine de maïs",
|
||||
"buckwheatFlour": "Farine de sarrasin",
|
||||
"riceFlour": "Farine de riz",
|
||||
"vegetableStockCube": "Bouillon cube légumes",
|
||||
"chickenStockCube": "Bouillon cube volaille",
|
||||
"tomatoPaste": "Concentré de tomate",
|
||||
"tomatoCoulis": "Coulis de tomate",
|
||||
"cannedPeeledTomatoes": "Tomates pelées (conserve)",
|
||||
"sunDriedTomatoes": "Tomates séchées",
|
||||
"vealStock": "Fond de veau",
|
||||
"chickenStock": "Fond de volaille",
|
||||
"beefStockCube": "Bouillon cube bœuf",
|
||||
"fishStockCube": "Bouillon cube poisson",
|
||||
"vegetableBroth": "Bouillon de légumes",
|
||||
"chickenBroth": "Bouillon de volaille",
|
||||
"beefBroth": "Bouillon de bœuf",
|
||||
"courtBouillon": "Court-bouillon",
|
||||
"dashi": "Dashi (bouillon japonais)",
|
||||
"shellfish_bisque": "Bisque de crustacés",
|
||||
"tapioca_flour": "Farine de tapioca",
|
||||
"masa_harina": "Masa harina",
|
||||
"shellfishBisque": "Bisque de crustacés",
|
||||
"tapiocaFlour": "Farine de tapioca",
|
||||
"masaHarina": "Masa harina",
|
||||
"water": "Eau",
|
||||
"sparkling_water": "Eau gazeuse",
|
||||
"orange_blossom_water": "Eau de fleur d'oranger",
|
||||
"rose_water": "Eau de rose",
|
||||
"fish_fumet": "Fumet de poisson",
|
||||
"bakers_yeast": "Levure boulangère",
|
||||
"baking_powder": "Levure chimique",
|
||||
"sparklingWater": "Eau gazeuse",
|
||||
"orangeBlossomWater": "Eau de fleur d'oranger",
|
||||
"roseWater": "Eau de rose",
|
||||
"fishFumet": "Fumet de poisson",
|
||||
"bakersYeast": "Levure boulangère",
|
||||
"bakingPowder": "Levure chimique",
|
||||
"cornstarch": "Maïzena",
|
||||
"lupin_flour": "Farine de lupin",
|
||||
"lupinFlour": "Farine de lupin",
|
||||
"gelatin": "Gélatine",
|
||||
"baking_soda": "Bicarbonate de soude",
|
||||
"potato_starch": "Fécule de pomme de terre",
|
||||
"bakingSoda": "Bicarbonate de soude",
|
||||
"potatoStarch": "Fécule de pomme de terre",
|
||||
"sugar": "Sucre",
|
||||
"honey": "Miel",
|
||||
"maple_syrup": "Sirop d'érable",
|
||||
"brown_sugar": "Sucre roux",
|
||||
"powdered_sugar": "Sucre glace",
|
||||
"demerara_sugar": "Cassonade",
|
||||
"dark_chocolate": "Chocolat noir",
|
||||
"milk_chocolate": "Chocolat au lait",
|
||||
"white_chocolate": "Chocolat blanc",
|
||||
"chocolate_chips": "Pépites de chocolat",
|
||||
"cocoa_powder": "Cacao en poudre",
|
||||
"vanilla_extract": "Extrait de vanille",
|
||||
"palm_sugar": "Sucre de palme",
|
||||
"cane_syrup": "Sirop de sucre de canne"
|
||||
"mapleSyrup": "Sirop d'érable",
|
||||
"brownSugar": "Sucre roux",
|
||||
"powderedSugar": "Sucre glace",
|
||||
"demeraraSugar": "Cassonade",
|
||||
"darkChocolate": "Chocolat noir",
|
||||
"milkChocolate": "Chocolat au lait",
|
||||
"whiteChocolate": "Chocolat blanc",
|
||||
"chocolateChips": "Pépites de chocolat",
|
||||
"cocoaPowder": "Cacao en poudre",
|
||||
"vanillaExtract": "Extrait de vanille",
|
||||
"palmSugar": "Sucre de palme",
|
||||
"caneSyrup": "Sirop de sucre de canne"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ErrorCode, type RecipeSummaryView, type RecipeTab } from "@batch-cooking/shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { ApiError, apiClient } from "../api/client";
|
||||
import { RecipeDetailPanel, type RecipeDetailState } from "../features/recipes/RecipeDetailPanel";
|
||||
import { RecipeTable } from "../features/recipes/RecipeTable";
|
||||
|
|
@ -30,10 +30,19 @@ export function RecipesPage() {
|
|||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const selectedId = id !== undefined ? Number(id) : null;
|
||||
// `?search=` lets another page (the recipe form's "faisable maison"
|
||||
// badge, see `ReproducibleBadge`) deep-link straight into a pre-filled
|
||||
// search — read once on mount, not kept in sync on every keystroke
|
||||
// afterwards (this page doesn't own the URL the way e.g. a shareable
|
||||
// filter view would).
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<RecipeTab>("favoris");
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const [search, setSearch] = useState(() => searchParams.get("search") ?? "");
|
||||
// Seeded from the same initial value as `search` — otherwise the first
|
||||
// fetch below would fire with an empty term (the debounce effect hasn't
|
||||
// run yet), then a second one 300ms later once it catches up.
|
||||
const [debouncedSearch, setDebouncedSearch] = useState(() => searchParams.get("search") ?? "");
|
||||
const [listState, setListState] = useState<RecipeListState>({ status: "loading" });
|
||||
const [detailState, setDetailState] = useState<RecipeDetailState>({ status: "empty" });
|
||||
const [dislikedIngredientIds, setDislikedIngredientIds] = useState<number[]>([]);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
* A dietary regime, as returned by `GET /reference/diets` — reference data
|
||||
* (`Diet`, seeded via `apps/api/prisma/seed.ts`), not user-specific.
|
||||
*
|
||||
* `key` is a stable slug (e.g. `"vegetarien"`), not a display label — it
|
||||
* never changes once seeded, unlike the label it stands in for. Callers
|
||||
* resolve the label themselves via i18n (`t(\`catalog.diets.${key}\`)`,
|
||||
* `apps/web`'s `locales/fr/translation.json`), the same way
|
||||
* `IngredientCategory`/`IngredientSubcategory` enum values already do (see
|
||||
* `recipes.form.category.*`/`recipes.form.subcategory.*` in that file).
|
||||
* `key` is a stable, English camelCase uid (e.g. `"vegetarian"`), not a
|
||||
* display label — it never changes once seeded, unlike the label it stands
|
||||
* in for. Callers resolve the label themselves via i18n
|
||||
* (`t(\`catalog.diets.${key}\`)`, `apps/web`'s `locales/fr/translation.json`),
|
||||
* the same way `IngredientCategory`/`IngredientSubcategory` enum values
|
||||
* already do (see `recipes.form.category.*`/`recipes.form.subcategory.*` in
|
||||
* that file).
|
||||
*/
|
||||
export interface DietView {
|
||||
id: number;
|
||||
|
|
@ -29,11 +30,12 @@ export type AllergenKind = "ALLERGY" | "INTOLERANCE";
|
|||
* table itself carries no key of its own (see `schema.prisma`), so this
|
||||
* flattens that split away: callers just get `{id, key}` and never need to
|
||||
* know a `Category` exists underneath. Like {@link DietView.key}, it's a
|
||||
* stable slug (e.g. `"gluten"`), not a display label — resolved via
|
||||
* `t(\`catalog.allergens.${key}\`)`. `kind` groups allergens into two
|
||||
* separate lists client-side (`AllergySelect`, `apps/web`) rather than one
|
||||
* flat "allergies & intolérances" list — a single `PATCH /profile/allergies`
|
||||
* call still covers both, this is a display grouping only.
|
||||
* stable English camelCase uid (e.g. `"gluten"`), not a display label —
|
||||
* resolved via `t(\`catalog.allergens.${key}\`)`. `kind` groups allergens
|
||||
* into two separate lists client-side (`AllergySelect`, `apps/web`) rather
|
||||
* than one flat "allergies & intolerances" list — a single
|
||||
* `PATCH /profile/allergies` call still covers both, this is a display
|
||||
* grouping only.
|
||||
*/
|
||||
export interface AllergyView {
|
||||
id: number;
|
||||
|
|
@ -51,13 +53,13 @@ export interface AllergyView {
|
|||
* rack within each aisle.
|
||||
*/
|
||||
export const INGREDIENT_CATEGORIES = [
|
||||
"PRODUITS_FRAIS",
|
||||
"BOUCHERIE_POISSONNERIE",
|
||||
"EPICERIE_SECHE",
|
||||
"BOULANGERIE",
|
||||
"CREMERIE_FROMAGE",
|
||||
"CONDIMENTS_EPICES",
|
||||
"AIDES_CULINAIRES",
|
||||
"freshProduce",
|
||||
"meatAndSeafood",
|
||||
"dryGoods",
|
||||
"bakery",
|
||||
"dairyAndCheese",
|
||||
"condimentsAndSpices",
|
||||
"cookingEssentials",
|
||||
] as const;
|
||||
/** Inferred TS type for one {@link INGREDIENT_CATEGORIES} member. */
|
||||
export type IngredientCategory = (typeof INGREDIENT_CATEGORIES)[number];
|
||||
|
|
@ -69,28 +71,28 @@ export type IngredientCategory = (typeof INGREDIENT_CATEGORIES)[number];
|
|||
* which category, and in what display order.
|
||||
*/
|
||||
export const INGREDIENT_SUBCATEGORIES = [
|
||||
"LEGUMES",
|
||||
"FRUITS",
|
||||
"HERBES_FRAICHES",
|
||||
"VIANDES",
|
||||
"VOLAILLES",
|
||||
"POISSONS",
|
||||
"CRUSTACES_FRUITS_DE_MER",
|
||||
"FECULENTS",
|
||||
"LEGUMINEUSES",
|
||||
"GRAINES_FRUITS_SECS",
|
||||
"AUTRES",
|
||||
"PAINS",
|
||||
"PATES_A_CUIRE",
|
||||
"PRODUITS_LAITIERS",
|
||||
"OEUFS",
|
||||
"ALTERNATIVES",
|
||||
"EPICES",
|
||||
"SAUCES",
|
||||
"ASSAISONNEMENTS",
|
||||
"BASES",
|
||||
"EPAISSISSANTS",
|
||||
"SUCRES",
|
||||
"vegetables",
|
||||
"fruits",
|
||||
"freshHerbs",
|
||||
"meats",
|
||||
"poultry",
|
||||
"fish",
|
||||
"shellfish",
|
||||
"starches",
|
||||
"legumes",
|
||||
"nutsAndSeeds",
|
||||
"other",
|
||||
"breads",
|
||||
"rawDough",
|
||||
"dairy",
|
||||
"eggs",
|
||||
"plantBasedAlternatives",
|
||||
"spices",
|
||||
"sauces",
|
||||
"seasonings",
|
||||
"bases",
|
||||
"thickeners",
|
||||
"sugars",
|
||||
] as const;
|
||||
/** Inferred TS type for one {@link INGREDIENT_SUBCATEGORIES} member. */
|
||||
export type IngredientSubcategory = (typeof INGREDIENT_SUBCATEGORIES)[number];
|
||||
|
|
@ -109,13 +111,13 @@ export const INGREDIENT_CATEGORY_SUBCATEGORIES: Record<
|
|||
IngredientCategory,
|
||||
readonly IngredientSubcategory[]
|
||||
> = {
|
||||
PRODUITS_FRAIS: ["LEGUMES", "FRUITS", "HERBES_FRAICHES"],
|
||||
BOUCHERIE_POISSONNERIE: ["VIANDES", "VOLAILLES", "POISSONS", "CRUSTACES_FRUITS_DE_MER"],
|
||||
EPICERIE_SECHE: ["FECULENTS", "LEGUMINEUSES", "GRAINES_FRUITS_SECS", "AUTRES"],
|
||||
BOULANGERIE: ["PAINS", "PATES_A_CUIRE"],
|
||||
CREMERIE_FROMAGE: ["PRODUITS_LAITIERS", "OEUFS", "ALTERNATIVES"],
|
||||
CONDIMENTS_EPICES: ["EPICES", "SAUCES", "ASSAISONNEMENTS"],
|
||||
AIDES_CULINAIRES: ["BASES", "EPAISSISSANTS", "SUCRES"],
|
||||
freshProduce: ["vegetables", "fruits", "freshHerbs"],
|
||||
meatAndSeafood: ["meats", "poultry", "fish", "shellfish"],
|
||||
dryGoods: ["starches", "legumes", "nutsAndSeeds", "other"],
|
||||
bakery: ["breads", "rawDough"],
|
||||
dairyAndCheese: ["dairy", "eggs", "plantBasedAlternatives"],
|
||||
condimentsAndSpices: ["spices", "sauces", "seasonings"],
|
||||
cookingEssentials: ["bases", "thickeners", "sugars"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -165,17 +167,18 @@ export type IngredientIcon = (typeof INGREDIENT_ICONS)[number];
|
|||
* `allergens` is resolved server-side from the `IngredientAllergy` join
|
||||
* table — empty for an ingredient that carries none of the 14 EU-regulated
|
||||
* allergens. `diets` is resolved from `IngredientDiet` the same way — the
|
||||
* regimes this ingredient is compatible with (e.g. `Végétarien`, `Végan`),
|
||||
* regimes this ingredient is compatible with (e.g. `vegetarian`, `vegan`),
|
||||
* 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
|
||||
* `omnivore` (every ingredient qualifies, so it's never stored) and
|
||||
* `glutenFree` (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.
|
||||
*
|
||||
* `key` is a stable slug (e.g. `"tomate"`), not a display label — like
|
||||
* {@link DietView.key}, resolved via `t(\`catalog.ingredients.${key}\`)`.
|
||||
* `key` is a stable English camelCase uid (e.g. `"tomato"`), not a display
|
||||
* label — like {@link DietView.key}, resolved via
|
||||
* `t(\`catalog.ingredients.${key}\`)`.
|
||||
*/
|
||||
export interface IngredientView {
|
||||
id: number;
|
||||
|
|
@ -183,6 +186,8 @@ export interface IngredientView {
|
|||
icon: IngredientIcon;
|
||||
category: IngredientCategory;
|
||||
subcategory: IngredientSubcategory;
|
||||
/** Whether this ingredient is reasonably makeable at home (a burger bun, a béchamel) rather than something you'd only ever buy — see `Ingredient.reproducible` in schema.prisma. Surfaced as a badge/link in the recipe form nudging toward the recipe catalog's own search, not a link to a specific recipe. */
|
||||
reproducible: boolean;
|
||||
allergens: AllergyView[];
|
||||
diets: DietView[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue