51e6722f4c
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>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { Shuffle } from "lucide-react";
|
|
import { useTranslations } from "next-intl";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { GRADIENT_OPTIONS, DISH_ICON_OPTIONS, DRINK_ICON_OPTIONS } from "@/lib/recipe-placeholder";
|
|
|
|
/** Lets the user pin a specific color + icon for a recipe's cover placeholder,
|
|
* used whenever there's no uploaded photo. Leaving both unset (null) keeps
|
|
* the automatic per-recipe pick from getRecipePlaceholder. */
|
|
export function RecipeCoverPicker({
|
|
recipeType,
|
|
color,
|
|
icon,
|
|
onChange,
|
|
}: {
|
|
recipeType: "dish" | "drink";
|
|
color: string | null;
|
|
icon: string | null;
|
|
onChange: (next: { color: string | null; icon: string | null }) => void;
|
|
}) {
|
|
const t = useTranslations("recipeForm");
|
|
const iconOptions = recipeType === "drink" ? DRINK_ICON_OPTIONS : DISH_ICON_OPTIONS;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-xs text-muted-foreground">{t("coverHint")}</p>
|
|
{(color || icon) && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs gap-1 text-muted-foreground"
|
|
onClick={() => onChange({ color: null, icon: null })}
|
|
>
|
|
<Shuffle className="h-3 w-3" />
|
|
{t("coverReset")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{GRADIENT_OPTIONS.map((g) => (
|
|
<button
|
|
key={g.key}
|
|
type="button"
|
|
onClick={() => onChange({ color: g.key, icon })}
|
|
className={cn(
|
|
"h-7 w-7 rounded-full bg-gradient-to-br border-2 transition-transform",
|
|
g.classes,
|
|
color === g.key ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
|
)}
|
|
aria-label={`Cover color ${g.key}`}
|
|
aria-pressed={color === g.key}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{iconOptions.map(({ key, Icon }) => (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => onChange({ color, icon: key })}
|
|
className={cn(
|
|
"h-8 w-8 rounded-lg border flex items-center justify-center transition-colors",
|
|
icon === key ? "border-foreground bg-accent" : "border-transparent hover:bg-accent/50"
|
|
)}
|
|
aria-label={`Cover icon ${key}`}
|
|
aria-pressed={icon === key}
|
|
>
|
|
<Icon className="h-4 w-4" strokeWidth={1.5} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|