Match the recipe detail page's action-row convention: Share, Generate
shopping list, Shopping lists link, Print, Export to calendar, and
Send to grocery delivery collapse to icon-only with a tooltip instead
of icon+text, freeing up header space. Week prev/next arrows moved
next to the week date instead of sitting at the end of the button row.
Note: verified via typecheck/lint only — this dev environment has no
authenticated session to visually confirm in-browser.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two separate components (the owner's own week view and collaborators'
shared view) hit two different API surfaces for the same underlying
entries, so neither ever saw the other's edits without a manual
reload. Both now poll a new lean GET on their respective entries
routes every 4s, merged with the same dirty-until-ref guard used for
shopping lists so a poll can't stomp an in-flight local edit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New bulk-delete route for the owner's own weekStart-scoped plan
(mirrors recipes/bulk's DELETE), plus ?ids= support added to the
existing shared-plan DELETE route (kept ?entryId= working
unchanged). UI: hover-reveal trash icon per day header, "Clear week"
button in the toolbar, one shared confirm dialog for both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Polls item state every 4s and merges it into the list view, guarding
any item with an in-flight local edit so a poll landing mid-edit can't
stomp on it. No websocket/SSE infra exists in this app yet, so this
follows the same polling convention already used for notifications and
message threads.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a "Batch cooking" button next to "Generate with AI" on the meal
plan page, opening the same wizard used on /batch-cooking. Closes the
loop on the original ask: batch-cooking sessions can now be started
directly from the meal planner, not just from the recipes list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Recipes page action buttons reordered/restyled to push AI generation
and batch cooking ahead of manual recipe creation.
- Recipe detail page hides meal/drink pairing (doesn't make sense for
a multi-dish batch session).
- Print pages force light mode regardless of app theme (dark bg +
hardcoded dark text was unreadable) and render sectioned steps +
dishes/storage for batch-cook recipes.
- Markdown export mirrors the same sectioned structure.
- Cooking mode tags each step with which dish(es) it belongs to.
- Meal planner: mealPlanEntries.batchDishId lets a slot point at one
specific dish within a batch session; picking a batch-cook recipe
now prompts for which dish, and the grid shows the dish name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every "nothing here" page had its own copy-pasted dashed-border box —
icon + one muted line, inconsistent (some had a CTA, some didn't, no
description text anywhere). Replaced with one shared EmptyState
component: icon in a soft tinted circle, a real heading plus optional
description, and primary/secondary actions (either a Link or an
arbitrary action slot for things like "New Collection" that open a
dialog rather than navigate).
Applied to: recipes (no recipes / no search match), favorites, feed
(no one followed / no new posts / trending / for-you), collections
(index + detail), shopping lists, pantry, notifications, can-cook.
Left the small inline "no trending"/"no recent" lines inside Explore's
already-labeled sections and the notification-bell dropdown alone —
different context, a full empty-state box would be heavier than the
space warrants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaced the hidden dropdown (change category submenu + delete, behind a
single unlabeled MoreVertical icon) with two directly visible controls,
matching the recipe detail page's icon-row pattern: a compact category
Select showing the item's current category at a glance (still supports
picking a predefined category, "Other", or typing a new custom one), and
a direct trash icon button with a tooltip. Row stays single-line via
truncation on the name and a fixed-width category trigger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-category drops worked (previous fix) but had no live preview — the
item stayed rendered in its original group the whole time you were
dragging over a different one, since dnd-kit only computes a transform
for items actually present in a group's array.
Added the standard multi-container dnd-kit pattern: onDragOver moves the
item into the target group locally as soon as you drag over it (pure
preview, nothing persisted yet); onDragEnd finalizes the exact position
and only fires a network request for whatever actually changed — order
within the final group, and the aisle itself only if it differs from
what it was when the drag started. Also handles dropping outside any
group: the local preview move is reverted instead of silently leaving an
unpersisted, visually-wrong position until refresh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each category group had its own isolated DndContext, so dnd-kit had no way
to resolve a drop that landed in a different group — cross-category drags
were silently no-ops by construction, not an edge-case bug.
Lifted to a single DndContext wrapping all groups, with one handleDragEnd
that looks up both the dragged item's and the drop target's current group
from live state: same group reorders as before, different group changes
the item's aisle (appended after the item it was dropped on) via the
existing changeCategory persistence path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed from a real screenshot: every item in an all-French shopping list
was uncategorized because guessAisle() only matched English keywords —
"sometimes reorganize doesn't work" was actually "always fails on non-English
ingredient names". Also fixed:
- Categories are now translated (grocery-categories.ts stores locale-
independent slugs like "produce", translated for display via
shoppingLists.categories.* instead of storing/rendering the English label
directly)
- Added French keyword coverage to the aisle guesser so auto-categorization
actually works for French recipes
- Users can now type a custom category name from the item's category menu
- The sort-mode dropdown was showing the raw value ("category") instead of
its label — base-ui's Select.Value has no built-in value->label lookup,
it needs an explicit render function, which no Select in this call site
had
- Widened the shopping list detail page (max-w-lg -> max-w-2xl)
- Root-caused a second bug visible in the same screenshot: ingredients with
the quantity embedded in the name (e.g. "1 avocat") still showed that way
even though quantity/unit were already populated separately, because the
extraction helper only fired when quantity was empty. Now it always
cleans the name but only backfills quantity/unit when they're missing,
and runs at shopping-list generation time too (not just recipe save) so
it also cleans up already-contaminated recipes, not just new ones.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fixed a real i18n bug: the checked-count line called the wrong
translation namespace and rendered the literal key on screen
- Shopping lists can now be renamed and deleted from both the list index
and detail pages (API already supported delete; rename was net new)
- Root-caused "long list UI is off": meal-plan-generated lists never set
an aisle, so every item fell into one undifferentiated "Other" bucket
despite the grouping UI existing. Added a keyword-based aisle guesser
wired into list generation (fallback only, never overrides an explicit
aisle) plus a one-click "auto-categorize" for existing lists
- Items can now be reordered by drag-and-drop within a category (dnd-kit),
recategorized via a dropdown, deleted, searched, and sorted (category /
alphabetical / unchecked-first); searching flattens the grouped view
- Fixed a separate bug: AI-generated ingredients sometimes embedded the
quantity/unit in the name itself (e.g. "2 cups flour" as one string).
Added extractIngredientQuantity() as a Zod transform at both recipe
create/update routes (the choke point every creation path funnels
through) to split it back out, plus schema descriptions on the AI
ingredient schemas as a prevention layer
New migration 0028 (shopping_list_items.sort_order), 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>
- Meal-plan page now has a direct "Add to shopping list" action for the
currently-viewed week, instead of requiring a trip to Shopping Lists and
manually typing the week's Monday date.
- Fixed a real under-shopping bug in shopping-lists/route.ts: when
generating from a meal-plan week, ingredients appearing in more than one
recipe were merged by keeping only the FIRST occurrence's quantity and
silently dropping the rest. New mergeIngredients() (pantry-shopping-match.ts)
groups by ingredientId (or normalized name+unit) and sums quantities
across every recipe in the week; incompatible/unparseable units are kept
as separate line items rather than guessed at.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
Six M-sized items from HANDOFF.md's new-features backlog:
- Profile tabs: cooking-history stats (total cooked, last-cooked, streak)
and a "cooked it" photo gallery, both owner-only
- Display-time unit conversion (metric<->imperial) for recipe ingredients,
respecting each user's unitPref; original value always shown alongside
the conversion
- Nutrition daily diary: per-day macro totals computed from cooking history
x recipe nutritionData, compared against user goals
- Pantry scan: real barcode lookup (zxing + Open Food Facts, no API key)
with an AI-vision fallback for unbarcoded items, always confirm-before-
insert, both paths tier/rate-limited like other AI features
- Weekly digest email: new followers/comments/ratings + trending recipes,
sent via a new `cron` Docker stage (alpine+crond+curl) and `digest-cron`
compose service hitting a bearer-token-protected internal route
- Meal-plan generation can now target a user's nutrition goals as a
prompt-level nudge (recipes are AI-invented, not DB-sourced, so this
can't be a hard macro constraint)
Caught a real deploy-breaking issue while adding the cron stage: appending
it after `runner` silently changed the Dockerfile's default build target,
and `web`'s compose config didn't pin one — fixed by pinning `target:
runner` explicitly. Verified with typecheck, lint, and three separate
`docker build --target` runs (runner/cron/migrator) plus `docker compose
config` validation.
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>
Full audit (bugs/UI-UX/backend/feature-gap) turned up a money-leak AI quota
bypass, webhook SSRF, and a long tail of missing pagination/auth/a11y work.
Fixes land together since HANDOFF.md tracked them as one backlog.
- AI routes charge tier quota before generating; nutrition POST is author-only
- Webhook dispatch re-validates URL per delivery (SSRF/DNS-rebinding), treats
redirects as failures; recipe.published now actually dispatches
- New indexes/unique constraints on recipes, meal-planning, comments FK cascade
- Recipe PUT/restore snapshot only inside the transaction, after validation
- Recipe DELETE cleans up S3 objects (recipe + review photos)
- Optimistic UI (favorite/star/follow/shopping-list) rolls back on failure
- Upload presign enforces file size cap + per-tier storage quota
- Route-level loading/error/not-found states across (app), admin, and root
- middleware.ts guards (app)/admin; requireAdmin checks DB role, not cached
session; rate limiting applied to both session and API-key branches,
bucketed per key; Stripe webhook dedupes by event id
- Pagination added to recipes, feed, profile, comments, pantry, admin tables
- Nav shows real avatar + profile link + dark-mode toggle; destructive actions
standardized on AlertDialog
- Unsaved-changes guard + real ingredient/step validation on recipe form;
canonical /recipes/[id] used in-app; next/image migration; aria-labels and
alt text across icon buttons, avatars, recipe photos
- packages/api-types removed (zero callers, too drifted to safely rewire);
openapi.ts and ai-keys error shape drift fixed; BYOK decrypt failures now
surface instead of silently falling back to the platform key
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pantry-page-header's Print button, the pantry-manager add/remove
form (item name, qty, unit, expiry badges), and the print/pantry
page (title, column headers, item count, empty state, footer) were
all hardcoded English.
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>
Implements the six previously-unscoped feature ideas plus a mobile
layout fix reported via screenshot:
- Mobile: Recipes/Collections/Pantry/Meal Plan/Shopping Lists headers
now stack and wrap instead of clipping buttons on narrow viewports.
- Recipe diff/compare view: word/list diff against any past version,
next to Restore in version history.
- Shared meal plans & shopping lists: new shoppingListMembers/
mealPlanMembers tables (viewer/editor roles, mirrors
collectionMembers), share dialogs, membership-checked routes.
- PDF cookbook export: /print/collection/[id] renders a whole
collection with page breaks, using the existing print-CSS pattern
instead of adding a PDF rendering dependency.
- Grocery delivery handoff: shopping lists can copy-as-text (works
today) or send to Instacart once INSTACART_API_KEY is configured
(stub adapter — real API needs a partner agreement).
- Personalized "For You" feed tab: ranks public recipes by tag/
dietary overlap with the user's favorited/highly-rated history.
- PWA: added manifest.json + icons on top of the existing service
worker so the app is installable; cook-mode pages were already
cached for offline use.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recipe cards, the recipe detail page, and meal planner still rendered raw
"{n} servings"/"{n}m cook"/"{n} srv" text and untranslated difficulty labels
outside the earlier i18n pass. Also wires the user's locale into AI features
that previously always answered in English regardless of app language:
recipe chat, ingredient substitution, recipe ideas, and generate-from-idea.
The recipe-language picker in the AI generate dialog now defaults to the
user's locale instead of always defaulting to English.
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.
- Substitute finder never applied the user's BYOK/admin AI key (only fell
back to raw process.env), so it silently failed whenever the key was
stored via settings rather than a literal env var. Now resolves via
getDefaultProviderWithKey like every other AI route. Popover also
surfaces real error messages instead of swallowing failures.
- Ingredients with quantity 0 (salt, pepper, "to taste") rendered the
literal digit "0" in cooking mode, print view, public recipe page,
shopping lists, and serving scaler — several sites relied on generic
truthiness/filter(Boolean), which doesn't catch a stored "0" string.
Added a shared hasQuantity() helper and applied it everywhere quantity
is rendered, plus in the AI recipe-chat context sent to the model.
- Recipe chat panel rendered a duplicate close button on top of shadcn
Sheet's own built-in close X, producing a garbled overlapping glyph.
Removed the duplicate.
- Recipe chat assistant replies are markdown from the model but were
rendered as raw text; added react-markdown so formatting actually
renders.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>