mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-29 05:27:08 -05:00
Batch update all date precision handling to use expanded DatePrecision, replace hardcoded day defaults, and adjust related validation, UI, and helpers. Includes wrapper migration across Phase 1-3 functions, updates to logs, displays, and formatting utilities to align frontend with new precision values ('exact', 'month', 'year', 'decade', 'century', 'approximate').
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
import { useState } from 'react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
|
import { SpeedDisplay } from '@/components/ui/speed-display';
|
|
import { MapPin, ArrowRight, Calendar, ExternalLink } from 'lucide-react';
|
|
import type { FieldChange } from '@/lib/submissionChangeDetection';
|
|
import { formatFieldValue } from '@/lib/submissionChangeDetection';
|
|
|
|
interface SpecialFieldDisplayProps {
|
|
change: FieldChange;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function SpecialFieldDisplay({ change, compact = false }: SpecialFieldDisplayProps) {
|
|
const fieldName = change.field.toLowerCase();
|
|
|
|
// Detect field type
|
|
if (fieldName.includes('speed') || fieldName === 'max_speed_kmh') {
|
|
return <SpeedFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
if (fieldName.includes('height') || fieldName.includes('length') ||
|
|
fieldName === 'max_height_meters' || fieldName === 'length_meters' ||
|
|
fieldName === 'drop_height_meters') {
|
|
return <MeasurementFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
if (fieldName === 'status') {
|
|
return <StatusFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
if (fieldName.includes('date') && !fieldName.includes('updated') && !fieldName.includes('created')) {
|
|
return <DateFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
if (fieldName.includes('_id') && fieldName !== 'id' && fieldName !== 'user_id' && fieldName !== 'entity_id') {
|
|
return <RelationshipFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
if (fieldName === 'latitude' || fieldName === 'longitude') {
|
|
return <CoordinateFieldDisplay change={change} compact={compact} />;
|
|
}
|
|
|
|
// Fallback to null, will be handled by regular FieldDiff
|
|
return null;
|
|
}
|
|
|
|
function SpeedFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
|
|
Speed
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<div className="text-red-600 dark:text-red-400 line-through">
|
|
<SpeedDisplay kmh={change.oldValue} />
|
|
</div>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<div className="text-green-600 dark:text-green-400">
|
|
<SpeedDisplay kmh={change.newValue} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
+ <SpeedDisplay kmh={change.newValue} />
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through">
|
|
<SpeedDisplay kmh={change.oldValue} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MeasurementFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-purple-600 dark:text-purple-400">
|
|
Measurement
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<div className="text-red-600 dark:text-red-400 line-through">
|
|
<MeasurementDisplay value={change.oldValue} type="distance" />
|
|
</div>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<div className="text-green-600 dark:text-green-400">
|
|
<MeasurementDisplay value={change.newValue} type="distance" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
+ <MeasurementDisplay value={change.newValue} type="distance" />
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through">
|
|
<MeasurementDisplay value={change.oldValue} type="distance" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
const getStatusColor = (status: string) => {
|
|
const statusLower = String(status).toLowerCase();
|
|
if (statusLower === 'operating' || statusLower === 'active') return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20';
|
|
if (statusLower === 'closed' || statusLower === 'inactive') return 'bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20';
|
|
if (statusLower === 'under_construction' || statusLower === 'pending') return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20';
|
|
return 'bg-muted/30 text-muted-foreground';
|
|
};
|
|
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-indigo-600 dark:text-indigo-400">
|
|
Status
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3">
|
|
<Badge className={`${getStatusColor(change.oldValue)} line-through w-fit shrink-0`}>
|
|
{formatFieldValue(change.oldValue)}
|
|
</Badge>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<Badge className={`${getStatusColor(change.newValue)} w-fit shrink-0`}>
|
|
{formatFieldValue(change.newValue)}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<Badge className={`${getStatusColor(change.newValue)} w-fit shrink-0`}>
|
|
{formatFieldValue(change.newValue)}
|
|
</Badge>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<Badge className={`${getStatusColor(change.oldValue)} line-through opacity-75 w-fit shrink-0`}>
|
|
{formatFieldValue(change.oldValue)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DateFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
// Extract precision from metadata
|
|
const precision = change.metadata?.precision;
|
|
const oldPrecision = change.metadata?.oldPrecision;
|
|
const newPrecision = change.metadata?.newPrecision;
|
|
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-teal-600 dark:text-teal-400">
|
|
<Calendar className="h-3 w-3 mr-1" />
|
|
Date
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium flex items-center gap-2">
|
|
<Calendar className="h-4 w-4" />
|
|
{formatFieldName(change.field)}
|
|
{precision && (
|
|
<Badge variant="outline" className="text-xs ml-2">
|
|
{precision === 'exact' ? 'Exact Day' :
|
|
precision === 'month' ? 'Month & Year' :
|
|
precision === 'year' ? 'Year Only' :
|
|
precision === 'decade' ? 'Decade' :
|
|
precision === 'century' ? 'Century' :
|
|
precision === 'approximate' ? 'Approximate' :
|
|
'Full Date'}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<span className="text-red-600 dark:text-red-400 line-through">
|
|
{formatFieldValue(change.oldValue, oldPrecision || precision)}
|
|
</span>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-green-600 dark:text-green-400">
|
|
{formatFieldValue(change.newValue, newPrecision || precision)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
+ {formatFieldValue(change.newValue, precision)}
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through">
|
|
{formatFieldValue(change.oldValue, precision)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RelationshipFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
// This would ideally fetch entity names, but for now we show IDs with better formatting
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_id$/, '').replace(/_/g, ' ').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-cyan-600 dark:text-cyan-400">
|
|
{formatFieldName(change.field)}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3 text-sm font-mono">
|
|
<span className="text-red-600 dark:text-red-400 line-through text-xs">
|
|
{String(change.oldValue).slice(0, 8)}...
|
|
</span>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-green-600 dark:text-green-400 text-xs">
|
|
{String(change.newValue).slice(0, 8)}...
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400 font-mono text-xs">
|
|
+ {String(change.newValue).slice(0, 8)}...
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through font-mono text-xs">
|
|
{String(change.oldValue).slice(0, 8)}...
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CoordinateFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
|
|
if (compact) {
|
|
return (
|
|
<Badge variant="outline" className="text-orange-600 dark:text-orange-400">
|
|
<MapPin className="h-3 w-3 mr-1" />
|
|
Coordinates
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const formatFieldName = (name: string) =>
|
|
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.replace(/^./, str => str.toUpperCase());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
|
<div className="text-sm font-medium flex items-center gap-2">
|
|
<MapPin className="h-4 w-4" />
|
|
{formatFieldName(change.field)}
|
|
</div>
|
|
|
|
{change.changeType === 'modified' && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<span className="text-red-600 dark:text-red-400 line-through font-mono">
|
|
{Number(change.oldValue).toFixed(6)}°
|
|
</span>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-green-600 dark:text-green-400 font-mono">
|
|
{Number(change.newValue).toFixed(6)}°
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400 font-mono">
|
|
+ {Number(change.newValue).toFixed(6)}°
|
|
</div>
|
|
)}
|
|
|
|
{change.changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through font-mono">
|
|
{Number(change.oldValue).toFixed(6)}°
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|