feat(recipes): remplace les emoji par des icônes SVG épurées
Deux usages d'emoji supprimés, remplacés par un vrai jeu d'icônes SVG
(style ligne, cohérent avec layouts/nav-icons.tsx) :
- Catégories/sous-catégories du picker (auparavant préfixées d'un
emoji dans les libellés) : nouvelles icônes CategoryIcon/
SubcategoryIcon, une par catégorie/sous-catégorie.
- Icône par ingrédient (auparavant un emoji différent pour chacun des
437 ingrédients) : remplacé par un petit vocabulaire de ~22
pictogrammes génériques ("un légume", "une bouteille", "un
fromage"...) réutilisés selon la nature de l'ingrédient plutôt
qu'un dessin par ingrédient (irréaliste à la main pour 437 items).
- Schéma : nouvel enum IngredientIcon (22 valeurs), colonne
Ingredient.icon passe de String? (texte libre) à IngredientIcon
(non nullable, toujours une valeur générique pertinente désormais).
- reference-seed-data.ts : chaque groupe porte un defaultIcon (calqué
sur sa sous-catégorie), avec override par ingrédient pour les
exceptions (ex. les fromages dans "produits laitiers", les jus/cafés
dans "assaisonnements").
- apps/web/src/features/recipes/ingredient-icons.tsx (nouveau) :
22 icônes SVG + CategoryIcon/SubcategoryIcon (réutilisent le même
vocabulaire pour représenter chaque catégorie/sous-catégorie).
- packages/shared : IngredientView.icon devient IngredientIcon (union
de 22 valeurs) au lieu de string | null.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9775cbe38f
commit
78e0f89503
10 changed files with 964 additions and 440 deletions
|
|
@ -0,0 +1,6 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "IngredientIcon" AS ENUM ('VEGETABLE', 'FRUIT', 'HERB', 'MEAT', 'POULTRY', 'FISH', 'SHELLFISH', 'GRAIN', 'LEGUME', 'NUT_SEED', 'BREAD', 'DOUGH', 'MILK', 'CHEESE', 'EGG', 'SPROUT', 'SPICE', 'JAR', 'BOTTLE', 'DRINK', 'STOCK_POT', 'SUGAR');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ingredients" DROP COLUMN "icon",
|
||||
ADD COLUMN "icon" "IngredientIcon" NOT NULL DEFAULT 'JAR';
|
||||
|
|
@ -384,10 +384,48 @@ enum IngredientSubcategory {
|
|||
SUCRES
|
||||
}
|
||||
|
||||
/// Generic pictogram *type* for an ingredient — not in the original spec
|
||||
/// doc. Started as a free-text emoji column (one character per ingredient,
|
||||
/// 437 different ones), which the product decision recorded in chat
|
||||
/// rejected as unprofessional/inconsistent. Rather than 437 hand-drawn SVG
|
||||
/// icons (unrealistic), ingredients share a small vocabulary of ~20
|
||||
/// generic shapes grouped by *what kind of thing* they are — a vegetable,
|
||||
/// a bottle of oil, a wedge of cheese — regardless of which specific
|
||||
/// ingredient. `apps/web`'s `features/recipes/ingredient-icons.tsx` maps
|
||||
/// each value to its actual SVG (matching the app's hand-drawn line-icon
|
||||
/// style, never emoji — see that file for the full reasoning and the
|
||||
/// exact `reference-seed-data.ts` assignment per ingredient).
|
||||
/// `@default(JAR)` — same NOT-NULL-migration-safety-net reasoning as
|
||||
/// `IngredientCategory`'s default, never the intended value for a real row.
|
||||
enum IngredientIcon {
|
||||
VEGETABLE
|
||||
FRUIT
|
||||
HERB
|
||||
MEAT
|
||||
POULTRY
|
||||
FISH
|
||||
SHELLFISH
|
||||
GRAIN
|
||||
LEGUME
|
||||
NUT_SEED
|
||||
BREAD
|
||||
DOUGH
|
||||
MILK
|
||||
CHEESE
|
||||
EGG
|
||||
SPROUT
|
||||
SPICE
|
||||
JAR
|
||||
BOTTLE
|
||||
DRINK
|
||||
STOCK_POT
|
||||
SUGAR
|
||||
}
|
||||
|
||||
model Ingredient {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
icon String?
|
||||
icon IngredientIcon @default(JAR)
|
||||
category IngredientCategory @default(EPICERIE_SECHE)
|
||||
subcategory IngredientSubcategory @default(AUTRES)
|
||||
alternateRecipeId Int? @map("alternate_recipe")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
import type { IngredientView } from "@batch-cooking/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IngredientPicker } from "../recipes/IngredientPicker";
|
||||
// Reuses `IngredientPicker` verbatim (built for the recipe form's
|
||||
// ingredient picker, features/recipes/) rather than a second search+browse
|
||||
// field — same reference ingredient list, same category/search browsing,
|
||||
|
|
@ -9,7 +10,7 @@ import { useTranslation } from "react-i18next";
|
|||
// recipes.scss — colocated there since it's the same "reference ingredient
|
||||
// picker" visual family, even though this field lives in the profile
|
||||
// feature).
|
||||
import { IngredientPicker } from "../recipes/IngredientPicker";
|
||||
import { IngredientTypeIcon } from "../recipes/ingredient-icons";
|
||||
import "../recipes/recipes.scss";
|
||||
import "./profile-forms.scss";
|
||||
|
||||
|
|
@ -50,7 +51,9 @@ export function DislikedIngredientsField({
|
|||
<ul className="disliked-ingredients-field__chips">
|
||||
{selected.map((ingredient) => (
|
||||
<li key={ingredient.id} className="disliked-ingredients-field__chip">
|
||||
<span aria-hidden="true">{ingredient.icon}</span>
|
||||
<span aria-hidden="true">
|
||||
<IngredientTypeIcon icon={ingredient.icon} />
|
||||
</span>
|
||||
{ingredient.name}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { AllergenIcon, DietPreferencesIcon } from "../../layouts/nav-icons";
|
||||
import { AllergenBadges } from "./AllergenBadges";
|
||||
import { DietBadges } from "./DietBadges";
|
||||
import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons";
|
||||
import "./recipes.scss";
|
||||
|
||||
/** "No filter at this level" — a UI-only pseudo-value, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES}/{@link INGREDIENT_CATEGORY_SUBCATEGORIES} for the real, closed sets). */
|
||||
|
|
@ -126,6 +127,7 @@ export function IngredientPicker({
|
|||
className={`ingredient-picker__category${category === c ? " active" : ""}`}
|
||||
onClick={() => selectCategory(c)}
|
||||
>
|
||||
<CategoryIcon category={c} />
|
||||
{t(`recipes.form.category.${c}`)}
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -147,6 +149,7 @@ export function IngredientPicker({
|
|||
className={`ingredient-picker__subcategory${subcategory === s ? " active" : ""}`}
|
||||
onClick={() => setSubcategory(s)}
|
||||
>
|
||||
<SubcategoryIcon subcategory={s} />
|
||||
{t(`recipes.form.subcategory.${s}`)}
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -165,7 +168,7 @@ export function IngredientPicker({
|
|||
onClick={() => handleSelect(ingredient)}
|
||||
>
|
||||
<span className="ingredient-picker__card-icon" aria-hidden="true">
|
||||
{ingredient.icon}
|
||||
<IngredientTypeIcon icon={ingredient.icon} />
|
||||
</span>
|
||||
<span className="ingredient-picker__card-name">{ingredient.name}</span>
|
||||
{showAllergens && <AllergenBadges allergens={ingredient.allergens} />}
|
||||
|
|
|
|||
|
|
@ -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 { IngredientTypeIcon } from "./ingredient-icons";
|
||||
import "./recipes.scss";
|
||||
|
||||
/** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientPicker`) plus its quantity/unit for this recipe. Quantity/unit are kept as raw strings while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input. */
|
||||
|
|
@ -25,7 +26,7 @@ export function IngredientRow({
|
|||
return (
|
||||
<li className="ingredient-row">
|
||||
<span className="ingredient-row__icon" aria-hidden="true">
|
||||
{ingredient.icon}
|
||||
<IngredientTypeIcon icon={ingredient.icon} />
|
||||
</span>
|
||||
<span className="ingredient-row__name">{ingredient.name}</span>
|
||||
<input
|
||||
|
|
|
|||
360
apps/web/src/features/recipes/ingredient-icons.tsx
Normal file
360
apps/web/src/features/recipes/ingredient-icons.tsx
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
import type {
|
||||
IngredientCategory,
|
||||
IngredientIcon as IngredientIconType,
|
||||
IngredientSubcategory,
|
||||
} from "@batch-cooking/shared";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Generic pictograms for ingredients — one per `IngredientIcon` value
|
||||
// (schema.prisma), reused across the category chips, subcategory chips and
|
||||
// ingredient cards in `IngredientPicker`/`IngredientRow`. Replaces the
|
||||
// earlier per-ingredient emoji (437 different characters, one per
|
||||
// ingredient) with a small, hand-drawn vocabulary grouped by *what kind of
|
||||
// thing* an ingredient is (a vegetable, a bottle of oil, a wedge of
|
||||
// cheese…) rather than a literal picture of each one — 437 unique,
|
||||
// professional icons isn't a realistic hand-drawn set, but ~20 generic
|
||||
// shapes reused across them is. Same 24×24, stroke-only house style as
|
||||
// `layouts/nav-icons.tsx` (kept separate from that file since these are
|
||||
// ingredient-domain, not app-nav).
|
||||
function Icon({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Carrot — root vegetables, leafy greens, and produce generally (`PRODUITS_FRAIS`/`LEGUMES`). */
|
||||
export function VegetableIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 3c1.5 1 2 2.5 1.3 4M15 2c1 1.2 1 2.6 0 4" />
|
||||
<path d="M13.3 7c2.8 0 4.7 2.9 4.1 5.6L15.8 20a2 2 0 0 1-3.9.2L9.2 12.6C8.3 9.9 10.3 7 13.3 7z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Apple — `PRODUITS_FRAIS`/`FRUITS`. */
|
||||
export function FruitIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 8c-.5-1.3-.2-3 1-4" />
|
||||
<path d="M12 8c-4 0-6.5 3-6.5 7a6.5 6.5 0 0 0 13 0C18.5 11 16 8 12 8z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A leaf sprig — `PRODUITS_FRAIS`/`HERBES_FRAICHES`. */
|
||||
export function HerbIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 21V7" />
|
||||
<path d="M12 8c0-3 2-5 5-5 0 3-2 5-5 5z" />
|
||||
<path d="M12 13c0-3-2-5-5-5 0 3 2 5 5 5z" />
|
||||
<path d="M12 18c0-2.5 1.7-4 4-4 0 2.5-1.7 4-4 4z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A cut of meat — `BOUCHERIE_POISSONNERIE`/`VIANDES`. */
|
||||
export function MeatIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M8 16 17 7a3 3 0 1 1 4 4l-9 9a5 5 0 0 1-8-2 5 5 0 0 1 4-4z" />
|
||||
<circle cx="9" cy="15" r="1.2" fill="currentColor" stroke="none" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Drumstick — `BOUCHERIE_POISSONNERIE`/`VOLAILLES`. */
|
||||
export function PoultryIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M13 4c2.5 0 4.5 2 4.5 4.5 0 2.8-2.3 4.8-4 6.4l-3.2 3.2a2.3 2.3 0 0 1-3.3-3.3l3.2-3.2C11.7 10 13.7 7.7 13 4z" />
|
||||
<path d="M6.5 21 5 22.5" />
|
||||
<path d="M9 18.5 7.5 20" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Fish — `BOUCHERIE_POISSONNERIE`/`POISSONS`. */
|
||||
export function FishIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M3 12c3-4 8-6 13-4 2.5 1 5 3 5 4s-2.5 3-5 4c-5 2-10 0-13-4z" />
|
||||
<path d="M16 9v6" />
|
||||
<circle cx="6.5" cy="11" r=".6" fill="currentColor" stroke="none" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Shrimp — `BOUCHERIE_POISSONNERIE`/`CRUSTACES_FRUITS_DE_MER`. */
|
||||
export function ShellfishIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M6 6c6-2 12 1 13 7-4 2-9 2-12-1C5 10 4.5 8 6 6z" />
|
||||
<path d="M6 6 4 4M7 8 5 7M8 11l-2.2.6" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Wheat ear — grains, pasta, rice, flour (`EPICERIE_SECHE`/`FECULENTS`, and flours under `AIDES_CULINAIRES`/`BASES`). */
|
||||
export function GrainIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 22V6" />
|
||||
<path d="M12 7c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 7c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
|
||||
<path d="M12 11c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 11c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
|
||||
<path d="M12 15c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 15c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
|
||||
<path d="M12 6c0-1.5.8-2.8 2-3.5" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bean pod — `EPICERIE_SECHE`/`LEGUMINEUSES`. */
|
||||
export function LegumeIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M5 13c0-5 3.5-9 9-9 1 3-1 5-1 5s3 0 5 3c0 5-4 9-9 9-3 0-4-4-4-8z" />
|
||||
<circle cx="9.5" cy="14.5" r="1" fill="currentColor" stroke="none" />
|
||||
<circle cx="13" cy="11" r="1" fill="currentColor" stroke="none" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Acorn — nuts, seeds, dried fruit (`EPICERIE_SECHE`/`GRAINES_FRUITS_SECS`). */
|
||||
export function NutSeedIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M8 9c0-3 1.8-5 4-5s4 2 4 5" />
|
||||
<path d="M8 9c-1.3.5-2 1.7-2 3 0 4 2.7 8 6 8s6-4 6-8c0-1.3-.7-2.5-2-3z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A loaf, scored on top — `BOULANGERIE`/`PAINS`. */
|
||||
export function BreadIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M4 12c0-4.4 3.6-8 8-8s8 3.6 8 8v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
||||
<path d="M9 8l1.5 3M14 7.5 15.5 11" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Rolling pin — raw, uncooked pastry (`BOULANGERIE`/`PATES_A_CUIRE`). */
|
||||
export function DoughIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<rect x="6" y="10" width="12" height="4" rx="2" />
|
||||
<circle cx="4" cy="12" r="2" />
|
||||
<circle cx="20" cy="12" r="2" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A glass of milk — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (non-cheese items). */
|
||||
export function MilkIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M9 2h6l1 4-1 2v11a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V8L8 6z" />
|
||||
<path d="M8 12h8" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A wedge of cheese with holes — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (cheese items). */
|
||||
export function CheeseIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M3 18 20 8l1 3-3 9H4z" />
|
||||
<circle cx="10" cy="15" r="1" fill="currentColor" stroke="none" />
|
||||
<circle cx="15" cy="13" r="1" fill="currentColor" stroke="none" />
|
||||
<circle cx="14" cy="17" r="1" fill="currentColor" stroke="none" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** An egg — `CREMERIE_FROMAGE`/`OEUFS`. */
|
||||
export function EggIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 3C8 8 6 12.5 6 15.5a6 6 0 0 0 12 0C18 12.5 16 8 12 3z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A seedling — plant-based dairy/meat alternatives (`CREMERIE_FROMAGE`/`ALTERNATIVES`). */
|
||||
export function SproutIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M12 21v-9" />
|
||||
<path d="M12 12C7 12 5 9 5 5c5 0 7 2 7 7z" />
|
||||
<path d="M12 12c4 0 6-2.5 6-6-4 0-6 1.5-6 6z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A shaker — dried spices/herbs (`CONDIMENTS_EPICES`/`EPICES`). */
|
||||
export function SpiceIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<rect x="8" y="8" width="8" height="13" rx="3" />
|
||||
<path d="M10 8V5a2 2 0 0 1 4 0v3" />
|
||||
<path d="M11 12h.01M13 12h.01M10.5 15h.01M13.5 15h.01M12 18h.01" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A condiment jar — sauces, pickles, tinned/preserved goods (`CONDIMENTS_EPICES`/`SAUCES` and the `EPICERIE_SECHE`/`AUTRES` catch-all). */
|
||||
export function JarIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<rect x="6" y="9" width="12" height="12" rx="2" />
|
||||
<path d="M9 9V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
|
||||
<path d="M6 13h12" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A bottle — oils, vinegars (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`, the pourable subset). */
|
||||
export function BottleIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M10 2h4v3.5l1.5 2V20a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V7.5L10 5.5z" />
|
||||
<path d="M9.5 11h5" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A glass — juices, coffee/tea, cooking alcohols, water (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`'s drinkable subset). */
|
||||
export function DrinkIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M6 3h12l-1.5 16.5A2 2 0 0 1 14.5 21h-5a2 2 0 0 1-2-1.5z" />
|
||||
<path d="M7 9h10" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A stockpot — broths, stocks, water bases (`AIDES_CULINAIRES`/`BASES`'s liquid-base subset). */
|
||||
export function StockPotIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<rect x="4" y="10" width="16" height="10" rx="2" />
|
||||
<path d="M2 12h2M20 12h2" />
|
||||
<path d="M9 10V7a3 3 0 0 1 6 0v3" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** A sugar cube — `AIDES_CULINAIRES`/`SUCRES`. */
|
||||
export function SugarIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M4 8 12 4l8 4-8 4z" />
|
||||
<path d="M4 8v8l8 4 8-4V8" />
|
||||
<path d="M12 12v8" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/** Every icon component, keyed by {@link IngredientIconType} — the single lookup `IngredientPicker`/`IngredientRow`/category chips render through. */
|
||||
export const INGREDIENT_ICON_COMPONENTS: Record<IngredientIconType, () => JSX.Element> = {
|
||||
VEGETABLE: VegetableIcon,
|
||||
FRUIT: FruitIcon,
|
||||
HERB: HerbIcon,
|
||||
MEAT: MeatIcon,
|
||||
POULTRY: PoultryIcon,
|
||||
FISH: FishIcon,
|
||||
SHELLFISH: ShellfishIcon,
|
||||
GRAIN: GrainIcon,
|
||||
LEGUME: LegumeIcon,
|
||||
NUT_SEED: NutSeedIcon,
|
||||
BREAD: BreadIcon,
|
||||
DOUGH: DoughIcon,
|
||||
MILK: MilkIcon,
|
||||
CHEESE: CheeseIcon,
|
||||
EGG: EggIcon,
|
||||
SPROUT: SproutIcon,
|
||||
SPICE: SpiceIcon,
|
||||
JAR: JarIcon,
|
||||
BOTTLE: BottleIcon,
|
||||
DRINK: DrinkIcon,
|
||||
STOCK_POT: StockPotIcon,
|
||||
SUGAR: SugarIcon,
|
||||
};
|
||||
|
||||
/** Renders the generic pictogram for one {@link IngredientIconType} — the actual component `IngredientPicker`'s cards and `IngredientRow` use, rather than importing {@link INGREDIENT_ICON_COMPONENTS} directly everywhere. */
|
||||
export function IngredientTypeIcon({ icon }: { icon: IngredientIconType }) {
|
||||
const Component = INGREDIENT_ICON_COMPONENTS[icon];
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
/**
|
||||
* One representative {@link IngredientIconType} per {@link IngredientCategory}
|
||||
* aisle, for `IngredientPicker`'s top-level category chips — a category
|
||||
* mixes several icon types (e.g. "Aides culinaires" has flour, stock, and
|
||||
* sugar items), so this is a deliberate, illustrative pick rather than a
|
||||
* 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",
|
||||
};
|
||||
|
||||
/** Renders {@link CATEGORY_ICON}'s pictogram for one category — used by the category chip row. */
|
||||
export function CategoryIcon({ category }: { category: IngredientCategory }) {
|
||||
return <IngredientTypeIcon icon={CATEGORY_ICON[category]} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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",
|
||||
};
|
||||
|
||||
/** Renders {@link SUBCATEGORY_ICON}'s pictogram for one subcategory — used by the subcategory chip row. */
|
||||
export function SubcategoryIcon({ subcategory }: { subcategory: IngredientSubcategory }) {
|
||||
return <IngredientTypeIcon icon={SUBCATEGORY_ICON[subcategory]} />;
|
||||
}
|
||||
|
|
@ -637,6 +637,17 @@
|
|||
background: var(--color-surface);
|
||||
border-radius: var(--radius-base);
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
margin-right: auto;
|
||||
|
|
@ -751,6 +762,9 @@
|
|||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
flex-shrink: 0;
|
||||
appearance: none;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
|
|
@ -763,6 +777,12 @@
|
|||
background: var(--color-surface-alt);
|
||||
color: var(--color-text-muted);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text);
|
||||
|
|
@ -788,6 +808,9 @@
|
|||
}
|
||||
|
||||
&__subcategory {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
appearance: none;
|
||||
padding: 2px var(--space-sm);
|
||||
|
|
@ -800,6 +823,12 @@
|
|||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-tag);
|
||||
color: var(--color-text);
|
||||
|
|
@ -841,8 +870,14 @@
|
|||
}
|
||||
|
||||
&__card-icon {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__card-name {
|
||||
|
|
@ -1007,6 +1042,12 @@
|
|||
background: var(--color-surface-alt);
|
||||
border-radius: var(--radius-pill);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -172,13 +172,13 @@
|
|||
"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"
|
||||
"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"
|
||||
},
|
||||
"subcategory": {
|
||||
"LEGUMES": "Légumes",
|
||||
|
|
|
|||
|
|
@ -109,6 +109,43 @@ export const INGREDIENT_CATEGORY_SUBCATEGORIES: Record<
|
|||
AIDES_CULINAIRES: ["BASES", "EPAISSISSANTS", "SUCRES"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic pictogram *type* for an ingredient — mirrors `IngredientIcon` in
|
||||
* schema.prisma, declared by hand for the same reason as
|
||||
* {@link AllergenKind}. Was a free-text emoji per ingredient (437 distinct
|
||||
* characters); replaced with this small, shared vocabulary of ~20 generic
|
||||
* shapes ("a vegetable", "a bottle", "a wedge of cheese"…) so
|
||||
* `apps/web`'s `features/recipes/ingredient-icons.tsx` can render a real
|
||||
* SVG line icon per ingredient instead — see that file for the actual
|
||||
* pictograms and the full reasoning.
|
||||
*/
|
||||
export const INGREDIENT_ICONS = [
|
||||
"VEGETABLE",
|
||||
"FRUIT",
|
||||
"HERB",
|
||||
"MEAT",
|
||||
"POULTRY",
|
||||
"FISH",
|
||||
"SHELLFISH",
|
||||
"GRAIN",
|
||||
"LEGUME",
|
||||
"NUT_SEED",
|
||||
"BREAD",
|
||||
"DOUGH",
|
||||
"MILK",
|
||||
"CHEESE",
|
||||
"EGG",
|
||||
"SPROUT",
|
||||
"SPICE",
|
||||
"JAR",
|
||||
"BOTTLE",
|
||||
"DRINK",
|
||||
"STOCK_POT",
|
||||
"SUGAR",
|
||||
] as const;
|
||||
/** Inferred TS type for one {@link INGREDIENT_ICONS} member. */
|
||||
export type IngredientIcon = (typeof INGREDIENT_ICONS)[number];
|
||||
|
||||
/**
|
||||
* A selectable ingredient, as returned by `GET /reference/ingredients` —
|
||||
* reference data (`Ingredient`, seeded via `apps/api/src/db/
|
||||
|
|
@ -131,7 +168,7 @@ export const INGREDIENT_CATEGORY_SUBCATEGORIES: Record<
|
|||
export interface IngredientView {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
icon: IngredientIcon;
|
||||
category: IngredientCategory;
|
||||
subcategory: IngredientSubcategory;
|
||||
allergens: AllergyView[];
|
||||
|
|
|
|||
Loading…
Reference in a new issue