feat: categories and popups #171

Merged
enes merged 22 commits from 116-categories into staging 2024-12-12 16:37:38 +00:00
4 changed files with 134 additions and 83 deletions
Showing only changes of commit 127c1fd8a6 - Show all commits

View File

@ -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

View File

@ -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

View File

@ -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
})
}}
/> />
)} )}
</> </>

View File

@ -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'>