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
@@ -47,6 +47,8 @@ const UpdateRecipeSchema = z.object({
key: z.string().min(1).max(500),
isCover: z.boolean().default(false),
})).max(20).optional(),
coverIcon: z.string().max(50).nullable().optional(),
coverColor: z.string().max(50).nullable().optional(),
isBatchCook: z.boolean().optional(),
dishes: z.array(z.object({
name: z.string().min(1).max(100),
@@ -169,6 +171,8 @@ export async function PUT(req: NextRequest, { params }: Params) {
if (data.tags !== undefined) updates.tags = data.tags;
if (data.dietaryTags !== undefined) updates.dietaryTags = data.dietaryTags;
if (data.isBatchCook !== undefined) updates.isBatchCook = data.isBatchCook;
if (data.coverIcon !== undefined) updates.coverIcon = data.coverIcon;
if (data.coverColor !== undefined) updates.coverColor = data.coverColor;
if (data.nutrition !== undefined) {
updates.nutritionData = data.nutrition ? { perServing: data.nutrition } : null;
updates.nutritionManual = !!data.nutrition;
+4
View File
@@ -51,6 +51,8 @@ const CreateRecipeSchema = z.object({
key: z.string().min(1).max(500),
isCover: z.boolean().default(false),
})).max(20).default([]),
coverIcon: z.string().max(50).nullable().optional(),
coverColor: z.string().max(50).nullable().optional(),
isBatchCook: z.boolean().default(false),
dishes: z.array(z.object({
name: z.string().min(1).max(100),
@@ -142,6 +144,8 @@ export async function POST(req: NextRequest) {
aiGenerated: data.aiGenerated ?? false,
language: data.language,
sourceUrl: data.sourceUrl,
coverIcon: data.coverIcon ?? null,
coverColor: data.coverColor ?? null,
isBatchCook: data.isBatchCook,
nutritionData: data.nutrition ? { perServing: data.nutrition } : undefined,
nutritionManual: !!data.nutrition,