feat: adapt recipe surfaces for batch-cook sessions

- Recipes page action buttons reordered/restyled to push AI generation
  and batch cooking ahead of manual recipe creation.
- Recipe detail page hides meal/drink pairing (doesn't make sense for
  a multi-dish batch session).
- Print pages force light mode regardless of app theme (dark bg +
  hardcoded dark text was unreadable) and render sectioned steps +
  dishes/storage for batch-cook recipes.
- Markdown export mirrors the same sectioned structure.
- Cooking mode tags each step with which dish(es) it belongs to.
- Meal planner: mealPlanEntries.batchDishId lets a slot point at one
  specific dish within a batch session; picking a batch-cook recipe
  now prompts for which dish, and the grid shows the dish name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-12 02:32:57 +02:00
parent 8e83cc0dc7
commit ba1614073b
16 changed files with 4867 additions and 36 deletions
+39 -5
View File
@@ -7,7 +7,15 @@ type RecipeMarkdownInput = {
difficulty: "easy" | "medium" | "hard" | null;
sourceUrl: string | null;
ingredients: Array<{ rawName: string; quantity: string | null; unit: string | null; note: string | null }>;
steps: Array<{ instruction: string; timerSeconds: number | null }>;
steps: Array<{ instruction: string; timerSeconds: number | null; appliesTo?: string[] }>;
isBatchCook?: boolean;
batchDishes?: Array<{
name: string;
fridgeDays: number;
freezerFriendly: boolean;
freezerNote: string | null;
dayOfInstructions: string;
}>;
};
function formatQuantity(quantity: string | null, unit: string | null): string {
@@ -39,13 +47,39 @@ export function recipeToMarkdown(recipe: RecipeMarkdownInput): string {
if (recipe.steps.length > 0) {
lines.push("## Instructions", "");
recipe.steps.forEach((step, i) => {
const timer = step.timerSeconds ? ` (${Math.round(step.timerSeconds / 60)} min)` : "";
lines.push(`${i + 1}. ${step.instruction}${timer}`);
});
if (recipe.isBatchCook) {
let lastLabel = "";
let n = 0;
for (const step of recipe.steps) {
const label = !step.appliesTo || step.appliesTo.length === 0 ? "Prep" : step.appliesTo.join(" + ");
if (label !== lastLabel) {
if (lastLabel) lines.push("");
lines.push(`### ${label}`, "");
lastLabel = label;
n = 0;
}
n += 1;
lines.push(`${n}. ${step.instruction}`);
}
} else {
recipe.steps.forEach((step, i) => {
const timer = step.timerSeconds ? ` (${Math.round(step.timerSeconds / 60)} min)` : "";
lines.push(`${i + 1}. ${step.instruction}${timer}`);
});
}
lines.push("");
}
if (recipe.isBatchCook && recipe.batchDishes && recipe.batchDishes.length > 0) {
lines.push("## Dishes & storage", "");
for (const dish of recipe.batchDishes) {
const storage = [`Fridge: ${dish.fridgeDays}d`];
if (dish.freezerFriendly) storage.push("Freezer-friendly");
lines.push(`**${dish.name}**`, storage.join(" · ") + (dish.freezerNote ? `${dish.freezerNote}` : ""));
lines.push(`On the day: ${dish.dayOfInstructions}`, "");
}
}
if (recipe.sourceUrl) {
lines.push(`Source: ${recipe.sourceUrl}`);
}