feat(category): category filter popup

This commit is contained in:
enes 2024-12-05 13:02:04 +01:00
parent ecbe839b30
commit 8d9bbbc7a5
3 changed files with 403 additions and 6 deletions

View File

@ -0,0 +1,3 @@
.noResult:not(:only-child) {
display: none;
}

View File

@ -1,3 +1,371 @@
export const CategoryFilterPopup = () => {
return <>Popup</>
import React, { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Category } from 'types'
import categoriesData from './../../assets/categories/categories.json'
import { capitalizeEachWord } from 'utils'
import styles from './CategoryFilterPopup.module.scss'
interface CategoryFilterPopupProps {
categories: string[]
setCategories: React.Dispatch<React.SetStateAction<string[]>>
heirarchies: string[]
setHeirarchies: React.Dispatch<React.SetStateAction<string[]>>
handleClose: () => void
handleApply: () => void
}
export const CategoryFilterPopup = ({
categories,
setCategories,
heirarchies,
setHeirarchies,
handleClose,
handleApply
}: CategoryFilterPopupProps) => {
const [inputValue, setInputValue] = useState<string>('')
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
}
const handleSingleSelection = (category: string, isSelected: boolean) => {
let updatedCategories = [...categories]
if (isSelected) {
updatedCategories.push(category)
} else {
updatedCategories = updatedCategories.filter((item) => item !== category)
}
setCategories(updatedCategories)
}
const handleCombinationSelection = (path: string[], isSelected: boolean) => {
const pathString = path.join(':')
let updatedHeirarchies = [...heirarchies]
if (isSelected) {
updatedHeirarchies.push(pathString)
} else {
updatedHeirarchies = updatedHeirarchies.filter(
(item) => item !== pathString
)
}
setHeirarchies(updatedHeirarchies)
}
const handleAddNew = () => {
if (inputValue) {
const values = inputValue
.trim()
.split('>')
.map((s) => s.trim())
if (values.length > 1) {
setHeirarchies([...categories, values.join(':')])
} else {
setCategories([...categories, values[0]])
}
setInputValue('')
}
}
return createPortal(
<div className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>Categories filter</h3>
</div>
<div className='popUpMainCardTopClose' onClick={handleClose}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<div className='inputLabelWrapperMain'>
<label
className='form-label labelMain'
style={{ fontWeight: 'bold' }}
>
Choose categories...
</label>
<p className='labelDescriptionMain'>
This is description for an input and how to use search here
</p>
</div>
<input
type='text'
className='inputMain inputMainWithBtn dropdown-toggle'
placeholder='Select some categories...'
aria-expanded='false'
data-bs-toggle='dropdown'
value={inputValue}
onChange={handleInputChange}
/>
{true && (
<div className='inputLabelWrapperMain'>
<label
className='form-label labelMain'
style={{ fontWeight: 'bold' }}
>
Custom categories
</label>
<p className='labelDescriptionMain'>Maybe</p>
</div>
)}
<div className='inputLabelWrapperMain'>
<div
className='inputMain'
style={{
minHeight: '40px',
maxHeight: '500px',
height: '100%',
overflow: 'auto'
}}
>
<div className={`${styles.noResult}`}>
<div>No results.</div>
<br />
<div
className='dropdown-item dropdownMainMenuItem'
onClick={handleAddNew}
>
Search for "{inputValue}" category
<button className='btn btnMain btnMainInsideField btnMainRemove'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z'></path>
</svg>
</button>
</div>
</div>
{(categoriesData as Category[]).map((category) => (
<CategoryCheckbox
inputValue={inputValue}
key={category.name}
category={category}
path={[category.name]}
handleSingleSelection={handleSingleSelection}
handleCombinationSelection={handleCombinationSelection}
selectedSingles={categories}
selectedCombinations={heirarchies}
/>
))}
</div>
</div>
<div
style={{
display: 'flex',
width: '100%',
gap: '10px'
}}
>
<button
className='btn btnMain btnMainPopup'
type='button'
onPointerDown={handleClose}
>
Cancel
</button>
<button
className='btn btnMain btnMainPopup'
type='button'
onPointerDown={() => {
setCategories([])
setHeirarchies([])
}}
>
Reset
</button>
<button
className='btn btnMain btnMainPopup'
type='button'
onPointerDown={() => {
handleApply()
handleClose()
}}
>
Apply
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
document.body
)
}
interface CategoryCheckboxProps {
inputValue: string
category: Category | string
path: string[]
handleSingleSelection: (category: string, isSelected: boolean) => void
handleCombinationSelection: (path: string[], isSelected: boolean) => void
selectedSingles: string[]
selectedCombinations: string[]
indentLevel?: number
}
const CategoryCheckbox: React.FC<CategoryCheckboxProps> = ({
inputValue,
category,
path,
handleSingleSelection,
handleCombinationSelection,
selectedSingles,
selectedCombinations,
indentLevel = 0
}) => {
const name = typeof category === 'string' ? category : category.name
const isMatching = path
.join(' > ')
.toLowerCase()
.includes(inputValue.toLowerCase())
const [isSingleChecked, setIsSingleChecked] = useState<boolean>(false)
const [isCombinationChecked, setIsCombinationChecked] =
useState<boolean>(false)
const [isIndeterminate, setIsIndeterminate] = useState<boolean>(false)
useEffect(() => {
const pathString = path.join(':')
setIsSingleChecked(selectedSingles.includes(name))
setIsCombinationChecked(selectedCombinations.includes(pathString))
const childPaths =
category.sub && Array.isArray(category.sub)
? category.sub.map((sub) =>
typeof sub === 'string'
? [...path, sub].join(':')
: [...path, sub.name].join(':')
)
: []
const anyChildCombinationSelected = childPaths.some((childPath) =>
selectedCombinations.includes(childPath)
)
if (
anyChildCombinationSelected &&
!selectedCombinations.includes(pathString)
) {
setIsIndeterminate(true)
} else {
setIsIndeterminate(false)
}
}, [selectedSingles, selectedCombinations, path, name, category.sub])
const handleSingleChange = () => {
setIsSingleChecked(!isSingleChecked)
handleSingleSelection(name, !isSingleChecked)
}
const handleCombinationChange = () => {
setIsCombinationChecked(!isCombinationChecked)
handleCombinationSelection(path, !isCombinationChecked)
}
return (
<>
{isMatching && (
<div
className='dropdown-item dropdownMainMenuItem'
style={{
display: 'flex',
alignItems: 'center',
marginLeft: `${indentLevel * 20}px`,
width: `calc(100% - ${indentLevel * 20}px)`
}}
>
<div
className={`inputLabelWrapperMain inputLabelWrapperMainAlt stylized`}
style={{
overflow: 'hidden'
}}
>
<input
id={name}
type='checkbox'
ref={(input) => {
if (input) {
input.indeterminate = isIndeterminate
}
}}
className='CheckboxMain'
checked={isCombinationChecked}
onChange={handleCombinationChange}
/>
<label
htmlFor={name}
className='form-label labelMain'
style={{
color: isIndeterminate ? 'green' : 'white'
}}
>
{capitalizeEachWord(name)}
</label>
<input
style={{
display: 'none'
}}
id={name}
type='checkbox'
className='CheckboxMain'
name={name}
checked={isSingleChecked}
onChange={handleSingleChange}
/>
</div>
</div>
)}
{typeof category !== 'string' &&
category.sub &&
Array.isArray(category.sub) && (
<>
{category.sub.map((subCategory) => {
if (typeof subCategory === 'string') {
return (
<CategoryCheckbox
inputValue={inputValue}
key={`${category.name}-${subCategory}`}
category={{ name: subCategory }}
path={[...path, subCategory]}
handleSingleSelection={handleSingleSelection}
handleCombinationSelection={handleCombinationSelection}
selectedSingles={selectedSingles}
selectedCombinations={selectedCombinations}
indentLevel={indentLevel + 1}
/>
)
} else {
return (
<CategoryCheckbox
inputValue={inputValue}
key={subCategory.name}
category={subCategory}
path={[...path, subCategory.name]}
handleSingleSelection={handleSingleSelection}
handleCombinationSelection={handleCombinationSelection}
selectedSingles={selectedSingles}
selectedCombinations={selectedCombinations}
indentLevel={indentLevel + 1}
/>
)
}
})}
</>
)}
</>
)
}

View File

@ -55,9 +55,7 @@ export const GamePage = () => {
// Categories filter
const [categories, setCategories] = useState(searchParams.getAll('l') || [])
const [heirarchies, setFullHeirarchies] = useState(
searchParams.getAll('h') || []
)
const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || [])
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
const handleSearch = () => {
@ -242,7 +240,35 @@ export const GamePage = () => {
</div>
</div>
</div>
{showCategoryPopup && <CategoryFilterPopup />}
{showCategoryPopup && (
<CategoryFilterPopup
categories={categories}
setCategories={setCategories}
heirarchies={heirarchies}
setHeirarchies={setHeirarchies}
handleClose={() => {
setShowCategoryPopup(false)
}}
handleApply={() => {
searchParams.delete('l')
searchParams.delete('h')
categories.forEach((l) => {
if (l) {
searchParams.delete('h')
searchParams.append('l', l)
}
})
heirarchies.forEach((h) => {
if (h) {
searchParams.append('h', h)
}
})
setSearchParams(searchParams, {
replace: true
})
}}
/>
)}
</>
)
}