mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 02:41:11 +01:00
feat: search dropdown (#150)
- new search dropdown component that adds search / sort functionality to dropdowns - replaces browser select component in select condition type dropdowns - add font size prop to sort dropdown / search bar to scale size for dropdowns - fix bad background color on tests page - add scrollable class description / condition area for format / regex cards
This commit is contained in:
25
CLAUDE.md
Normal file
25
CLAUDE.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Profilarr Development Guide
|
||||
|
||||
## Commands
|
||||
- **Frontend**: `cd frontend && npm run dev` - Start React dev server
|
||||
- **Backend**: `cd backend && gunicorn -b 0.0.0.0:5000 app.main:app` - Run Flask server
|
||||
- **Docker**: `docker compose up` - Start both frontend/backend in dev mode
|
||||
- **Lint**: `cd frontend && npx eslint 'src/**/*.{js,jsx}'` - Check frontend code style
|
||||
- **Build**: `cd frontend && npm run build` - Build for production
|
||||
|
||||
## Code Style
|
||||
### Frontend (React)
|
||||
- **Imports**: React first, third-party libs next, components, then utils
|
||||
- **Components**: Functional components with hooks, PascalCase naming
|
||||
- **Props**: PropTypes for validation, destructure props in component signature
|
||||
- **State**: Group related state, useCallback for memoized handlers
|
||||
- **JSX**: 4-space indentation, attributes on new lines for readability
|
||||
- **Error Handling**: try/catch for async operations, toast notifications
|
||||
|
||||
### Backend (Python)
|
||||
- **Imports**: Standard lib first, third-party next, local modules last
|
||||
- **Naming**: snake_case for functions/vars/files, PascalCase for classes
|
||||
- **Functions**: Single responsibility, descriptive docstrings
|
||||
- **Error Handling**: Specific exception catches, return (success, message) tuples
|
||||
- **Indentation**: 4 spaces consistently
|
||||
- **Modularity**: Related functionality grouped in directories
|
||||
@@ -168,7 +168,7 @@ function FormatCard({
|
||||
: 'translate-x-0'
|
||||
}`}>
|
||||
{/* Conditions */}
|
||||
<div className='w-full flex-shrink-0 overflow-y-auto'>
|
||||
<div className='w-full flex-shrink-0 overflow-y-auto scrollable'>
|
||||
<div className='flex flex-wrap gap-1.5 content-start'>
|
||||
{content.conditions?.map((condition, index) => (
|
||||
<span
|
||||
@@ -189,7 +189,7 @@ function FormatCard({
|
||||
: 'translate-x-full'
|
||||
}`}>
|
||||
{/* Description */}
|
||||
<div className='w-full h-full overflow-y-auto'>
|
||||
<div className='w-full h-full overflow-y-auto scrollable'>
|
||||
{content.description ? (
|
||||
<div className='text-gray-300 text-xs prose prose-invert prose-gray max-w-none'>
|
||||
<ReactMarkdown>
|
||||
|
||||
@@ -120,7 +120,7 @@ const FormatTestingTab = ({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-lg'>
|
||||
<div className='text-center py-12 rounded-lg'>
|
||||
<p className='text-gray-500 dark:text-gray-400'>
|
||||
No tests added yet
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {CONDITION_TYPES, createCondition} from './conditionTypes';
|
||||
import {ArrowUp, ArrowDown, X, ChevronsUp, ChevronsDown} from 'lucide-react';
|
||||
import BrowserSelect from '@ui/BrowserSelect';
|
||||
import SearchDropdown from '@ui/SearchDropdown';
|
||||
|
||||
const ConditionCard = ({
|
||||
condition,
|
||||
@@ -21,7 +21,8 @@ const ConditionCard = ({
|
||||
|
||||
const typeOptions = Object.values(CONDITION_TYPES).map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
label: type.name,
|
||||
description: type.description || ''
|
||||
}));
|
||||
|
||||
const handleTypeChange = e => {
|
||||
@@ -57,14 +58,13 @@ const ConditionCard = ({
|
||||
|
||||
<div className='flex items-center gap-4'>
|
||||
{/* Type Selection */}
|
||||
<BrowserSelect
|
||||
<SearchDropdown
|
||||
value={condition.type || ''}
|
||||
onChange={handleTypeChange}
|
||||
options={typeOptions}
|
||||
placeholder='Select type...'
|
||||
className='min-w-[140px] px-3 py-2 text-sm rounded-md
|
||||
bg-gray-700 border border-gray-700
|
||||
text-gray-200'
|
||||
className='min-w-[200px] condition-type-dropdown'
|
||||
width='w-auto'
|
||||
/>
|
||||
|
||||
{/* Render the specific condition component */}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import BrowserSelect from '@ui/BrowserSelect';
|
||||
import SearchDropdown from '@ui/SearchDropdown';
|
||||
|
||||
const EditionCondition = ({condition, onChange, patterns}) => {
|
||||
// Convert patterns to options format
|
||||
// Format patterns for the dropdown with descriptions if available
|
||||
const patternOptions = patterns.map(pattern => ({
|
||||
value: pattern.name,
|
||||
label: pattern.name
|
||||
label: pattern.name,
|
||||
description: pattern.description || 'No description available',
|
||||
priority: pattern.priority
|
||||
}));
|
||||
|
||||
const handlePatternChange = e => {
|
||||
onChange({...condition, pattern: e.target.value});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
<BrowserSelect
|
||||
<div className="flex-1">
|
||||
<SearchDropdown
|
||||
value={condition.pattern || ''}
|
||||
onChange={e =>
|
||||
onChange({...condition, pattern: e.target.value})
|
||||
}
|
||||
onChange={handlePatternChange}
|
||||
options={patternOptions}
|
||||
placeholder='Select edition pattern...'
|
||||
className='w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600
|
||||
rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'
|
||||
searchableFields={['label', 'description']}
|
||||
className='min-w-[200px]'
|
||||
width='w-auto'
|
||||
dropdownWidth='100%'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SearchDropdown from '@ui/SearchDropdown';
|
||||
|
||||
const ReleaseGroupCondition = ({condition, onChange, patterns}) => {
|
||||
const sortedPatterns = [...patterns].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
// Format patterns for the dropdown with descriptions if available
|
||||
const patternOptions = patterns.map(pattern => ({
|
||||
value: pattern.name,
|
||||
label: pattern.name,
|
||||
description: pattern.description || 'No description available',
|
||||
priority: pattern.priority
|
||||
}));
|
||||
|
||||
const handlePatternChange = e => {
|
||||
onChange({...condition, pattern: e.target.value});
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
className='flex-1 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700'
|
||||
value={condition.pattern || ''}
|
||||
onChange={e => onChange({...condition, pattern: e.target.value})}>
|
||||
<option value=''>Select release group pattern...</option>
|
||||
{sortedPatterns.map(pattern => (
|
||||
<option key={pattern.name} value={pattern.name}>
|
||||
{pattern.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex-1">
|
||||
<SearchDropdown
|
||||
value={condition.pattern || ''}
|
||||
onChange={handlePatternChange}
|
||||
options={patternOptions}
|
||||
placeholder='Select release group pattern...'
|
||||
searchableFields={['label', 'description']}
|
||||
className='min-w-[200px]'
|
||||
width='w-auto'
|
||||
dropdownWidth='100%'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SearchDropdown from '@ui/SearchDropdown';
|
||||
|
||||
const ReleaseTitleCondition = ({condition, onChange, patterns}) => {
|
||||
const sortedPatterns = [...patterns].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
// Format patterns for the dropdown with enhanced descriptions
|
||||
const patternOptions = patterns.map(pattern => ({
|
||||
value: pattern.name,
|
||||
label: pattern.name,
|
||||
description: pattern.description || 'No description available',
|
||||
priority: pattern.priority
|
||||
}));
|
||||
|
||||
const handlePatternChange = e => {
|
||||
onChange({...condition, pattern: e.target.value});
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
className='flex-1 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700'
|
||||
value={condition.pattern || ''}
|
||||
onChange={e => onChange({...condition, pattern: e.target.value})}>
|
||||
<option value=''>Select release title pattern...</option>
|
||||
{sortedPatterns.map(pattern => (
|
||||
<option key={pattern.name} value={pattern.name}>
|
||||
{pattern.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex-1">
|
||||
<SearchDropdown
|
||||
value={condition.pattern || ''}
|
||||
onChange={handlePatternChange}
|
||||
options={patternOptions}
|
||||
placeholder='Select release title pattern...'
|
||||
searchableFields={['label', 'description']}
|
||||
className='min-w-[200px]'
|
||||
width='w-auto'
|
||||
dropdownWidth='100%'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ const RegexCard = ({
|
||||
[&>ul]:list-disc [&>ul]:ml-4 [&>ul]:mt-2 [&>ul]:mb-4
|
||||
[&>ol]:list-decimal [&>ol]:ml-4 [&>ol]:mt-2 [&>ol]:mb-4
|
||||
[&>ul>li]:mt-0.5 [&>ol>li]:mt-0.5
|
||||
[&_code]:bg-gray-900/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-md [&_code]:text-blue-300 [&_code]:border [&_code]:border-gray-700/50'>
|
||||
[&_code]:bg-gray-900/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-md [&_code]:text-blue-300 [&_code]:border [&_code]:border-gray-700/50 scrollable'>
|
||||
<ReactMarkdown>{pattern.description}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,7 +148,7 @@ const RegexTestingTab = ({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-lg'>
|
||||
<div className='text-center py-12 rounded-lg'>
|
||||
<p className='text-gray-500 dark:text-gray-400'>
|
||||
No tests added yet
|
||||
</p>
|
||||
|
||||
@@ -12,7 +12,11 @@ const SearchBar = ({
|
||||
onInputChange,
|
||||
onAddTerm,
|
||||
onRemoveTerm,
|
||||
onClearTerms
|
||||
onClearTerms,
|
||||
textSize = 'text-sm', // Default text size
|
||||
badgeTextSize = 'text-sm', // Default badge text size
|
||||
iconSize = 'h-4 w-4', // Default icon size
|
||||
minHeight = 'min-h-10' // Default min height
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
@@ -47,7 +51,7 @@ const SearchBar = ({
|
||||
<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
|
||||
absolute left-3 top-1/2 -translate-y-1/2 ${iconSize}
|
||||
transition-colors duration-200
|
||||
${
|
||||
isFocused
|
||||
@@ -58,9 +62,14 @@ const SearchBar = ({
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
w-full min-h-10 pl-9 pr-8 rounded-md
|
||||
w-full ${minHeight} 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
|
||||
border shadow-sm flex items-center gap-2 p-2
|
||||
${
|
||||
minHeight && minHeight.startsWith('h-')
|
||||
? 'overflow-x-auto overflow-y-hidden whitespace-nowrap'
|
||||
: ''
|
||||
}
|
||||
${
|
||||
isFocused
|
||||
? 'border-blue-500 ring-2 ring-blue-500/20 bg-white/5'
|
||||
@@ -71,17 +80,24 @@ const SearchBar = ({
|
||||
{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
|
||||
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'>
|
||||
className={`
|
||||
flex items-center gap-1.5 px-2
|
||||
${
|
||||
minHeight && minHeight.startsWith('h-')
|
||||
? 'py-0.5'
|
||||
: '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
|
||||
hover:bg-blue-500/15 dark:hover:bg-blue-500/25
|
||||
hover:border-blue-500/30 dark:hover:border-blue-400/30
|
||||
group/badge flex-shrink-0
|
||||
transition-all duration-200
|
||||
`}>
|
||||
<span
|
||||
className={`${badgeTextSize} font-medium leading-none`}>
|
||||
{term}
|
||||
</span>
|
||||
<button
|
||||
@@ -106,10 +122,10 @@ const SearchBar = ({
|
||||
? 'Add another filter...'
|
||||
: placeholder
|
||||
}
|
||||
className='flex-1 min-w-[200px] bg-transparent
|
||||
text-gray-900 dark:text-gray-100
|
||||
className={`flex-1 min-w-[200px] bg-transparent
|
||||
${textSize} text-gray-900 dark:text-gray-100
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||
focus:outline-none'
|
||||
focus:outline-none`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +138,7 @@ const SearchBar = ({
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700
|
||||
transition-all duration-200
|
||||
group/clear'>
|
||||
<X className='h-4 w-4' />
|
||||
<X className={iconSize} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
223
frontend/src/components/ui/SearchDropdown.jsx
Normal file
223
frontend/src/components/ui/SearchDropdown.jsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, {useEffect, useRef, useState, useCallback} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ChevronDown, Check, Search} from 'lucide-react';
|
||||
import SearchBar from './DataBar/SearchBar';
|
||||
import SortDropdown from './SortDropdown';
|
||||
import useSearch from '@hooks/useSearch';
|
||||
import {useSorting} from '@hooks/useSorting';
|
||||
|
||||
const SearchDropdown = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
className,
|
||||
width = 'w-full',
|
||||
dropdownWidth,
|
||||
labelKey = 'label',
|
||||
valueKey = 'value',
|
||||
searchableFields = ['label', 'description']
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState(
|
||||
options.find(opt => opt[valueKey] === value) || null
|
||||
);
|
||||
|
||||
const dropdownRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
const {
|
||||
searchTerms,
|
||||
currentInput,
|
||||
setCurrentInput,
|
||||
addSearchTerm,
|
||||
removeSearchTerm,
|
||||
clearSearchTerms,
|
||||
items: filteredOptions
|
||||
} = useSearch(options, {
|
||||
searchableFields,
|
||||
initialSortBy: 'label'
|
||||
});
|
||||
|
||||
const {sortConfig, updateSort, sortData} = useSorting({
|
||||
field: 'label',
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
// Sort options configuration for the dropdown (name only)
|
||||
const sortOptions = [{value: 'label', label: 'Name (A-Z)'}];
|
||||
|
||||
// Update selected option when value changes externally
|
||||
useEffect(() => {
|
||||
setSelectedOption(options.find(opt => opt[valueKey] === value) || null);
|
||||
}, [value, options, valueKey]);
|
||||
|
||||
// Handle dropdown visibility
|
||||
useEffect(() => {
|
||||
// Handle clicks outside dropdown (close the dropdown)
|
||||
const handleClickOutside = event => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Apply final sorting to the filtered results
|
||||
const sortedOptions = useCallback(() => {
|
||||
return sortData(filteredOptions);
|
||||
}, [sortData, filteredOptions]);
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = useCallback(
|
||||
option => {
|
||||
setSelectedOption(option);
|
||||
onChange({target: {value: option[valueKey]}});
|
||||
setIsOpen(false);
|
||||
},
|
||||
[onChange, valueKey]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`relative ${width}`} ref={dropdownRef}>
|
||||
{/* Selected Value Button */}
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 text-sm
|
||||
border rounded-md
|
||||
bg-gray-700 border-gray-600 text-gray-100 hover:border-gray-500
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
transition-colors ${className}`}>
|
||||
<span className='truncate'>
|
||||
{selectedOption
|
||||
? selectedOption[labelKey]
|
||||
: placeholder || 'Select option...'}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 ml-2 transition-transform ${
|
||||
isOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className='absolute z-50 mt-1
|
||||
bg-gray-800 border border-gray-700 rounded-md shadow-lg
|
||||
flex flex-col overflow-hidden'
|
||||
style={{
|
||||
width: dropdownWidth || '650px',
|
||||
maxHeight: '700px',
|
||||
left: '0'
|
||||
}}>
|
||||
<div className='p-3 bg-gray-800 shadow-sm relative'>
|
||||
<div className='absolute left-0 right-0 bottom-0 h-px bg-gray-700/50'></div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex-grow'>
|
||||
<SearchBar
|
||||
placeholder='Search options...'
|
||||
searchTerms={searchTerms}
|
||||
currentInput={currentInput}
|
||||
onInputChange={setCurrentInput}
|
||||
onAddTerm={addSearchTerm}
|
||||
onRemoveTerm={removeSearchTerm}
|
||||
onClearTerms={clearSearchTerms}
|
||||
requireEnter={true}
|
||||
textSize='text-xs'
|
||||
badgeTextSize='text-xs'
|
||||
iconSize='h-3.5 w-3.5'
|
||||
minHeight='h-8'
|
||||
/>
|
||||
</div>
|
||||
<SortDropdown
|
||||
sortOptions={sortOptions}
|
||||
currentSort={sortConfig}
|
||||
onSortChange={updateSort}
|
||||
className='flex-shrink-0'
|
||||
textSize='text-xs'
|
||||
menuTextSize='text-xs'
|
||||
iconSize={14}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options List */}
|
||||
<div className='flex-1 p-2 pt-3 overflow-auto'>
|
||||
{sortedOptions().length > 0 ? (
|
||||
<div className='flex flex-col'>
|
||||
{sortedOptions().map(option => (
|
||||
<div
|
||||
key={option[valueKey]}
|
||||
onClick={() => handleSelect(option)}
|
||||
className={`px-2.5 py-1.5 text-xs cursor-pointer rounded
|
||||
${
|
||||
selectedOption?.[valueKey] ===
|
||||
option[valueKey]
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-100 hover:bg-gray-700'
|
||||
}`}>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex-grow truncate'>
|
||||
{option[labelKey]}
|
||||
</div>
|
||||
{selectedOption?.[valueKey] ===
|
||||
option[valueKey] && (
|
||||
<Check className='w-3.5 h-3.5 ml-1.5 flex-shrink-0' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='px-3 py-12 text-center'>
|
||||
<div className='bg-gray-700/30 rounded-lg p-4 max-w-xs mx-auto'>
|
||||
<Search className='w-6 h-6 mb-2 mx-auto text-gray-500 opacity-40' />
|
||||
<p className='text-gray-300 text-xs'>
|
||||
No options match your search
|
||||
</p>
|
||||
<button
|
||||
onClick={clearSearchTerms}
|
||||
className='mt-2 text-xs text-blue-400 hover:underline'>
|
||||
Clear search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchDropdown.propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
description: PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
dropdownWidth: PropTypes.string,
|
||||
labelKey: PropTypes.string,
|
||||
valueKey: PropTypes.string,
|
||||
searchableFields: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default SearchDropdown;
|
||||
@@ -7,7 +7,10 @@ const SortDropdown = ({
|
||||
sortOptions,
|
||||
currentSort,
|
||||
onSortChange,
|
||||
className = ''
|
||||
className = '',
|
||||
textSize = 'text-sm', // Default text size
|
||||
menuTextSize = 'text-xs', // Default menu text size
|
||||
iconSize = 16 // Default icon size
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -30,7 +33,7 @@ const SortDropdown = ({
|
||||
onClick={toggleDropdown}
|
||||
className={`
|
||||
inline-flex items-center justify-between
|
||||
min-h-[40px] px-4 py-2 text-sm
|
||||
px-4 py-2 ${textSize}
|
||||
bg-white dark:bg-gray-800
|
||||
border border-gray-300 dark:border-gray-700
|
||||
text-gray-900 dark:text-gray-100
|
||||
@@ -42,9 +45,9 @@ const SortDropdown = ({
|
||||
<span className='flex items-center gap-2'>
|
||||
{getCurrentSortLabel()}
|
||||
{currentSort.direction === 'asc' ? (
|
||||
<ArrowUp size={16} />
|
||||
<ArrowUp size={iconSize} />
|
||||
) : (
|
||||
<ArrowDown size={16} />
|
||||
<ArrowDown size={iconSize} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -63,17 +66,17 @@ const SortDropdown = ({
|
||||
key={option.value}
|
||||
type='button'
|
||||
onClick={() => handleSortClick(option.value)}
|
||||
className='
|
||||
className={`
|
||||
flex items-center justify-between w-full px-4 py-2
|
||||
text-xs text-gray-700 dark:text-gray-200
|
||||
${menuTextSize} text-gray-700 dark:text-gray-200
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700
|
||||
'>
|
||||
`}>
|
||||
<span>{option.label}</span>
|
||||
{currentSort.field === option.value &&
|
||||
(currentSort.direction === 'asc' ? (
|
||||
<ArrowUp size={16} />
|
||||
<ArrowUp size={iconSize} />
|
||||
) : (
|
||||
<ArrowDown size={16} />
|
||||
<ArrowDown size={iconSize} />
|
||||
))}
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user