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
+13 -12
View File
@@ -176,6 +176,7 @@ function SelectableRecipeCard({
export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] }) {
const t = useTranslations("recipe");
const tCommon = useTranslations("common");
const [recipes, setRecipes] = useState(initialRecipes);
const [selectMode, setSelectMode] = useState(false);
const [selected, setSelected] = useState<Set<string>>(new Set());
@@ -201,7 +202,7 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
};
async function bulkDelete() {
if (!confirm(`Delete ${selected.size} recipe${selected.size !== 1 ? "s" : ""}? This cannot be undone.`)) return;
if (!confirm(t("bulkDeleteConfirm", { count: selected.size }))) return;
setBusy(true);
try {
const res = await fetch("/api/v1/recipes/bulk", {
@@ -255,12 +256,12 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
onClick={toggleAll}
className="text-sm font-medium text-primary hover:underline"
>
{allSelected ? "Deselect all" : "Select all"}
{allSelected ? t("deselectAll") : t("selectAll")}
</button>
<span className="text-sm text-muted-foreground">
{selected.size > 0
? `${selected.size} selected`
: "Click cards to select"}
? t("selectedCount", { count: selected.size })
: t("clickToSelect")}
</span>
</div>
) : (
@@ -273,9 +274,9 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
className={cn("gap-1.5", selectMode && "text-muted-foreground")}
>
{selectMode ? (
<><X className="h-4 w-4" />Cancel</>
<><X className="h-4 w-4" />{tCommon("cancel")}</>
) : (
<><ListChecks className="h-4 w-4" />Select</>
<><ListChecks className="h-4 w-4" />{t("select")}</>
)}
</Button>
</div>
@@ -298,7 +299,7 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-bottom-4 duration-200">
<div className="flex items-center gap-2 bg-popover border shadow-2xl rounded-2xl px-5 py-3">
<span className="text-sm font-semibold tabular-nums mr-1">
{selected.size} selected
{t("selectedCount", { count: selected.size })}
</span>
<div className="w-px h-5 bg-border mx-1" />
<DropdownMenu>
@@ -307,17 +308,17 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
className={buttonVariants({ variant: "ghost", size: "sm" }) + " gap-1.5"}
>
<Globe className="h-4 w-4" />
Visibility
{t("visibility")}
</DropdownMenuTrigger>
<DropdownMenuContent align="center" side="top">
<DropdownMenuItem onClick={() => { void bulkSetVisibility("public"); }}>
<Globe className="h-4 w-4 mr-2 text-green-500" /> Make public
<Globe className="h-4 w-4 mr-2 text-green-500" /> {t("makePublic")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { void bulkSetVisibility("unlisted"); }}>
<Link2 className="h-4 w-4 mr-2 text-yellow-500" /> Make unlisted
<Link2 className="h-4 w-4 mr-2 text-yellow-500" /> {t("makeUnlisted")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { void bulkSetVisibility("private"); }}>
<Lock className="h-4 w-4 mr-2 text-muted-foreground" /> Make private
<Lock className="h-4 w-4 mr-2 text-muted-foreground" /> {t("makePrivate")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -329,7 +330,7 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
className="gap-1.5"
>
<Trash2 className="h-4 w-4" />
Delete
{tCommon("delete")}
</Button>
</div>
</div>