feat: enhance DataBar and SearchBar components with multi-term search functionality

This commit is contained in:
Sam Chau
2025-01-12 02:52:07 +10:30
parent 8d28227da6
commit c2fa764e24
2 changed files with 84 additions and 104 deletions

View File

@@ -17,8 +17,13 @@ const FloatingBar = ({children}) => (
);
const DataBar = ({
onSearch,
searchPlaceholder = 'Search by name or tag...',
searchTerms,
currentInput,
onInputChange,
onAddTerm,
onRemoveTerm,
onClearTerms,
filterType,
setFilterType,
filterValue,
@@ -34,43 +39,28 @@ const DataBar = ({
className
}) => {
const [isFloating, setIsFloating] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [activeSearch, setActiveSearch] = useState('');
useEffect(() => {
const handleScroll = () => {
setIsFloating(window.scrollY > 64);
};
window.addEventListener('scroll', handleScroll, {passive: true});
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleSearch = term => {
// First set the search term and trigger the search
setActiveSearch(term);
onSearch(term);
// Then smoothly scroll to top
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
const controls = (
<>
<SearchBar
onSearch={handleSearch}
searchTerms={searchTerms}
currentInput={currentInput}
onInputChange={onInputChange}
onAddTerm={onAddTerm}
onRemoveTerm={onRemoveTerm}
onClearTerms={onClearTerms}
placeholder={searchPlaceholder}
className='flex-1'
requireEnter
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
activeSearch={activeSearch}
setActiveSearch={setActiveSearch}
/>
<div className='flex items-center gap-3'>
<SortDropdown
options={[
@@ -81,7 +71,6 @@ const DataBar = ({
currentDirection='desc'
onSort={key => setSortBy(key)}
/>
<FilterMenu
filterType={filterType}
setFilterType={setFilterType}
@@ -89,11 +78,9 @@ const DataBar = ({
setFilterValue={setFilterValue}
allTags={allTags}
/>
{showAddButton && !isSelectionMode && (
<AddButton onClick={onAdd} label={addButtonLabel} />
)}
<ToggleSelectButton
isSelectionMode={isSelectionMode}
onClick={toggleSelectionMode}

View File

@@ -2,14 +2,15 @@ import React, {useState, useEffect} from 'react';
import {Search, X} from 'lucide-react';
const SearchBar = ({
onSearch,
placeholder = 'Search...',
className = '',
requireEnter = false,
searchTerm,
setSearchTerm,
activeSearch,
setActiveSearch
requireEnter = true,
searchTerms = [],
currentInput = '',
onInputChange,
onAddTerm,
onRemoveTerm,
onClearTerms
}) => {
const [isFocused, setIsFocused] = useState(false);
@@ -17,111 +18,103 @@ const SearchBar = ({
const handleKeyDown = e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
document.querySelector('input[type="text"]').focus();
document.querySelector('input[type="text"]')?.focus();
} else if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
clearSearch();
onClearTerms();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
const handleChange = e => {
const newValue = e.target.value;
setSearchTerm(newValue);
if (!requireEnter) {
onSearch(newValue);
setActiveSearch(newValue);
}
};
}, [onClearTerms]);
const handleKeyPress = e => {
if (requireEnter && e.key === 'Enter' && searchTerm.trim()) {
onSearch(searchTerm);
setActiveSearch(searchTerm);
setSearchTerm('');
if (requireEnter && e.key === 'Enter' && currentInput.trim()) {
onAddTerm(currentInput);
}
};
const clearSearch = () => {
setSearchTerm('');
setActiveSearch('');
onSearch('');
};
return (
<div className={`relative flex-1 min-w-0 group ${className}`}>
<Search
className={`
absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4
transition-colors duration-200
${
isFocused
? 'text-blue-500'
: 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300'
}
`}
absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4
transition-colors duration-200
${
isFocused
? 'text-blue-500'
: 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300'
}
`}
/>
<div
className={`
w-full h-10 pl-9 pr-8 rounded-md
transition-all duration-200 ease-in-out
border shadow-sm
${
isFocused
? 'border-blue-500 ring-2 ring-blue-500/20 bg-white/5'
: 'border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-600'
}
bg-white dark:bg-gray-800
flex items-center
`}>
{activeSearch ? (
w-full min-h-10 pl-9 pr-8 rounded-md
transition-all duration-200 ease-in-out
border shadow-sm flex items-center flex-wrap gap-2 p-2
${
isFocused
? 'border-blue-500 ring-2 ring-blue-500/20 bg-white/5'
: 'border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-600'
}
bg-white dark:bg-gray-800
`}>
{searchTerms.map((term, index) => (
<div
key={index}
className='flex items-center gap-1.5 px-2 py-1
bg-blue-500/10 dark:bg-blue-500/20
border border-blue-500/20 dark:border-blue-400/20
text-blue-600 dark:text-blue-400
rounded-md shadow-sm
transition-all duration-200'>
bg-blue-500/10 dark:bg-blue-500/20
border border-blue-500/20 dark:border-blue-400/20
text-blue-600 dark:text-blue-400
rounded-md shadow-sm
hover:bg-blue-500/15 dark:hover:bg-blue-500/25
hover:border-blue-500/30 dark:hover:border-blue-400/30
group/badge
transition-all duration-200'>
<span className='text-sm font-medium leading-none'>
{activeSearch}
{term}
</span>
<button
onClick={clearSearch}
className='p-0.5 hover:bg-blue-500/20 rounded-sm transition-colors'
aria-label='Clear search'>
onClick={() => onRemoveTerm(term)}
className='p-0.5 hover:bg-blue-500/20
rounded-sm transition-colors
opacity-70 group-hover/badge:opacity-100'
aria-label={`Remove ${term} filter`}>
<X className='h-3 w-3' />
</button>
</div>
) : (
<input
type='text'
value={searchTerm}
onChange={handleChange}
onKeyPress={handleKeyPress}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={placeholder}
className='w-full bg-transparent
text-gray-900 dark:text-gray-100
placeholder:text-gray-500 dark:placeholder:text-gray-400
focus:outline-none'
/>
)}
))}
<input
type='text'
value={currentInput}
onChange={e => onInputChange(e.target.value)}
onKeyPress={handleKeyPress}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={
searchTerms.length
? 'Add another filter...'
: placeholder
}
className='flex-1 min-w-[200px] bg-transparent
text-gray-900 dark:text-gray-100
placeholder:text-gray-500 dark:placeholder:text-gray-400
focus:outline-none'
/>
</div>
{searchTerm && !activeSearch && (
{(searchTerms.length > 0 || currentInput) && (
<button
onClick={() => setSearchTerm('')}
onClick={onClearTerms}
className='absolute right-3 top-1/2 -translate-y-1/2
p-1.5 rounded-full
text-gray-400 hover:text-gray-600
hover:bg-gray-100 dark:hover:bg-gray-700
transition-all duration-200'
aria-label='Clear search input'>
p-1.5 rounded-full
text-gray-400 hover:text-gray-600
hover:bg-gray-100 dark:hover:bg-gray-700
transition-all duration-200
group/clear'
aria-label='Clear all searches'>
<X className='h-4 w-4' />
</button>
)}