diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ea0d6..d107d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.56.0 — 2026-07-20 09:10 + +### Added +- Step timers in the recipe editor now take a unit (seconds/minutes/hours) instead of forcing everyone to do the math into seconds. Editing an existing recipe shows the timer in whichever unit divides evenly into what's stored. + +### Fixed +- Ingredient list on the recipe page didn't line up — the quantity column only had a minimum width, so a longer value pushed that row's ingredient name further right than the others. Switched to a shared grid column so every row's name starts at the same spot. + ## 0.55.3 — 2026-07-19 19:00 ### Fixed diff --git a/apps/web/app/(app)/recipes/[id]/edit/page.tsx b/apps/web/app/(app)/recipes/[id]/edit/page.tsx index 16546b7..4b56c73 100644 --- a/apps/web/app/(app)/recipes/[id]/edit/page.tsx +++ b/apps/web/app/(app)/recipes/[id]/edit/page.tsx @@ -49,12 +49,23 @@ export default async function EditRecipePage({ params }: Params) { unit: ing.unit ?? "", note: ing.note ?? "", })), - steps: recipe.steps.map((step) => ({ - id: step.id, - instruction: step.instruction, - timerSeconds: step.timerSeconds ? String(step.timerSeconds) : "", - appliesTo: step.appliesTo ?? [], - })), + steps: recipe.steps.map((step) => { + // Show the largest unit that divides evenly into the stored seconds, + // so editing a 90-minute braise shows "90 min" rather than "5400 sec". + const seconds = step.timerSeconds ?? 0; + const timer = seconds > 0 && seconds % 3600 === 0 + ? { value: String(seconds / 3600), unit: "hours" as const } + : seconds > 0 && seconds % 60 === 0 + ? { value: String(seconds / 60), unit: "minutes" as const } + : { value: seconds > 0 ? String(seconds) : "", unit: "seconds" as const }; + return { + id: step.id, + instruction: step.instruction, + timerSeconds: timer.value, + timerUnit: timer.unit, + appliesTo: step.appliesTo ?? [], + }; + }), photos: recipe.photos.map((photo) => ({ key: photo.storageKey, isCover: photo.isCover, diff --git a/apps/web/components/recipe/recipe-form.tsx b/apps/web/components/recipe/recipe-form.tsx index c8d5fc9..73491bd 100644 --- a/apps/web/components/recipe/recipe-form.tsx +++ b/apps/web/components/recipe/recipe-form.tsx @@ -46,13 +46,26 @@ type IngredientRow = { note: string; }; +type TimerUnit = "seconds" | "minutes" | "hours"; + type StepRow = { id: string; instruction: string; timerSeconds: string; + timerUnit: TimerUnit; appliesTo: string[]; }; +const TIMER_UNIT_SECONDS: Record = { seconds: 1, minutes: 60, hours: 3600 }; + +/** Picks the largest unit that divides evenly into the stored seconds, so + * editing a 90-minute braise shows "90 min" rather than "5400 sec". */ +function secondsToTimerInput(totalSeconds: number): { value: string; unit: TimerUnit } { + if (totalSeconds > 0 && totalSeconds % 3600 === 0) return { value: String(totalSeconds / 3600), unit: "hours" }; + if (totalSeconds > 0 && totalSeconds % 60 === 0) return { value: String(totalSeconds / 60), unit: "minutes" }; + return { value: String(totalSeconds), unit: "seconds" }; +} + type DishRow = { id: string; name: string; @@ -110,7 +123,7 @@ function newIngredient(): IngredientRow { } function newStep(): StepRow { - return { id: crypto.randomUUID(), instruction: "", timerSeconds: "", appliesTo: [] }; + return { id: crypto.randomUUID(), instruction: "", timerSeconds: "", timerUnit: "minutes", appliesTo: [] }; } function newDish(): DishRow { @@ -241,12 +254,16 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) { unit: ing.unit ?? "", note: ing.note ?? "", }))); - setSteps(recipe.steps.map((step) => ({ - id: crypto.randomUUID(), - instruction: step.instruction, - timerSeconds: step.timerSeconds !== undefined ? String(step.timerSeconds) : "", - appliesTo: [], - }))); + setSteps(recipe.steps.map((step) => { + const timer = step.timerSeconds !== undefined ? secondsToTimerInput(step.timerSeconds) : null; + return { + id: crypto.randomUUID(), + instruction: step.instruction, + timerSeconds: timer?.value ?? "", + timerUnit: timer?.unit ?? "minutes", + appliesTo: [], + }; + })); } function addTag(raw: string) { @@ -355,7 +372,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) { .filter((s) => s.instruction.trim()) .map((s, i) => ({ instruction: s.instruction.trim(), - timerSeconds: s.timerSeconds ? parseInt(s.timerSeconds) : undefined, + timerSeconds: s.timerSeconds ? parseInt(s.timerSeconds) * TIMER_UNIT_SECONDS[s.timerUnit] : undefined, order: i, appliesTo: isBatchCook ? s.appliesTo.filter((n) => dishNames.has(n)) : [], })); @@ -880,8 +897,18 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) { placeholder={t("timerSeconds")} type="number" min={0} - className="w-28 shrink-0" + className="w-20 shrink-0" /> +