Files
Epicure/apps/web/lib/recipe-placeholder.ts
T
Arnaud 520d992de4 feat: more icon choices for custom recipe covers (v0.51.1)
Dish pool 16 -> 29 (banana, grape, citrus, hamburger, popcorn, bean,
vegan, utensils-crossed, leaf, shrimp, cooking-pot, candy, nut), drink
pool +milk. Existing recipes' auto-picked icon may shift since the
deterministic hash is modulo pool size — expected, cosmetic only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:44:20 +02:00

104 lines
4.5 KiB
TypeScript

import {
Soup, Salad, Pizza, Sandwich, IceCream, Cookie, Beef, Fish, Cherry,
Carrot, Egg, Drumstick, Croissant, Donut, Cake, ChefHat,
Banana, Grape, Citrus, Hamburger, Popcorn, Bean, Vegan, UtensilsCrossed,
Leaf, Shrimp, CookingPot, Candy, Nut,
Coffee, Wine, Beer, GlassWater, Martini, CupSoda, Milk,
type LucideIcon,
} from "lucide-react";
// Muted on purpose — these sit behind a lucide icon on every photo-less card
// in a grid, so anything more saturated than "barely-there tint" overpowers
// real cover photos next to it and reads as loud rather than as a placeholder.
export const GRADIENT_OPTIONS = [
{ key: "orange-rose", classes: "from-orange-100/70 to-rose-100/70 dark:from-orange-950/40 dark:to-rose-950/40" },
{ key: "amber-lime", classes: "from-amber-100/70 to-lime-100/70 dark:from-amber-950/40 dark:to-lime-950/40" },
{ key: "sky-indigo", classes: "from-sky-100/70 to-indigo-100/70 dark:from-sky-950/40 dark:to-indigo-950/40" },
{ key: "emerald-teal", classes: "from-emerald-100/70 to-teal-100/70 dark:from-emerald-950/40 dark:to-teal-950/40" },
{ key: "fuchsia-purple", classes: "from-fuchsia-100/70 to-purple-100/70 dark:from-fuchsia-950/40 dark:to-purple-950/40" },
{ key: "yellow-orange", classes: "from-yellow-100/70 to-orange-100/70 dark:from-yellow-950/40 dark:to-orange-950/40" },
{ key: "rose-pink", classes: "from-rose-100/70 to-pink-100/70 dark:from-rose-950/40 dark:to-pink-950/40" },
{ key: "teal-cyan", classes: "from-teal-100/70 to-cyan-100/70 dark:from-teal-950/40 dark:to-cyan-950/40" },
] as const;
export const DISH_ICON_OPTIONS = [
{ key: "chef-hat", Icon: ChefHat },
{ key: "soup", Icon: Soup },
{ key: "salad", Icon: Salad },
{ key: "pizza", Icon: Pizza },
{ key: "sandwich", Icon: Sandwich },
{ key: "ice-cream", Icon: IceCream },
{ key: "cookie", Icon: Cookie },
{ key: "beef", Icon: Beef },
{ key: "fish", Icon: Fish },
{ key: "cherry", Icon: Cherry },
{ key: "carrot", Icon: Carrot },
{ key: "egg", Icon: Egg },
{ key: "drumstick", Icon: Drumstick },
{ key: "croissant", Icon: Croissant },
{ key: "donut", Icon: Donut },
{ key: "cake", Icon: Cake },
{ key: "banana", Icon: Banana },
{ key: "grape", Icon: Grape },
{ key: "citrus", Icon: Citrus },
{ key: "hamburger", Icon: Hamburger },
{ key: "popcorn", Icon: Popcorn },
{ key: "bean", Icon: Bean },
{ key: "vegan", Icon: Vegan },
{ key: "utensils-crossed", Icon: UtensilsCrossed },
{ key: "leaf", Icon: Leaf },
{ key: "shrimp", Icon: Shrimp },
{ key: "cooking-pot", Icon: CookingPot },
{ key: "candy", Icon: Candy },
{ key: "nut", Icon: Nut },
] as const;
export const DRINK_ICON_OPTIONS = [
{ key: "coffee", Icon: Coffee },
{ key: "wine", Icon: Wine },
{ key: "beer", Icon: Beer },
{ key: "glass-water", Icon: GlassWater },
{ key: "martini", Icon: Martini },
{ key: "cup-soda", Icon: CupSoda },
{ key: "milk", Icon: Milk },
] as const;
export const ALL_ICON_OPTIONS: { key: string; Icon: LucideIcon }[] = [...DISH_ICON_OPTIONS, ...DRINK_ICON_OPTIONS];
const ICON_BY_KEY = new Map<string, LucideIcon>(ALL_ICON_OPTIONS.map((o) => [o.key, o.Icon]));
const GRADIENT_BY_KEY = new Map<string, string>(GRADIENT_OPTIONS.map((o) => [o.key, o.classes]));
/** Small, fast, deterministic string hash (djb2) — same recipe always gets the same placeholder. */
function hashString(s: string): number {
let hash = 5381;
for (let i = 0; i < s.length; i++) {
hash = (hash * 33) ^ s.charCodeAt(i);
}
return hash >>> 0;
}
/** Resolves a recipe's cover placeholder — user-chosen coverIcon/coverColor
* win when set and recognized, else falls back to a deterministic pick
* from the recipe id so it's still stable without a fresh hash on rename. */
export function getRecipePlaceholder(recipe: {
id: string;
recipeType?: "dish" | "drink";
coverIcon?: string | null;
coverColor?: string | null;
}): { gradient: string; Icon: LucideIcon } {
const hash = hashString(recipe.id);
const dishOrDrinkIcons = recipe.recipeType === "drink" ? DRINK_ICON_OPTIONS : DISH_ICON_OPTIONS;
const gradient =
(recipe.coverColor ? GRADIENT_BY_KEY.get(recipe.coverColor) : undefined) ??
GRADIENT_OPTIONS[hash % GRADIENT_OPTIONS.length]!.classes;
const Icon =
(recipe.coverIcon ? ICON_BY_KEY.get(recipe.coverIcon) : undefined) ??
// Different modulus base than gradient's so icon/color pairing doesn't
// repeat in lockstep across ids that happen to share a gradient.
dishOrDrinkIcons[Math.floor(hash / GRADIENT_OPTIONS.length) % dishOrDrinkIcons.length]!.Icon;
return { gradient, Icon };
}