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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user