Improve search: normalize punctuation, token matching, 1-char typo tolerance
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-15 22:47:44 +02:00
parent 3dd3c13c4b
commit 6aa3dcf41b
2 changed files with 32 additions and 2 deletions
+2 -2
View File
@@ -4,6 +4,7 @@ import { CardContext } from '../context/CardContext';
import { fetchCards, fetchCardImage } from '../services/api'; import { fetchCards, fetchCardImage } from '../services/api';
import { useDebounce } from '../hooks/useDebounce'; import { useDebounce } from '../hooks/useDebounce';
import { useMediaQuery } from '../hooks/useMediaQuery'; import { useMediaQuery } from '../hooks/useMediaQuery';
import { fuzzyMatch } from '../utils/search';
import CardRow from '../components/CardRow/CardRow'; import CardRow from '../components/CardRow/CardRow';
import SearchBar from '../components/SearchBar/SearchBar'; import SearchBar from '../components/SearchBar/SearchBar';
import FilterBar from '../components/FilterBar/FilterBar'; import FilterBar from '../components/FilterBar/FilterBar';
@@ -111,8 +112,7 @@ function HomePage() {
const filteredCards = useMemo(() => { const filteredCards = useMemo(() => {
let result = cards; let result = cards;
if (debouncedSearch) { if (debouncedSearch) {
const q = debouncedSearch.toLowerCase(); result = result.filter(c => fuzzyMatch(c.name, debouncedSearch));
result = result.filter(c => c.name.toLowerCase().includes(q));
} }
if (typeFilter !== 'All') { if (typeFilter !== 'All') {
const q = typeFilter.toLowerCase(); const q = typeFilter.toLowerCase();
+30
View File
@@ -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));
}