diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx
index d16fc732..5c0bb3ed 100644
--- a/src/components/admin/ManufacturerForm.tsx
+++ b/src/components/admin/ManufacturerForm.tsx
@@ -57,7 +57,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
founded_date: initialData?.founded_date || (initialData?.founded_year ? `${initialData.founded_year}-01-01` : undefined),
- founded_date_precision: initialData?.founded_date_precision || (initialData?.founded_year ? ('year' as const) : ('day' as const)),
+ founded_date_precision: initialData?.founded_date_precision || (initialData?.founded_year ? ('year' as const) : ('exact' as const)),
headquarters_location: initialData?.headquarters_location || '',
source_url: initialData?.source_url || '',
submission_notes: initialData?.submission_notes || '',
diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx
index 3b1c6eb0..60e0a204 100644
--- a/src/components/admin/ParkForm.tsx
+++ b/src/components/admin/ParkForm.tsx
@@ -38,9 +38,9 @@ const parkSchema = z.object({
park_type: z.string().min(1, 'Park type is required'),
status: z.string().min(1, 'Status is required'),
opening_date: z.string().optional().transform(val => val || undefined),
- opening_date_precision: z.enum(['day', 'month', 'year']).optional(),
+ opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional(),
closing_date: z.string().optional().transform(val => val || undefined),
- closing_date_precision: z.enum(['day', 'month', 'year']).optional(),
+ closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional(),
location: z.object({
name: z.string(),
street_address: z.string().optional(),
diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx
index a573bfcf..4317cf02 100644
--- a/src/components/admin/RideForm.tsx
+++ b/src/components/admin/RideForm.tsx
@@ -227,9 +227,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
ride_sub_type: initialData?.ride_sub_type || '',
status: initialData?.status || 'operating' as const, // Store DB value directly
opening_date: initialData?.opening_date || undefined,
- opening_date_precision: initialData?.opening_date_precision || 'day',
+ opening_date_precision: initialData?.opening_date_precision || 'exact',
closing_date: initialData?.closing_date || undefined,
- closing_date_precision: initialData?.closing_date_precision || 'day',
+ closing_date_precision: initialData?.closing_date_precision || 'exact',
// Convert metric values to user's preferred unit for display
height_requirement: initialData?.height_requirement
? convertValueFromMetric(initialData.height_requirement, getDisplayUnit('cm', measurementSystem), 'cm')
diff --git a/src/components/filters/TimeZoneIndependentDateRangePicker.tsx b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx
index 370cafe2..c41f84b6 100644
--- a/src/components/filters/TimeZoneIndependentDateRangePicker.tsx
+++ b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx
@@ -102,11 +102,11 @@ export function TimeZoneIndependentDateRangePicker({
if (!fromDate && !toDate) return null;
if (fromDate && toDate) {
- return `${formatDateDisplay(fromDate, 'day')} - ${formatDateDisplay(toDate, 'day')}`;
+ return `${formatDateDisplay(fromDate, 'exact')} - ${formatDateDisplay(toDate, 'exact')}`;
} else if (fromDate) {
- return `From ${formatDateDisplay(fromDate, 'day')}`;
+ return `From ${formatDateDisplay(fromDate, 'exact')}`;
} else if (toDate) {
- return `Until ${formatDateDisplay(toDate, 'day')}`;
+ return `Until ${formatDateDisplay(toDate, 'exact')}`;
}
return null;
diff --git a/src/components/moderation/FieldComparison.tsx b/src/components/moderation/FieldComparison.tsx
index 34b826e4..fe983960 100644
--- a/src/components/moderation/FieldComparison.tsx
+++ b/src/components/moderation/FieldComparison.tsx
@@ -5,8 +5,10 @@ import { ArrowRight } from 'lucide-react';
import { ArrayFieldDiff } from './ArrayFieldDiff';
import { SpecialFieldDisplay } from './SpecialFieldDisplay';
+import type { DatePrecision } from '@/components/ui/flexible-date-input';
+
// Helper to format compact values (truncate long strings)
-function formatCompactValue(value: unknown, precision?: 'day' | 'month' | 'year', maxLength = 30): string {
+function formatCompactValue(value: unknown, precision?: DatePrecision, maxLength = 30): string {
const formatted = formatFieldValue(value, precision);
if (formatted.length > maxLength) {
return formatted.substring(0, maxLength) + '...';
diff --git a/src/components/moderation/SpecialFieldDisplay.tsx b/src/components/moderation/SpecialFieldDisplay.tsx
index b35cf678..92f65a98 100644
--- a/src/components/moderation/SpecialFieldDisplay.tsx
+++ b/src/components/moderation/SpecialFieldDisplay.tsx
@@ -211,7 +211,13 @@ function DateFieldDisplay({ change, compact }: { change: FieldChange; compact: b
{formatFieldName(change.field)}
{precision && (
- {precision === 'year' ? 'Year Only' : precision === 'month' ? 'Month & Year' : 'Full Date'}
+ {precision === 'exact' ? 'Exact Day' :
+ precision === 'month' ? 'Month & Year' :
+ precision === 'year' ? 'Year Only' :
+ precision === 'decade' ? 'Decade' :
+ precision === 'century' ? 'Century' :
+ precision === 'approximate' ? 'Approximate' :
+ 'Full Date'}
)}
diff --git a/src/components/timeline/TimelineEventEditorDialog.tsx b/src/components/timeline/TimelineEventEditorDialog.tsx
index f5d32eb5..c9d54f29 100644
--- a/src/components/timeline/TimelineEventEditorDialog.tsx
+++ b/src/components/timeline/TimelineEventEditorDialog.tsx
@@ -72,7 +72,7 @@ const timelineEventSchema = z.object({
event_date: z.date({
message: 'Event date is required',
}),
- event_date_precision: z.enum(['day', 'month', 'year']).default('day'),
+ event_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).default('exact'),
title: z.string().min(1, 'Title is required').max(200, 'Title is too long'),
description: z.string().max(1000, 'Description is too long').optional(),
@@ -133,7 +133,7 @@ export function TimelineEventEditorDialog({
} : {
event_type: 'milestone',
event_date: new Date(),
- event_date_precision: 'day',
+ event_date_precision: 'exact',
title: '',
description: '',
},
@@ -319,9 +319,12 @@ export function TimelineEventEditorDialog({
- Exact Day
+ Exact Day
Month Only
Year Only
+ Decade
+ Century
+ Approximate
diff --git a/src/components/ui/flexible-date-display.tsx b/src/components/ui/flexible-date-display.tsx
index cebb174c..a46b3939 100644
--- a/src/components/ui/flexible-date-display.tsx
+++ b/src/components/ui/flexible-date-display.tsx
@@ -11,7 +11,7 @@ interface FlexibleDateDisplayProps {
export function FlexibleDateDisplay({
date,
- precision = 'day',
+ precision = 'exact',
fallback = 'Unknown',
className
}: FlexibleDateDisplayProps) {
@@ -36,7 +36,16 @@ export function FlexibleDateDisplay({
case 'month':
formatted = format(dateObj, 'MMMM yyyy');
break;
- case 'day':
+ case 'decade':
+ formatted = `${Math.floor(dateObj.getFullYear() / 10) * 10}s`;
+ break;
+ case 'century':
+ formatted = `${Math.ceil(dateObj.getFullYear() / 100)}th century`;
+ break;
+ case 'approximate':
+ formatted = `circa ${format(dateObj, 'yyyy')}`;
+ break;
+ case 'exact':
default:
formatted = format(dateObj, 'PPP');
break;
diff --git a/src/components/ui/flexible-date-input.tsx b/src/components/ui/flexible-date-input.tsx
index d3e021fb..2c33bfbb 100644
--- a/src/components/ui/flexible-date-input.tsx
+++ b/src/components/ui/flexible-date-input.tsx
@@ -16,7 +16,7 @@ import {
} from "@/components/ui/select";
import { toDateOnly, toDateWithPrecision } from "@/lib/dateUtils";
-export type DatePrecision = 'day' | 'month' | 'year';
+export type DatePrecision = 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate';
interface FlexibleDateInputProps {
value?: Date;
@@ -34,7 +34,7 @@ interface FlexibleDateInputProps {
export function FlexibleDateInput({
value,
- precision = 'day',
+ precision = 'exact',
onChange,
placeholder = "Select date",
disabled = false,
@@ -71,13 +71,16 @@ export function FlexibleDateInput({
let newDate: Date;
switch (newPrecision) {
case 'year':
+ case 'decade':
+ case 'century':
+ case 'approximate':
newDate = new Date(year, 0, 1); // January 1st (local timezone)
setYearValue(year.toString());
break;
case 'month':
newDate = new Date(year, month, 1); // 1st of month (local timezone)
break;
- case 'day':
+ case 'exact':
default:
newDate = value; // Keep existing date
break;
@@ -104,10 +107,13 @@ export function FlexibleDateInput({
const getPlaceholderText = () => {
switch (localPrecision) {
case 'year':
+ case 'decade':
+ case 'century':
+ case 'approximate':
return 'Enter year (e.g., 2005)';
case 'month':
return 'Select month and year';
- case 'day':
+ case 'exact':
default:
return placeholder;
}
@@ -119,10 +125,10 @@ export function FlexibleDateInput({
- {localPrecision === 'day' && (
+ {(localPrecision === 'exact') && (
onChange(date, 'day')}
+ onSelect={(date) => onChange(date, 'exact')}
placeholder={getPlaceholderText()}
disabled={disabled}
disableFuture={disableFuture}
@@ -143,7 +149,7 @@ export function FlexibleDateInput({
/>
)}
- {localPrecision === 'year' && (
+ {(localPrecision === 'year' || localPrecision === 'decade' || localPrecision === 'century' || localPrecision === 'approximate') && (
- Use Full Date
- Use Month/Year
- Use Year Only
+ Exact Day
+ Month & Year
+ Year Only
+ Decade
+ Century
+ Approximate
diff --git a/src/lib/dateUtils.ts b/src/lib/dateUtils.ts
index 2624811b..2874398e 100644
--- a/src/lib/dateUtils.ts
+++ b/src/lib/dateUtils.ts
@@ -72,7 +72,7 @@ export function getCurrentDateLocal(): string {
*/
export function formatDateDisplay(
dateString: string | null | undefined,
- precision: 'day' | 'month' | 'year' = 'day'
+ precision: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate' = 'exact'
): string {
if (!dateString) return '';
@@ -83,7 +83,13 @@ export function formatDateDisplay(
return date.getFullYear().toString();
case 'month':
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
- case 'day':
+ case 'decade':
+ return `${Math.floor(date.getFullYear() / 10) * 10}s`;
+ case 'century':
+ return `${Math.ceil(date.getFullYear() / 100)}th century`;
+ case 'approximate':
+ return `circa ${date.getFullYear()}`;
+ case 'exact':
default:
return date.toLocaleDateString('en-US', {
year: 'numeric',
@@ -182,7 +188,7 @@ export function parseDateForDisplay(date: string | Date): Date {
*/
export function toDateWithPrecision(
date: Date,
- precision: 'day' | 'month' | 'year'
+ precision: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'
): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
@@ -193,7 +199,13 @@ export function toDateWithPrecision(
return `${year}-01-01`;
case 'month':
return `${year}-${String(month).padStart(2, '0')}-01`;
- case 'day':
+ case 'decade':
+ return `${Math.floor(year / 10) * 10}-01-01`;
+ case 'century':
+ return `${Math.floor((year - 1) / 100) * 100 + 1}-01-01`;
+ case 'approximate':
+ return `${year}-01-01`;
+ case 'exact':
default:
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts
index 8f4bcb8a..e5486c81 100644
--- a/src/lib/entityValidationSchemas.ts
+++ b/src/lib/entityValidationSchemas.ts
@@ -51,9 +51,9 @@ export const parkValidationSchema = z.object({
const date = new Date(val);
return date <= new Date();
}, 'Opening date cannot be in the future'),
- opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
+ opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(),
closing_date: z.string().nullish().transform(val => val ?? undefined),
- closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
+ closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(),
location_id: z.string().uuid().optional().nullable(),
location: z.object({
name: z.string(),
@@ -139,9 +139,9 @@ export const rideValidationSchema = z.object({
.optional()
.nullable(),
opening_date: z.string().nullish().transform(val => val ?? undefined),
- opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
+ opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(),
closing_date: z.string().nullish().transform(val => val ?? undefined),
- closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
+ closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(),
height_requirement: z.preprocess(
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
z.number().int().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional()
@@ -322,7 +322,7 @@ export const companyValidationSchema = z.object({
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullish().transform(val => val ?? undefined),
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
founded_date: z.string().nullish().transform(val => val ?? undefined),
- founded_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
+ founded_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(),
founded_year: z.preprocess(
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
z.number().int().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be after ${currentYear}`).optional()
@@ -401,7 +401,7 @@ export const milestoneValidationSchema = z.object({
fiveYearsFromNow.setFullYear(fiveYearsFromNow.getFullYear() + 5);
return date <= fiveYearsFromNow;
}, 'Event date cannot be more than 5 years in the future'),
- event_date_precision: z.enum(['day', 'month', 'year']).optional().default('day'),
+ event_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional().default('exact'),
entity_type: z.string().min(1, 'Entity type is required'),
entity_id: z.string().uuid('Invalid entity ID'),
is_public: z.boolean().optional(),
diff --git a/src/lib/submissionChangeDetection.ts b/src/lib/submissionChangeDetection.ts
index d762e19a..20fb4406 100644
--- a/src/lib/submissionChangeDetection.ts
+++ b/src/lib/submissionChangeDetection.ts
@@ -21,9 +21,9 @@ export interface FieldChange {
changeType: 'added' | 'removed' | 'modified';
metadata?: {
isCreatingNewLocation?: boolean;
- precision?: 'day' | 'month' | 'year';
- oldPrecision?: 'day' | 'month' | 'year';
- newPrecision?: 'day' | 'month' | 'year';
+ precision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate';
+ oldPrecision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate';
+ newPrecision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate';
};
}
@@ -803,7 +803,7 @@ function formatEntityType(entityType: string): string {
/**
* Format field value for display
*/
-export function formatFieldValue(value: any, precision?: 'day' | 'month' | 'year'): string {
+export function formatFieldValue(value: any, precision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'): string {
if (value === null || value === undefined) return 'None';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
@@ -817,9 +817,15 @@ export function formatFieldValue(value: any, precision?: 'day' | 'month' | 'year
return date.getFullYear().toString();
} else if (precision === 'month') {
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
+ } else if (precision === 'decade') {
+ return `${Math.floor(date.getFullYear() / 10) * 10}s`;
+ } else if (precision === 'century') {
+ return `${Math.ceil(date.getFullYear() / 100)}th century`;
+ } else if (precision === 'approximate') {
+ return `circa ${date.getFullYear()}`;
}
- // Default: full date
+ // Default: full date (exact)
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
} catch {
return String(value);
diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx
index 42e1ca6c..b6fe943d 100644
--- a/src/pages/ParkDetail.tsx
+++ b/src/pages/ParkDetail.tsx
@@ -659,9 +659,9 @@ export default function ParkDetail() {
park_type: park?.park_type,
status: park?.status,
opening_date: park?.opening_date ?? undefined,
- opening_date_precision: (park?.opening_date_precision as 'day' | 'month' | 'year') ?? undefined,
+ opening_date_precision: (park?.opening_date_precision as 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate') ?? undefined,
closing_date: park?.closing_date ?? undefined,
- closing_date_precision: (park?.closing_date_precision as 'day' | 'month' | 'year') ?? undefined,
+ closing_date_precision: (park?.closing_date_precision as 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate') ?? undefined,
location_id: park?.location?.id,
location: park?.location ? {
name: park.location.name || '',
diff --git a/src/types/timeline.ts b/src/types/timeline.ts
index 94bad60d..b73692c9 100644
--- a/src/types/timeline.ts
+++ b/src/types/timeline.ts
@@ -22,7 +22,7 @@ export type TimelineEventType =
export type EntityType = 'park' | 'ride' | 'company' | 'ride_model';
-export type DatePrecision = 'day' | 'month' | 'year';
+export type DatePrecision = 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate';
/**
* Timeline event stored in database after approval
diff --git a/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql b/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql
new file mode 100644
index 00000000..ea580fae
--- /dev/null
+++ b/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql
@@ -0,0 +1,99 @@
+-- Phase 3: Migrate 'day' precision to 'exact' across all tables
+-- This fixes the check constraint violations and aligns with the new precision system
+
+-- Parks
+UPDATE parks
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE parks
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Park Submissions
+UPDATE park_submissions
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE park_submissions
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Park Versions
+UPDATE park_versions
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE park_versions
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Rides
+UPDATE rides
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE rides
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Ride Submissions
+UPDATE ride_submissions
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE ride_submissions
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Ride Versions
+UPDATE ride_versions
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE ride_versions
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Companies
+UPDATE companies
+SET founded_date_precision = 'exact'
+WHERE founded_date_precision = 'day';
+
+-- Company Submissions
+UPDATE company_submissions
+SET founded_date_precision = 'exact'
+WHERE founded_date_precision = 'day';
+
+-- Company Versions
+UPDATE company_versions
+SET founded_date_precision = 'exact'
+WHERE founded_date_precision = 'day';
+
+-- Entity Timeline Events
+UPDATE entity_timeline_events
+SET event_date_precision = 'exact'
+WHERE event_date_precision = 'day';
+
+-- Timeline Event Submissions
+UPDATE timeline_event_submissions
+SET event_date_precision = 'exact'
+WHERE event_date_precision = 'day';
+
+-- Historical Parks
+UPDATE historical_parks
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE historical_parks
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
+
+-- Historical Rides
+UPDATE historical_rides
+SET opening_date_precision = 'exact'
+WHERE opening_date_precision = 'day';
+
+UPDATE historical_rides
+SET closing_date_precision = 'exact'
+WHERE closing_date_precision = 'day';
\ No newline at end of file