fix: shopping list categories were English-only and untranslated, page too narrow
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>
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
* untouched and just flag `inPantry` so the user can decide for themselves.
|
||||
*/
|
||||
|
||||
import { extractIngredientQuantity } from "./extract-ingredient-quantity";
|
||||
|
||||
export type PantrySourceItem = {
|
||||
ingredientId: string | null;
|
||||
rawName: string;
|
||||
@@ -138,17 +140,22 @@ export function mergeIngredients(ingredients: MergeSourceIngredient[]): MergedIn
|
||||
const groups = new Map<string, { rawName: string; ingredientId: string | null; unit: string | null; entries: (string | null)[] }>();
|
||||
|
||||
for (const ing of ingredients) {
|
||||
const unitKey = normalizeUnit(ing.unit);
|
||||
// Clean up ingredients whose stored rawName still has a quantity baked into it
|
||||
// (older AI-generated recipes, before that was fixed at the source) BEFORE grouping —
|
||||
// otherwise "1 avocat" and a cleanly-named "avocat" from another recipe would
|
||||
// normalize to different group keys and never merge.
|
||||
const cleaned = extractIngredientQuantity(ing.rawName, ing.quantity ?? undefined, ing.unit ?? undefined);
|
||||
const unitKey = normalizeUnit(cleaned.unit);
|
||||
const groupKey = ing.ingredientId
|
||||
? `id:${ing.ingredientId}:${unitKey}`
|
||||
: `name:${normalizeName(ing.rawName)}:${unitKey}`;
|
||||
: `name:${normalizeName(cleaned.rawName)}:${unitKey}`;
|
||||
|
||||
let group = groups.get(groupKey);
|
||||
if (!group) {
|
||||
group = { rawName: ing.rawName, ingredientId: ing.ingredientId, unit: ing.unit, entries: [] };
|
||||
group = { rawName: cleaned.rawName, ingredientId: ing.ingredientId, unit: cleaned.unit ?? null, entries: [] };
|
||||
groups.set(groupKey, group);
|
||||
}
|
||||
group.entries.push(ing.quantity);
|
||||
group.entries.push(cleaned.quantity ?? null);
|
||||
}
|
||||
|
||||
return Array.from(groups.values()).map((group) => {
|
||||
|
||||
Reference in New Issue
Block a user