feat: categories and popups #171
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 = () => {
|
import React, { useEffect, useState } from 'react'
|
||||||
return <>Popup</>
|
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
|
// Categories filter
|
||||||
const [categories, setCategories] = useState(searchParams.getAll('l') || [])
|
const [categories, setCategories] = useState(searchParams.getAll('l') || [])
|
||||||
const [heirarchies, setFullHeirarchies] = useState(
|
const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || [])
|
||||||
searchParams.getAll('h') || []
|
|
||||||
)
|
|
||||||
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
|
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@ -242,7 +240,35 @@ export const GamePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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