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