2024-10-14 19:20:43 +05:00
import {
NDKFilter ,
NDKKind ,
NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk'
2024-09-18 21:42:50 +05:00
import { ModCard } from 'components/ModCard'
2024-11-28 16:47:10 +01:00
import { ModFilter } from 'components/Filters/ModsFilter'
2024-09-18 21:42:50 +05:00
import { PaginationWithPageNumbers } from 'components/Pagination'
2024-10-29 09:35:39 +01:00
import { SearchInput } from 'components/SearchInput'
2024-09-18 21:42:50 +05:00
import { MAX_MODS_PER_PAGE , T_TAG_VALUE } from 'constants.ts'
2024-10-07 15:45:21 +05:00
import {
useAppSelector ,
useFilteredMods ,
2024-10-29 13:38:13 +01:00
useLocalStorage ,
2024-10-07 15:45:21 +05:00
useMuteLists ,
2024-10-14 19:20:43 +05:00
useNDKContext ,
2024-12-05 21:03:00 +01:00
useNSFWList ,
useSessionStorage
2024-10-07 15:45:21 +05:00
} from 'hooks'
2024-10-29 09:35:39 +01:00
import { useEffect , useMemo , useRef , useState } from 'react'
import { useParams , useSearchParams } from 'react-router-dom'
2024-10-29 13:38:13 +01:00
import { FilterOptions , ModDetails } from 'types'
2024-10-07 15:45:21 +05:00
import {
2024-11-28 16:47:10 +01:00
CurationSetIdentifiers ,
2024-10-29 13:38:13 +01:00
DEFAULT_FILTER_OPTIONS ,
extractModData ,
isModDataComplete ,
scrollIntoView
} from 'utils'
2024-11-28 16:47:10 +01:00
import { useCuratedSet } from 'hooks/useCuratedSet'
2024-12-04 14:16:39 +01:00
import { CategoryFilterPopup } from 'components/Filters/CategoryFilterPopup'
2024-09-18 21:42:50 +05:00
export const GamePage = ( ) = > {
2024-10-21 15:21:09 +02:00
const scrollTargetRef = useRef < HTMLDivElement > ( null )
2024-09-18 21:42:50 +05:00
const params = useParams ( )
const { name : gameName } = params
2024-10-14 19:20:43 +05:00
const { ndk } = useNDKContext ( )
2024-09-18 21:42:50 +05:00
const muteLists = useMuteLists ( )
2024-10-07 15:45:21 +05:00
const nsfwList = useNSFWList ( )
2024-11-28 16:47:10 +01:00
const repostList = useCuratedSet ( CurationSetIdentifiers . Repost )
2024-09-18 21:42:50 +05:00
2024-10-29 13:38:13 +01:00
const [ filterOptions ] = useLocalStorage < FilterOptions > (
'filter' ,
DEFAULT_FILTER_OPTIONS
)
2024-09-18 21:42:50 +05:00
const [ mods , setMods ] = useState < ModDetails [ ] > ( [ ] )
const [ currentPage , setCurrentPage ] = useState ( 1 )
const userState = useAppSelector ( ( state ) = > state . user )
2024-10-29 09:35:39 +01:00
// Search
const searchTermRef = useRef < HTMLInputElement > ( null )
const [ searchParams , setSearchParams ] = useSearchParams ( )
const [ searchTerm , setSearchTerm ] = useState ( searchParams . get ( 'q' ) || '' )
2024-12-04 14:16:39 +01:00
// Categories filter
2024-12-05 21:03:00 +01:00
const [ categories , setCategories ] = useSessionStorage < string [ ] > ( 'l' , [ ] )
const [ hierarchies , setHierarchies ] = useSessionStorage < string [ ] > ( 'h' , [ ] )
2024-12-04 14:16:39 +01:00
const [ showCategoryPopup , setShowCategoryPopup ] = useState ( false )
2024-12-05 21:03:00 +01:00
const linkedHierarchy = searchParams . get ( 'h' )
const isCategoryFilterActive = categories . length + hierarchies . length > 0
2024-12-04 14:16:39 +01:00
2024-10-29 09:35:39 +01:00
const handleSearch = ( ) = > {
const value = searchTermRef . current ? . value || '' // Access the input value from the ref
setSearchTerm ( value )
if ( value ) {
searchParams . set ( 'q' , value )
} else {
searchParams . delete ( 'q' )
}
setSearchParams ( searchParams , {
replace : true
} )
}
// Handle "Enter" key press inside the input
const handleKeyDown = ( event : React.KeyboardEvent < HTMLInputElement > ) = > {
if ( event . key === 'Enter' ) {
handleSearch ( )
}
}
const filteredMods = useMemo ( ( ) = > {
const filterSourceFn = ( mod : ModDetails ) = > {
if ( filterOptions . source === window . location . host ) {
return mod . rTag === filterOptions . source
}
return true
}
2024-12-11 14:03:33 +01:00
const filterCategoryFn = ( mod : ModDetails ) = > {
// Linked overrides the category popup selection
if ( linkedHierarchy && linkedHierarchy !== '' ) {
return mod . LTags . includes ( linkedHierarchy )
}
// If no selections are active return true
if ( ! ( hierarchies . length || categories . length ) ) {
return true
}
// Hierarchy selection active
if ( hierarchies . length ) {
const isMatch = mod . LTags . some ( ( item ) = > hierarchies . includes ( item ) )
// Matched hierarchy, return true immediately otherwise check categories
if ( isMatch ) return isMatch
}
// Category selection
if ( categories . length ) {
// Return result immediately
return mod . lTags . some ( ( item ) = > categories . includes ( item ) )
}
// No matches
return false
}
// If search term is missing, only filter by sources and category
if ( searchTerm === '' )
return mods . filter ( filterSourceFn ) . filter ( filterCategoryFn )
2024-10-29 09:35:39 +01:00
const lowerCaseSearchTerm = searchTerm . toLowerCase ( )
const filterFn = ( mod : ModDetails ) = >
mod . title . toLowerCase ( ) . includes ( lowerCaseSearchTerm ) ||
mod . game . toLowerCase ( ) . includes ( lowerCaseSearchTerm ) ||
mod . summary . toLowerCase ( ) . includes ( lowerCaseSearchTerm ) ||
mod . body . toLowerCase ( ) . includes ( lowerCaseSearchTerm ) ||
mod . tags . findIndex ( ( tag ) = >
tag . toLowerCase ( ) . includes ( lowerCaseSearchTerm )
) > - 1
2024-12-11 14:03:33 +01:00
return mods . filter ( filterFn ) . filter ( filterSourceFn ) . filter ( filterCategoryFn )
} , [
categories ,
filterOptions . source ,
hierarchies ,
linkedHierarchy ,
mods ,
searchTerm
] )
2024-10-29 09:35:39 +01:00
const filteredModList = useFilteredMods (
filteredMods ,
2024-10-07 15:45:21 +05:00
userState ,
filterOptions ,
nsfwList ,
2024-11-28 16:47:10 +01:00
muteLists ,
repostList
2024-10-07 15:45:21 +05:00
)
2024-09-18 21:42:50 +05:00
// Pagination logic
2024-10-29 09:35:39 +01:00
const totalGames = filteredModList . length
2024-09-18 21:42:50 +05:00
const totalPages = Math . ceil ( totalGames / MAX_MODS_PER_PAGE )
const startIndex = ( currentPage - 1 ) * MAX_MODS_PER_PAGE
const endIndex = startIndex + MAX_MODS_PER_PAGE
2024-10-29 09:35:39 +01:00
const currentMods = filteredModList . slice ( startIndex , endIndex )
2024-09-18 21:42:50 +05:00
const handlePageChange = ( page : number ) = > {
if ( page >= 1 && page <= totalPages ) {
2024-10-21 15:21:09 +02:00
scrollIntoView ( scrollTargetRef . current )
2024-09-18 21:42:50 +05:00
setCurrentPage ( page )
}
}
useEffect ( ( ) = > {
2024-12-11 14:03:33 +01:00
if ( ! gameName ) return
2024-10-14 19:20:43 +05:00
const filter : NDKFilter = {
kinds : [ NDKKind . Classified ] ,
2024-09-18 21:42:50 +05:00
'#t' : [ T_TAG_VALUE ]
}
2024-10-14 19:20:43 +05:00
const subscription = ndk . subscribe ( filter , {
cacheUsage : NDKSubscriptionCacheUsage.PARALLEL ,
closeOnEose : true
} )
subscription . on ( 'event' , ( ndkEvent ) = > {
if ( isModDataComplete ( ndkEvent ) ) {
const mod = extractModData ( ndkEvent )
if ( mod . game === gameName )
setMods ( ( prev ) = > {
if ( prev . find ( ( e ) = > e . aTag === mod . aTag ) ) return [ . . . prev ]
2024-09-18 21:42:50 +05:00
2024-10-14 19:20:43 +05:00
return [ . . . prev , mod ]
} )
}
} )
2024-09-18 21:42:50 +05:00
2024-10-14 19:20:43 +05:00
subscription . start ( )
2024-09-18 21:42:50 +05:00
2024-10-14 19:20:43 +05:00
// Cleanup function to stop subscription
2024-09-18 21:42:50 +05:00
return ( ) = > {
2024-10-14 19:20:43 +05:00
subscription . stop ( )
2024-09-18 21:42:50 +05:00
}
2024-12-11 14:03:33 +01:00
} , [ gameName , ndk ] )
2024-09-18 21:42:50 +05:00
if ( ! gameName ) return null
return (
< >
< div className = 'InnerBodyMain' >
< div className = 'ContainerMain' >
2024-10-21 15:21:09 +02:00
< div
className = 'IBMSecMainGroup IBMSecMainGroupAlt'
ref = { scrollTargetRef }
>
2024-09-18 21:42:50 +05:00
< div className = 'IBMSecMain' >
< div className = 'SearchMainWrapper' >
< div className = 'IBMSMTitleMain' >
< h2 className = 'IBMSMTitleMainHeading' >
Game : & nbsp ;
< span className = 'IBMSMTitleMainHeadingSpan' >
{ gameName }
< / span >
2024-10-29 09:35:39 +01:00
{ searchTerm !== '' && (
< >
& nbsp ; & mdash ; & nbsp ;
< span className = 'IBMSMTitleMainHeadingSpan' >
{ searchTerm }
< / span >
< / >
) }
2024-09-18 21:42:50 +05:00
< / h2 >
< / div >
2024-10-29 09:35:39 +01:00
< SearchInput
handleKeyDown = { handleKeyDown }
handleSearch = { handleSearch }
ref = { searchTermRef }
/ >
2024-09-18 21:42:50 +05:00
< / div >
< / div >
2024-12-04 14:16:39 +01:00
< ModFilter >
2024-12-05 21:03:00 +01:00
{ linkedHierarchy && linkedHierarchy !== '' ? (
< span
className = 'IBMSMSMBSSTagsTag'
style = { {
display : 'flex' ,
gap : '10px' ,
alignItems : 'center'
} }
2024-12-04 14:16:39 +01:00
onClick = { ( ) = > {
2024-12-05 21:03:00 +01:00
searchParams . delete ( 'h' )
setSearchParams ( searchParams )
2024-12-04 14:16:39 +01:00
} }
>
< svg
xmlns = 'http://www.w3.org/2000/svg'
2024-12-05 21:03:00 +01:00
viewBox = '0 0 576 512'
2024-12-04 14:16:39 +01:00
width = '1em'
height = '1em'
fill = 'currentColor'
>
2024-12-05 21:03:00 +01:00
< 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' / >
2024-12-04 14:16:39 +01:00
< / svg >
2024-12-05 21:03:00 +01:00
{ 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 >
) }
2024-12-04 14:16:39 +01:00
< / ModFilter >
2024-09-18 21:42:50 +05:00
< div className = 'IBMSecMain IBMSMListWrapper' >
< div className = 'IBMSMList' >
2024-09-23 20:58:50 +05:00
{ currentMods . map ( ( mod ) = > (
< ModCard key = { mod . id } { ...mod } / >
) ) }
2024-09-18 21:42:50 +05:00
< / div >
< / div >
< PaginationWithPageNumbers
currentPage = { currentPage }
totalPages = { totalPages }
handlePageChange = { handlePageChange }
/ >
< / div >
< / div >
< / div >
2024-12-05 13:02:04 +01:00
{ showCategoryPopup && (
< CategoryFilterPopup
categories = { categories }
setCategories = { setCategories }
2024-12-05 21:03:00 +01:00
hierarchies = { hierarchies }
setHierarchies = { setHierarchies }
2024-12-05 13:02:04 +01:00
handleClose = { ( ) = > {
setShowCategoryPopup ( false )
} }
/ >
) }
2024-09-18 21:42:50 +05:00
< / >
)
}