# Date Handling Philosophy ## Core Principle **All calendar dates in ThrillWiki are timezone-agnostic and stored as DATE (not TIMESTAMP) in the database.** 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 // ❌ WRONG - Creates timezone shifts const date = new Date('2024-01-01T23:00:00-08:00'); // User in UTC-8 selects Jan 1, 2024 11:00 PM date.toISOString().split('T')[0]; // Returns "2024-01-02" ❌ (shifted to UTC) ``` When a user in UTC-8 (Pacific Time) selects "January 1, 2024" at 11:00 PM local time: - Their local time: `2024-01-01 23:00 PST` - UTC time: `2024-01-02 07:00 UTC` - `.toISOString()` returns: `"2024-01-02T07:00:00.000Z"` - `.split('T')[0]` extracts: `"2024-01-02"` ❌ **Result**: User selected Jan 1, but we stored Jan 2! ### The Solution for Input: `toDateOnly()` ```typescript // ✅ CORRECT - Preserves local date import { toDateOnly } from '@/lib/dateUtils'; 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`): ```sql -- Parks opening_date DATE closing_date DATE -- Rides opening_date DATE closing_date DATE -- Companies founded_date DATE -- Photos date_taken DATE -- Reviews visit_date DATE ``` ## When to Use What ### Use `DATE` and `toDateOnly()` for: - Opening/closing dates (parks, rides) - Founded dates (companies) - Birth dates (user profiles) - Visit dates (reviews) - Photo capture dates - Any calendar date where the specific time doesn't matter ### Use `TIMESTAMP` and `.toISOString()` for: - `created_at` / `updated_at` timestamps - Submission timestamps - Upload timestamps - Any moment in time where the exact second matters ## API Reference ### Core Functions #### `toDateOnly(date: Date): string` Converts a Date object to YYYY-MM-DD string in LOCAL timezone. ```typescript const date = new Date('2024-01-01T23:00:00-08:00'); toDateOnly(date); // "2024-01-01" ``` #### `parseDateOnly(dateString: string): Date` Parses a YYYY-MM-DD string to a Date object at midnight local time. ```typescript parseDateOnly('2024-01-01'); // Date object for Jan 1, 2024 00:00:00 local ``` #### `formatDateDisplay(dateString: string, precision: DatePrecision): string` Formats a date string for display based on precision. ```typescript formatDateDisplay('2024-01-01', 'year'); // "2024" formatDateDisplay('2024-01-01', 'month'); // "January 2024" formatDateDisplay('2024-01-01', 'day'); // "January 1, 2024" ``` #### `getCurrentDateLocal(): string` Gets current date as YYYY-MM-DD string in local timezone. ```typescript 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` Validates YYYY-MM-DD format. ```typescript isValidDateString('2024-01-01'); // true isValidDateString('01/01/2024'); // false ``` #### `isDateInRange(dateString: string, minYear?: number, maxYear?: number): boolean` Checks if a date is within a valid range. ```typescript isDateInRange('2024-01-01', 1800, 2030); // true isDateInRange('1799-01-01', 1800, 2030); // false ``` ## Form Implementation Pattern ### Correct Implementation ```typescript import { toDateOnly } from '@/lib/dateUtils'; { setValue('opening_date', date ? toDateOnly(date) : undefined); setValue('opening_date_precision', precision); }} label="Opening Date" /> ``` ### Incorrect Implementation ```typescript // ❌ DON'T DO THIS { setValue('opening_date', date ? date.toISOString().split('T')[0] : undefined); // ❌ }} /> ``` ## Date Precision Handling ThrillWiki supports three precision levels for historical accuracy: ### Year Precision Used when only the year is known (e.g., "Founded in 1972") - Stored as: `1972-01-01` - Precision field: `year` - Displayed as: `"1972"` ### Month Precision Used when month and year are known (e.g., "Opened June 1985") - Stored as: `1985-06-01` - Precision field: `month` - Displayed as: `"June 1985"` ### Day Precision Used when exact date is known (e.g., "Opened July 4, 1976") - Stored as: `1976-07-04` - Precision field: `day` - Displayed as: `"July 4, 1976"` ## Common Mistakes and Fixes ### Mistake 1: Using `.toISOString()` for Date Input ```typescript // ❌ WRONG - Storing dates date.toISOString().split('T')[0] // ✅ CORRECT - Storing dates toDateOnly(date) ``` ### 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 created_at TIMESTAMP // OK for creation timestamp opening_date TIMESTAMP // ❌ Wrong! Use DATE // ✅ CORRECT created_at TIMESTAMP // OK for creation timestamp opening_date DATE // ✅ Correct for calendar date ``` ### Mistake 4: Not Handling Precision ```typescript // ❌ WRONG - Always shows full date, uses new Date() {format(new Date(park.opening_date), 'PPP')} // ✅ CORRECT - Respects precision, uses parseDateForDisplay() ``` ## Testing Checklist When implementing date handling, test these scenarios: ### 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 - [ ] Day precision stores as YYYY-MM-DD - [ ] Display respects precision setting ### Date Comparisons - [ ] Closing date validation: closing > opening - [ ] Future date restrictions work correctly - [ ] Historical date limits (nothing before 1800) enforced ### Form Persistence - [ ] Edit park with opening_date → Date doesn't shift when re-opening form - [ ] Moderation queue shows correct dates in previews - [ ] Date survives form validation and submission ## Migration Notes ### Updating Existing Code 1. **Import the utility**: ```typescript import { toDateOnly } from '@/lib/dateUtils'; ``` 2. **Replace `.toISOString().split('T')[0]`**: ```typescript // Before setValue('date', date ? date.toISOString().split('T')[0] : undefined); // After setValue('date', date ? toDateOnly(date) : undefined); ``` 3. **Update form components** (See files updated in Phase 2 of implementation) ### Database Schema No database migrations needed - all date columns are already using the `DATE` type. ## Related Files - **Core Utilities**: `src/lib/dateUtils.ts` - **Form Components**: - `src/components/admin/ParkForm.tsx` - `src/components/admin/RideForm.tsx` - `src/components/admin/ManufacturerForm.tsx` - `src/components/reviews/ReviewForm.tsx` - **UI Components**: - `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` ## Support If you encounter date-related bugs: 1. Check the browser console for the actual date values being submitted 2. Verify the database column type is `DATE` (not `TIMESTAMP`) 3. Ensure `toDateOnly()` is being used instead of `.toISOString().split('T')[0]` 4. Test in different timezones (use browser DevTools to simulate)