- First: {format(new Date(credit.first_ride_date), 'MMM d, yyyy')}
+ {/* ⚠️ Use parseDateForDisplay to prevent timezone shifts */}
+ First: {format(parseDateForDisplay(credit.first_ride_date), 'MMM d, yyyy')}
)}
{credit.last_ride_date && (
- Last: {format(new Date(credit.last_ride_date), 'MMM d, yyyy')}
+ Last: {format(parseDateForDisplay(credit.last_ride_date), 'MMM d, yyyy')}
)}
@@ -410,7 +412,7 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
{credit.first_ride_date && (
- {format(new Date(credit.first_ride_date), 'MMM d, yyyy')}
+ {format(parseDateForDisplay(credit.first_ride_date), 'MMM d, yyyy')}
)}
diff --git a/src/components/rides/FormerNames.tsx b/src/components/rides/FormerNames.tsx
index 60f6a1bf..3d112bed 100644
--- a/src/components/rides/FormerNames.tsx
+++ b/src/components/rides/FormerNames.tsx
@@ -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
)}
{former.date_changed && (
- Changed: {format(new Date(former.date_changed), 'MMM d, yyyy')}
+ Changed: {format(parseDateForDisplay(former.date_changed), 'MMM d, yyyy')}
)}
{former.reason && (
{former.reason}
diff --git a/src/components/timeline/TimelineEventCard.tsx b/src/components/timeline/TimelineEventCard.tsx
index e33ca45e..495f2c50 100644
--- a/src/components/timeline/TimelineEventCard.tsx
+++ b/src/components/timeline/TimelineEventCard.tsx
@@ -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':
diff --git a/src/components/ui/flexible-date-display.tsx b/src/components/ui/flexible-date-display.tsx
index 821d1bc6..cebb174c 100644
--- a/src/components/ui/flexible-date-display.tsx
+++ b/src/components/ui/flexible-date-display.tsx
@@ -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 {fallback};
}
- 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())) {
diff --git a/src/components/versioning/HistoricalEntityCard.tsx b/src/components/versioning/HistoricalEntityCard.tsx
index bdb1a84e..1eb7595b 100644
--- a/src/components/versioning/HistoricalEntityCard.tsx
+++ b/src/components/versioning/HistoricalEntityCard.tsx
@@ -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
Operated:
- {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')}
diff --git a/src/lib/dateUtils.ts b/src/lib/dateUtils.ts
index aaddfe35..2624811b 100644
--- a/src/lib/dateUtils.ts
+++ b/src/lib/dateUtils.ts
@@ -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