feat: custom recipe cover color/icon + mute auto placeholder palette (v0.51.0)

Two changes to the no-photo cover placeholder shipped last version:

1. Muted the gradient palette (100/40-opacity tints instead of solid -200/
   -950 stops) — the original was too saturated next to real cover photos
   in the same grid, per feedback.
2. New coverIcon/coverColor columns on recipes (nullable text, migration
   0048, additive-only) let the author pin a specific color+icon from the
   recipe editor instead of the automatic per-id pick. getRecipePlaceholder
   now checks these first, falling back to the deterministic hash pick when
   unset — existing recipes are unaffected until edited.

Wired coverIcon/coverColor through every explicit-column recipe select
that feeds a grid card (explore trending/recent, search, feed, for-you) —
the relational-query call sites (recipes page, collections, profile pages)
already return all columns and needed no changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-19 10:32:31 +02:00
parent 955c4bd9dd
commit 51e6722f4c
25 changed files with 5721 additions and 30 deletions
@@ -24,6 +24,7 @@ import {
} from "@/components/ui/alert-dialog";
import { DietaryTagPicker } from "./dietary-tag-picker";
import { PhotoUploader, type PhotoEntry } from "./photo-uploader";
import { RecipeCoverPicker } from "./recipe-cover-picker";
import { RegenerateRecipeButton } from "./regenerate-recipe-button";
import type { RegeneratedRecipe } from "@/lib/ai/features/regenerate-recipe";
@@ -78,6 +79,8 @@ type RecipeFormProps = {
ingredients?: IngredientRow[];
steps?: StepRow[];
photos?: PhotoEntry[];
coverIcon?: string | null;
coverColor?: string | null;
isBatchCook?: boolean;
dishes?: DishRow[];
nutrition?: {
@@ -155,6 +158,8 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
defaultValues?.steps?.length ? defaultValues.steps : [newStep()]
);
const [photos, setPhotos] = useState<PhotoEntry[]>(defaultValues?.photos ?? []);
const [coverIcon, setCoverIcon] = useState<string | null>(defaultValues?.coverIcon ?? null);
const [coverColor, setCoverColor] = useState<string | null>(defaultValues?.coverColor ?? null);
const [isBatchCook, setIsBatchCook] = useState(defaultValues?.isBatchCook ?? false);
const [dishes, setDishes] = useState<DishRow[]>(
defaultValues?.dishes?.length ? defaultValues.dishes : [newDish()]
@@ -376,6 +381,8 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
ingredients: filteredIngredients,
steps: filteredSteps,
photos: photos.map((p) => ({ key: p.key, isCover: p.isCover })),
coverIcon,
coverColor,
isBatchCook,
dishes: filteredDishes,
nutrition: addNutrition
@@ -684,6 +691,20 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
<PhotoUploader recipeId={id} photos={photos} onChange={setPhotos} />
</div>
{/* Cover placeholder (used when there's no photo above) */}
<div className="space-y-2">
<Label>{t("cover")}</Label>
<RecipeCoverPicker
recipeType={recipeType}
color={coverColor}
icon={coverIcon}
onChange={({ color, icon }) => {
setCoverColor(color);
setCoverIcon(icon);
}}
/>
</div>
<Separator />
{/* Ingredients */}