Compare commits

..

3 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
0f742f36b6 Fix timezone-independent date display 2025-11-02 19:24:54 +00:00
gpt-engineer-app[bot]
4215c8ad52 Fix date parsing bug 2025-11-02 19:18:19 +00:00
gpt-engineer-app[bot]
dae687292b Add documentation for hooks 2025-11-02 19:09:45 +00:00
12 changed files with 1673 additions and 29 deletions

1524
docs/DATABASE_DIRECT_EDIT.md Normal file

File diff suppressed because it is too large Load Diff

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. 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 ## Why This Matters
### The Input Problem: Storing Dates
When storing dates (user input → database), using `.toISOString()` causes timezone shifts.
### The Problem with `.toISOString()` ### The Problem with `.toISOString()`
```typescript ```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! **Result**: User selected Jan 1, but we stored Jan 2!
### The Solution: `toDateOnly()` ### The Solution for Input: `toDateOnly()`
```typescript ```typescript
// ✅ CORRECT - Preserves local date // ✅ 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) 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 ## Database Schema
All calendar date columns use the `DATE` type (not `TIMESTAMP`): 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" 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 ### Validation Functions
#### `isValidDateString(dateString: string): boolean` #### `isValidDateString(dateString: string): boolean`
@@ -179,17 +229,38 @@ Used when exact date is known (e.g., "Opened July 4, 1976")
## Common Mistakes and Fixes ## Common Mistakes and Fixes
### Mistake 1: Using `.toISOString()` for Dates ### Mistake 1: Using `.toISOString()` for Date Input
```typescript ```typescript
// ❌ WRONG // ❌ WRONG - Storing dates
date.toISOString().split('T')[0] date.toISOString().split('T')[0]
// ✅ CORRECT // ✅ CORRECT - Storing dates
toDateOnly(date) 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 ```typescript
// ❌ WRONG - Don't store time for calendar dates // ❌ 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 opening_date DATE // ✅ Correct for calendar date
``` ```
### Mistake 3: Not Handling Precision ### Mistake 4: Not Handling Precision
```typescript ```typescript
// ❌ WRONG - Always shows full date // ❌ WRONG - Always shows full date, uses new Date()
<span>{format(new Date(park.opening_date), 'PPP')}</span> <span>{format(new Date(park.opening_date), 'PPP')}</span>
// ✅ CORRECT - Respects precision // ✅ CORRECT - Respects precision, uses parseDateForDisplay()
<FlexibleDateDisplay <FlexibleDateDisplay
date={park.opening_date} date={park.opening_date}
precision={park.opening_date_precision} precision={park.opening_date_precision}
@@ -218,11 +289,17 @@ opening_date DATE // ✅ Correct for calendar date
When implementing date handling, test these scenarios: 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-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+14 selects January 1, 2024 1:00 AM → Stores as `2024-01-01`
- [ ] User in UTC-8 selects date → No timezone shift occurs - [ ] 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 ### Precision Handling
- [ ] Year precision stores as YYYY-01-01 - [ ] Year precision stores as YYYY-01-01
- [ ] Month precision stores as YYYY-MM-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/admin/ManufacturerForm.tsx`
- `src/components/reviews/ReviewForm.tsx` - `src/components/reviews/ReviewForm.tsx`
- **UI Components**: - **UI Components**:
- `src/components/ui/flexible-date-input.tsx` - `src/components/ui/flexible-date-input.tsx` (Input handling)
- `src/components/ui/flexible-date-display.tsx` - `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` - **Validation**: `src/lib/entityValidationSchemas.ts`
- **Server Validation**: `supabase/functions/process-selective-approval/validation.ts` - **Server Validation**: `supabase/functions/process-selective-approval/validation.ts`

View File

