import PhotoIcon from "@mui/icons-material/Photo"; import SearchIcon from "@mui/icons-material/Search"; import TuneIcon from '@mui/icons-material/Tune'; import { Autocomplete, Avatar, Box, Chip, FormControl, FormControlLabel, FormGroup, IconButton, InputAdornment, InputLabel, ListItem, ListItemIcon, ListItemText, MenuItem, Popover, Select, Slider, Stack, Switch, TextField, Typography, } from "@mui/material"; import { Ingredient } from "@/components/Nutrition/models/Ingredient"; import { useSearchIngredientQuery } from "@/components/Nutrition/queries"; import { NutriScoreBadge } from "@/components/Nutrition/widgets/NutriScoreBadge"; import debounce from "lodash/debounce"; import * as React from 'react'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from "react-i18next"; import { NUTRI_SCORES, NutriScoreValue } from "@/types"; import { SearchLanguageFilter } from "@/core/ui/Widgets/SearchLanguageFilter"; import { LANGUAGE_SHORT_ENGLISH } from "@/core/lib/consts"; type IngredientAutocompleterProps = { callback: (ingredient: Ingredient | null) => void; initialIngredient?: Ingredient | null; }; export const STORAGE_KEY_LANGUAGE_FILTER = "wger.ingredientSearch.languageFilter"; export const STORAGE_KEY_VEGAN = "wger.ingredientSearch.filterVegan"; export const STORAGE_KEY_VEGETARIAN = "wger.ingredientSearch.filterVegetarian"; export const STORAGE_KEY_NUTRISCORE_MAX = "wger.ingredientSearch.filterNutriscoreMax"; export const SEARCH_DEBOUNCE_MS = 400; const NUTRISCORE_OFF_INDEX = 0; const isNutriScoreValue = (value: string | null): value is NutriScoreValue => value !== null && (NUTRI_SCORES as readonly string[]).includes(value); const sliderIndexToNutriscore = (index: number): NutriScoreValue | null => index === NUTRISCORE_OFF_INDEX ? null : NUTRI_SCORES[index - 1]; const nutriscoreToSliderIndex = (value: NutriScoreValue | null): number => value === null ? NUTRISCORE_OFF_INDEX : NUTRI_SCORES.indexOf(value) + 1; export function IngredientAutocompleter({ callback, initialIngredient }: IngredientAutocompleterProps) { const initialData = initialIngredient ?? null; const [t, i18n] = useTranslation(); const defaultLanguageFilter: SearchLanguageFilter = i18n.language === LANGUAGE_SHORT_ENGLISH ? "current" : "current_english"; const [languageFilter, setLanguageFilter] = useState(() => { const stored = localStorage.getItem(STORAGE_KEY_LANGUAGE_FILTER); return (stored as SearchLanguageFilter | null) ?? defaultLanguageFilter; }); const [filterVegan, setFilterVegan] = useState(() => { return localStorage.getItem(STORAGE_KEY_VEGAN) === "true"; }); const [filterVegetarian, setFilterVegetarian] = useState(() => { return localStorage.getItem(STORAGE_KEY_VEGETARIAN) === "true"; }); const [nutriscoreMax, setNutriscoreMax] = useState(() => { const stored = localStorage.getItem(STORAGE_KEY_NUTRISCORE_MAX); return isNutriScoreValue(stored) ? stored : null; }); const [filtersAnchorEl, setFiltersAnchorEl] = useState(null); const [value, setValue] = useState(initialData); const [inputValue, setInputValue] = useState(""); const [options, setOptions] = useState([]); useEffect(() => { if (i18n.language === LANGUAGE_SHORT_ENGLISH && languageFilter === "current_english") { setLanguageFilter("current"); } }, [i18n.language, languageFilter]); useEffect(() => { localStorage.setItem(STORAGE_KEY_LANGUAGE_FILTER, languageFilter); }, [languageFilter]); useEffect(() => { localStorage.setItem(STORAGE_KEY_VEGAN, String(filterVegan)); }, [filterVegan]); useEffect(() => { localStorage.setItem(STORAGE_KEY_VEGETARIAN, String(filterVegetarian)); }, [filterVegetarian]); useEffect(() => { if (nutriscoreMax === null) { localStorage.removeItem(STORAGE_KEY_NUTRISCORE_MAX); } else { localStorage.setItem(STORAGE_KEY_NUTRISCORE_MAX, nutriscoreMax); } }, [nutriscoreMax]); const languageOptions = useMemo(() => { const options: Array<{ value: SearchLanguageFilter; label: string }> = [ { value: "current", label: t("nutrition.languageFilterCurrentOnly", { lang: i18n.language }), }, ]; if (i18n.language !== LANGUAGE_SHORT_ENGLISH) { options.push({ value: "current_english", label: t("nutrition.languageFilterCurrentAndEnglish", { lang: i18n.language }), }); } options.push({ value: "all", label: t("nutrition.languageFilterAll"), }); return options; }, [i18n.language, t]); const searchIngredient = useSearchIngredientQuery(); const fetchName = useMemo( () => debounce( (request: string) => searchIngredient(request, { languageCode: i18n.language, languageFilter, isVegan: filterVegan || undefined, isVegetarian: filterVegetarian || undefined, nutriscoreMax: nutriscoreMax ?? undefined, }).then((res) => setOptions(res)), SEARCH_DEBOUNCE_MS ), [searchIngredient, i18n.language, languageFilter, filterVegan, filterVegetarian, nutriscoreMax] ); useEffect(() => { if (inputValue === "") { setOptions(value ? [value] : []); return undefined; } fetchName(inputValue); return () => { fetchName.cancel(); }; }, [value, inputValue, fetchName]); const isFiltersOpen = Boolean(filtersAnchorEl); const filtersPopoverId = isFiltersOpen ? "ingredient-filters-popover" : undefined; return ( option.name} data-testid="autocomplete" filterOptions={(x) => x} options={options} autoComplete includeInputInList filterSelectedOptions value={value} noOptionsText={t("noResults")} isOptionEqualToValue={(option, value) => option.id === value.id} onChange={(event: unknown, newValue: Ingredient | null) => { setOptions(newValue ? [newValue, ...options] : options); setValue(newValue); callback(newValue); }} onInputChange={(event, newInputValue) => { setInputValue(newInputValue); }} renderInput={(params) => ( {params.slotProps?.input?.startAdornment} ), endAdornment: ( <> {params.slotProps?.input?.endAdornment} event.preventDefault()} onClick={(event) => setFiltersAnchorEl((current) => current ? null : (event.currentTarget as HTMLElement) ) } edge="end" size="small" > ), }, }} /> )} renderOption={(props, ingredient) => { return (
  • {ingredient.isVegan === true && ( )} {ingredient.isVegetarian === true && !ingredient.isVegan && ( )} {ingredient.nutriscore !== null && ( )}
  • ); }} /> setFiltersAnchorEl(null)} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > {t("language")} setFilterVegan(checked)} /> } label={t("nutrition.filterVegan")} /> setFilterVegetarian(checked)} /> } label={t("nutrition.filterVegetarian")} /> {t("nutrition.filterNutriscore")} {nutriscoreMax === null ? t("nutrition.filterNutriscoreNoFilter") : t("nutrition.filterNutriscoreOrBetter", { grade: nutriscoreMax.toUpperCase() })} setNutriscoreMax(sliderIndexToNutriscore(value as number)) } getAriaValueText={(value) => value === NUTRISCORE_OFF_INDEX ? t("nutrition.filterNutriscoreNoFilter") : t("nutrition.filterNutriscoreOrBetter", { grade: NUTRI_SCORES[value - 1].toUpperCase() }) } step={1} min={0} max={NUTRI_SCORES.length} marks={[ { value: NUTRISCORE_OFF_INDEX, label: t("nutrition.filterNutriscoreOff") }, ...NUTRI_SCORES.map((score, index) => ({ value: index + 1, label: score.toUpperCase(), })), ]} />
    ); }