Enhance search #215
@ -5,11 +5,14 @@ import { Game } from 'types'
|
|||||||
import { log, LogType } from 'utils'
|
import { log, LogType } from 'utils'
|
||||||
import gameFiles from '../utils/games'
|
import gameFiles from '../utils/games'
|
||||||
|
|
||||||
|
let cachedGamesData: Game[] | null = null
|
||||||
|
|
||||||
export const useGames = () => {
|
export const useGames = () => {
|
||||||
const hasProcessedFiles = useRef(false)
|
const hasProcessedFiles = useRef(false)
|
||||||
const [games, setGames] = useState<Game[]>([])
|
const [games, setGames] = useState<Game[]>(cachedGamesData || [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (cachedGamesData) return
|
||||||
if (hasProcessedFiles.current) return
|
if (hasProcessedFiles.current) return
|
||||||
|
|
||||||
hasProcessedFiles.current = true
|
hasProcessedFiles.current = true
|
||||||
@ -52,6 +55,7 @@ export const useGames = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
cachedGamesData = uniqueGames
|
||||||
setGames(uniqueGames)
|
setGames(uniqueGames)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(
|
log(
|
||||||
|
@ -9,7 +9,7 @@ import '../../styles/pagination.css'
|
|||||||
import '../../styles/search.css'
|
import '../../styles/search.css'
|
||||||
import '../../styles/styles.css'
|
import '../../styles/styles.css'
|
||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||||
import { scrollIntoView } from 'utils'
|
import { normalizeSearchString, scrollIntoView } from 'utils'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
import { Filter } from 'components/Filters'
|
import { Filter } from 'components/Filters'
|
||||||
import { Dropdown } from 'components/Filters/Dropdown'
|
import { Dropdown } from 'components/Filters/Dropdown'
|
||||||
@ -63,15 +63,17 @@ export const BlogsPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filtered = blogs?.filter(filterNsfwFn) || []
|
let filtered = blogs?.filter(filterNsfwFn) || []
|
||||||
const lowerCaseSearchTerm = searchTerm.toLowerCase()
|
const normalizedSearchTerm = normalizeSearchString(searchTerm)
|
||||||
|
|
||||||
if (searchTerm !== '') {
|
if (normalizedSearchTerm !== '') {
|
||||||
const filterSearchTermFn = (blog: Partial<BlogCardDetails>) =>
|
const filterSearchTermFn = (blog: Partial<BlogCardDetails>) =>
|
||||||
(blog.title || '').toLowerCase().includes(lowerCaseSearchTerm) ||
|
normalizeSearchString(blog.title || '').includes(
|
||||||
(blog.summary || '').toLowerCase().includes(lowerCaseSearchTerm) ||
|
normalizedSearchTerm
|
||||||
(blog.content || '').toLowerCase().includes(lowerCaseSearchTerm) ||
|
) ||
|
||||||
|
(blog.summary || '').toLowerCase().includes(normalizedSearchTerm) ||
|
||||||
|
(blog.content || '').toLowerCase().includes(normalizedSearchTerm) ||
|
||||||
(blog.tTags || []).findIndex((tag) =>
|
(blog.tTags || []).findIndex((tag) =>
|
||||||
tag.toLowerCase().includes(lowerCaseSearchTerm)
|
tag.toLowerCase().includes(normalizedSearchTerm)
|
||||||
) > -1
|
) > -1
|
||||||
filtered = filtered.filter(filterSearchTermFn)
|
filtered = filtered.filter(filterSearchTermFn)
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,9 @@ import {
|
|||||||
DEFAULT_FILTER_OPTIONS,
|
DEFAULT_FILTER_OPTIONS,
|
||||||
extractModData,
|
extractModData,
|
||||||
isModDataComplete,
|
isModDataComplete,
|
||||||
|
memoizedNormalizeSearchString,
|
||||||
|
normalizeSearchString,
|
||||||
|
normalizeUserSearchString,
|
||||||
scrollIntoView
|
scrollIntoView
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { useCuratedSet } from 'hooks/useCuratedSet'
|
import { useCuratedSet } from 'hooks/useCuratedSet'
|
||||||
@ -293,18 +296,17 @@ const ModsResult = ({
|
|||||||
}, [searchTerm])
|
}, [searchTerm])
|
||||||
|
|
||||||
const filteredMods = useMemo(() => {
|
const filteredMods = useMemo(() => {
|
||||||
|
const normalizedSearchTerm = normalizeSearchString(searchTerm)
|
||||||
// Search page requires search term
|
// Search page requires search term
|
||||||
if (searchTerm === '') return []
|
if (normalizedSearchTerm === '') return []
|
||||||
|
|
||||||
const lowerCaseSearchTerm = searchTerm.toLowerCase()
|
|
||||||
|
|
||||||
const filterFn = (mod: ModDetails) =>
|
const filterFn = (mod: ModDetails) =>
|
||||||
mod.title.toLowerCase().includes(lowerCaseSearchTerm) ||
|
normalizeSearchString(mod.title).includes(normalizedSearchTerm) ||
|
||||||
mod.game.toLowerCase().includes(lowerCaseSearchTerm) ||
|
memoizedNormalizeSearchString(mod.game).includes(normalizedSearchTerm) ||
|
||||||
mod.summary.toLowerCase().includes(lowerCaseSearchTerm) ||
|
mod.summary.toLowerCase().includes(normalizedSearchTerm) ||
|
||||||
mod.body.toLowerCase().includes(lowerCaseSearchTerm) ||
|
mod.body.toLowerCase().includes(normalizedSearchTerm) ||
|
||||||
mod.tags.findIndex((tag) =>
|
mod.tags.findIndex((tag) =>
|
||||||
tag.toLowerCase().includes(lowerCaseSearchTerm)
|
tag.toLowerCase().includes(normalizedSearchTerm)
|
||||||
) > -1
|
) > -1
|
||||||
|
|
||||||
const filterSourceFn = (mod: ModDetails) => {
|
const filterSourceFn = (mod: ModDetails) => {
|
||||||
@ -377,13 +379,14 @@ const UsersResult = ({
|
|||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchTerm === '') {
|
const normalizedSearchTerm = normalizeUserSearchString(searchTerm)
|
||||||
|
if (normalizedSearchTerm === '') {
|
||||||
setProfiles([])
|
setProfiles([])
|
||||||
} else {
|
} else {
|
||||||
const sub = ndk.subscribe(
|
const sub = ndk.subscribe(
|
||||||
{
|
{
|
||||||
kinds: [NDKKind.Metadata],
|
kinds: [NDKKind.Metadata],
|
||||||
search: searchTerm
|
search: normalizedSearchTerm
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeOnEose: true,
|
closeOnEose: true,
|
||||||
@ -395,7 +398,7 @@ const UsersResult = ({
|
|||||||
|
|
||||||
// Stop the sub after 10 seconds if we are still searching the same term as before
|
// Stop the sub after 10 seconds if we are still searching the same term as before
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (sub.filter.search === searchTerm) {
|
if (sub.filter.search === normalizedSearchTerm) {
|
||||||
sub.stop()
|
sub.stop()
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000)
|
||||||
@ -500,12 +503,13 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
|||||||
}, [searchTerm])
|
}, [searchTerm])
|
||||||
|
|
||||||
const filteredGames = useMemo(() => {
|
const filteredGames = useMemo(() => {
|
||||||
if (searchTerm === '') return []
|
const normalizedSearchTerm = normalizeSearchString(searchTerm)
|
||||||
|
if (normalizedSearchTerm === '') return []
|
||||||
const lowerCaseSearchTerm = searchTerm.toLowerCase()
|
|
||||||
|
|
||||||
return games.filter((game) =>
|
return games.filter((game) =>
|
||||||
game['Game Name'].toLowerCase().includes(lowerCaseSearchTerm)
|
memoizedNormalizeSearchString(game['Game Name']).includes(
|
||||||
|
normalizedSearchTerm
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}, [searchTerm, games])
|
}, [searchTerm, games])
|
||||||
|
|
||||||
|
@ -198,3 +198,78 @@ export function adjustTextareaHeight(textarea: HTMLTextAreaElement) {
|
|||||||
textarea.style.height = 'auto'
|
textarea.style.height = 'auto'
|
||||||
textarea.style.height = `${textarea.scrollHeight}px`
|
textarea.style.height = `${textarea.scrollHeight}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalizing search terms
|
||||||
|
const removeAccents = (str: string): string => {
|
||||||
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSpecialCharacters = (str: string): string => {
|
||||||
|
return str.replace(/[.,/#!$%^&*;:{}=\-_`~()&\s]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace Roman numerals with their Arabic counterparts
|
||||||
|
const ROMAN_TO_ARABIC_MAP: { [key: string]: string } = {
|
||||||
|
i: '1',
|
||||||
|
ii: '2',
|
||||||
|
iii: '3',
|
||||||
|
iv: '4',
|
||||||
|
v: '5',
|
||||||
|
vi: '6',
|
||||||
|
vii: '7',
|
||||||
|
viii: '8',
|
||||||
|
ix: '9',
|
||||||
|
x: '10',
|
||||||
|
xi: '11',
|
||||||
|
xii: '12',
|
||||||
|
xiii: '13',
|
||||||
|
xiv: '14',
|
||||||
|
xv: '15',
|
||||||
|
xvi: '16',
|
||||||
|
xvii: '17',
|
||||||
|
xviii: '18',
|
||||||
|
xix: '19',
|
||||||
|
xx: '20'
|
||||||
|
}
|
||||||
|
|
||||||
|
const romanRegex = new RegExp(
|
||||||
|
`\\b(${Object.keys(ROMAN_TO_ARABIC_MAP).join('|')})\\b`,
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const normalizeSearchString = (str: string): string => {
|
||||||
|
str = str.trim()
|
||||||
|
str = str.toLowerCase()
|
||||||
|
str = str.replace(romanRegex, (match) => ROMAN_TO_ARABIC_MAP[match])
|
||||||
|
str = removeAccents(str)
|
||||||
|
str = removeSpecialCharacters(str)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoization function to cache normalized results
|
||||||
|
const memoizeNormalize = (func: (str: string) => string) => {
|
||||||
|
const cache: { [key: string]: string } = {}
|
||||||
|
return (str: string): string => {
|
||||||
|
if (cache[str] !== undefined) {
|
||||||
|
return cache[str]
|
||||||
|
}
|
||||||
|
const result = func(str)
|
||||||
|
cache[str] = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoize normalized search strings
|
||||||
|
* Should only be used for games (large list)
|
||||||
|
*/
|
||||||
|
export const memoizedNormalizeSearchString = memoizeNormalize(
|
||||||
|
normalizeSearchString
|
||||||
|
)
|
||||||
|
|
||||||
|
export const normalizeUserSearchString = (str: string): string => {
|
||||||
|
str = str.trim()
|
||||||
|
str = str.toLowerCase()
|
||||||
|
str = removeAccents(str)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user