mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 03:27:04 -05:00
Compare commits
2 Commits
c966b6c5ee
...
80d823a1b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d823a1b9 | ||
|
|
7c35f2932b |
195
src/components/filters/TimeZoneIndependentDateRangePicker.tsx
Normal file
195
src/components/filters/TimeZoneIndependentDateRangePicker.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{label && <Label>{label}</Label>}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'w-full justify-start text-left font-normal',
|
||||||
|
!displayText && 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{displayText || `${fromPlaceholder} - ${toPlaceholder}`}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<div className="flex flex-col sm:flex-row">
|
||||||
|
{/* Presets sidebar */}
|
||||||
|
<div className="border-b sm:border-b-0 sm:border-r border-border p-3 space-y-1">
|
||||||
|
<div className="text-sm font-semibold mb-2 text-muted-foreground">Presets</div>
|
||||||
|
{activePresets.map((preset) => (
|
||||||
|
<Button
|
||||||
|
key={preset.label}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start font-normal"
|
||||||
|
onClick={() => handlePresetSelect(preset)}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="p-3">
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={dateRange}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
numberOfMonths={2}
|
||||||
|
defaultMonth={dateRange?.from || new Date()}
|
||||||
|
fromYear={fromYear}
|
||||||
|
toYear={toYear}
|
||||||
|
className="pointer-events-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{displayText && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="shrink-0"
|
||||||
|
title="Clear date range"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{displayText && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{fromDate && toDate
|
||||||
|
? `${fromDate} to ${toDate}`
|
||||||
|
: fromDate
|
||||||
|
? `From ${fromDate}`
|
||||||
|
: toDate
|
||||||
|
? `Until ${toDate}`
|
||||||
|
: ''}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -134,6 +134,17 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
};
|
};
|
||||||
}, [queueManager, toast]);
|
}, [queueManager, toast]);
|
||||||
|
|
||||||
|
// Auto-dismiss lock restored banner after 10 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (lockRestored && queueManager.queue.currentLock) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLockRestored(false);
|
||||||
|
}, 10000); // Auto-dismiss after 10 seconds
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [lockRestored, queueManager.queue.currentLock]);
|
||||||
|
|
||||||
// Fetch active locks count for superusers
|
// Fetch active locks count for superusers
|
||||||
const isSuperuserValue = isSuperuser();
|
const isSuperuserValue = isSuperuser();
|
||||||
|
|
||||||
@@ -377,15 +388,43 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Lock Restored Alert */}
|
{/* Lock Restored Alert */}
|
||||||
{lockRestored && queueManager.queue.currentLock && (
|
{lockRestored && queueManager.queue.currentLock && (() => {
|
||||||
<Alert className="border-blue-500/50 bg-blue-500/5">
|
// Check if restored submission is in current queue
|
||||||
<Info className="h-4 w-4 text-blue-600" />
|
const restoredSubmissionInQueue = queueManager.items.some(
|
||||||
<AlertTitle>Active Claim Restored</AlertTitle>
|
item => item.id === queueManager.queue.currentLock?.submissionId
|
||||||
<AlertDescription>
|
);
|
||||||
Your previous claim was restored. You still have time to review this submission.
|
|
||||||
</AlertDescription>
|
if (!restoredSubmissionInQueue) return null;
|
||||||
</Alert>
|
|
||||||
)}
|
// Calculate time remaining
|
||||||
|
const timeRemainingMs = queueManager.queue.currentLock.expiresAt.getTime() - Date.now();
|
||||||
|
const timeRemainingSec = Math.max(0, Math.floor(timeRemainingMs / 1000));
|
||||||
|
const isExpiringSoon = timeRemainingSec < 300; // Less than 5 minutes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert className={isExpiringSoon
|
||||||
|
? "border-orange-500/50 bg-orange-500/10"
|
||||||
|
: "border-blue-500/50 bg-blue-500/5"
|
||||||
|
}>
|
||||||
|
<Info className={isExpiringSoon
|
||||||
|
? "h-4 w-4 text-orange-600"
|
||||||
|
: "h-4 w-4 text-blue-600"
|
||||||
|
} />
|
||||||
|
<AlertTitle>
|
||||||
|
{isExpiringSoon
|
||||||
|
? `Lock Expiring Soon (${Math.floor(timeRemainingSec / 60)}m ${timeRemainingSec % 60}s)`
|
||||||
|
: "Active Claim Restored"
|
||||||
|
}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{isExpiringSoon
|
||||||
|
? "Your lock is about to expire. Complete your review or extend the lock."
|
||||||
|
: "Your previous claim was restored. You still have time to review this submission."
|
||||||
|
}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Filter Bar */}
|
{/* Filter Bar */}
|
||||||
<QueueFilters
|
<QueueFilters
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Park } from '@/types/database';
|
|||||||
import { FilterState } from '@/pages/Parks';
|
import { FilterState } from '@/pages/Parks';
|
||||||
import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
|
import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
|
||||||
import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
|
import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
|
||||||
|
import { TimeZoneIndependentDateRangePicker } from '@/components/filters/TimeZoneIndependentDateRangePicker';
|
||||||
import { FilterSection } from '@/components/filters/FilterSection';
|
import { FilterSection } from '@/components/filters/FilterSection';
|
||||||
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
|
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
|
||||||
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
@@ -128,6 +129,8 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp
|
|||||||
maxReviews: maxReviews,
|
maxReviews: maxReviews,
|
||||||
openingYearStart: null,
|
openingYearStart: null,
|
||||||
openingYearEnd: null,
|
openingYearEnd: null,
|
||||||
|
openingDateFrom: null,
|
||||||
|
openingDateTo: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,6 +228,18 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp
|
|||||||
fromPlaceholder="From year"
|
fromPlaceholder="From year"
|
||||||
toPlaceholder="To year"
|
toPlaceholder="To year"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TimeZoneIndependentDateRangePicker
|
||||||
|
label="Opening Date Range (Full Date)"
|
||||||
|
fromDate={filters.openingDateFrom || null}
|
||||||
|
toDate={filters.openingDateTo || null}
|
||||||
|
onFromChange={(date) => onFiltersChange({ ...filters, openingDateFrom: date })}
|
||||||
|
onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date })}
|
||||||
|
fromPlaceholder="From date"
|
||||||
|
toPlaceholder="To date"
|
||||||
|
fromYear={1800}
|
||||||
|
toYear={new Date().getFullYear()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FilterSection>
|
</FilterSection>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { RotateCcw } from 'lucide-react';
|
import { RotateCcw } from 'lucide-react';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
|
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 { FilterSection } from '@/components/filters/FilterSection';
|
||||||
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
|
import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
|
||||||
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
@@ -43,8 +43,8 @@ export interface RideFilterState {
|
|||||||
maxLength: number;
|
maxLength: number;
|
||||||
minInversions: number;
|
minInversions: number;
|
||||||
maxInversions: number;
|
maxInversions: number;
|
||||||
openingDateFrom: Date | null;
|
openingDateFrom: string | null;
|
||||||
openingDateTo: Date | null;
|
openingDateTo: string | null;
|
||||||
hasInversions: boolean;
|
hasInversions: boolean;
|
||||||
operatingOnly: boolean;
|
operatingOnly: boolean;
|
||||||
}
|
}
|
||||||
@@ -468,14 +468,14 @@ export function RideFilters({ filters, onFiltersChange, rides }: RideFiltersProp
|
|||||||
{/* Date Filters */}
|
{/* Date Filters */}
|
||||||
<FilterSection title="Dates">
|
<FilterSection title="Dates">
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<FilterDateRangePicker
|
<TimeZoneIndependentDateRangePicker
|
||||||
label="Opening Date"
|
label="Opening Date Range"
|
||||||
fromDate={filters.openingDateFrom}
|
fromDate={filters.openingDateFrom}
|
||||||
toDate={filters.openingDateTo}
|
toDate={filters.openingDateTo}
|
||||||
onFromChange={(date) => onFiltersChange({ ...filters, openingDateFrom: date || null })}
|
onFromChange={(date) => onFiltersChange({ ...filters, openingDateFrom: date })}
|
||||||
onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date || null })}
|
onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date })}
|
||||||
fromPlaceholder="From year"
|
fromPlaceholder="From date"
|
||||||
toPlaceholder="To year"
|
toPlaceholder="To date"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FilterSection>
|
</FilterSection>
|
||||||
|
|||||||
@@ -187,6 +187,26 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
|
|
||||||
// Only restore if lock hasn't expired (race condition check)
|
// Only restore if lock hasn't expired (race condition check)
|
||||||
if (data.locked_until && expiresAt > new Date()) {
|
if (data.locked_until && expiresAt > new Date()) {
|
||||||
|
const timeRemaining = expiresAt.getTime() - new Date().getTime();
|
||||||
|
const minTimeMs = 60 * 1000; // 60 seconds minimum
|
||||||
|
|
||||||
|
if (timeRemaining < minTimeMs) {
|
||||||
|
// Lock expires too soon - auto-release it
|
||||||
|
logger.info('Lock expired or expiring soon, auto-releasing', {
|
||||||
|
submissionId: data.id,
|
||||||
|
timeRemainingSeconds: Math.floor(timeRemaining / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Release the stale lock
|
||||||
|
await supabase.rpc('release_submission_lock', {
|
||||||
|
submission_id: data.id,
|
||||||
|
moderator_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return; // Don't restore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock has sufficient time - restore it
|
||||||
setCurrentLock({
|
setCurrentLock({
|
||||||
submissionId: data.id,
|
submissionId: data.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
@@ -198,6 +218,7 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
logger.info('Lock state restored from database', {
|
logger.info('Lock state restored from database', {
|
||||||
submissionId: data.id,
|
submissionId: data.id,
|
||||||
expiresAt: expiresAt.toISOString(),
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
timeRemainingSeconds: Math.floor(timeRemaining / 1000),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,6 +420,15 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if trying to claim same submission user already has locked
|
||||||
|
if (currentLock && currentLock.submissionId === submissionId) {
|
||||||
|
toast({
|
||||||
|
title: 'Already Claimed',
|
||||||
|
description: 'You already have this submission claimed. Review it below.',
|
||||||
|
});
|
||||||
|
return true; // Return success, don't re-claim
|
||||||
|
}
|
||||||
|
|
||||||
// Check if user already has an active lock on a different submission
|
// Check if user already has an active lock on a different submission
|
||||||
if (currentLock && currentLock.submissionId !== submissionId) {
|
if (currentLock && currentLock.submissionId !== submissionId) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export interface FilterState {
|
|||||||
maxReviews?: number;
|
maxReviews?: number;
|
||||||
openingYearStart: number | null;
|
openingYearStart: number | null;
|
||||||
openingYearEnd: number | null;
|
openingYearEnd: number | null;
|
||||||
|
openingDateFrom?: string | null;
|
||||||
|
openingDateTo?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SortState {
|
export interface SortState {
|
||||||
@@ -85,6 +87,8 @@ const initialFilters: FilterState = {
|
|||||||
maxReviews: 1000,
|
maxReviews: 1000,
|
||||||
openingYearStart: null,
|
openingYearStart: null,
|
||||||
openingYearEnd: null,
|
openingYearEnd: null,
|
||||||
|
openingDateFrom: null,
|
||||||
|
openingDateTo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialSort: SortState = {
|
const initialSort: SortState = {
|
||||||
@@ -213,13 +217,26 @@ export default function Parks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening year filter
|
// Opening date filter (timezone-independent)
|
||||||
if (filters.openingYearStart || filters.openingYearEnd) {
|
if (filters.openingDateFrom || filters.openingDateTo || filters.openingYearStart || filters.openingYearEnd) {
|
||||||
const openingYear = park.opening_date ? parseInt(park.opening_date.split("-")[0]) : null;
|
if (!park.opening_date) {
|
||||||
if (openingYear) {
|
return false;
|
||||||
if (filters.openingYearStart && openingYear < filters.openingYearStart) return false;
|
}
|
||||||
if (filters.openingYearEnd && openingYear > filters.openingYearEnd) return false;
|
|
||||||
} else if (filters.openingYearStart || filters.openingYearEnd) {
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,16 +233,17 @@ export default function Rides() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening date filter
|
// Opening date filter (timezone-independent string comparison)
|
||||||
if (filters.openingDateFrom || filters.openingDateTo) {
|
if (filters.openingDateFrom || filters.openingDateTo) {
|
||||||
if (!ride.opening_date) {
|
if (!ride.opening_date) {
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (filters.openingDateTo && openingDate > filters.openingDateTo) {
|
if (filters.openingDateTo && ride.opening_date > filters.openingDateTo) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user