fix: mobile layout fixes, i18n coverage, and recipe share link

Mobile:
- Recipes search bar full-width on mobile instead of capped narrow
- Cook mode ingredients panel stacks above the step instead of
  squeezing it into a narrow column
- Version history Compare/Restore buttons wrap onto their own row
- Recipe edit ingredient fields wrap instead of forcing horizontal
  scroll on narrow viewports

i18n: translates remaining hardcoded strings across recipes
filter/sort, adapt-recipe and AI variations dialogs, the full
settings section (sidebar + 6 sub-pages + BYOK/model-prefs/
API-keys/webhooks managers), explore tab, collections (new/fork/
share dialogs), meal planning (planner, AI generation phases, new
shopping list, shared-plan view), photo import, recipe bulk-select
toolbar, and recipe action-button tooltips. Also fixes the recipes
page subtitle, which wasn't just unworded but missing its {count}
interpolation entirely — it always rendered as the bare word
"results" regardless of how many recipes existed.

Feature: adds a ShareRecipeButton that copies the public /r/{id}
link to the clipboard, with a notice when the recipe isn't Public
yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-02 15:13:51 +02:00
parent b07bada291
commit eb424d8c04
44 changed files with 932 additions and 376 deletions
+16 -15
View File
@@ -61,7 +61,7 @@ function AddEntryModal({
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ day, mealType, recipeId: recipe.id, servings: parseInt(servings) || 2 }),
});
if (!res.ok) { toast.error("Failed to add"); return; }
if (!res.ok) { toast.error(t("addFailed")); return; }
const { id } = await res.json() as { id: string };
onAdded({ id, day, mealType, servings: parseInt(servings) || 2, recipe, note: null });
onClose();
@@ -133,15 +133,16 @@ export function MealPlanner({
const [pantryMode, setPantryMode] = useState(false);
const [aiDifficulty, setAiDifficulty] = useState<"" | "easy" | "medium" | "hard">("");
const t = useTranslations("mealPlan");
const tRecipe = useTranslations("recipe");
async function generateWithAi() {
setAiGenerating(true);
setAiPhase("Analyzing your preferences…");
setAiPhase(t("aiPhaseAnalyzing"));
const phases = [
[1500, "Planning breakfast, lunch & dinner…"],
[5000, "Selecting recipes for each day…"],
[10000, "Balancing nutrition across the week…"],
[16000, "Finalizing your meal plan…"],
[1500, t("aiPhasePlanning")],
[5000, t("aiPhaseSelecting")],
[10000, t("aiPhaseBalancing")],
[16000, t("aiPhaseFinalizing")],
] as const;
const timers = phases.map(([delay, label]) =>
setTimeout(() => setAiPhase(label), delay)
@@ -161,7 +162,7 @@ export function MealPlanner({
});
if (!res.ok) {
const data = await res.json() as { error?: string };
throw new Error(data.error ?? "Generation failed");
throw new Error(data.error ?? t("generationFailed"));
}
const data = await res.json() as {
entries: Array<{ id: string; day: string; mealType: string; recipeId: string; recipeTitle: string }>;
@@ -187,7 +188,7 @@ export function MealPlanner({
setShowAiModal(false);
toast.success(t("aiGenerated"));
} catch (err) {
toast.error(err instanceof Error ? err.message : "Generation failed");
toast.error(err instanceof Error ? err.message : t("generationFailed"));
} finally {
timers.forEach(clearTimeout);
setAiGenerating(false);
@@ -227,7 +228,7 @@ export function MealPlanner({
if (res.ok) {
setEntries((prev) => prev.filter((e) => e.id !== entry.id));
} else {
toast.error("Failed to remove");
toast.error(t("removeFailed"));
}
}
@@ -345,16 +346,16 @@ export function MealPlanner({
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Difficulty</label>
<label className="text-sm font-medium">{t("difficulty")}</label>
<Select value={aiDifficulty} onValueChange={(v) => setAiDifficulty(v as typeof aiDifficulty)} disabled={aiGenerating}>
<SelectTrigger>
<SelectValue placeholder="Any" />
<SelectValue placeholder={t("anyDifficulty")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Any</SelectItem>
<SelectItem value="easy">Easy</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="hard">Hard</SelectItem>
<SelectItem value="">{t("anyDifficulty")}</SelectItem>
<SelectItem value="easy">{tRecipe("difficulty.easy")}</SelectItem>
<SelectItem value="medium">{tRecipe("difficulty.medium")}</SelectItem>
<SelectItem value="hard">{tRecipe("difficulty.hard")}</SelectItem>
</SelectContent>
</Select>
</div>