diff --git a/src/components/CategoryAutocomplete.tsx b/src/components/CategoryAutocomplete.tsx new file mode 100644 index 0000000..dff35c5 --- /dev/null +++ b/src/components/CategoryAutocomplete.tsx @@ -0,0 +1,333 @@ +import { useLocalStorage } from 'hooks' +import { useMemo, useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { getGamePageRoute } from 'routes' +import { ModFormState, Categories, Category } from 'types' +import { + getCategories, + flattenCategories, + addToUserCategories, + capitalizeEachWord +} from 'utils' + +interface CategoryAutocompleteProps { + game: string + LTags: string[] + setFormState: (value: React.SetStateAction) => void +} + +export const CategoryAutocomplete = ({ + game, + LTags, + setFormState +}: CategoryAutocompleteProps) => { + // Fetch the hardcoded categories from assets + const flattenedCategories = useMemo(() => getCategories(), []) + + // Fetch the user categories from local storage + const [userHierarchies, setUserHierarchies] = useLocalStorage< + (string | Category)[] + >('user-hierarchies', []) + const flattenedUserCategories = useMemo( + () => flattenCategories(userHierarchies, []), + [userHierarchies] + ) + + // Create options and select categories from the mod LTags (hierarchies) + const { selectedCategories, combinedOptions } = useMemo(() => { + const combinedCategories = [ + ...flattenedCategories, + ...flattenedUserCategories + ] + const hierarchies = LTags.map((hierarchy) => { + const existingCategory = combinedCategories.find( + (cat) => cat.hierarchy === hierarchy.replace(/:/g, ' > ') + ) + if (existingCategory) { + return existingCategory + } else { + const segments = hierarchy.split(':') + const lastSegment = segments[segments.length - 1] + return { name: lastSegment, hierarchy: hierarchy, l: [lastSegment] } + } + }) + + // Selected categorires (based on the LTags) + const selectedCategories = Array.from(new Set([...hierarchies])) + + // Combine user, predefined category hierarchies and selected values (LTags in case some are missing) + const combinedOptions = Array.from( + new Set([...combinedCategories, ...selectedCategories]) + ) + + return { selectedCategories, combinedOptions } + }, [LTags, flattenedCategories, flattenedUserCategories]) + + const [inputValue, setInputValue] = useState('') + const filteredOptions = useMemo( + () => + combinedOptions.filter((option) => + option.hierarchy.toLowerCase().includes(inputValue.toLowerCase()) + ), + [combinedOptions, inputValue] + ) + + const getSelectedCategories = (cats: Categories[]) => { + const uniqueValues = new Set( + cats.reduce((prev, cat) => [...prev, ...cat.l], []) + ) + const concatenatedValue = Array.from(uniqueValues) + return concatenatedValue + } + const getSelectedHierarchy = (cats: Categories[]) => { + const hierarchies = cats.reduce( + (prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')], + [] + ) + const concatenatedValue = Array.from(hierarchies) + return concatenatedValue + } + const handleReset = () => { + setFormState((prevState) => ({ + ...prevState, + ['lTags']: [], + ['LTags']: [] + })) + setInputValue('') + } + const handleRemove = (option: Categories) => { + const updatedCategories = selectedCategories.filter( + (cat) => cat.hierarchy !== option.hierarchy + ) + setFormState((prevState) => ({ + ...prevState, + ['lTags']: getSelectedCategories(updatedCategories), + ['LTags']: getSelectedHierarchy(updatedCategories) + })) + } + const handleSelect = (option: Categories) => { + if (!selectedCategories.some((cat) => cat.hierarchy === option.hierarchy)) { + const updatedCategories = [...selectedCategories, option] + setFormState((prevState) => ({ + ...prevState, + ['lTags']: getSelectedCategories(updatedCategories), + ['LTags']: getSelectedHierarchy(updatedCategories) + })) + } + setInputValue('') + } + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value) + } + const handleAddNew = () => { + if (inputValue) { + const value = inputValue.trim().toLowerCase() + const values = value.split('>').map((s) => s.trim()) + const newOption: Categories = { + name: value, + hierarchy: value, + l: values + } + setUserHierarchies((prev) => { + addToUserCategories(prev, value) + return [...prev] + }) + const updatedCategories = [...selectedCategories, newOption] + setFormState((prevState) => ({ + ...prevState, + ['lTags']: getSelectedCategories(updatedCategories), + ['LTags']: getSelectedHierarchy(updatedCategories) + })) + setInputValue('') + } + } + const handleAddNewCustom = (option: Categories) => { + setUserHierarchies((prev) => { + addToUserCategories(prev, option.hierarchy) + return [...prev] + }) + } + + const Row = ({ index }: { index: number }) => { + return ( +
handleSelect(filteredOptions[index])} + > + {capitalizeEachWord(filteredOptions[index].hierarchy)} + + {/* Show "Remove" button when the category is selected */} + {selectedCategories.some( + (cat) => cat.hierarchy === filteredOptions[index].hierarchy + ) && ( + + )} + + {/* Show "Add" button when the category is not included in the predefined or userdefined lists */} + {!flattenedCategories.some( + (cat) => cat.hierarchy === filteredOptions[index].hierarchy + ) && + !flattenedUserCategories.some( + (cat) => cat.hierarchy === filteredOptions[index].hierarchy + ) && ( + + )} +
+ ) + } + + return ( +
+ +

You can select multiple categories

+
+
+ + + +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((c, i) => ) + ) : ( +
+ {inputValue && + !filteredOptions?.find( + (option) => + option.hierarchy.toLowerCase() === inputValue.toLowerCase() + ) ? ( + <> + Add "{inputValue}" + + + ) : ( + <>No matches + )} +
+ )} +
+
+
+ {LTags.length > 0 && ( +
+ {LTags.map((hierarchy) => { + const hierarchicalCategories = hierarchy.split(`:`) + const categories = hierarchicalCategories + .map((c, i) => { + const partialHierarchy = hierarchicalCategories + .slice(0, i + 1) + .join(':') + + return game ? ( + +

{capitalizeEachWord(c)}

+ + ) : ( +

+ {capitalizeEachWord(c)} +

+ ) + }) + .reduce((prev, curr, i) => [ + prev, +
+

>

+
, + curr + ]) + + return ( +
+ {categories} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx index a3da21e..d118991 100644 --- a/src/components/Filters/CategoryFilterPopup.tsx +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -43,13 +43,13 @@ export const CategoryFilterPopup = ({ ), [inputValue, userHierarchies] ) - const hierarchiesMatching = useMemo( - () => - flattenCategories(categoriesData, []).some((h) => - h.hierarchy.includes(inputValue.toLowerCase()) - ), - [inputValue] - ) + // const hierarchiesMatching = useMemo( + // () => + // flattenCategories(categoriesData, []).some((h) => + // h.hierarchy.includes(inputValue.toLowerCase()) + // ), + // [inputValue] + // ) const handleInputChange = (e: React.ChangeEvent) => { setInputValue(e.target.value) @@ -230,7 +230,7 @@ export const CategoryFilterPopup = ({
Already defined in your categories
) : (
Add and search for "{inputValue}" category @@ -392,10 +392,8 @@ const CategoryCheckbox: React.FC = ({ <> {isMatching && (
= ({ checked={isCombinationChecked} onChange={handleCombinationChange} /> -