Improve search: normalize punctuation, token matching, 1-char typo tolerance
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user