feat: categories and popups #171
@ -9,44 +9,48 @@ import styles from './CategoryFilterPopup.module.scss'
|
|||||||
interface CategoryFilterPopupProps {
|
interface CategoryFilterPopupProps {
|
||||||
categories: string[]
|
categories: string[]
|
||||||
setCategories: React.Dispatch<React.SetStateAction<string[]>>
|
setCategories: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
heirarchies: string[]
|
hierarchies: string[]
|
||||||
setHeirarchies: React.Dispatch<React.SetStateAction<string[]>>
|
setHierarchies: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
handleApply: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryFilterPopup = ({
|
export const CategoryFilterPopup = ({
|
||||||
categories,
|
categories,
|
||||||
setCategories,
|
setCategories,
|
||||||
heirarchies,
|
hierarchies,
|
||||||
setHeirarchies,
|
setHierarchies,
|
||||||
handleClose,
|
handleClose
|
||||||
handleApply
|
|
||||||
}: CategoryFilterPopupProps) => {
|
}: CategoryFilterPopupProps) => {
|
||||||
|
const [filterCategories, setFilterCategories] = useState(categories)
|
||||||
|
const [filterHierarchies, setFilterHierarchies] = useState(hierarchies)
|
||||||
|
const handleApply = () => {
|
||||||
|
setCategories(filterCategories)
|
||||||
|
setHierarchies(filterHierarchies)
|
||||||
|
}
|
||||||
const [inputValue, setInputValue] = useState<string>('')
|
const [inputValue, setInputValue] = useState<string>('')
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(e.target.value)
|
setInputValue(e.target.value)
|
||||||
}
|
}
|
||||||
const handleSingleSelection = (category: string, isSelected: boolean) => {
|
const handleSingleSelection = (category: string, isSelected: boolean) => {
|
||||||
let updatedCategories = [...categories]
|
let updatedCategories = [...filterCategories]
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
updatedCategories.push(category)
|
updatedCategories.push(category)
|
||||||
} else {
|
} else {
|
||||||
updatedCategories = updatedCategories.filter((item) => item !== category)
|
updatedCategories = updatedCategories.filter((item) => item !== category)
|
||||||
}
|
}
|
||||||
setCategories(updatedCategories)
|
setFilterCategories(updatedCategories)
|
||||||
}
|
}
|
||||||
const handleCombinationSelection = (path: string[], isSelected: boolean) => {
|
const handleCombinationSelection = (path: string[], isSelected: boolean) => {
|
||||||
const pathString = path.join(':')
|
const pathString = path.join(':')
|
||||||
let updatedHeirarchies = [...heirarchies]
|
let updatedHierarchies = [...filterHierarchies]
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
updatedHeirarchies.push(pathString)
|
updatedHierarchies.push(pathString)
|
||||||
} else {
|
} else {
|
||||||
updatedHeirarchies = updatedHeirarchies.filter(
|
updatedHierarchies = updatedHierarchies.filter(
|
||||||
(item) => item !== pathString
|
(item) => item !== pathString
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setHeirarchies(updatedHeirarchies)
|
setFilterHierarchies(updatedHierarchies)
|
||||||
}
|
}
|
||||||
const handleAddNew = () => {
|
const handleAddNew = () => {
|
||||||
if (inputValue) {
|
if (inputValue) {
|
||||||
@ -55,9 +59,9 @@ export const CategoryFilterPopup = ({
|
|||||||
.split('>')
|
.split('>')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
if (values.length > 1) {
|
if (values.length > 1) {
|
||||||
setHeirarchies([...categories, values.join(':')])
|
setFilterHierarchies([...filterHierarchies, values.join(':')])
|
||||||
} else {
|
} else {
|
||||||
setCategories([...categories, values[0]])
|
setFilterCategories([...filterCategories, values[0]])
|
||||||
}
|
}
|
||||||
setInputValue('')
|
setInputValue('')
|
||||||
}
|
}
|
||||||
@ -157,8 +161,8 @@ export const CategoryFilterPopup = ({
|
|||||||
path={[category.name]}
|
path={[category.name]}
|
||||||
handleSingleSelection={handleSingleSelection}
|
handleSingleSelection={handleSingleSelection}
|
||||||
handleCombinationSelection={handleCombinationSelection}
|
handleCombinationSelection={handleCombinationSelection}
|
||||||
selectedSingles={categories}
|
selectedSingles={filterCategories}
|
||||||
selectedCombinations={heirarchies}
|
selectedCombinations={filterHierarchies}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -181,8 +185,8 @@ export const CategoryFilterPopup = ({
|
|||||||
className='btn btnMain btnMainPopup'
|
className='btn btnMain btnMainPopup'
|
||||||
type='button'
|
type='button'
|
||||||
onPointerDown={() => {
|
onPointerDown={() => {
|
||||||
setCategories([])
|
setFilterCategories([])
|
||||||
setHeirarchies([])
|
setFilterHierarchies([])
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
|
@ -235,7 +235,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepend com.degmods to avoid leaking categories to 3rd party client's search
|
// Prepend com.degmods to avoid leaking categories to 3rd party client's search
|
||||||
// Add heirarchical namespaces labels
|
// Add hierarchical namespaces labels
|
||||||
if (formState.LTags.length > 0) {
|
if (formState.LTags.length > 0) {
|
||||||
for (let i = 0; i < formState.LTags.length; i++) {
|
for (let i = 0; i < formState.LTags.length; i++) {
|
||||||
tags.push(['L', `com.degmods:${formState.LTags[i]}`])
|
tags.push(['L', `com.degmods:${formState.LTags[i]}`])
|
||||||
@ -946,12 +946,12 @@ export const CategoryAutocomplete = ({
|
|||||||
const concatenatedValue = Array.from(uniqueValues)
|
const concatenatedValue = Array.from(uniqueValues)
|
||||||
return concatenatedValue
|
return concatenatedValue
|
||||||
}
|
}
|
||||||
const getSelectedHeirarchy = (cats: Categories[]) => {
|
const getSelectedhierarchy = (cats: Categories[]) => {
|
||||||
const heirarchies = cats.reduce<string[]>(
|
const hierarchies = cats.reduce<string[]>(
|
||||||
(prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')],
|
(prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const concatenatedValue = Array.from(heirarchies)
|
const concatenatedValue = Array.from(hierarchies)
|
||||||
return concatenatedValue
|
return concatenatedValue
|
||||||
}
|
}
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@ -989,7 +989,7 @@ export const CategoryAutocomplete = ({
|
|||||||
setFormState((prevState) => ({
|
setFormState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
['lTags']: getSelectedCategories(selectedCategories),
|
['lTags']: getSelectedCategories(selectedCategories),
|
||||||
['LTags']: getSelectedHeirarchy(selectedCategories)
|
['LTags']: getSelectedhierarchy(selectedCategories)
|
||||||
}))
|
}))
|
||||||
}, [selectedCategories, setFormState])
|
}, [selectedCategories, setFormState])
|
||||||
|
|
||||||
@ -1134,16 +1134,20 @@ export const CategoryAutocomplete = ({
|
|||||||
{LTags.length > 0 && (
|
{LTags.length > 0 && (
|
||||||
<div className='IBMSMSMBSSCategories'>
|
<div className='IBMSMSMBSSCategories'>
|
||||||
{LTags.map((hierarchy) => {
|
{LTags.map((hierarchy) => {
|
||||||
const heirarchicalCategories = hierarchy.split(`:`)
|
const hierarchicalCategories = hierarchy.split(`:`)
|
||||||
const categories = heirarchicalCategories
|
const categories = hierarchicalCategories
|
||||||
.map<React.ReactNode>((c, i) =>
|
.map<React.ReactNode>((c, i) => {
|
||||||
game ? (
|
const partialHierarchy = hierarchicalCategories
|
||||||
|
.slice(0, i + 1)
|
||||||
|
.join(':')
|
||||||
|
|
||||||
|
return game ? (
|
||||||
<Link
|
<Link
|
||||||
key={`category-${i}`}
|
key={`category-${i}`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
to={{
|
to={{
|
||||||
pathname: getGamePageRoute(game),
|
pathname: getGamePageRoute(game),
|
||||||
search: `l=${c}`
|
search: `h=${partialHierarchy}`
|
||||||
}}
|
}}
|
||||||
className='IBMSMSMBSSCategoriesBoxItem'
|
className='IBMSMSMBSSCategoriesBoxItem'
|
||||||
>
|
>
|
||||||
@ -1154,7 +1158,7 @@ export const CategoryAutocomplete = ({
|
|||||||
{capitalizeEachWord(c)}
|
{capitalizeEachWord(c)}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)
|
})
|
||||||
.reduce((prev, curr, i) => [
|
.reduce((prev, curr, i) => [
|
||||||
prev,
|
prev,
|
||||||
<div
|
<div
|
||||||
|
@ -14,7 +14,8 @@ import {
|
|||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
useMuteLists,
|
useMuteLists,
|
||||||
useNDKContext,
|
useNDKContext,
|
||||||
useNSFWList
|
useNSFWList,
|
||||||
|
useSessionStorage
|
||||||
} from 'hooks'
|
} from 'hooks'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useParams, useSearchParams } from 'react-router-dom'
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
@ -54,9 +55,11 @@ export const GamePage = () => {
|
|||||||
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
|
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
|
||||||
|
|
||||||
// Categories filter
|
// Categories filter
|
||||||
const [categories, setCategories] = useState(searchParams.getAll('l') || [])
|
const [categories, setCategories] = useSessionStorage<string[]>('l', [])
|
||||||
const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || [])
|
const [hierarchies, setHierarchies] = useSessionStorage<string[]>('h', [])
|
||||||
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
|
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
|
||||||
|
const linkedHierarchy = searchParams.get('h')
|
||||||
|
const isCategoryFilterActive = categories.length + hierarchies.length > 0
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
||||||
@ -134,12 +137,17 @@ export const GamePage = () => {
|
|||||||
'#t': [T_TAG_VALUE]
|
'#t': [T_TAG_VALUE]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categories.length) {
|
// Linked category will override the filter
|
||||||
filter['#l'] = categories.map((l) => `com.degmods:${l}`)
|
if (linkedHierarchy && linkedHierarchy !== '') {
|
||||||
}
|
filter['#L'] = [`com.degmods:${linkedHierarchy}`]
|
||||||
|
} else {
|
||||||
|
if (categories.length) {
|
||||||
|
filter['#l'] = categories.map((l) => `com.degmods:${l}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (heirarchies.length) {
|
if (hierarchies.length) {
|
||||||
filter['#L'] = heirarchies.map((L) => `com.degmods:${L}`)
|
filter['#L'] = hierarchies.map((L) => `com.degmods:${L}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = ndk.subscribe(filter, {
|
const subscription = ndk.subscribe(filter, {
|
||||||
@ -165,7 +173,7 @@ export const GamePage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
subscription.stop()
|
subscription.stop()
|
||||||
}
|
}
|
||||||
}, [gameName, ndk, categories, heirarchies])
|
}, [gameName, ndk, linkedHierarchy, categories, hierarchies])
|
||||||
|
|
||||||
if (!gameName) return null
|
if (!gameName) return null
|
||||||
|
|
||||||
@ -203,26 +211,73 @@ export const GamePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModFilter>
|
<ModFilter>
|
||||||
<div className='FiltersMainElement'>
|
{linkedHierarchy && linkedHierarchy !== '' ? (
|
||||||
<button
|
<span
|
||||||
className='btn btnMain btnMainDropdown'
|
className='IBMSMSMBSSTagsTag'
|
||||||
type='button'
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCategoryPopup(true)
|
searchParams.delete('h')
|
||||||
|
setSearchParams(searchParams)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Categories
|
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
viewBox='0 0 512 512'
|
viewBox='0 0 576 512'
|
||||||
width='1em'
|
width='1em'
|
||||||
height='1em'
|
height='1em'
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
>
|
>
|
||||||
<path d='M3.9 54.9C10.5 40.9 24.5 32 40 32l432 0c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9 320 448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6l0-79.1L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z' />
|
<path d='M 3.9,22.9 C 10.5,8.9 24.5,0 40,0 h 432 c 15.5,0 29.5,8.9 36.1,22.9 6.6,14 4.6,30.5 -5.2,42.5 L 396.4,195.6 C 316.2,212.1 256,283 256,368 c 0,27.4 6.3,53.4 17.5,76.5 -1.6,-0.8 -3.2,-1.8 -4.7,-2.9 l -64,-48 C 196.7,387.6 192,378.1 192,368 V 288.9 L 9,65.3 C -0.7,53.4 -2.8,36.8 3.9,22.9 Z M 432,224 c 79.52906,0 143.99994,64.471 143.99994,144 0,79.529 -64.47088,144 -143.99994,144 -79.52906,0 -143.99994,-64.471 -143.99994,-144 0,-79.529 64.47088,-144 143.99994,-144 z' />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
{linkedHierarchy.replace(/:/g, ' > ')}
|
||||||
</div>
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='0.8em'
|
||||||
|
height='0.8em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z' />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className='FiltersMainElement'>
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainDropdown'
|
||||||
|
type='button'
|
||||||
|
onClick={() => {
|
||||||
|
setShowCategoryPopup(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Categories
|
||||||
|
{isCategoryFilterActive ? (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 576 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M 3.9,22.9 C 10.5,8.9 24.5,0 40,0 h 432 c 15.5,0 29.5,8.9 36.1,22.9 6.6,14 4.6,30.5 -5.2,42.5 L 396.4,195.6 C 316.2,212.1 256,283 256,368 c 0,27.4 6.3,53.4 17.5,76.5 -1.6,-0.8 -3.2,-1.8 -4.7,-2.9 l -64,-48 C 196.7,387.6 192,378.1 192,368 V 288.9 L 9,65.3 C -0.7,53.4 -2.8,36.8 3.9,22.9 Z M 432,224 c 79.52906,0 143.99994,64.471 143.99994,144 0,79.529 -64.47088,144 -143.99994,144 -79.52906,0 -143.99994,-64.471 -143.99994,-144 0,-79.529 64.47088,-144 143.99994,-144 z' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M3.9 54.9C10.5 40.9 24.5 32 40 32l432 0c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9 320 448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6l0-79.1L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ModFilter>
|
</ModFilter>
|
||||||
|
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
@ -244,29 +299,11 @@ export const GamePage = () => {
|
|||||||
<CategoryFilterPopup
|
<CategoryFilterPopup
|
||||||
categories={categories}
|
categories={categories}
|
||||||
setCategories={setCategories}
|
setCategories={setCategories}
|
||||||
heirarchies={heirarchies}
|
hierarchies={hierarchies}
|
||||||
setHeirarchies={setHeirarchies}
|
setHierarchies={setHierarchies}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
setShowCategoryPopup(false)
|
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
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -543,20 +543,26 @@ const Body = ({
|
|||||||
{LTags.length > 0 && (
|
{LTags.length > 0 && (
|
||||||
<div className='IBMSMSMBSSCategories'>
|
<div className='IBMSMSMBSSCategories'>
|
||||||
{LTags.map((hierarchy) => {
|
{LTags.map((hierarchy) => {
|
||||||
const heirarchicalCategories = hierarchy.split(`:`)
|
const hierarchicalCategories = hierarchy.split(`:`)
|
||||||
const categories = heirarchicalCategories
|
const categories = hierarchicalCategories
|
||||||
.map<React.ReactNode>((c: string) => (
|
.map<React.ReactNode>((c, i) => {
|
||||||
<ReactRouterLink
|
const partialHierarchy = hierarchicalCategories
|
||||||
className='IBMSMSMBSSCategoriesBoxItem'
|
.slice(0, i + 1)
|
||||||
target='_blank'
|
.join(':')
|
||||||
to={{
|
|
||||||
pathname: getGamePageRoute(game),
|
return (
|
||||||
search: `l=${c}`
|
<ReactRouterLink
|
||||||
}}
|
className='IBMSMSMBSSCategoriesBoxItem'
|
||||||
>
|
target='_blank'
|
||||||
<p>{capitalizeEachWord(c)}</p>
|
to={{
|
||||||
</ReactRouterLink>
|
pathname: getGamePageRoute(game),
|
||||||
))
|
search: `h=${partialHierarchy}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>{capitalizeEachWord(c)}</p>
|
||||||
|
</ReactRouterLink>
|
||||||
|
)
|
||||||
|
})
|
||||||
.reduce((prev, curr) => [
|
.reduce((prev, curr) => [
|
||||||
prev,
|
prev,
|
||||||
<div className='IBMSMSMBSSCategoriesBoxSeparator'>
|
<div className='IBMSMSMBSSCategoriesBoxSeparator'>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user