feat: Further improve recipe filter search and shopping list and recipe ingredient editor (#7063)

This commit is contained in:
Michael Genson
2026-02-14 00:34:17 -06:00
committed by GitHub
parent 8e225ee796
commit 73d86f6f6b
16 changed files with 267 additions and 160 deletions

View File

@@ -1,7 +1,7 @@
export { useFraction } from "./use-fraction";
export { useRecipe } from "./use-recipe";
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
export { useIngredientTextParser } from "./use-recipe-ingredients";
export { useNutritionLabels } from "./use-recipe-nutrition";
export { useTools } from "./use-recipe-tools";
export { useRecipePermissions } from "./use-recipe-permissions";

View File

@@ -1,17 +1,19 @@
import { describe, test, expect, vi, beforeEach } from "vitest";
import { parseIngredientText } from "./use-recipe-ingredients";
import { useIngredientTextParser } from "./use-recipe-ingredients";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { useLocales } from "../use-locales";
vi.mock("../use-locales");
describe(parseIngredientText.name, () => {
let parseIngredientText: (ingredient: RecipeIngredient, scale?: number, includeFormating?: boolean) => string;
describe("parseIngredientText", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useLocales).mockReturnValue({
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
locale: { value: "en-US", pluralFoodHandling: "always" },
} as any);
({ parseIngredientText } = useIngredientTextParser());
});
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
@@ -145,6 +147,7 @@ describe(parseIngredientText.name, () => {
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
locale: { value: "en-US", pluralFoodHandling: "always" },
} as any);
const { parseIngredientText } = useIngredientTextParser();
const ingredient = createRecipeIngredient({
quantity: 2,
@@ -160,6 +163,7 @@ describe(parseIngredientText.name, () => {
locales: [{ value: "en-US", pluralFoodHandling: "never" }],
locale: { value: "en-US", pluralFoodHandling: "never" },
} as any);
const { parseIngredientText } = useIngredientTextParser();
const ingredient = createRecipeIngredient({
quantity: 2,
@@ -175,6 +179,7 @@ describe(parseIngredientText.name, () => {
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
} as any);
const { parseIngredientText } = useIngredientTextParser();
const ingredient = createRecipeIngredient({
quantity: 2,
@@ -190,6 +195,7 @@ describe(parseIngredientText.name, () => {
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
} as any);
const { parseIngredientText } = useIngredientTextParser();
const ingredient = createRecipeIngredient({
quantity: 2,

View File

@@ -76,51 +76,59 @@ function shouldUsePluralFood(quantity: number, hasUnit: boolean, pluralFoodHandl
}
}
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
export function useIngredientTextParser() {
const { locales, locale } = useLocales();
const filteredLocales = locales.filter(lc => lc.value === locale.value);
const pluralFoodHandling = filteredLocales.length ? filteredLocales[0].pluralFoodHandling : "without-unit";
const { quantity, food, unit, note, referencedRecipe } = ingredient;
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
const usePluralFood = shouldUsePluralFood((quantity || 0) * scale, !!unit, pluralFoodHandling);
function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
const filteredLocales = locales.filter(lc => lc.value === locale.value);
const pluralFoodHandling = filteredLocales.length ? filteredLocales[0].pluralFoodHandling : "without-unit";
let returnQty = "";
const { quantity, food, unit, note, referencedRecipe } = ingredient;
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
const usePluralFood = shouldUsePluralFood((quantity || 0) * scale, !!unit, pluralFoodHandling);
// casting to number is required as sometimes quantity is a string
if (quantity && Number(quantity) !== 0) {
if (unit && !unit.fraction) {
returnQty = Number((quantity * scale).toPrecision(3)).toString();
}
else {
const fraction = frac(quantity * scale, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {
returnQty += fraction[0];
let returnQty = "";
// casting to number is required as sometimes quantity is a string
if (quantity && Number(quantity) !== 0) {
if (unit && !unit.fraction) {
returnQty = Number((quantity * scale).toPrecision(3)).toString();
}
else {
const fraction = frac(quantity * scale, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {
returnQty += fraction[0];
}
if (fraction[1] > 0) {
returnQty += includeFormating
? `<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>`
: ` ${fraction[1]}/${fraction[2]}`;
if (fraction[1] > 0) {
returnQty += includeFormating
? `<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>`
: ` ${fraction[1]}/${fraction[2]}`;
}
}
}
}
const unitName = useUnitName(unit || undefined, usePluralUnit);
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
const unitName = useUnitName(unit || undefined, usePluralUnit);
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
return {
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
note: note ? sanitizeIngredientHTML(note) : undefined,
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
};
};
function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
return sanitizeIngredientHTML(text);
};
return {
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
note: note ? sanitizeIngredientHTML(note) : undefined,
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
useParsedIngredientText,
parseIngredientText,
};
}
export function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
return sanitizeIngredientHTML(text);
}