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 { 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();
|
||||||
|
|||||||
@@ -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