From 4bba7c888e5a58d8bb08e57b53e7498f6b32a0cc Mon Sep 17 00:00:00 2001 From: en Date: Mon, 3 Feb 2025 19:52:26 +0100 Subject: [PATCH 1/4] refactor(games): cache games data --- src/hooks/useGames.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useGames.ts b/src/hooks/useGames.ts index 6e49003..b6397ba 100644 --- a/src/hooks/useGames.ts +++ b/src/hooks/useGames.ts @@ -5,11 +5,14 @@ import { Game } from 'types' import { log, LogType } from 'utils' import gameFiles from '../utils/games' +let cachedGamesData: Game[] | null = null + export const useGames = () => { const hasProcessedFiles = useRef(false) - const [games, setGames] = useState([]) + const [games, setGames] = useState(cachedGamesData || []) useEffect(() => { + if (cachedGamesData) return if (hasProcessedFiles.current) return hasProcessedFiles.current = true @@ -52,6 +55,7 @@ export const useGames = () => { }) await Promise.all(promises) + cachedGamesData = uniqueGames setGames(uniqueGames) } catch (err) { log( From 6236d925c5aae5dd2498fb491a9dab4e14736e7b Mon Sep 17 00:00:00 2001 From: en Date: Mon, 3 Feb 2025 19:53:48 +0100 Subject: [PATCH 2/4] feat(games): improve search for games --- src/pages/search.tsx | 7 +++-- src/utils/utils.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index b294ce9..8b365dc 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -36,6 +36,7 @@ import { DEFAULT_FILTER_OPTIONS, extractModData, isModDataComplete, + memoizedNormalizeSearchString, scrollIntoView } from 'utils' import { useCuratedSet } from 'hooks/useCuratedSet' @@ -502,10 +503,12 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => { const filteredGames = useMemo(() => { if (searchTerm === '') return [] - const lowerCaseSearchTerm = searchTerm.toLowerCase() + const normalizedSearchTerm = memoizedNormalizeSearchString(searchTerm) return games.filter((game) => - game['Game Name'].toLowerCase().includes(lowerCaseSearchTerm) + memoizedNormalizeSearchString(game['Game Name']).includes( + normalizedSearchTerm + ) ) }, [searchTerm, games]) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7cc8a73..19fc9bb 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -198,3 +198,65 @@ export function adjustTextareaHeight(textarea: HTMLTextAreaElement) { textarea.style.height = 'auto' 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.toLowerCase() + str = removeAccents(str) + str = removeSpecialCharacters(str) + return str.replace(romanRegex, (match) => ROMAN_TO_ARABIC_MAP[match]) +} + +// 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 + } +} + +export const memoizedNormalizeSearchString = memoizeNormalize( + normalizeSearchString +) From 688e9fcf6de1181cf3bda833f4acc73c66b9558b Mon Sep 17 00:00:00 2001 From: en Date: Mon, 3 Feb 2025 20:14:55 +0100 Subject: [PATCH 3/4] fix(games): update search to handle roman numerals first, skip memoize on searchTerm --- src/pages/search.tsx | 3 ++- src/utils/utils.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 8b365dc..78e44b0 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -37,6 +37,7 @@ import { extractModData, isModDataComplete, memoizedNormalizeSearchString, + normalizeSearchString, scrollIntoView } from 'utils' import { useCuratedSet } from 'hooks/useCuratedSet' @@ -503,7 +504,7 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => { const filteredGames = useMemo(() => { if (searchTerm === '') return [] - const normalizedSearchTerm = memoizedNormalizeSearchString(searchTerm) + const normalizedSearchTerm = normalizeSearchString(searchTerm) return games.filter((game) => memoizedNormalizeSearchString(game['Game Name']).includes( diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 19fc9bb..ed2a3e1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -239,9 +239,10 @@ const romanRegex = new RegExp( export const normalizeSearchString = (str: string): string => { str = str.toLowerCase() + str = str.replace(romanRegex, (match) => ROMAN_TO_ARABIC_MAP[match]) str = removeAccents(str) str = removeSpecialCharacters(str) - return str.replace(romanRegex, (match) => ROMAN_TO_ARABIC_MAP[match]) + return str } // Memoization function to cache normalized results From d5924ebf4b9ef75ab03af45874dfe9f12c095d9a Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 10:23:57 +0100 Subject: [PATCH 4/4] refactor(search): apply normalization on user, blog and mod search --- src/pages/blogs/index.tsx | 16 +++++++++------- src/pages/search.tsx | 26 +++++++++++++------------- src/utils/utils.ts | 12 ++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx index d4b4fab..64815d7 100644 --- a/src/pages/blogs/index.tsx +++ b/src/pages/blogs/index.tsx @@ -9,7 +9,7 @@ import '../../styles/pagination.css' import '../../styles/search.css' import '../../styles/styles.css' import { PaginationWithPageNumbers } from 'components/Pagination' -import { scrollIntoView } from 'utils' +import { normalizeSearchString, scrollIntoView } from 'utils' import { LoadingSpinner } from 'components/LoadingSpinner' import { Filter } from 'components/Filters' import { Dropdown } from 'components/Filters/Dropdown' @@ -63,15 +63,17 @@ export const BlogsPage = () => { } let filtered = blogs?.filter(filterNsfwFn) || [] - const lowerCaseSearchTerm = searchTerm.toLowerCase() + const normalizedSearchTerm = normalizeSearchString(searchTerm) - if (searchTerm !== '') { + if (normalizedSearchTerm !== '') { const filterSearchTermFn = (blog: Partial) => - (blog.title || '').toLowerCase().includes(lowerCaseSearchTerm) || - (blog.summary || '').toLowerCase().includes(lowerCaseSearchTerm) || - (blog.content || '').toLowerCase().includes(lowerCaseSearchTerm) || + normalizeSearchString(blog.title || '').includes( + normalizedSearchTerm + ) || + (blog.summary || '').toLowerCase().includes(normalizedSearchTerm) || + (blog.content || '').toLowerCase().includes(normalizedSearchTerm) || (blog.tTags || []).findIndex((tag) => - tag.toLowerCase().includes(lowerCaseSearchTerm) + tag.toLowerCase().includes(normalizedSearchTerm) ) > -1 filtered = filtered.filter(filterSearchTermFn) } diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 78e44b0..018e2fb 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -38,6 +38,7 @@ import { isModDataComplete, memoizedNormalizeSearchString, normalizeSearchString, + normalizeUserSearchString, scrollIntoView } from 'utils' import { useCuratedSet } from 'hooks/useCuratedSet' @@ -295,18 +296,17 @@ const ModsResult = ({ }, [searchTerm]) const filteredMods = useMemo(() => { + const normalizedSearchTerm = normalizeSearchString(searchTerm) // Search page requires search term - if (searchTerm === '') return [] - - const lowerCaseSearchTerm = searchTerm.toLowerCase() + if (normalizedSearchTerm === '') return [] const filterFn = (mod: ModDetails) => - mod.title.toLowerCase().includes(lowerCaseSearchTerm) || - mod.game.toLowerCase().includes(lowerCaseSearchTerm) || - mod.summary.toLowerCase().includes(lowerCaseSearchTerm) || - mod.body.toLowerCase().includes(lowerCaseSearchTerm) || + normalizeSearchString(mod.title).includes(normalizedSearchTerm) || + memoizedNormalizeSearchString(mod.game).includes(normalizedSearchTerm) || + mod.summary.toLowerCase().includes(normalizedSearchTerm) || + mod.body.toLowerCase().includes(normalizedSearchTerm) || mod.tags.findIndex((tag) => - tag.toLowerCase().includes(lowerCaseSearchTerm) + tag.toLowerCase().includes(normalizedSearchTerm) ) > -1 const filterSourceFn = (mod: ModDetails) => { @@ -379,13 +379,14 @@ const UsersResult = ({ const userState = useAppSelector((state) => state.user) useEffect(() => { - if (searchTerm === '') { + const normalizedSearchTerm = normalizeUserSearchString(searchTerm) + if (normalizedSearchTerm === '') { setProfiles([]) } else { const sub = ndk.subscribe( { kinds: [NDKKind.Metadata], - search: searchTerm + search: normalizedSearchTerm }, { closeOnEose: true, @@ -397,7 +398,7 @@ const UsersResult = ({ // Stop the sub after 10 seconds if we are still searching the same term as before window.setTimeout(() => { - if (sub.filter.search === searchTerm) { + if (sub.filter.search === normalizedSearchTerm) { sub.stop() } }, 10000) @@ -502,9 +503,8 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => { }, [searchTerm]) const filteredGames = useMemo(() => { - if (searchTerm === '') return [] - const normalizedSearchTerm = normalizeSearchString(searchTerm) + if (normalizedSearchTerm === '') return [] return games.filter((game) => memoizedNormalizeSearchString(game['Game Name']).includes( diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ed2a3e1..5f75773 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -238,6 +238,7 @@ const romanRegex = new RegExp( ) 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) @@ -258,6 +259,17 @@ const memoizeNormalize = (func: (str: string) => string) => { } } +/** + * 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 +}