New Settings → Notifications section with a toggle per category (follow,
comment, reply, reaction, rating, mention, leftover-expiring, shared
shopping list) — previously it was all-or-nothing (browser permission only).
userNotificationPrefs (one row per user, defaults all-on so existing users
see no behavior change until they opt out of something). Gated the push
send in the three places that dispatch one: lib/notifications.ts (the 6
in-app notification types), the leftover-expiry cron, and shopping-list-notify
— in-app notification-center entries and email are unaffected, this only
gates the push itself, matching the literal ask.
Verified locally: GET/PUT round-trips correctly, disabling a category
and confirming the settings page renders all 8 toggles.
New users get a Gravatar-backed avatar automatically (computed from email
at signup); users can upload a custom photo instead via Settings, or
revert to the Gravatar/initials fallback. avatarUrl stays the single
resolved value (custom photo, OAuth photo, or precomputed Gravatar URL)
so every existing avatar-rendering spot across the app needs zero changes.
- users.hasCustomAvatar tracks whether avatarUrl is a real upload vs a
computed Gravatar fallback, so "remove photo" knows what to revert to.
- New /api/v1/upload/avatar-presign route (session-scoped, generic —
the existing recipe-photo presign route required a recipeId).
- CSP img-src needed www.gravatar.com added, or every browser blocks
the fallback avatar outright.
- Settings page now reads avatarUrl from a fresh DB query instead of
Better Auth's session object — the session cookie cache (5 min TTL)
was serving a stale image right after upload, showing the initials
fallback until the cache happened to expire.
Verified locally: signup auto-sets a Gravatar URL, upload persists
across reload, remove correctly reverts to Gravatar/initials.
- Private accounts: users.isPrivate hides a user from search and their
recipes from search/trending/for-you discovery surfaces (follow-aware
where the route already has session context, blanket exclusion where it
doesn't); existing followers and direct links are unaffected, no
follow-request approval flow was built (explicit scope limit)
- Merged /people into the Explore tab (tab=people query param); the old
standalone route now redirects there
- "Get Ideas" vs "Surprise Me" were doing the same empty-prompt call;
Surprise Me now injects a real random constraint (5-ingredient, one-pot,
etc.), matching the existing pattern in the AI recipe-generate dialog
- Meal-plan day cells now link to their recipe (was dead text) and gained
a one-click "mark as cooked" action
- Theme toggle is now a real three-way light/dark/system control instead
of a binary flip
- Nutrition goals form had zero i18n wiring; fully localized now
New migration 0027 (users.is_private) generated, left unapplied like the
others. Verified with typecheck, lint, and a clean --no-cache docker build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Five S-sized items from HANDOFF.md's new-features backlog, all wiring up
previously-orphaned infra:
- createNotification now sends web push + email for every notification type
(follow/comment/reply/reaction/rating/mention), not just comments
- Personal recipe notes: private per-user notes on any viewable recipe
(recipeNotes table had zero API/UI before this)
- Recipe fork/clone: deep-copies a viewable recipe into your own library as
a private draft, linked via recipeVariations, respects tier quota
- Pantry-aware shopping lists: meal-plan-generated lists now subtract
on-hand pantry quantities (ingredientId match, falling back to normalized
name match) and flag partial/ambiguous matches instead of guessing
- GDPR data export: downloadable JSON of a user's own content and activity
across every relevant table, secrets/internal tables excluded
New migrations 0025 (unique index for recipe-notes upsert) and 0026
(shopping_list_items.in_pantry) generated, left unapplied like 0023/0024.
Verified with typecheck, lint, and a full local `docker build`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Settings sidebar was a fixed-width vertical list with no responsive
breakpoint, squeezing the form into a sliver on mobile. Turn it into
a horizontal scrollable tab bar under md, vertical sidebar above.
- setLocale() fired the PATCH and swallowed the result with
.catch(() => {}) — never checked res.ok, no success/failure feedback.
A failed save (expired session, network blip) looked identical to a
successful one. Now returns a boolean, reverts local state on
failure, and the settings form toasts success/failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Wires dozens of components and server pages to next-intl instead of hardcoded
English text, and adds a lightweight getMessages/formatMessage helper for
server components (print pages, public recipe page, cook metadata) since
next-intl/server isn't wired into this app's routing. Backfills missing
en/fr message keys that existing code already referenced but fr.json lacked.