diff --git a/src/components/filters/TimeZoneIndependentDateRangePicker.tsx b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx
new file mode 100644
index 00000000..370cafe2
--- /dev/null
+++ b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx
@@ -0,0 +1,195 @@
+import { useState, useMemo } from 'react';
+import { Label } from '@/components/ui/label';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Calendar } from '@/components/ui/calendar';
+import { CalendarIcon, X } from 'lucide-react';
+import { toDateOnly, parseDateForDisplay, getCurrentDateLocal, formatDateDisplay } from '@/lib/dateUtils';
+import { cn } from '@/lib/utils';
+import type { DateRange } from 'react-day-picker';
+
+interface TimeZoneIndependentDateRangePickerProps {
+ label?: string;
+ fromDate?: string | null;
+ toDate?: string | null;
+ onFromChange: (date: string | null) => void;
+ onToChange: (date: string | null) => void;
+ fromPlaceholder?: string;
+ toPlaceholder?: string;
+ fromYear?: number;
+ toYear?: number;
+ presets?: Array<{
+ label: string;
+ from?: string;
+ to?: string;
+ }>;
+}
+
+export function TimeZoneIndependentDateRangePicker({
+ label = 'Date Range',
+ fromDate,
+ toDate,
+ onFromChange,
+ onToChange,
+ fromPlaceholder = 'From date',
+ toPlaceholder = 'To date',
+ fromYear = 1800,
+ toYear = new Date().getFullYear(),
+ presets,
+}: TimeZoneIndependentDateRangePickerProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Default presets for ride/park filtering
+ const defaultPresets = useMemo(() => {
+ const currentYear = new Date().getFullYear();
+ return [
+ { label: 'Last Year', from: `${currentYear - 1}-01-01`, to: `${currentYear - 1}-12-31` },
+ { label: 'Last 5 Years', from: `${currentYear - 5}-01-01`, to: getCurrentDateLocal() },
+ { label: 'Last 10 Years', from: `${currentYear - 10}-01-01`, to: getCurrentDateLocal() },
+ { label: '1990s', from: '1990-01-01', to: '1999-12-31' },
+ { label: '2000s', from: '2000-01-01', to: '2009-12-31' },
+ { label: '2010s', from: '2010-01-01', to: '2019-12-31' },
+ { label: '2020s', from: '2020-01-01', to: '2029-12-31' },
+ ];
+ }, []);
+
+ const activePresets = presets || defaultPresets;
+
+ // Convert YYYY-MM-DD strings to Date objects for calendar display
+ const dateRange: DateRange | undefined = useMemo(() => {
+ if (!fromDate && !toDate) return undefined;
+
+ return {
+ from: fromDate ? parseDateForDisplay(fromDate) : undefined,
+ to: toDate ? parseDateForDisplay(toDate) : undefined,
+ };
+ }, [fromDate, toDate]);
+
+ // Handle calendar selection
+ const handleSelect = (range: DateRange | undefined) => {
+ if (range?.from) {
+ const fromString = toDateOnly(range.from);
+ onFromChange(fromString);
+ } else {
+ onFromChange(null);
+ }
+
+ if (range?.to) {
+ const toString = toDateOnly(range.to);
+ onToChange(toString);
+ } else if (!range?.from) {
+ // If from is cleared, clear to as well
+ onToChange(null);
+ }
+ };
+
+ // Handle preset selection
+ const handlePresetSelect = (preset: { from?: string; to?: string }) => {
+ onFromChange(preset.from || null);
+ onToChange(preset.to || null);
+ setIsOpen(false);
+ };
+
+ // Handle clear
+ const handleClear = () => {
+ onFromChange(null);
+ onToChange(null);
+ };
+
+ // Format range for display
+ const formatRange = () => {
+ if (!fromDate && !toDate) return null;
+
+ if (fromDate && toDate) {
+ return `${formatDateDisplay(fromDate, 'day')} - ${formatDateDisplay(toDate, 'day')}`;
+ } else if (fromDate) {
+ return `From ${formatDateDisplay(fromDate, 'day')}`;
+ } else if (toDate) {
+ return `Until ${formatDateDisplay(toDate, 'day')}`;
+ }
+
+ return null;
+ };
+
+ const displayText = formatRange();
+
+ return (
+
+ {label &&
}
+
+
+
+
+
+
+
+ {/* Presets sidebar */}
+
+
Presets
+ {activePresets.map((preset) => (
+
+ ))}
+
+
+ {/* Calendar */}
+
+
+
+
+
+
+
+ {displayText && (
+
+ )}
+
+
+ {displayText && (
+
+ {fromDate && toDate
+ ? `${fromDate} to ${toDate}`
+ : fromDate
+ ? `From ${fromDate}`
+ : toDate
+ ? `Until ${toDate}`
+ : ''}
+
+ )}
+
+ );
+}
diff --git a/src/components/parks/ParkFilters.tsx b/src/components/parks/ParkFilters.tsx
index 63779d4a..aa5616b2 100644
--- a/src/components/parks/ParkFilters.tsx
+++ b/src/components/parks/ParkFilters.tsx
@@ -10,6 +10,7 @@ import { Park } from '@/types/database';
import { FilterState } from '@/pages/Parks';
import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
+import { TimeZoneIndependentDateRangePicker } from '@/components/filters/TimeZoneIndependentDateRangePicker';
import { FilterSection } from '@/components/filters/FilterSection';
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
@@ -128,6 +129,8 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp
maxReviews: maxReviews,
openingYearStart: null,
openingYearEnd: null,
+ openingDateFrom: null,
+ openingDateTo: null,
});
};
@@ -225,6 +228,18 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp
fromPlaceholder="From year"
toPlaceholder="To year"
/>
+
+ onFiltersChange({ ...filters, openingDateFrom: date })}
+ onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date })}
+ fromPlaceholder="From date"
+ toPlaceholder="To date"
+ fromYear={1800}
+ toYear={new Date().getFullYear()}
+ />
diff --git a/src/components/rides/RideFilters.tsx b/src/components/rides/RideFilters.tsx
index d0520453..087fab46 100644
--- a/src/components/rides/RideFilters.tsx
+++ b/src/components/rides/RideFilters.tsx
@@ -8,7 +8,7 @@ import { Separator } from '@/components/ui/separator';
import { RotateCcw } from 'lucide-react';
import { supabase } from '@/lib/supabaseClient';
import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
-import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
+import { TimeZoneIndependentDateRangePicker } from '@/components/filters/TimeZoneIndependentDateRangePicker';
import { FilterSection } from '@/components/filters/FilterSection';
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
@@ -43,8 +43,8 @@ export interface RideFilterState {
maxLength: number;
minInversions: number;
maxInversions: number;
- openingDateFrom: Date | null;
- openingDateTo: Date | null;
+ openingDateFrom: string | null;
+ openingDateTo: string | null;
hasInversions: boolean;
operatingOnly: boolean;
}
@@ -468,14 +468,14 @@ export function RideFilters({ filters, onFiltersChange, rides }: RideFiltersProp
{/* Date Filters */}
- onFiltersChange({ ...filters, openingDateFrom: date || null })}
- onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date || null })}
- fromPlaceholder="From year"
- toPlaceholder="To year"
+ onFromChange={(date) => onFiltersChange({ ...filters, openingDateFrom: date })}
+ onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date })}
+ fromPlaceholder="From date"
+ toPlaceholder="To date"
/>
diff --git a/src/pages/Parks.tsx b/src/pages/Parks.tsx
index bdfc3bc4..4deb601c 100644
--- a/src/pages/Parks.tsx
+++ b/src/pages/Parks.tsx
@@ -59,6 +59,8 @@ export interface FilterState {
maxReviews?: number;
openingYearStart: number | null;
openingYearEnd: number | null;
+ openingDateFrom?: string | null;
+ openingDateTo?: string | null;
}
export interface SortState {
@@ -85,6 +87,8 @@ const initialFilters: FilterState = {
maxReviews: 1000,
openingYearStart: null,
openingYearEnd: null,
+ openingDateFrom: null,
+ openingDateTo: null,
};
const initialSort: SortState = {
@@ -213,13 +217,26 @@ export default function Parks() {
}
}
- // Opening year filter
- if (filters.openingYearStart || filters.openingYearEnd) {
- const openingYear = park.opening_date ? parseInt(park.opening_date.split("-")[0]) : null;
- if (openingYear) {
- if (filters.openingYearStart && openingYear < filters.openingYearStart) return false;
- if (filters.openingYearEnd && openingYear > filters.openingYearEnd) return false;
- } else if (filters.openingYearStart || filters.openingYearEnd) {
+ // Opening date filter (timezone-independent)
+ if (filters.openingDateFrom || filters.openingDateTo || filters.openingYearStart || filters.openingYearEnd) {
+ if (!park.opening_date) {
+ return false;
+ }
+
+ // Full date filtering (if date range is set)
+ if (filters.openingDateFrom && park.opening_date < filters.openingDateFrom) {
+ return false;
+ }
+ if (filters.openingDateTo && park.opening_date > filters.openingDateTo) {
+ return false;
+ }
+
+ // Year-only filtering (for backward compatibility)
+ const openingYear = parseInt(park.opening_date.split("-")[0]);
+ if (filters.openingYearStart && openingYear < filters.openingYearStart) {
+ return false;
+ }
+ if (filters.openingYearEnd && openingYear > filters.openingYearEnd) {
return false;
}
}
diff --git a/src/pages/Rides.tsx b/src/pages/Rides.tsx
index 6e5adc79..0266f410 100644
--- a/src/pages/Rides.tsx
+++ b/src/pages/Rides.tsx
@@ -233,16 +233,17 @@ export default function Rides() {
return false;
}
- // Opening date filter
+ // Opening date filter (timezone-independent string comparison)
if (filters.openingDateFrom || filters.openingDateTo) {
if (!ride.opening_date) {
return false;
}
- const openingDate = new Date(ride.opening_date);
- if (filters.openingDateFrom && openingDate < filters.openingDateFrom) {
+
+ // Direct YYYY-MM-DD string comparison (lexicographically correct)
+ if (filters.openingDateFrom && ride.opening_date < filters.openingDateFrom) {
return false;
}
- if (filters.openingDateTo && openingDate > filters.openingDateTo) {
+ if (filters.openingDateTo && ride.opening_date > filters.openingDateTo) {
return false;
}
}