fix: resolve date range picker timezone offset bug
All checks were successful
Build and Deploy / build-and-push (push) Successful in 56s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 56s
- Fixed a bug where selecting a date in the native date picker resulted in the previous day being selected due to the browser converting the 'YYYY-MM-DD' string to UTC midnight and then shifting it back to local time (e.g. UTC-3 in Brazil). - Explicitly parsed the date string and constructed the Date object using local time coordinates to ensure visual and data consistency.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Calendar } from 'lucide-react';
|
import { Calendar } from 'lucide-react';
|
||||||
import { DateRange } from '../types';
|
import { DateRange } from '../types';
|
||||||
|
|
||||||
@@ -8,55 +8,96 @@ interface DateRangePickerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange }) => {
|
export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange }) => {
|
||||||
const [startType, setStartType] = useState<'text' | 'date'>('text');
|
const startRef = useRef<HTMLInputElement>(null);
|
||||||
const [endType, setEndType] = useState<'text' | 'date'>('text');
|
const endRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const formatDateForInput = (date: Date) => {
|
const formatDateForInput = (date: Date) => {
|
||||||
return date.toISOString().split('T')[0]; // YYYY-MM-DD for <input type="date">
|
// Format to local YYYY-MM-DD to avoid timezone shifts
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateForDisplay = (date: Date) => {
|
const formatShortDate = (date: Date) => {
|
||||||
return date.toLocaleDateString('pt-BR'); // DD/MM/YYYY for visual text
|
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', timeZone: 'America/Sao_Paulo' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLocalDate = (value: string) => {
|
||||||
|
// Split "YYYY-MM-DD" and create date in local timezone to avoid UTC midnight shift
|
||||||
|
if (!value) return null;
|
||||||
|
const [year, month, day] = value.split('-');
|
||||||
|
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newStart = new Date(e.target.value);
|
const newStart = parseLocalDate(e.target.value);
|
||||||
if (!isNaN(newStart.getTime())) {
|
if (newStart && !isNaN(newStart.getTime())) {
|
||||||
onChange({ ...dateRange, start: newStart });
|
onChange({ ...dateRange, start: newStart });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newEnd = new Date(e.target.value);
|
const newEnd = parseLocalDate(e.target.value);
|
||||||
if (!isNaN(newEnd.getTime())) {
|
if (newEnd && !isNaN(newEnd.getTime())) {
|
||||||
|
// Set to end of day to ensure the query includes the whole day
|
||||||
|
newEnd.setHours(23, 59, 59, 999);
|
||||||
onChange({ ...dateRange, end: newEnd });
|
onChange({ ...dateRange, end: newEnd });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openPicker = (ref: React.RefObject<HTMLInputElement>) => {
|
||||||
|
if (ref.current) {
|
||||||
|
try {
|
||||||
|
if ('showPicker' in HTMLInputElement.prototype) {
|
||||||
|
ref.current.showPicker();
|
||||||
|
} else {
|
||||||
|
ref.current.focus();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ref.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 bg-white dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg shadow-sm hover:border-zinc-300 dark:hover:border-dark-border transition-colors">
|
<div className="flex items-center gap-2 bg-white dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg shadow-sm hover:border-zinc-300 dark:hover:border-dark-border transition-colors">
|
||||||
<Calendar size={16} className="text-zinc-500 dark:text-dark-muted shrink-0" />
|
<Calendar size={16} className="text-zinc-500 dark:text-dark-muted shrink-0" />
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
|
||||||
|
|
||||||
|
{/* Start Date */}
|
||||||
|
<div
|
||||||
|
className="relative cursor-pointer hover:text-brand-yellow transition-colors"
|
||||||
|
onClick={() => openPicker(startRef)}
|
||||||
|
>
|
||||||
|
{formatShortDate(dateRange.start)}
|
||||||
<input
|
<input
|
||||||
type={startType}
|
ref={startRef}
|
||||||
value={startType === 'date' ? formatDateForInput(dateRange.start) : formatDateForDisplay(dateRange.start)}
|
type="date"
|
||||||
onFocus={() => setStartType('date')}
|
value={formatDateForInput(dateRange.start)}
|
||||||
onBlur={() => setStartType('text')}
|
|
||||||
onChange={handleStartChange}
|
onChange={handleStartChange}
|
||||||
lang="pt-BR"
|
className="absolute opacity-0 w-0 h-0 overflow-hidden"
|
||||||
className="bg-transparent text-zinc-700 dark:text-zinc-200 font-medium outline-none cursor-pointer w-24 sm:w-28 md:w-auto"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-zinc-400 dark:text-dark-muted">até</span>
|
</div>
|
||||||
|
|
||||||
|
<span className="text-zinc-400 dark:text-dark-muted font-normal text-xs">até</span>
|
||||||
|
|
||||||
|
{/* End Date */}
|
||||||
|
<div
|
||||||
|
className="relative cursor-pointer hover:text-brand-yellow transition-colors"
|
||||||
|
onClick={() => openPicker(endRef)}
|
||||||
|
>
|
||||||
|
{formatShortDate(dateRange.end)}
|
||||||
<input
|
<input
|
||||||
type={endType}
|
ref={endRef}
|
||||||
value={endType === 'date' ? formatDateForInput(dateRange.end) : formatDateForDisplay(dateRange.end)}
|
type="date"
|
||||||
onFocus={() => setEndType('date')}
|
value={formatDateForInput(dateRange.end)}
|
||||||
onBlur={() => setEndType('text')}
|
|
||||||
onChange={handleEndChange}
|
onChange={handleEndChange}
|
||||||
lang="pt-BR"
|
className="absolute opacity-0 w-0 h-0 overflow-hidden"
|
||||||
className="bg-transparent text-zinc-700 dark:text-zinc-200 font-medium outline-none cursor-pointer w-24 sm:w-28 md:w-auto"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user