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:
@@ -654,6 +654,16 @@
|
||||
"aisleOther": "Other",
|
||||
"listEmptyState": "This list is empty.",
|
||||
"checkedCount": "{checked}/{total} checked",
|
||||
"searchItemsPlaceholder": "Search items…",
|
||||
"sortByCategory": "By category",
|
||||
"sortByAlpha": "Alphabetical",
|
||||
"sortByUnchecked": "Unchecked first",
|
||||
"noSearchResults": "No items match your search.",
|
||||
"autoCategorize": "Auto-categorize",
|
||||
"autoCategorizeSuccess": "{count, plural, one {1 item categorized} other {{count} items categorized}}",
|
||||
"autoCategorizeNoneFound": "Couldn't guess a category for the remaining items",
|
||||
"removeItemConfirmTitle": "Remove item?",
|
||||
"removeItemConfirmDescription": "Remove \"{name}\" from this list?",
|
||||
"moveToPantry": "Move {count} to pantry",
|
||||
"listCreated": "List created",
|
||||
"days": {
|
||||
@@ -803,7 +813,20 @@
|
||||
"ingredientsWillBeAdded": "{count, plural, one {1 ingredient will be added.} other {{count} ingredients will be added.}}",
|
||||
"adding": "Adding…",
|
||||
"addIngredients": "Add ingredients",
|
||||
"addedCount": "{count} ingredients added to list"
|
||||
"addedCount": "{count} ingredients added to list",
|
||||
"actionsMenuLabel": "List actions",
|
||||
"rename": "Rename",
|
||||
"renameListTitle": "Rename list",
|
||||
"renameListLabel": "List name",
|
||||
"renaming": "Saving…",
|
||||
"listRenamed": "List renamed",
|
||||
"listRenameFailed": "Failed to rename list",
|
||||
"deleteListConfirmTitle": "Delete list?",
|
||||
"deleteListConfirmDescription": "This action cannot be undone. The list and all its items will be permanently deleted.",
|
||||
"listDeleted": "List deleted",
|
||||
"listDeleteFailed": "Failed to delete list",
|
||||
"changeCategory": "Change category",
|
||||
"deleteItem": "Delete item"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
|
||||
@@ -642,6 +642,16 @@
|
||||
"aisleOther": "Autre",
|
||||
"listEmptyState": "Cette liste est vide.",
|
||||
"checkedCount": "{checked}/{total} cochés",
|
||||
"searchItemsPlaceholder": "Rechercher des articles…",
|
||||
"sortByCategory": "Par catégorie",
|
||||
"sortByAlpha": "Alphabétique",
|
||||
"sortByUnchecked": "Non cochés d'abord",
|
||||
"noSearchResults": "Aucun article ne correspond à votre recherche.",
|
||||
"autoCategorize": "Catégoriser automatiquement",
|
||||
"autoCategorizeSuccess": "{count, plural, one {1 article catégorisé} other {{count} articles catégorisés}}",
|
||||
"autoCategorizeNoneFound": "Impossible de deviner une catégorie pour les articles restants",
|
||||
"removeItemConfirmTitle": "Supprimer l'article ?",
|
||||
"removeItemConfirmDescription": "Supprimer « {name} » de cette liste ?",
|
||||
"moveToPantry": "Déplacer {count} vers le garde-manger",
|
||||
"listCreated": "Liste créée",
|
||||
"days": {
|
||||
@@ -791,7 +801,20 @@
|
||||
"ingredientsWillBeAdded": "{count, plural, one {1 ingrédient sera ajouté.} other {{count} ingrédients seront ajoutés.}}",
|
||||
"adding": "Ajout…",
|
||||
"addIngredients": "Ajouter les ingrédients",
|
||||
"addedCount": "{count} ingrédients ajoutés à la liste"
|
||||
"addedCount": "{count} ingrédients ajoutés à la liste",
|
||||
"actionsMenuLabel": "Actions de la liste",
|
||||
"rename": "Renommer",
|
||||
"renameListTitle": "Renommer la liste",
|
||||
"renameListLabel": "Nom de la liste",
|
||||
"renaming": "Enregistrement…",
|
||||
"listRenamed": "Liste renommée",
|
||||
"listRenameFailed": "Échec du renommage de la liste",
|
||||
"deleteListConfirmTitle": "Supprimer la liste ?",
|
||||
"deleteListConfirmDescription": "Cette action est irréversible. La liste et tous ses articles seront définitivement supprimés.",
|
||||
"listDeleted": "Liste supprimée",
|
||||
"listDeleteFailed": "Échec de la suppression de la liste",
|
||||
"changeCategory": "Changer de catégorie",
|
||||
"deleteItem": "Supprimer l'article"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
|
||||
Reference in New Issue
Block a user