diff --git a/src/components/LogoSearchForm.jsx b/src/components/LogoSearchForm.jsx index 1e10f514b9..1e8e8baa7e 100644 --- a/src/components/LogoSearchForm.jsx +++ b/src/components/LogoSearchForm.jsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import LabelFilter from "../components/QuestionFilter/LabelFilter"; import { TYPE_WITHOUT_VALUE } from "../const"; +import TaxonomyAutoSelect from "./TaxonomyAutoSelect"; export const logoTypeOptions = [ { value: "", labelKey: "logos.type" }, @@ -83,6 +84,16 @@ const LogoSearchForm = (props) => { label={t("logos.value")} size="small" /> + ) : innerType === "brand" ? ( + ) : ( { /> )} - & { + /** + * The taxonly to querry + */ + taxonomy: TaxonomyNames; + onChange: (itemId: string) => void; + value: string; + /** + * The the id bellow the text field + */ + showKey?: boolean; +}; + +const isOptionEqualToValue = (option: string | TaxonomyItem, value: string) => + (typeof option === "string" ? option : option.id) === value; + +export default function TaxonomyAutoSelect(props: TaxonomyAutoSelectProps) { + const { taxonomy, value, onChange, showKey, fullWidth, ...other } = props; + const [inputValue, setInputValue] = React.useState(""); + const [selectedValue, setSelectedValue] = React.useState(null); + const [options, setOptions] = React.useState< + readonly (string | TaxonomyItem)[] + >([]); + + const language = "en"; //getLang(); + + const fetch = React.useMemo( + () => + debounce( + ( + request: { input: string }, + callback: (results?: readonly TaxonomyItem[]) => void, + ) => { + searchTaxonomy[taxonomy](request.input, language).then(({ data }) => { + callback((data?.options as TaxonomyItem[]) ?? []); + }); + }, + 400, + ), + [language], + ); + + React.useEffect(() => { + let active = true; + + if (inputValue === "") { + setOptions(value ? [value] : []); + return undefined; + } + + fetch({ input: inputValue }, (results?: readonly TaxonomyItem[]) => { + if (active) { + let newOptions: readonly (string | TaxonomyItem)[] = []; + + // if (value) { + // newOptions = []; + // } + + if (results) { + newOptions = [...newOptions, ...results]; + } + + setOptions(newOptions); + } + }); + + return () => { + active = false; + }; + }, [value, inputValue, fetch]); + + const selectedOption = options.find((option) => + isOptionEqualToValue(option, value), + ); + return ( + + typeof option === "string" ? option : option.text + } + freeSolo + filterOptions={(x) => x} + options={options} + autoComplete + includeInputInList + filterSelectedOptions + value={ + selectedValue && selectedValue.id === value ? selectedValue : value + } + isOptionEqualToValue={isOptionEqualToValue} + // noOptionsText="No locations" + onChange={(event: any, newValue: TaxonomyItem | null | string) => { + console.log({ newValue }); + if (typeof newValue === "object") { + onChange(newValue.id); + setSelectedValue(newValue); + return; + } + onChange(newValue); + }} + onInputChange={(event, newInputValue) => { + setInputValue(newInputValue); + }} + onBlur={() => { + console.log("blur"); + console.log({ + inputValue, + value, + selectedOption, + }); + if ( + inputValue === value || + (selectedValue && selectedValue.text === inputValue) + ) { + return; + } + onChange(inputValue); + }} + fullWidth={fullWidth} + renderInput={(params) => ( + { + console.log(event.key); + if (event.key === "Enter") { + event.preventDefault(); + } + }} + helperText={ + showKey && value + ? selectedValue && selectedValue.id === value + ? selectedValue.id + : `⚠️ unknown: "${value}"` + : "" + } + /> + )} + /> + ); +} diff --git a/src/const.ts b/src/const.ts index d805b3eaa5..d1e5f133f3 100644 --- a/src/const.ts +++ b/src/const.ts @@ -6,6 +6,8 @@ export const OFF_API_URL_V2 = `${OFF_URL}/api/v2`; export const OFF_API_URL_V3 = `${OFF_URL}/api/v3`; export const OFF_IMAGE_URL = `https://static.${OFF_DOMAIN}/images/products`; export const OFF_SEARCH = `${OFF_URL}/cgi/search.pl`; +export const OFF_SEARCH_A_LISIOUS = + "https://search.openfoodfacts.org/autocomplete"; export const IS_DEVELOPMENT_MODE = process.env.NODE_ENV === "development"; export const URL_ORIGINE = IS_DEVELOPMENT_MODE ? "http://localhost:5173" diff --git a/src/offSearch.ts b/src/offSearch.ts new file mode 100644 index 0000000000..378b179564 --- /dev/null +++ b/src/offSearch.ts @@ -0,0 +1,88 @@ +import axios, { AxiosResponse } from "axios"; +import { OFF_SEARCH_A_LISIOUS } from "./const"; + +export type TaxonomyNames = + | "additive" + | "allergen" + | "amino_acid" + | "brand" + | "category" + | "country" + | "data_quality" + | "label" + | "food_group" + | "improvement" + | "ingredient" + | "ingredients_analysis" + | "ingredients_processing" + | "language" + | "mineral" + | "misc" + | "nova_group" + | "nucleotide" + | "nutrient" + | "origin" + | "other_nutritional_substance" + | "packaging_material" + | "packaging_recycling" + | "packaging_shape" + | "periods_after_opening" + | "preservation" + | "state" + | "vitamin"; + +const taxonomy_names: TaxonomyNames[] = [ + "additive", + "allergen", + "amino_acid", + "brand", + "category", + "country", + "data_quality", + "label", + "food_group", + "improvement", + "ingredient", + "ingredients_analysis", + "ingredients_processing", + "language", + "mineral", + "misc", + "nova_group", + "nucleotide", + "nutrient", + "origin", + "other_nutritional_substance", + "packaging_material", + "packaging_recycling", + "packaging_shape", + "periods_after_opening", + "preservation", + "state", + "vitamin", +]; + +const searchTaxonomy = {}; + +taxonomy_names.forEach((taxonomy) => { + searchTaxonomy[taxonomy] = (querry: string, language?: string) => + axios.get( + `${OFF_SEARCH_A_LISIOUS}?taxonomy_names=${taxonomy}&q=${querry}&lang=${ + language || "en" + }`, + ); +}); + +export type TaxonomyItem = { + id: string; + text: string; + taxonomy_name: string; +}; + +export default searchTaxonomy as Record< + TaxonomyNames, + ( + querry: string, + language?: string, + ) => Promise> +>; diff --git a/src/robotoff.ts b/src/robotoff.ts index 4c1cd90a8d..0097dc006a 100644 --- a/src/robotoff.ts +++ b/src/robotoff.ts @@ -106,7 +106,7 @@ const robotoff = { }, searchLogos(barcode, value, type, count = 25, random = false) { - const formattedValue = ["label", "category"].includes(type) + const formattedValue = value.test(/^[a-z][a-z]:/) ? { taxonomy_value: value } : { value };