diff --git a/components/repeto/FilterSection.tsx b/components/repeto/FilterSection.tsx index 9ffe2a5..c56162c 100644 --- a/components/repeto/FilterSection.tsx +++ b/components/repeto/FilterSection.tsx @@ -1,4 +1,6 @@ -import { useState } from 'react'; +"use client"; + +import React, { useState, useEffect, useRef } from 'react'; import { ChevronDown, ChevronUp, Filter, X } from 'lucide-react'; import filtersData from '@/data/filters.json'; @@ -7,6 +9,12 @@ interface FilterOption { options: string[]; } +// Special filter types interface +interface RangeFilter { + min: number; + max: number; +} + interface FilterSectionProps { onFilterSubmit?: (selectedFilters: Record) => void; onClearFilters?: () => void; @@ -17,6 +25,14 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter const [filters] = useState(filtersData); const [selectedOptions, setSelectedOptions] = useState>>({}); const [openDropdowns, setOpenDropdowns] = useState>({}); + + // Get current year for year range filter + const currentYear = new Date().getFullYear(); + // Year range filter state + const [yearRangeFilter, setYearRangeFilter] = useState({ + min: 2020, // Default minimum year + max: currentYear + 5 // Default maximum year (current year + 5 for future years) + }); const toggleDropdown = (filterTitle: string) => { @@ -47,17 +63,36 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter const handleSubmit = () => { const formattedFilters: Record = {}; + + // Add all selected filters except for Year of Submission (which will be handled by range) Object.entries(selectedOptions).forEach(([title, optionsSet]) => { - if (optionsSet.size > 0) { + if (optionsSet.size > 0 && title !== "Year of Submission") { formattedFilters[title] = Array.from(optionsSet); } }); + + // Generate all years between min and max for the Year of Submission filter + const yearsInRange: string[] = []; + for (let year = yearRangeFilter.min; year <= yearRangeFilter.max; year++) { + yearsInRange.push(year.toString()); + } + + // Add the year range to the filters + if (yearsInRange.length > 0) { + formattedFilters["Year of Submission"] = yearsInRange; + } + onFilterSubmit?.(Object.keys(formattedFilters).length > 0 ? formattedFilters : {}); setIsOpen(false); }; const clearFilters = () => { setSelectedOptions({}); + // Reset year range filter to defaults + setYearRangeFilter({ + min: 2020, + max: currentYear + 5 + }); setIsOpen(false); onClearFilters?.(); }; @@ -91,6 +126,8 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter isSelected={isSelected} toggleOption={toggleOption} /> + ) : filter.title === "Year of Submission" ? ( + ) : (
{filter.options.map((option) => ( @@ -133,6 +170,523 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter ); // Domain filter options component with search functionality + // Year Range Filter Component + const YearRangeFilterOptions = () => { + // Create a range of all possible years for the year picker + const minPossibleYear = 2000; + const maxPossibleYear = currentYear + 10; + const allYears = Array.from( + { length: maxPossibleYear - minPossibleYear + 1 }, + (_, i) => minPossibleYear + i + ); + + // State to control the year picker popup + const [showYearPicker, setShowYearPicker] = useState(false); + const [activePickerType, setActivePickerType] = useState<'min' | 'max' | null>(null); + + // State for drag handling + const [isDragging, setIsDragging] = useState(false); + const [currentThumb, setCurrentThumb] = useState<'min' | 'max' | null>(null); + + // References to slider elements + const yearPickerRef = useRef(null); + const sliderRef = useRef(null); + + // Effect to handle clicks outside the year picker + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (yearPickerRef.current && !yearPickerRef.current.contains(event.target as Node)) { + setShowYearPicker(false); + } + } + + // Add event listener when the picker is shown + if (showYearPicker) { + document.addEventListener('mousedown', handleClickOutside); + } + + // Clean up + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showYearPicker]); + + // Function to handle mouse down on thumb elements + const handleThumbMouseDown = (event: React.MouseEvent, thumbType: 'min' | 'max') => { + // Prevent default and stop propagation + event.preventDefault(); + event.stopPropagation(); + + // Remove focus from any active element to prevent keyboard events + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + // Set dragging state immediately + setCurrentThumb(thumbType); + setIsDragging(true); + + // Add dragging styles and prevent text selection globally + document.body.classList.add('slider-dragging'); + document.body.style.userSelect = 'none'; + document.body.style.cursor = 'grabbing'; + + // Capture the initial position + const sliderRect = sliderRef.current?.getBoundingClientRect(); + if (sliderRect) { + const position = (event.clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + if (thumbType === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + } + + // Add the event listeners right away + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleEnd); + }; + + // Function to handle touch events for mobile devices + const handleThumbTouchStart = (event: React.TouchEvent, thumbType: 'min' | 'max') => { + // Prevent default to avoid scrolling + event.preventDefault(); + event.stopPropagation(); + + // Set dragging state immediately + setCurrentThumb(thumbType); + setIsDragging(true); + + // Add dragging styles and prevent text selection globally + document.body.classList.add('slider-dragging'); + document.body.style.userSelect = 'none'; + document.body.style.touchAction = 'none'; + + // Capture the initial position + const sliderRect = sliderRef.current?.getBoundingClientRect(); + if (sliderRect && event.touches[0]) { + const position = (event.touches[0].clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + if (thumbType === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + } + + // Add the event listeners right away + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleEnd); + document.addEventListener('touchcancel', handleEnd); + if (sliderRect && event.touches[0]) { + const position = (event.touches[0].clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + setYearRangeFilter(prev => ({ + ...prev, + [thumbType]: thumbType === 'min' ? Math.min(year, prev.max) : Math.max(year, prev.min) + })); + } + }; + + // Functions for handling drag movement and end + const updateThumbPosition = (clientX: number) => { + if (!sliderRef.current || !currentThumb) return; + + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Update the appropriate year based on which thumb is being dragged + if (currentThumb === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + }; + + // Handler for mouse movement (defined outside useEffect for direct binding) + const handleMouseMove = (event: MouseEvent) => { + if (!isDragging) return; + event.preventDefault(); + requestAnimationFrame(() => updateThumbPosition(event.clientX)); + }; + + // Handler for touch movement + const handleTouchMove = (event: TouchEvent) => { + if (!isDragging || !event.touches[0]) return; + event.preventDefault(); + requestAnimationFrame(() => updateThumbPosition(event.touches[0].clientX)); + }; + + // Handler for ending the drag + const handleEnd = (event?: MouseEvent | TouchEvent) => { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + // Remove event listeners + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleEnd); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleEnd); + document.removeEventListener('touchcancel', handleEnd); + + // Reset state and styles + setIsDragging(false); + setCurrentThumb(null); + document.body.classList.remove('slider-dragging'); + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + document.body.style.touchAction = ''; + }; + + // Clean up event listeners on unmount + useEffect(() => { + return () => { + if (isDragging) { + handleEnd(); + } + }; + }, []); + + // Function to handle direct clicks on the slider track + const handleTrackClick = (event: React.MouseEvent) => { + if (sliderRef.current) { + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (event.clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Calculate current thumb positions as percentages + const minPos = minThumbPosition / 100; // Convert percentage to decimal + const maxPos = maxThumbPosition / 100; + + // Create a visual ripple effect at click position + const clickEffect = document.createElement('div'); + clickEffect.className = 'absolute w-4 h-4 bg-blue-400 rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-ping'; + clickEffect.style.left = `${event.clientX - sliderRect.left}px`; + clickEffect.style.top = '50%'; + clickEffect.style.opacity = '0.6'; + clickEffect.style.pointerEvents = 'none'; + sliderRef.current.appendChild(clickEffect); + + // Remove the effect after animation + setTimeout(() => { + if (sliderRef.current && sliderRef.current.contains(clickEffect)) { + sliderRef.current.removeChild(clickEffect); + } + }, 500); + + // Determine which thumb to move based on which side of the range the click is on + let thumbType: 'min' | 'max' = 'min'; + + if (position <= minPos) { + // If click is to left of min thumb, move min thumb + thumbType = 'min'; + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else if (position >= maxPos) { + // If click is to right of max thumb, move max thumb + thumbType = 'max'; + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } else { + // If click is inside range, move whichever thumb is closer + const minDistance = Math.abs(position - minPos); + const maxDistance = Math.abs(position - maxPos); + + if (minDistance <= maxDistance) { + thumbType = 'min'; + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else { + thumbType = 'max'; + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } + } + + // Animate the thumb that was moved + setCurrentThumb(thumbType); + setTimeout(() => { + setCurrentThumb(null); + }, 300); + } + }; + + // Function to handle touch events on the slider track + const handleTrackTouch = (event: React.TouchEvent) => { + if (sliderRef.current && event.touches[0]) { + // Prevent default to avoid scrolling while interacting + event.preventDefault(); + + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (event.touches[0].clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Use the same logic as the click handler + const minPos = minThumbPosition / 100; + const maxPos = maxThumbPosition / 100; + + if (position <= minPos) { + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else if (position >= maxPos) { + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } else { + const minDistance = Math.abs(position - minPos); + const maxDistance = Math.abs(position - maxPos); + + if (minDistance <= maxDistance) { + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } + } + } + }; + + // Function to toggle the year picker and set the active type + const toggleYearPicker = (type: 'min' | 'max') => { + setActivePickerType(type); + setShowYearPicker(!showYearPicker && type === activePickerType ? false : true); + }; + + // Function to select a year + const selectYear = (year: number) => { + if (activePickerType === 'min') { + // Update the min year + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + + // Automatically switch to max year picker + // No delay needed as we're not closing the popup + setActivePickerType('max'); + } else if (activePickerType === 'max') { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + setShowYearPicker(false); + } + }; + + // Calculate the percentage for the range slider thumbs + const minThumbPosition = ((yearRangeFilter.min - minPossibleYear) / (maxPossibleYear - minPossibleYear)) * 100; + const maxThumbPosition = ((yearRangeFilter.max - minPossibleYear) / (maxPossibleYear - minPossibleYear)) * 100; + + return ( +
+ {/* Year display with buttons */} +
+ + to + +
+ + {/* Range Slider */} +
+
+ {/* Filled area between thumbs */} +
+ + + {/* Min thumb */} +
handleThumbMouseDown(e, 'min')} + onTouchStart={(e) => handleThumbTouchStart(e, 'min')} + onClick={(e) => { + e.stopPropagation(); + if (!isDragging) { + toggleYearPicker('min'); + } + }} + > + {/* Drag handle indicators */} +
+
+
+
+
+
+
+ +
+ + {/* Max thumb */} +
handleThumbMouseDown(e, 'max')} + onTouchStart={(e) => handleThumbTouchStart(e, 'max')} + onClick={(e) => { + e.stopPropagation(); + if (!isDragging) { + toggleYearPicker('max'); + } + }} + > + {/* Drag handle indicators */} +
+
+
+
+
+
+
+ +
+
+ + {/* Year labels */} +
+ {minPossibleYear} + {maxPossibleYear} +
+
+ + {/* Year picker popup */} + {showYearPicker && ( +
+
+ {allYears.map(year => ( + + ))} +
+
+ )} + +
+
+ Projects from {yearRangeFilter.min} to {yearRangeFilter.max} will be shown +
+
+
+
+
+ ); + }; + const DomainFilterOptions = ({ filter, isSelected,