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

@@ -6,8 +6,14 @@
When a user selects "January 1, 2024," that date should be stored as `2024-01-01` regardless of the user's timezone. We never use UTC conversion for calendar date fields.
**CRITICAL**: When displaying dates from YYYY-MM-DD strings, always use `parseDateOnly()` or `parseDateForDisplay()` to prevent timezone shifts.
## Why This Matters
### The Input Problem: Storing Dates
When storing dates (user input → database), using `.toISOString()` causes timezone shifts.
### The Problem with `.toISOString()`
```typescript
@@ -24,7 +30,7 @@ When a user in UTC-8 (Pacific Time) selects "January 1, 2024" at 11:00 PM local
**Result**: User selected Jan 1, but we stored Jan 2!
### The Solution: `toDateOnly()`
### The Solution for Input: `toDateOnly()`
```typescript
// ✅ CORRECT - Preserves local date
@@ -34,6 +40,33 @@ const date = new Date('2024-01-01T23:00:00-08:00');
toDateOnly(date); // Returns "2024-01-01" ✅ (local timezone preserved)
```
### The Display Problem: Showing Dates
When displaying dates (database → user display), using `new Date("YYYY-MM-DD")` also causes timezone shifts!
```typescript
// ❌ WRONG - Creates timezone shifts
// User in UTC-8 (Pacific Time)
const dateStr = "1972-10-01"; // October 1, 1972 in database
const dateObj = new Date(dateStr); // Interprets as Oct 1 00:00 UTC (Sep 30 16:00 PST)
format(dateObj, "PPP"); // Shows "September 30, 1972" ❌
```
**Why this happens**: `new Date("YYYY-MM-DD")` interprets date-only strings as **UTC midnight**, not local midnight. When formatting in local timezone, users in negative UTC offsets (UTC-8, UTC-5, etc.) see the previous day.
### The Solution for Display: `parseDateForDisplay()`
```typescript
// ✅ CORRECT - Preserves local date for display
import { parseDateForDisplay } from '@/lib/dateUtils';
const dateStr = "1972-10-01"; // October 1, 1972 in database
const dateObj = parseDateForDisplay(dateStr); // Creates Oct 1 00:00 LOCAL time
format(dateObj, "PPP"); // Shows "October 1, 1972" ✅ (correct in all timezones)
```
**Key Rule**: If you're displaying a YYYY-MM-DD string from the database, NEVER use `new Date(dateString)` directly. Always use `parseDateForDisplay()` or `parseDateOnly()`.
## Database Schema
All calendar date columns use the `DATE` type (not `TIMESTAMP`):
@@ -108,6 +141,23 @@ Gets current date as YYYY-MM-DD string in local timezone.
getCurrentDateLocal(); // "2024-01-15"
```
#### `parseDateForDisplay(date: string | Date): Date`
Safely parses a date value for display formatting. Handles both YYYY-MM-DD strings and Date objects.
```typescript
// With YYYY-MM-DD string (uses parseDateOnly internally)
parseDateForDisplay("1972-10-01"); // Date object for Oct 1, 1972 00:00 LOCAL ✅
// With Date object (returns as-is)
parseDateForDisplay(new Date()); // Pass-through
// Then format for display
const dateObj = parseDateForDisplay("1972-10-01");
format(dateObj, "PPP"); // "October 1, 1972" ✅ (correct in all timezones)
```
**When to use**: In any display component that receives dates that might be YYYY-MM-DD strings from the database. This is safer than `parseDateOnly()` because it handles both strings and Date objects.
### Validation Functions
#### `isValidDateString(dateString: string): boolean`
@@ -179,17 +229,38 @@ Used when exact date is known (e.g., "Opened July 4, 1976")
## Common Mistakes and Fixes
### Mistake 1: Using `.toISOString()` for Dates
### Mistake 1: Using `.toISOString()` for Date Input
```typescript
// ❌ WRONG
// ❌ WRONG - Storing dates
date.toISOString().split('T')[0]
// ✅ CORRECT
// ✅ CORRECT - Storing dates
toDateOnly(date)
```
### Mistake 2: Storing Time Components for Calendar Dates
### Mistake 2: Using `new Date()` for Date Display
```typescript
// ❌ WRONG - Displaying dates from database
const dateObj = new Date(dateString); // Interprets as UTC!
format(dateObj, "PPP");
// ✅ CORRECT - Displaying dates from database
const dateObj = parseDateForDisplay(dateString); // Interprets as local!
format(dateObj, "PPP");
```
**Critical Display Components** that were fixed:
- `FlexibleDateDisplay.tsx` - General date display component
- `TimelineEventCard.tsx` - Timeline event dates
- `HistoricalEntityCard.tsx` - Historical entity operating dates
- `FormerNames.tsx` - Ride name change dates
- `RideCreditCard.tsx` - User ride credit dates
All these components now use `parseDateForDisplay()` instead of `new Date()`.
### Mistake 3: Storing Time Components for Calendar Dates
```typescript
// ❌ WRONG - Don't store time for calendar dates
@@ -201,13 +272,13 @@ created_at TIMESTAMP // OK for creation timestamp
opening_date DATE // ✅ Correct for calendar date
```
### Mistake 3: Not Handling Precision
### Mistake 4: Not Handling Precision
```typescript
// ❌ WRONG - Always shows full date
// ❌ WRONG - Always shows full date, uses new Date()
<span>{format(new Date(park.opening_date), 'PPP')}</span>
// ✅ CORRECT - Respects precision
// ✅ CORRECT - Respects precision, uses parseDateForDisplay()
<FlexibleDateDisplay
date={park.opening_date}
precision={park.opening_date_precision}
@@ -218,11 +289,17 @@ opening_date DATE // ✅ Correct for calendar date
When implementing date handling, test these scenarios:
### Timezone Edge Cases
### Timezone Edge Cases (Input)
- [ ] User in UTC-12 selects December 31, 2024 11:00 PM → Stores as `2024-12-31`
- [ ] User in UTC+14 selects January 1, 2024 1:00 AM → Stores as `2024-01-01`
- [ ] User in UTC-8 selects date → No timezone shift occurs
### Timezone Edge Cases (Display)
- [ ] User in UTC-8 views "1972-10-01" → Displays "October 1, 1972" (not September 30)
- [ ] User in UTC-5 views "2024-12-31" → Displays "December 31, 2024" (not December 30)
- [ ] User in UTC+10 views "1985-01-01" → Displays "January 1, 1985" (not January 2)
- [ ] All historical dates (parks, rides, events) show correctly in all timezones
### Precision Handling
- [ ] Year precision stores as YYYY-01-01
- [ ] Month precision stores as YYYY-MM-01
@@ -272,8 +349,13 @@ No database migrations needed - all date columns are already using the `DATE` ty
- `src/components/admin/ManufacturerForm.tsx`
- `src/components/reviews/ReviewForm.tsx`
- **UI Components**:
- `src/components/ui/flexible-date-input.tsx`
- `src/components/ui/flexible-date-display.tsx`
- `src/components/ui/flexible-date-input.tsx` (Input handling)
- `src/components/ui/flexible-date-display.tsx` (Display - uses `parseDateForDisplay()`)
- **Display Components** (All use `parseDateForDisplay()`):
- `src/components/timeline/TimelineEventCard.tsx`
- `src/components/versioning/HistoricalEntityCard.tsx`
- `src/components/rides/FormerNames.tsx`
- `src/components/profile/RideCreditCard.tsx`
- **Validation**: `src/lib/entityValidationSchemas.ts`
- **Server Validation**: `supabase/functions/process-selective-approval/validation.ts`