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
@@ -63,6 +63,7 @@ export function VariationsDialog({
onOpenChange: (open: boolean) => void;
}) {
const t = useTranslations("recipe");
const tv = useTranslations("ai.variations");
const router = useRouter();
const [generating, setGenerating] = useState(false);
const [applying, setApplying] = useState<number | null>(null);
@@ -80,7 +81,7 @@ export function VariationsDialog({
});
if (!res.ok) {
const err = await res.json() as { error?: string };
toast.error(err.error ?? "Failed to generate variations");
toast.error(err.error ?? tv("error"));
return;
}
const data = await res.json() as { variations: Variation[] };
@@ -140,12 +141,12 @@ export function VariationsDialog({
});
if (!saveRes.ok) {
toast.error("Failed to save variation");
toast.error(tv("saveError"));
return;
}
const saved = await saveRes.json() as { id: string };
toast.success("Variation created — review and edit before publishing");
toast.success(tv("success"));
onOpenChange(false);
router.push(`/recipes/${saved.id}/edit`);
} finally {
@@ -159,17 +160,17 @@ export function VariationsDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
AI Recipe Variations
{tv("title")}
</DialogTitle>
<DialogDescription>
Generate creative variations of this recipe dietary adaptations, flavor twists, technique changes.
{tv("description")}
</DialogDescription>
</DialogHeader>
{variations.length === 0 ? (
<div className="flex flex-col gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="directions">Directions <span className="text-muted-foreground font-normal">(optional)</span></Label>
<Label htmlFor="directions">{tv("directions")} <span className="text-muted-foreground font-normal">({tv("optional")})</span></Label>
<Textarea
id="directions"
placeholder={t("variationsDirectionsPlaceholder")}
@@ -184,16 +185,16 @@ export function VariationsDialog({
{generating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Generating variations
{tv("generating")}
</>
) : (
<>
<Sparkles className="h-4 w-4" />
Generate 3 variations
{tv("generate")}
</>
)}
</Button>
<FakeProgressBar active={generating} durationMs={22000} label={generating ? "Generating 3 creative variations…" : undefined} />
<FakeProgressBar active={generating} durationMs={22000} label={generating ? tv("generating") : undefined} />
</div>
) : (
<div className="space-y-4">
@@ -214,13 +215,13 @@ export function VariationsDialog({
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
Apply
{tv("apply")}
</Button>
</div>
{v.changedIngredients.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Ingredient changes</p>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{tv("ingredientChanges")}</p>
<div className="space-y-1">
{v.changedIngredients.map((c, j) => (
<div key={j} className="flex items-center gap-2 text-sm">
@@ -237,11 +238,11 @@ export function VariationsDialog({
{v.changedSteps && v.changedSteps.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Step changes</p>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{tv("stepChanges")}</p>
<div className="space-y-1">
{v.changedSteps.map((c, j) => (
<div key={j} className="flex gap-2 text-sm">
<Badge variant="outline" className="text-xs shrink-0">Step {c.stepNumber}</Badge>
<Badge variant="outline" className="text-xs shrink-0">{tv("stepLabel", { n: c.stepNumber })}</Badge>
<span className="text-muted-foreground">{c.change}</span>
</div>
))}
@@ -265,7 +266,7 @@ export function VariationsDialog({
<Button variant="outline" className="w-full" onClick={generate} disabled={generating}>
{generating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
Regenerate
{tv("regenerate")}
</Button>
</div>
)}