From 6aa3dcf41b31eacdfdd25da7436fd5a9f75c921b Mon Sep 17 00:00:00 2001 From: Syco21 Date: Fri, 15 May 2026 22:47:44 +0200 Subject: [PATCH] Improve search: normalize punctuation, token matching, 1-char typo tolerance --- src/pages/HomePage.jsx | 4 ++-- src/utils/search.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/utils/search.js diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index c912591..600231a 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -4,6 +4,7 @@ import { CardContext } from '../context/CardContext'; import { fetchCards, fetchCardImage } from '../services/api'; import { useDebounce } from '../hooks/useDebounce'; import { useMediaQuery } from '../hooks/useMediaQuery'; +import { fuzzyMatch } from '../utils/search'; import CardRow from '../components/CardRow/CardRow'; import SearchBar from '../components/SearchBar/SearchBar'; import FilterBar from '../components/FilterBar/FilterBar'; @@ -111,8 +112,7 @@ function HomePage() { const filteredCards = useMemo(() => { let result = cards; if (debouncedSearch) { - const q = debouncedSearch.toLowerCase(); - result = result.filter(c => c.name.toLowerCase().includes(q)); + result = result.filter(c => fuzzyMatch(c.name, debouncedSearch)); } if (typeFilter !== 'All') { const q = typeFilter.toLowerCase(); diff --git a/src/utils/search.js b/src/utils/search.js new file mode 100644 index 0000000..908f5d4 --- /dev/null +++ b/src/utils/search.js @@ -0,0 +1,30 @@ +function normalize(str) { + return str.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim(); +} + +function tokenMatches(nameNorm, token) { + if (nameNorm.includes(token)) return true; + // Allow 1 substitution for tokens of 4+ chars + if (token.length < 4) return false; + for (let i = 0; i <= nameNorm.length - token.length; i++) { + let diff = 0; + for (let j = 0; j < token.length; j++) { + if (nameNorm[i + j] !== token[j] && ++diff > 1) break; + } + if (diff <= 1) return true; + } + return false; +} + +export function fuzzyMatch(cardName, query) { + const nameNorm = normalize(cardName); + const queryNorm = normalize(query); + if (!queryNorm) return true; + + // Fast path: exact substring match on normalized strings + if (nameNorm.includes(queryNorm)) return true; + + // Token path: every query word must match somewhere in the name + const tokens = queryNorm.split(' ').filter(Boolean); + return tokens.every(token => tokenMatches(nameNorm, token)); +}