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
@@ -67,6 +67,7 @@ function formatDateTime(iso: string) {
export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[] }) {
const t = useTranslations("settingsForm");
const tCommon = useTranslations("common");
const [webhookList, setWebhookList] = useState<Webhook[]>(initialWebhooks);
const [url, setUrl] = useState("");
const [selectedEvents, setSelectedEvents] = useState<WebhookEventType[]>([]);
@@ -201,14 +202,14 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
<div className="space-y-8">
<div className="text-sm text-muted-foreground mb-4">
<Link href="/settings/webhooks/docs" className="text-primary hover:underline">
View webhook docs &amp; Zapier integration
{t("webhookDocsLink")}
</Link>
</div>
{/* Create form */}
<form onSubmit={handleCreate} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="webhook-url">Endpoint URL</Label>
<Label htmlFor="webhook-url">{t("endpointUrlLabel")}</Label>
<Input
id="webhook-url"
type="url"
@@ -221,9 +222,9 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
</div>
<div className="space-y-2">
<Label>Events</Label>
<Label>{t("eventsLabel")}</Label>
<p className="text-xs text-muted-foreground">
Select which events trigger this webhook. Leave all unchecked to receive all events.
{t("eventsHint")}
</p>
<div className="flex flex-wrap gap-3">
{ALL_EVENTS.map((event) => {
@@ -252,28 +253,28 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
{newSecret && (
<div className="rounded-md border border-yellow-400 bg-yellow-50 p-4 space-y-3 dark:bg-yellow-950 dark:border-yellow-700">
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Save this signing secret it will not be shown again.
{t("secretRevealNotice")}
</p>
<p className="text-xs text-yellow-700 dark:text-yellow-300">
Use it to verify the <code className="font-mono">X-Epicure-Signature</code> header on incoming requests.
{t("secretUsageHint")}
</p>
<div className="flex items-center gap-2">
<code className="flex-1 rounded bg-white dark:bg-black border px-3 py-2 text-sm font-mono break-all">
{newSecret.secret}
</code>
<Button type="button" variant="outline" onClick={() => { void handleCopySecret(); }}>
{copied ? "Copied!" : "Copy"}
{copied ? t("copied") : t("copy")}
</Button>
</div>
<Button type="button" variant="ghost" size="sm" onClick={() => setNewSecret(null)} className="text-muted-foreground">
Dismiss
{t("dismiss")}
</Button>
</div>
)}
{/* Webhook list */}
{webhookList.length === 0 ? (
<p className="text-sm text-muted-foreground">No webhooks yet.</p>
<p className="text-sm text-muted-foreground">{t("noWebhooks")}</p>
) : (
<div className="divide-y rounded-md border">
{webhookList.map((w) => {
@@ -285,10 +286,10 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
<div className="min-w-0 flex-1 space-y-1">
<p className="text-sm font-mono truncate">{w.url}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>Added {formatDate(w.createdAt)}</span>
<span>{t("addedOn", { date: formatDate(w.createdAt) })}</span>
<span>·</span>
<Badge variant={w.active ? "default" : "secondary"} className="text-xs">
{w.active ? "Active" : "Inactive"}
{w.active ? t("active") : t("inactive")}
</Badge>
</div>
{w.events.length > 0 && (
@@ -299,7 +300,7 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
</div>
)}
{w.events.length === 0 && (
<p className="text-xs text-muted-foreground pt-1">All events</p>
<p className="text-xs text-muted-foreground pt-1">{t("allEvents")}</p>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
@@ -311,13 +312,13 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
className="text-muted-foreground gap-1"
>
{isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
Deliveries
{t("deliveries")}
</Button>
<Button type="button" variant="outline" size="sm" onClick={() => { void handleToggleActive(w.id, w.active); }}>
{w.active ? "Disable" : "Enable"}
{w.active ? t("disable") : t("enable")}
</Button>
<Button type="button" variant="destructive" size="sm" onClick={() => { void handleDelete(w.id); }}>
Delete
{tCommon("delete")}
</Button>
</div>
</div>
@@ -326,9 +327,9 @@ export function WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[
{isExpanded && (
<div className="mt-2 rounded-md border bg-muted/30">
{loadingDeliveries === w.id ? (
<p className="text-xs text-muted-foreground px-3 py-2">Loading</p>
<p className="text-xs text-muted-foreground px-3 py-2">{tCommon("loading")}</p>
) : wDeliveries.length === 0 ? (
<p className="text-xs text-muted-foreground px-3 py-2">No deliveries yet.</p>
<p className="text-xs text-muted-foreground px-3 py-2">{t("noDeliveriesYet")}</p>
) : (
<div className="divide-y">
{wDeliveries.map((d) => (