feat(catalog): searchable tag filter in global catalog overlay
Adds a typing-to-filter input above the tag chip list in the filter sidebar, rendered only when there are more than eight tags. Case- insensitive substring match; shows "No tags match" when the query empties the list. Selected tags filtered out of the sidebar remain active as header pills. Resets with the rest of overlay state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ export function CatalogSearchOverlay() {
|
|||||||
const [debouncedQuery, setDebouncedQuery] = useState("");
|
const [debouncedQuery, setDebouncedQuery] = useState("");
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
const [tagSearch, setTagSearch] = useState("");
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("grid");
|
const [viewMode, setViewMode] = useState<ViewMode>("grid");
|
||||||
const [manualEntryMode, setManualEntryMode] = useState(false);
|
const [manualEntryMode, setManualEntryMode] = useState(false);
|
||||||
const [savedItemName, setSavedItemName] = useState<string | null>(null);
|
const [savedItemName, setSavedItemName] = useState<string | null>(null);
|
||||||
@@ -87,6 +88,7 @@ export function CatalogSearchOverlay() {
|
|||||||
setDebouncedQuery("");
|
setDebouncedQuery("");
|
||||||
setSelectedTags([]);
|
setSelectedTags([]);
|
||||||
setFilterOpen(false);
|
setFilterOpen(false);
|
||||||
|
setTagSearch("");
|
||||||
setWeightMin(0);
|
setWeightMin(0);
|
||||||
setWeightMax(5000);
|
setWeightMax(5000);
|
||||||
setPriceMin(0);
|
setPriceMin(0);
|
||||||
@@ -334,24 +336,48 @@ export function CatalogSearchOverlay() {
|
|||||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">
|
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">
|
||||||
Tags
|
Tags
|
||||||
</h3>
|
</h3>
|
||||||
|
{tags.length > 8 && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={tagSearch}
|
||||||
|
onChange={(e) => setTagSearch(e.target.value)}
|
||||||
|
placeholder="Filter tags..."
|
||||||
|
className="w-full px-2 py-1 mb-2 border border-gray-200 rounded-md text-xs text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-300 focus:border-transparent transition-colors"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{tags.map((tag) => {
|
{(() => {
|
||||||
const isActive = selectedTags.includes(tag.name);
|
const q = tagSearch.trim().toLowerCase();
|
||||||
return (
|
const filteredTags = q
|
||||||
<button
|
? tags.filter((t) =>
|
||||||
key={tag.id}
|
t.name.toLowerCase().includes(q),
|
||||||
type="button"
|
)
|
||||||
onClick={() => toggleTag(tag.name)}
|
: tags;
|
||||||
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-sm transition-colors ${
|
if (filteredTags.length === 0) {
|
||||||
isActive
|
return (
|
||||||
? "bg-blue-50 text-blue-700 font-medium"
|
<p className="text-xs text-gray-400 px-2.5 py-1.5">
|
||||||
: "text-gray-600 hover:bg-gray-50"
|
No tags match
|
||||||
}`}
|
</p>
|
||||||
>
|
);
|
||||||
{tag.name}
|
}
|
||||||
</button>
|
return filteredTags.map((tag) => {
|
||||||
);
|
const isActive = selectedTags.includes(tag.name);
|
||||||
})}
|
return (
|
||||||
|
<button
|
||||||
|
key={tag.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleTag(tag.name)}
|
||||||
|
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-sm transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "bg-blue-50 text-blue-700 font-medium"
|
||||||
|
: "text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user