feat(category): category filter popup
This commit is contained in:
parent
ecbe839b30
commit
8d9bbbc7a5
3
src/components/Filters/CategoryFilterPopup.module.scss
Normal file
3
src/components/Filters/CategoryFilterPopup.module.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.noResult:not(:only-child) {
|
||||
display: none;
|
||||
}
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user