feat: recipe type (dish/drink) for better cocktail handling (v0.31.0)
Add recipes.recipeType enum (dish|drink, default dish). Recipe form now shows a Type selector and hides food-only fields for drinks (prep/cook time, difficulty, batch-cook toggle) instead of forcing every cocktail through meal-oriented UI. Detail page skips the meal/drink pairing suggestion buttons on drink recipes (pairing a drink with a drink doesn't make sense). Recipes list gains a Type filter facet alongside difficulty/batch-cook. Scoped deliberately narrow per investigation: no ABV/glass-type/ garnish structured fields, no meal-plan schema changes, and the existing AI drink-pairing-suggestion feature (ephemeral, never saved as a recipe) is left as-is rather than wired into recipe creation — those suggestions are wine/beer/spirit recommendations without ingredients/steps, not really recipes to save. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@ type RecipeFormProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
baseServings?: number;
|
||||
recipeType?: "dish" | "drink";
|
||||
visibility?: "private" | "unlisted" | "public";
|
||||
difficulty?: "easy" | "medium" | "hard" | null;
|
||||
prepMins?: number | null;
|
||||
@@ -103,6 +104,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
const [title, setTitle] = useState(defaultValues?.title ?? "");
|
||||
const [description, setDescription] = useState(defaultValues?.description ?? "");
|
||||
const [baseServings, setBaseServings] = useState(String(defaultValues?.baseServings ?? 4));
|
||||
const [recipeType, setRecipeType] = useState<"dish" | "drink">(defaultValues?.recipeType ?? "dish");
|
||||
const [visibility, setVisibility] = useState<"private" | "unlisted" | "public">(defaultValues?.visibility ?? "private");
|
||||
const [difficulty, setDifficulty] = useState<"easy" | "medium" | "hard" | "">(defaultValues?.difficulty ?? "");
|
||||
const [prepMins, setPrepMins] = useState(String(defaultValues?.prepMins ?? ""));
|
||||
@@ -150,6 +152,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
photos,
|
||||
isBatchCook,
|
||||
dishes,
|
||||
recipeType,
|
||||
]);
|
||||
|
||||
// Browser-level guard: warn on tab close / reload / external navigation while dirty.
|
||||
@@ -301,6 +304,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
baseServings: parseInt(baseServings) || 4,
|
||||
recipeType,
|
||||
visibility,
|
||||
difficulty: difficulty || undefined,
|
||||
prepMins: prepMins ? parseInt(prepMins) : undefined,
|
||||
@@ -365,9 +369,22 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="recipeType">{t("recipeType")}</Label>
|
||||
<select
|
||||
id="recipeType"
|
||||
value={recipeType}
|
||||
onChange={(e) => setRecipeType(e.target.value as "dish" | "drink")}
|
||||
className="flex h-8 w-full max-w-[200px] rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
<option value="dish">{t("recipeTypeDish")}</option>
|
||||
<option value="drink">{t("recipeTypeDrink")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={cn("grid grid-cols-2 gap-4", recipeType === "dish" && "md:grid-cols-4")}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="servings">{t("servings")}</Label>
|
||||
<Label htmlFor="servings">{recipeType === "drink" ? t("servingsDrink") : t("servings")}</Label>
|
||||
<Input
|
||||
id="servings"
|
||||
type="number"
|
||||
@@ -377,42 +394,46 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
onChange={(e) => setBaseServings(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="prep">{t("prepMins")}</Label>
|
||||
<Input
|
||||
id="prep"
|
||||
type="number"
|
||||
min={0}
|
||||
value={prepMins}
|
||||
onChange={(e) => setPrepMins(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cook">{t("cookMins")}</Label>
|
||||
<Input
|
||||
id="cook"
|
||||
type="number"
|
||||
min={0}
|
||||
value={cookMins}
|
||||
onChange={(e) => setCookMins(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="difficulty">{t("difficulty")}</Label>
|
||||
<select
|
||||
id="difficulty"
|
||||
value={difficulty}
|
||||
onChange={(e) => setDifficulty(e.target.value as "easy" | "medium" | "hard" | "")}
|
||||
className="flex h-8 w-full rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="easy">{t_recipe("difficulty.easy")}</option>
|
||||
<option value="medium">{t_recipe("difficulty.medium")}</option>
|
||||
<option value="hard">{t_recipe("difficulty.hard")}</option>
|
||||
</select>
|
||||
</div>
|
||||
{recipeType === "dish" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="prep">{t("prepMins")}</Label>
|
||||
<Input
|
||||
id="prep"
|
||||
type="number"
|
||||
min={0}
|
||||
value={prepMins}
|
||||
onChange={(e) => setPrepMins(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cook">{t("cookMins")}</Label>
|
||||
<Input
|
||||
id="cook"
|
||||
type="number"
|
||||
min={0}
|
||||
value={cookMins}
|
||||
onChange={(e) => setCookMins(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="difficulty">{t("difficulty")}</Label>
|
||||
<select
|
||||
id="difficulty"
|
||||
value={difficulty}
|
||||
onChange={(e) => setDifficulty(e.target.value as "easy" | "medium" | "hard" | "")}
|
||||
className="flex h-8 w-full rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="easy">{t_recipe("difficulty.easy")}</option>
|
||||
<option value="medium">{t_recipe("difficulty.medium")}</option>
|
||||
<option value="hard">{t_recipe("difficulty.hard")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -477,16 +498,20 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
<DietaryTagPicker value={dietaryTags} onChange={setDietaryTags} />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
{recipeType === "dish" && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
{/* Batch cooking */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch id="batch-cook" checked={isBatchCook} onCheckedChange={setIsBatchCook} />
|
||||
<Label htmlFor="batch-cook" className="cursor-pointer">{t("batchCookToggle")}</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("batchCookToggleHelp")}</p>
|
||||
</div>
|
||||
{/* Batch cooking */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch id="batch-cook" checked={isBatchCook} onCheckedChange={setIsBatchCook} />
|
||||
<Label htmlFor="batch-cook" className="cursor-pointer">{t("batchCookToggle")}</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("batchCookToggleHelp")}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user