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:
@@ -36,27 +36,27 @@ function TagFilterInput({ value, onChange }: { value: string; onChange: (v: stri
|
||||
}
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SORT_LABELS: Record<string, string> = {
|
||||
updated_desc: "Recently updated",
|
||||
updated_asc: "Oldest updated",
|
||||
created_desc: "Newest first",
|
||||
created_asc: "Oldest first",
|
||||
title_asc: "Title A–Z",
|
||||
title_desc: "Title Z–A",
|
||||
const SORT_KEYS: Record<string, string> = {
|
||||
updated_desc: "sortRecentlyUpdated",
|
||||
updated_asc: "sortOldestUpdated",
|
||||
created_desc: "sortNewestFirst",
|
||||
created_asc: "sortOldestFirst",
|
||||
title_asc: "sortTitleAZ",
|
||||
title_desc: "sortTitleZA",
|
||||
};
|
||||
|
||||
const VISIBILITY_LABELS: Record<string, string> = {
|
||||
"": "All",
|
||||
private: "Private",
|
||||
unlisted: "Unlisted",
|
||||
public: "Public",
|
||||
const VISIBILITY_KEYS: Record<string, string> = {
|
||||
"": "all",
|
||||
private: "private",
|
||||
unlisted: "unlisted",
|
||||
public: "public",
|
||||
};
|
||||
|
||||
const DIFFICULTY_LABELS: Record<string, string> = {
|
||||
"": "Any difficulty",
|
||||
easy: "Easy",
|
||||
medium: "Medium",
|
||||
hard: "Hard",
|
||||
const DIFFICULTY_KEYS: Record<string, string> = {
|
||||
"": "anyDifficulty",
|
||||
easy: "easy",
|
||||
medium: "medium",
|
||||
hard: "hard",
|
||||
};
|
||||
|
||||
export function RecipesHeader({
|
||||
@@ -77,6 +77,7 @@ export function RecipesHeader({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const t = useTranslations("recipes");
|
||||
const tRecipe = useTranslations("recipe");
|
||||
const [aiOpen, setAiOpen] = useState(false);
|
||||
const [urlOpen, setUrlOpen] = useState(false);
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
@@ -138,7 +139,7 @@ export function RecipesHeader({
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[200px] max-w-sm">
|
||||
<div className="relative w-full sm:flex-1 sm:min-w-[200px] sm:max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
value={query}
|
||||
@@ -165,19 +166,19 @@ export function RecipesHeader({
|
||||
)}
|
||||
>
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
{SORT_LABELS[initialSort] ?? "Sort"}
|
||||
{SORT_KEYS[initialSort] ? t(SORT_KEYS[initialSort]) : t("sort")}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t("sortBy")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{Object.entries(SORT_LABELS).map(([value, label]) => (
|
||||
{Object.entries(SORT_KEYS).map(([value, key]) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onClick={() => pushParams({ sort: value })}
|
||||
className={cn(initialSort === value && "font-medium text-primary")}
|
||||
>
|
||||
{label}
|
||||
{t(key)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
@@ -193,7 +194,7 @@ export function RecipesHeader({
|
||||
)}
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
Filter
|
||||
{t("filter")}
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-4 min-w-4 px-1 text-xs">
|
||||
{activeFilterCount}
|
||||
@@ -202,33 +203,33 @@ export function RecipesHeader({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Visibility</DropdownMenuLabel>
|
||||
{Object.entries(VISIBILITY_LABELS).map(([value, label]) => (
|
||||
<DropdownMenuLabel>{t("visibilityLabel")}</DropdownMenuLabel>
|
||||
{Object.entries(VISIBILITY_KEYS).map(([value, key]) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onClick={() => pushParams({ visibility: value })}
|
||||
className={cn(initialVisibility === value && "font-medium text-primary")}
|
||||
>
|
||||
{label}
|
||||
{value === "" ? t(key) : tRecipe(`visibility.${key}`)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Difficulty</DropdownMenuLabel>
|
||||
{Object.entries(DIFFICULTY_LABELS).map(([value, label]) => (
|
||||
<DropdownMenuLabel>{t("difficultyLabel")}</DropdownMenuLabel>
|
||||
{Object.entries(DIFFICULTY_KEYS).map(([value, key]) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onClick={() => pushParams({ difficulty: value })}
|
||||
className={cn(initialDifficulty === value && "font-medium text-primary")}
|
||||
>
|
||||
{label}
|
||||
{value === "" ? t(key) : tRecipe(`difficulty.${key}`)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Tag</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t("tagLabel")}</DropdownMenuLabel>
|
||||
<div className="px-2 py-1">
|
||||
<TagFilterInput
|
||||
value={initialTag}
|
||||
@@ -244,7 +245,7 @@ export function RecipesHeader({
|
||||
onClick={() => pushParams({ visibility: "", difficulty: "", tag: "" })}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X className="h-3 w-3 mr-1.5" /> Clear filters
|
||||
<X className="h-3 w-3 mr-1.5" /> {t("clearFilters")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user