feat: shopping list rename/delete, item reorder+categories+search, ingredient-quantity parsing fix
- 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>
This commit is contained in:
@@ -8,6 +8,7 @@ import { db, shoppingLists, eq } from "@epicure/db";
|
||||
import { ShoppingListView } from "@/components/meal-plan/shopping-list-view";
|
||||
import { ShareShoppingListButton } from "@/components/shopping-lists/share-shopping-list-button";
|
||||
import { GroceryExportButton } from "@/components/shopping-lists/grocery-export-button";
|
||||
import { ShoppingListActionsMenu } from "@/components/shopping-lists/shopping-list-actions-menu";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getShoppingListAccess, canWriteShoppingList } from "@/lib/shopping-list-access";
|
||||
@@ -30,7 +31,7 @@ export default async function ShoppingListPage({ params }: Params) {
|
||||
|
||||
const list = await db.query.shoppingLists.findFirst({
|
||||
where: eq(shoppingLists.id, id),
|
||||
with: { items: { orderBy: (t, { asc }) => [asc(t.aisle), asc(t.rawName)] } },
|
||||
with: { items: { orderBy: (t, { asc }) => [asc(t.aisle), asc(t.sortOrder), asc(t.rawName)] } },
|
||||
});
|
||||
if (!list) notFound();
|
||||
|
||||
@@ -58,6 +59,9 @@ export default async function ShoppingListPage({ params }: Params) {
|
||||
markdown={shoppingListToMarkdown({ name: list.name, items: list.items })}
|
||||
filename={list.name}
|
||||
/>
|
||||
{access.role === "owner" && (
|
||||
<ShoppingListActionsMenu listId={id} name={list.name} redirectAfterDeleteTo="/shopping-lists" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ShoppingListView
|
||||
@@ -70,6 +74,7 @@ export default async function ShoppingListPage({ params }: Params) {
|
||||
unit: i.unit,
|
||||
aisle: i.aisle,
|
||||
checked: i.checked,
|
||||
sortOrder: i.sortOrder,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user