From b850803f701f449055fa366e366c775e2104ceff Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 16 Jan 2026 13:11:10 +0100 Subject: [PATCH] feat: add Smart Range filter for Weeks --- components/FilterBar.tsx | 1 + components/MultiSelectDropdown.tsx | 104 +++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/components/FilterBar.tsx b/components/FilterBar.tsx index 34f1a0b..9eb7cae 100644 --- a/components/FilterBar.tsx +++ b/components/FilterBar.tsx @@ -49,6 +49,7 @@ const FilterBar: React.FC = ({ filters, onFilterChange, options options={options.week} onChange={(v) => onFilterChange('week', v)} className="flex-1" + enableRangeSelect={true} /> void; className?: string; + enableRangeSelect?: boolean; } -const MultiSelectDropdown: React.FC = ({ label, selected, options, onChange, className }) => { +const MultiSelectDropdown: React.FC = ({ label, selected, options, onChange, className, enableRangeSelect }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [rangeInput, setRangeInput] = useState(''); const dropdownRef = useRef(null); const inputRef = useRef(null); @@ -72,21 +74,64 @@ const MultiSelectDropdown: React.FC = ({ label, select }; const handleClear = () => { - onChange([]); + onChange([]); }; + const handleRangeApply = () => { + if (!rangeInput.trim()) return; + + const input = rangeInput.trim().toLowerCase().replace('w', ''); // Allow "W5" -> "5" + let predicate: (num: number) => boolean = () => false; + + // Parse Input + if (input.includes('-')) { + const [start, end] = input.split('-').map(s => parseFloat(s.trim())); + if (!isNaN(start) && !isNaN(end)) { + predicate = (n) => n >= start && n <= end; + } + } else if (input.startsWith('<=')) { + const val = parseFloat(input.substring(2).trim()); + if (!isNaN(val)) predicate = (n) => n <= val; + } else if (input.startsWith('>=')) { + const val = parseFloat(input.substring(2).trim()); + if (!isNaN(val)) predicate = (n) => n >= val; + } else if (input.startsWith('<')) { + const val = parseFloat(input.substring(1).trim()); + if (!isNaN(val)) predicate = (n) => n < val; + } else if (input.startsWith('>')) { + const val = parseFloat(input.substring(1).trim()); + if (!isNaN(val)) predicate = (n) => n > val; + } else { + // Exact match + const val = parseFloat(input); + if (!isNaN(val)) predicate = (n) => n === val; + } + + // Apply to Options + const newSelected = options.filter(opt => { + // Extract number from option (e.g. "W1" -> 1, "2023" -> 2023) + const num = parseFloat(opt.replace(/\D/g, '')); + if (isNaN(num)) return false; + return predicate(num); + }); + + onChange(newSelected); + setRangeInput(''); // Clear input after apply + }; + + return (
- + +
+ )} + +
- - + +
- +
{filteredOptions.map((opt) => ( ))}