Fix timezone-independent date display

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 19:24:54 +00:00
parent 4215c8ad52
commit 0f742f36b6
7 changed files with 139 additions and 19 deletions

View File

@@ -11,6 +11,7 @@ import { toast } from 'sonner';
import { getErrorMessage } from '@/lib/errorHandler';
import { UserRideCredit } from '@/types/database';
import { convertValueFromMetric, getDisplayUnit } from '@/lib/units';
import { parseDateForDisplay } from '@/lib/dateUtils';
import {
AlertDialog,
AlertDialogAction,
@@ -233,13 +234,14 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
{credit.first_ride_date && (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>First: {format(new Date(credit.first_ride_date), 'MMM d, yyyy')}</span>
{/* ⚠️ Use parseDateForDisplay to prevent timezone shifts */}
<span>First: {format(parseDateForDisplay(credit.first_ride_date), 'MMM d, yyyy')}</span>
</div>
)}
{credit.last_ride_date && (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>Last: {format(new Date(credit.last_ride_date), 'MMM d, yyyy')}</span>
<span>Last: {format(parseDateForDisplay(credit.last_ride_date), 'MMM d, yyyy')}</span>
</div>
)}
</div>
@@ -410,7 +412,7 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
{credit.first_ride_date && (
<div className="text-xs text-muted-foreground">
<Calendar className="w-3 h-3 inline mr-1" />
{format(new Date(credit.first_ride_date), 'MMM d, yyyy')}
{format(parseDateForDisplay(credit.first_ride_date), 'MMM d, yyyy')}
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { History } from 'lucide-react';
import { RideNameHistory } from '@/types/database';
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
interface FormerName {
name: string;
@@ -83,7 +84,7 @@ export function FormerNames({ formerNames, nameHistory, currentName }: FormerNam
</div>
)}
{former.date_changed && (
<div>Changed: {format(new Date(former.date_changed), 'MMM d, yyyy')}</div>
<div>Changed: {format(parseDateForDisplay(former.date_changed), 'MMM d, yyyy')}</div>
)}
{former.reason && (
<div className="italic">{former.reason}</div>

View File

@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
import type { TimelineEvent } from '@/types/timeline';
interface TimelineEventCardProps {
@@ -14,8 +15,10 @@ interface TimelineEventCardProps {
isPending?: boolean;
}
// ⚠️ IMPORTANT: Use parseDateForDisplay to prevent timezone shifts
// YYYY-MM-DD strings must be interpreted as local dates, not UTC
const formatEventDate = (date: string, precision: string = 'day') => {
const dateObj = new Date(date);
const dateObj = parseDateForDisplay(date);
switch (precision) {
case 'year':

View File

@@ -1,4 +1,5 @@
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
import type { DatePrecision } from './flexible-date-input';
interface FlexibleDateDisplayProps {
@@ -18,7 +19,9 @@ export function FlexibleDateDisplay({
return <span className={className || "text-muted-foreground"}>{fallback}</span>;
}
const dateObj = typeof date === 'string' ? new Date(date) : date;
// ⚠️ IMPORTANT: Use parseDateForDisplay to prevent timezone shifts
// YYYY-MM-DD strings must be interpreted as local dates, not UTC
const dateObj = parseDateForDisplay(date);
// Check for invalid date
if (isNaN(dateObj.getTime())) {

View File

@@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar, MapPin, ArrowRight, Building2 } from 'lucide-react';
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
interface HistoricalEntityCardProps {
type: 'park' | 'ride';
@@ -65,9 +66,10 @@ export function HistoricalEntityCard({ type, entity, onViewDetails }: Historical
<span>Operated:</span>
</div>
<div className="font-medium">
{entity.operated_from && format(new Date(entity.operated_from), 'MMM d, yyyy')}
{/* ⚠️ Use parseDateForDisplay to prevent timezone shifts */}
{entity.operated_from && format(parseDateForDisplay(entity.operated_from), 'MMM d, yyyy')}
{' - '}
{entity.operated_until && format(new Date(entity.operated_until), 'MMM d, yyyy')}
{entity.operated_until && format(parseDateForDisplay(entity.operated_until), 'MMM d, yyyy')}
</div>
</div>

View File

@@ -139,6 +139,33 @@ export function compareDateStrings(date1: string, date2: string): number {
return date1 < date2 ? -1 : 1;
}
/**
* Safely parses a date value (string or Date) for display formatting
* Ensures YYYY-MM-DD strings are interpreted as local dates, not UTC
*
* This prevents timezone bugs where "1972-10-01" would display as
* "September 30, 1972" for users in negative UTC offset timezones.
*
* @param date - Date string (YYYY-MM-DD) or Date object
* @returns Date object in local timezone
*
* @example
* // User in UTC-8 viewing "1972-10-01"
* parseDateForDisplay("1972-10-01"); // Returns Oct 1, 1972 00:00 PST ✅
* // NOT Sep 30, 1972 16:00 PST (what new Date() would create)
*/
export function parseDateForDisplay(date: string | Date): Date {
if (date instanceof Date) {
return date;
}
// If it's a YYYY-MM-DD string, use parseDateOnly for local interpretation
if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
return parseDateOnly(date);
}
// Fallback for other date strings (timestamps, ISO strings, etc.)
return new Date(date);
}
/**
* Creates a date string for a specific precision
* Sets the date to the first day of the period for month/year precision