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 {
categories: string[]
setCategories: React.Dispatch<React.SetStateAction<string[]>>
heirarchies: string[]
setHeirarchies: React.Dispatch<React.SetStateAction<string[]>>
hierarchies: string[]
setHierarchies: React.Dispatch<React.SetStateAction<string[]>>
handleClose: () => void
handleApply: () => void
}
export const CategoryFilterPopup = ({
categories,
setCategories,
heirarchies,
setHeirarchies,
handleClose,
handleApply
hierarchies,
setHierarchies,
handleClose
}: CategoryFilterPopupProps) => {
const [filterCategories, setFilterCategories] = useState(categories)
const [filterHierarchies, setFilterHierarchies] = useState(hierarchies)
const handleApply = () => {
setCategories(filterCategories)
setHierarchies(filterHierarchies)
}
const [inputValue, setInputValue] = useState<string>('')
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
}
const handleSingleSelection = (category: string, isSelected: boolean) => {
let updatedCategories = [...categories]
let updatedCategories = [...filterCategories]
if (isSelected) {
updatedCategories.push(category)
} else {
updatedCategories = updatedCategories.filter((item) => item !== category)
}
setCategories(updatedCategories)
setFilterCategories(updatedCategories)
}
const handleCombinationSelection = (path: string[], isSelected: boolean) => {
const pathString = path.join(':')
let updatedHeirarchies = [...heirarchies]
let updatedHierarchies = [...filterHierarchies]
if (isSelected) {
updatedHeirarchies.push(pathString)
updatedHierarchies.push(pathString)
} else {
updatedHeirarchies = updatedHeirarchies.filter(
updatedHierarchies = updatedHierarchies.filter(
(item) => item !== pathString
)
}
setHeirarchies(updatedHeirarchies)
setFilterHierarchies(updatedHierarchies)
}
const handleAddNew = () => {
if (inputValue) {
@ -55,9 +59,9 @@ export const CategoryFilterPopup = ({
.split('>')
.map((s) => s.trim())
if (values.length > 1) {
setHeirarchies([...categories, values.join(':')])
setFilterHierarchies([...filterHierarchies, values.join(':')])
} else {
setCategories([...categories, values[0]])
setFilterCategories([...filterCategories, values[0]])
}
setInputValue('')
}
@ -157,8 +161,8 @@ export const CategoryFilterPopup = ({
path={[category.name]}
handleSingleSelection={handleSingleSelection}
handleCombinationSelection={handleCombinationSelection}
selectedSingles={categories}
selectedCombinations={heirarchies}
selectedSingles={filterCategories}
selectedCombinations={filterHierarchies}
/>
))}
</div>
@ -181,8 +185,8 @@ export const CategoryFilterPopup = ({
className='btn btnMain btnMainPopup'
type='button'
onPointerDown={() => {
setCategories([])
setHeirarchies([])
setFilterCategories([])
setFilterHierarchies([])
}}
>
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
// Add heirarchical namespaces labels
// Add hierarchical namespaces labels
if (formState.LTags.length > 0) {
for (let i = 0; i < formState.LTags.length; i++) {
tags.push(['L', `com.degmods:${formState.LTags[i]}`])
@ -946,12 +946,12 @@ export const CategoryAutocomplete = ({
const concatenatedValue = Array.from(uniqueValues)
return concatenatedValue
}
const getSelectedHeirarchy = (cats: Categories[]) => {
const heirarchies = cats.reduce<string[]>(
const getSelectedhierarchy = (cats: Categories[]) => {
const hierarchies = cats.reduce<string[]>(
(prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')],
[]
)
const concatenatedValue = Array.from(heirarchies)
const concatenatedValue = Array.from(hierarchies)
return concatenatedValue
}
const handleReset = () => {
@ -989,7 +989,7 @@ export const CategoryAutocomplete = ({
setFormState((prevState) => ({
...prevState,
['lTags']: getSelectedCategories(selectedCategories),
['LTags']: getSelectedHeirarchy(selectedCategories)
['LTags']: getSelectedhierarchy(selectedCategories)
}))
}, [selectedCategories, setFormState])
@ -1134,16 +1134,20 @@ export const CategoryAutocomplete = ({
{LTags.length > 0 && (
<div className='IBMSMSMBSSCategories'>
{LTags.map((hierarchy) => {
const heirarchicalCategories = hierarchy.split(`:`)
const categories = heirarchicalCategories
.map<React.ReactNode>((c, i) =>
game ? (
const hierarchicalCategories = hierarchy.split(`:`)
const categories = hierarchicalCategories
.map<React.ReactNode>((c, i) => {
const partialHierarchy = hierarchicalCategories
.slice(0, i + 1)
.join(':')
return game ? (
<Link
key={`category-${i}`}
target='_blank'
to={{
pathname: getGamePageRoute(game),
search: `l=${c}`
search: `h=${partialHierarchy}`
}}
className='IBMSMSMBSSCategoriesBoxItem'
>
@ -1154,7 +1158,7 @@ export const CategoryAutocomplete = ({
{capitalizeEachWord(c)}
</p>
)
)
})
.reduce((prev, curr, i) => [
prev,
<div

View File

@ -14,7 +14,8 @@ import {
useLocalStorage,
useMuteLists,
useNDKContext,
useNSFWList
useNSFWList,
useSessionStorage
} from 'hooks'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom'
@ -54,9 +55,11 @@ export const GamePage = () => {
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
// Categories filter
const [categories, setCategories] = useState(searchParams.getAll('l') || [])
const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || [])
const [categories, setCategories] = useSessionStorage<string[]>('l', [])
const [hierarchies, setHierarchies] = useSessionStorage<string[]>('h', [])
const [showCategoryPopup, setShowCategoryPopup] = useState(false)
const linkedHierarchy = searchParams.get('h')
const isCategoryFilterActive = categories.length + hierarchies.length > 0
const handleSearch = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref
@ -134,12 +137,17 @@ export const GamePage = () => {
'#t': [T_TAG_VALUE]
}
if (categories.length) {
filter['#l'] = categories.map((l) => `com.degmods:${l}`)
}
// Linked category will override the filter
if (linkedHierarchy && linkedHierarchy !== '') {
filter['#L'] = [`com.degmods:${linkedHierarchy}`]
} else {
if (categories.length) {
filter['#l'] = categories.map((l) => `com.degmods:${l}`)
}
if (heirarchies.length) {
filter['#L'] = heirarchies.map((L) => `com.degmods:${L}`)
if (hierarchies.length) {
filter['#L'] = hierarchies.map((L) => `com.degmods:${L}`)
}
}
const subscription = ndk.subscribe(filter, {
@ -165,7 +173,7 @@ export const GamePage = () => {
return () => {
subscription.stop()
}
}, [gameName, ndk, categories, heirarchies])
}, [gameName, ndk, linkedHierarchy, categories, hierarchies])
if (!gameName) return null
@ -203,26 +211,73 @@ export const GamePage = () => {
</div>
</div>
<ModFilter>
<div className='FiltersMainElement'>
<button
className='btn btnMain btnMainDropdown'
type='button'
{linkedHierarchy && linkedHierarchy !== '' ? (
<span
className='IBMSMSMBSSTagsTag'
style={{
display: 'flex',
gap: '10px',
alignItems: 'center'
}}
onClick={() => {
setShowCategoryPopup(true)
searchParams.delete('h')
setSearchParams(searchParams)
}}
>
Categories
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
viewBox='0 0 576 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' />
<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>
</button>
</div>
{linkedHierarchy.replace(/:/g, ' > ')}
<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>
<div className='IBMSecMain IBMSMListWrapper'>
@ -244,29 +299,11 @@ export const GamePage = () => {
<CategoryFilterPopup
categories={categories}
setCategories={setCategories}
heirarchies={heirarchies}
setHeirarchies={setHeirarchies}
hierarchies={hierarchies}
setHierarchies={setHierarchies}
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
})
}}
/>
)}
</>

View File

@ -543,20 +543,26 @@ const Body = ({
{LTags.length > 0 && (
<div className='IBMSMSMBSSCategories'>
{LTags.map((hierarchy) => {
const heirarchicalCategories = hierarchy.split(`:`)
const categories = heirarchicalCategories
.map<React.ReactNode>((c: string) => (
<ReactRouterLink
className='IBMSMSMBSSCategoriesBoxItem'
target='_blank'
to={{
pathname: getGamePageRoute(game),
search: `l=${c}`
}}
>
<p>{capitalizeEachWord(c)}</p>
</ReactRouterLink>
))
const hierarchicalCategories = hierarchy.split(`:`)
const categories = hierarchicalCategories
.map<React.ReactNode>((c, i) => {
const partialHierarchy = hierarchicalCategories
.slice(0, i + 1)
.join(':')
return (
<ReactRouterLink
className='IBMSMSMBSSCategoriesBoxItem'
target='_blank'
to={{
pathname: getGamePageRoute(game),
search: `h=${partialHierarchy}`
}}
>
<p>{capitalizeEachWord(c)}</p>
</ReactRouterLink>
)
})
.reduce((prev, curr) => [
prev,
<div className='IBMSMSMBSSCategoriesBoxSeparator'>