@@ -18,7 +18,7 @@ import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { handleError } from '@/lib/errorHandler'; import { handleError } from '@/lib/errorHandler';
import { toDateOnly } from '@/lib/dateUtils'; import { toDateOnly, parseDateOnly } from '@/lib/dateUtils';
import type { UploadedImage } from '@/types/company'; import type { UploadedImage } from '@/types/company';
// Zod output type (after transformation) // Zod output type (after transformation)
@@ -163,7 +163,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
{/* Additional Details */} {/* Additional Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FlexibleDateInput <FlexibleDateInput
value={watch('founded_date') ? new Date(watch('founded_date')) : undefined} value={watch('founded_date') ? parseDateOnly(watch('founded_date')) : undefined}
precision={(watch('founded_date_precision') as DatePrecision) || 'year'} precision={(watch('founded_date_precision') as DatePrecision) || 'year'}
onChange={(date, precision) => { onChange={(date, precision) => {
setValue('founded_date', date ? toDateOnly(date) : undefined); setValue('founded_date', date ? toDateOnly(date) : undefined);

View File

@@ -18,7 +18,7 @@ import { SlugField } from '@/components/ui/slug-field';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { handleError } from '@/lib/errorHandler'; import { handleError } from '@/lib/errorHandler';
import { MapPin, Save, X, Plus } from 'lucide-react'; import { MapPin, Save, X, Plus } from 'lucide-react';
import { toDateOnly } from '@/lib/dateUtils'; import { toDateOnly, parseDateOnly } from '@/lib/dateUtils';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
@@ -343,7 +343,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
{/* Dates */} {/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FlexibleDateInput <FlexibleDateInput
value={watch('opening_date') ? new Date(watch('opening_date')) : undefined} value={watch('opening_date') ? parseDateOnly(watch('opening_date')) : undefined}
precision={(watch('opening_date_precision') as DatePrecision) || 'day'} precision={(watch('opening_date_precision') as DatePrecision) || 'day'}
onChange={(date, precision) => { onChange={(date, precision) => {
setValue('opening_date', date ? toDateOnly(date) : undefined); setValue('opening_date', date ? toDateOnly(date) : undefined);
@@ -356,7 +356,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
/> />
<FlexibleDateInput <FlexibleDateInput
value={watch('closing_date') ? new Date(watch('closing_date')) : undefined} value={watch('closing_date') ? parseDateOnly(watch('closing_date')) : undefined}
precision={(watch('closing_date_precision') as DatePrecision) || 'day'} precision={(watch('closing_date_precision') as DatePrecision) || 'day'}
onChange={(date, precision) => { onChange={(date, precision) => {
setValue('closing_date', date ? toDateOnly(date) : undefined); setValue('closing_date', date ? toDateOnly(date) : undefined);

View File

@@ -24,7 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { handleError } from '@/lib/errorHandler'; import { handleError } from '@/lib/errorHandler';
import { Plus, Zap, Save, X } from 'lucide-react'; import { Plus, Zap, Save, X } from 'lucide-react';
import { toDateOnly } from '@/lib/dateUtils'; import { toDateOnly, parseDateOnly } from '@/lib/dateUtils';
import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { useUnitPreferences } from '@/hooks/useUnitPreferences';
import { useManufacturers, useRideModels } from '@/hooks/useAutocompleteData'; import { useManufacturers, useRideModels } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
@@ -566,7 +566,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
{/* Dates */} {/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FlexibleDateInput <FlexibleDateInput
value={watch('opening_date') ? new Date(watch('opening_date')) : undefined} value={watch('opening_date') ? parseDateOnly(watch('opening_date')) : undefined}
precision={(watch('opening_date_precision') as DatePrecision) || 'day'} precision={(watch('opening_date_precision') as DatePrecision) || 'day'}
onChange={(date, precision) => { onChange={(date, precision) => {
setValue('opening_date', date ? toDateOnly(date) : undefined); setValue('opening_date', date ? toDateOnly(date) : undefined);
@@ -579,7 +579,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
/> />
<FlexibleDateInput <FlexibleDateInput
value={watch('closing_date') ? new Date(watch('closing_date')) : undefined} value={watch('closing_date') ? parseDateOnly(watch('closing_date')) : undefined}
precision={(watch('closing_date_precision') as DatePrecision) || 'day'} precision={(watch('closing_date_precision') as DatePrecision) || 'day'}
onChange={(date, precision) => { onChange={(date, precision) => {
setValue('closing_date', date ? toDateOnly(date) : undefined); setValue('closing_date', date ? toDateOnly(date) : undefined);

View File

@@ -11,6 +11,7 @@ import { toast } from 'sonner';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage } from '@/lib/errorHandler';
import { UserRideCredit } from '@/types/database'; import { UserRideCredit } from '@/types/database';
import { convertValueFromMetric, getDisplayUnit } from '@/lib/units'; import { convertValueFromMetric, getDisplayUnit } from '@/lib/units';
import { parseDateForDisplay } from '@/lib/dateUtils';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -233,13 +234,14 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
{credit.first_ride_date && ( {credit.first_ride_date && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-3 h-3" /> <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> </div>
)} )}
{credit.last_ride_date && ( {credit.last_ride_date && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-3 h-3" /> <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>
)} )}
</div> </div>
@@ -410,7 +412,7 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
{credit.first_ride_date && ( {credit.first_ride_date && (
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
<Calendar className="w-3 h-3 inline mr-1" /> <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> </div>
)} )}

View File

@@ -14,7 +14,7 @@ import { supabase } from '@/integrations/supabase/client';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { StarRating } from './StarRating'; import { StarRating } from './StarRating';
import { toDateOnly } from '@/lib/dateUtils'; import { toDateOnly, parseDateOnly } from '@/lib/dateUtils';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
const reviewSchema = z.object({ const reviewSchema = z.object({
@@ -176,7 +176,7 @@ export function ReviewForm({
<div className="space-y-2"> <div className="space-y-2">
<Label>Visit Date</Label> <Label>Visit Date</Label>
<DatePicker <DatePicker
date={watch('visit_date') ? new Date(watch('visit_date')) : undefined} date={watch('visit_date') ? parseDateOnly(watch('visit_date')) : undefined}
onSelect={(date) => setValue('visit_date', date ? toDateOnly(date) : undefined)} onSelect={(date) => setValue('visit_date', date ? toDateOnly(date) : undefined)}
placeholder="When did you visit?" placeholder="When did you visit?"
disableFuture={true} disableFuture={true}

View File

@@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { History } from 'lucide-react'; import { History } from 'lucide-react';
import { RideNameHistory } from '@/types/database'; import { RideNameHistory } from '@/types/database';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
interface FormerName { interface FormerName {
name: string; name: string;
@@ -83,7 +84,7 @@ export function FormerNames({ formerNames, nameHistory, currentName }: FormerNam
</div> </div>
)} )}
{former.date_changed && ( {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 && ( {former.reason && (
<div className="italic">{former.reason}</div> <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 { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
import type { TimelineEvent } from '@/types/timeline'; import type { TimelineEvent } from '@/types/timeline';
interface TimelineEventCardProps { interface TimelineEventCardProps {
@@ -14,8 +15,10 @@ interface TimelineEventCardProps {
isPending?: boolean; 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 formatEventDate = (date: string, precision: string = 'day') => {
const dateObj = new Date(date); const dateObj = parseDateForDisplay(date);
switch (precision) { switch (precision) {
case 'year': case 'year':

View File

@@ -1,4 +1,5 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
import type { DatePrecision } from './flexible-date-input'; import type { DatePrecision } from './flexible-date-input';
interface FlexibleDateDisplayProps { interface FlexibleDateDisplayProps {
@@ -18,7 +19,9 @@ export function FlexibleDateDisplay({
return <span className={className || "text-muted-foreground"}>{fallback}</span>; 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 // Check for invalid date
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {

View File

@@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar, MapPin, ArrowRight, Building2 } from 'lucide-react'; import { Calendar, MapPin, ArrowRight, Building2 } from 'lucide-react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
interface HistoricalEntityCardProps { interface HistoricalEntityCardProps {
type: 'park' | 'ride'; type: 'park' | 'ride';
@@ -65,9 +66,10 @@ export function HistoricalEntityCard({ type, entity, onViewDetails }: Historical
<span>Operated:</span> <span>Operated:</span>
</div> </div>
<div className="font-medium"> <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>
</div> </div>

View File

@@ -139,6 +139,33 @@ export function compareDateStrings(date1: string, date2: string): number {
return date1 < date2 ? -1 : 1; 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 * Creates a date string for a specific precision
* Sets the date to the first day of the period for month/year precision * Sets the date to the first day of the period for month/year precision