7.6 KiB
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.
Why This Matters
The Problem with .toISOString()
// ❌ 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: toDateOnly()
// ✅ 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)
Database Schema
All calendar date columns use the DATE type (not TIMESTAMP):
-- 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_attimestamps- 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.
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.
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.
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.
getCurrentDateLocal(); // "2024-01-15"
Validation Functions
isValidDateString(dateString: string): boolean
Validates YYYY-MM-DD format.
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.
isDateInRange('2024-01-01', 1800, 2030); // true
isDateInRange('1799-01-01', 1800, 2030); // false
Form Implementation Pattern
Correct Implementation
import { toDateOnly } from '@/lib/dateUtils';
<FlexibleDateInput
value={watch('opening_date') ? new Date(watch('opening_date')) : undefined}
precision={(watch('opening_date_precision') as DatePrecision) || 'day'}
onChange={(date, precision) => {
setValue('opening_date', date ? toDateOnly(date) : undefined);
setValue('opening_date_precision', precision);
}}
label="Opening Date"
/>
Incorrect Implementation
// ❌ DON'T DO THIS
<FlexibleDateInput
onChange={(date, precision) => {
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 Dates
// ❌ WRONG
date.toISOString().split('T')[0]
// ✅ CORRECT
toDateOnly(date)
Mistake 2: Storing Time Components for Calendar Dates
// ❌ 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 3: Not Handling Precision
// ❌ WRONG - Always shows full date
<span>{format(new Date(park.opening_date), 'PPP')}</span>
// ✅ CORRECT - Respects precision
<FlexibleDateDisplay
date={park.opening_date}
precision={park.opening_date_precision}
/>
Testing Checklist
When implementing date handling, test these scenarios:
Timezone Edge Cases
- 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
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
-
Import the utility:
import { toDateOnly } from '@/lib/dateUtils'; -
Replace
.toISOString().split('T')[0]:// Before setValue('date', date ? date.toISOString().split('T')[0] : undefined); // After setValue('date', date ? toDateOnly(date) : undefined); -
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.tsxsrc/components/admin/RideForm.tsxsrc/components/admin/ManufacturerForm.tsxsrc/components/reviews/ReviewForm.tsx
- UI Components:
src/components/ui/flexible-date-input.tsxsrc/components/ui/flexible-date-display.tsx
- Validation:
src/lib/entityValidationSchemas.ts - Server Validation:
supabase/functions/process-selective-approval/validation.ts
Support
If you encounter date-related bugs:
- Check the browser console for the actual date values being submitted
- Verify the database column type is
DATE(notTIMESTAMP) - Ensure
toDateOnly()is being used instead of.toISOString().split('T')[0] - Test in different timezones (use browser DevTools to simulate)