feat: game page search, cached filters #105
39
src/components/SearchInput.tsx
Normal file
39
src/components/SearchInput.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface SearchInputProps {
|
||||
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
|
||||
handleSearch: () => void
|
||||
}
|
||||
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({ handleKeyDown, handleSearch }, ref) => (
|
||||
<div className='SearchMain'>
|
||||
<div className='SearchMainInside'>
|
||||
<div className='SearchMainInsideWrapper'>
|
||||
<input
|
||||
type='text'
|
||||
className='SMIWInput'
|
||||
ref={ref}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Enter search term'
|
||||
/>
|
||||
<button
|
||||
className='btn btnMain SMIWButton'
|
||||
type='button'
|
||||
onClick={handleSearch}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
@ -6,6 +6,7 @@ import {
|
||||
import { ModCard } from 'components/ModCard'
|
||||
import { ModFilter } from 'components/ModsFilter'
|
||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||
import { SearchInput } from 'components/SearchInput'
|
||||
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
|
||||
import {
|
||||
useAppSelector,
|
||||
@ -14,8 +15,8 @@ import {
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
@ -45,8 +46,60 @@ export const GamePage = () => {
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const filteredMods = useFilteredMods(
|
||||
mods,
|
||||
// Search
|
||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// If search term is missing, only filter by sources
|
||||
if (searchTerm === '') return mods.filter(filterSourceFn)
|
||||
|
||||
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
|
||||
|
||||
return mods.filter(filterFn).filter(filterSourceFn)
|
||||
}, [filterOptions.source, mods, searchTerm])
|
||||
|
||||
const filteredModList = useFilteredMods(
|
||||
filteredMods,
|
||||
userState,
|
||||
filterOptions,
|
||||
nsfwList,
|
||||
@ -54,11 +107,11 @@ export const GamePage = () => {
|
||||
)
|
||||
|
||||
// Pagination logic
|
||||
const totalGames = filteredMods.length
|
||||
const totalGames = filteredModList.length
|
||||
const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE)
|
||||
const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE
|
||||
const endIndex = startIndex + MAX_MODS_PER_PAGE
|
||||
const currentMods = filteredMods.slice(startIndex, endIndex)
|
||||
const currentMods = filteredModList.slice(startIndex, endIndex)
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
@ -116,8 +169,21 @@ export const GamePage = () => {
|
||||
<span className='IBMSMTitleMainHeadingSpan'>
|
||||
{gameName}
|
||||
</span>
|
||||
{searchTerm !== '' && (
|
||||
<>
|
||||
—
|
||||
<span className='IBMSMTitleMainHeadingSpan'>
|
||||
{searchTerm}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<SearchInput
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleSearch={handleSearch}
|
||||
ref={searchTermRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModFilter
|
||||
|
Loading…
Reference in New Issue
Block a user