mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:31:13 -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').
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import { Badge } from '@/components/ui/badge';
|
|
import { formatFieldName, formatFieldValue } from '@/lib/submissionChangeDetection';
|
|
import type { FieldChange, ImageChange } from '@/lib/submissionChangeDetection';
|
|
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?: DatePrecision, maxLength = 30): string {
|
|
const formatted = formatFieldValue(value, precision);
|
|
if (formatted.length > maxLength) {
|
|
return formatted.substring(0, maxLength) + '...';
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
interface FieldDiffProps {
|
|
change: FieldChange;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function FieldDiff({ change, compact = false }: FieldDiffProps) {
|
|
const { field, oldValue, newValue, changeType, metadata } = change;
|
|
|
|
// Extract precision for date fields
|
|
const precision = metadata?.precision;
|
|
const oldPrecision = metadata?.oldPrecision;
|
|
const newPrecision = metadata?.newPrecision;
|
|
|
|
// Check if this is an array field that needs special handling
|
|
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
return (
|
|
<ArrayFieldDiff
|
|
fieldName={formatFieldName(field)}
|
|
oldArray={oldValue}
|
|
newArray={newValue}
|
|
compact={compact}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Check if this is a special field type that needs custom rendering
|
|
const specialDisplay = SpecialFieldDisplay({ change, compact });
|
|
if (specialDisplay) {
|
|
return specialDisplay;
|
|
}
|
|
|
|
const getChangeColor = () => {
|
|
switch (changeType) {
|
|
case 'added': return 'text-green-600 dark:text-green-400';
|
|
case 'removed': return 'text-red-600 dark:text-red-400';
|
|
case 'modified': return 'text-amber-600 dark:text-amber-400';
|
|
default: return '';
|
|
}
|
|
};
|
|
|
|
if (compact) {
|
|
const fieldName = formatFieldName(field);
|
|
|
|
if (changeType === 'added') {
|
|
return (
|
|
<Badge variant="outline" className={getChangeColor()}>
|
|
{fieldName}: + {formatCompactValue(newValue, precision)}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
if (changeType === 'removed') {
|
|
return (
|
|
<Badge variant="outline" className={getChangeColor()}>
|
|
{fieldName}: <span className="line-through">{formatCompactValue(oldValue, precision)}</span>
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
if (changeType === 'modified') {
|
|
return (
|
|
<Badge variant="outline" className={getChangeColor()}>
|
|
{fieldName}: {formatCompactValue(oldValue, oldPrecision || precision)} → {formatCompactValue(newValue, newPrecision || precision)}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Badge variant="outline" className={getChangeColor()}>
|
|
{fieldName}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1 p-2 rounded-md bg-muted/50">
|
|
<div className="text-sm font-medium">{formatFieldName(field)}</div>
|
|
|
|
{changeType === 'added' && (
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
+ {formatFieldValue(newValue, precision)}
|
|
</div>
|
|
)}
|
|
|
|
{changeType === 'removed' && (
|
|
<div className="text-sm text-red-600 dark:text-red-400 line-through">
|
|
{formatFieldValue(oldValue, precision)}
|
|
</div>
|
|
)}
|
|
|
|
{changeType === 'modified' && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="text-red-600 dark:text-red-400 line-through">
|
|
{formatFieldValue(oldValue, oldPrecision || precision)}
|
|
</span>
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-green-600 dark:text-green-400">
|
|
{formatFieldValue(newValue, newPrecision || precision)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ImageDiffProps {
|
|
change: ImageChange;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function ImageDiff({ change, compact = false }: ImageDiffProps) {
|
|
const { type, oldUrl, newUrl } = change;
|
|
|
|
if (compact) {
|
|
const imageLabel = type === 'banner' ? 'Banner' : 'Card';
|
|
const isAddition = !oldUrl && newUrl;
|
|
const isRemoval = oldUrl && !newUrl;
|
|
const isReplacement = oldUrl && newUrl;
|
|
|
|
let action = '';
|
|
let colorClass = 'text-blue-600 dark:text-blue-400';
|
|
|
|
if (isAddition) {
|
|
action = ' (Added)';
|
|
colorClass = 'text-green-600 dark:text-green-400';
|
|
} else if (isRemoval) {
|
|
action = ' (Removed)';
|
|
colorClass = 'text-red-600 dark:text-red-400';
|
|
} else if (isReplacement) {
|
|
action = ' (Changed)';
|
|
colorClass = 'text-amber-600 dark:text-amber-400';
|
|
}
|
|
|
|
return (
|
|
<Badge variant="outline" className={colorClass}>
|
|
{imageLabel} Image{action}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// Determine scenario
|
|
const isAddition = !oldUrl && newUrl;
|
|
const isRemoval = oldUrl && !newUrl;
|
|
const isReplacement = oldUrl && newUrl;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/50">
|
|
<div className="text-sm font-medium">
|
|
{type === 'banner' ? 'Banner' : 'Card'} Image
|
|
{isAddition && <span className="text-green-600 dark:text-green-400 ml-2">(New)</span>}
|
|
{isRemoval && <span className="text-red-600 dark:text-red-400 ml-2">(Removed)</span>}
|
|
{isReplacement && <span className="text-amber-600 dark:text-amber-400 ml-2">(Changed)</span>}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{oldUrl && (
|
|
<div className="flex-1">
|
|
<div className="text-xs text-muted-foreground mb-1">Before</div>
|
|
<img
|
|
src={oldUrl}
|
|
alt="Previous"
|
|
className="w-full h-32 object-cover rounded border-2 border-red-500/50"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{oldUrl && newUrl && (
|
|
<ArrowRight className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
|
)}
|
|
|
|
{newUrl && (
|
|
<div className="flex-1">
|
|
<div className="text-xs text-muted-foreground mb-1">{isAddition ? 'New Image' : 'After'}</div>
|
|
<img
|
|
src={newUrl}
|
|
alt="New"
|
|
className="w-full h-32 object-cover rounded border-2 border-green-500/50"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface LocationData {
|
|
location_id?: string;
|
|
city?: string;
|
|
state_province?: string;
|
|
country?: string;
|
|
postal_code?: string;
|
|
latitude?: number | string;
|
|
longitude?: number | string;
|
|
}
|
|
|
|
interface LocationDiffProps {
|
|
oldLocation: LocationData | string | null | undefined;
|
|
newLocation: LocationData | string | null | undefined;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function LocationDiff({ oldLocation, newLocation, compact = false }: LocationDiffProps) {
|
|
// Type guards for LocationData
|
|
const isLocationData = (loc: unknown): loc is LocationData => {
|
|
return typeof loc === 'object' && loc !== null && !Array.isArray(loc);
|
|
};
|
|
|
|
// Check if we're creating a new location entity
|
|
const isCreatingNewLocation = isLocationData(oldLocation) &&
|
|
oldLocation.location_id &&
|
|
!oldLocation.city &&
|
|
isLocationData(newLocation) &&
|
|
newLocation.city;
|
|
|
|
const formatLocation = (loc: LocationData | string | null | undefined) => {
|
|
if (!loc) return 'None';
|
|
if (typeof loc === 'string') return loc;
|
|
|
|
// Handle location_id reference
|
|
if (loc.location_id && !loc.city) {
|
|
return `Location ID: ${loc.location_id.substring(0, 8)}...`;
|
|
}
|
|
|
|
if (typeof loc === 'object') {
|
|
const parts: string[] = [];
|
|
if (loc.city) parts.push(String(loc.city));
|
|
if (loc.state_province) parts.push(String(loc.state_province));
|
|
if (loc.country && loc.country !== loc.state_province) parts.push(String(loc.country));
|
|
if (loc.postal_code) parts.push(String(loc.postal_code));
|
|
|
|
let locationStr = parts.join(', ') || 'Unknown';
|
|
|
|
// Add coordinates if available
|
|
if (loc.latitude && loc.longitude) {
|
|
const lat = Number(loc.latitude).toFixed(6);
|
|
const lng = Number(loc.longitude).toFixed(6);
|
|
locationStr += ` (${lat}°, ${lng}°)`;
|
|
}
|
|
|
|
return locationStr;
|
|
}
|
|
return String(loc);
|
|
};
|
|
|
|
// Check if only coordinates changed
|
|
const onlyCoordinatesChanged = isLocationData(oldLocation) &&
|
|
isLocationData(newLocation) &&
|
|
oldLocation.city === newLocation.city &&
|
|
oldLocation.state_province === newLocation.state_province &&
|
|
oldLocation.country === newLocation.country &&
|
|
oldLocation.postal_code === newLocation.postal_code &&
|
|
(Number(oldLocation.latitude) !== Number(newLocation.latitude) ||
|
|
Number(oldLocation.longitude) !== Number(newLocation.longitude));
|
|
|
|
if (compact) {
|
|
const oldLoc = formatLocation(oldLocation);
|
|
const newLoc = formatLocation(newLocation);
|
|
|
|
if (!oldLocation && newLocation) {
|
|
return (
|
|
<Badge variant="outline" className="text-green-600 dark:text-green-400">
|
|
Location: + {newLoc}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
if (oldLocation && !newLocation) {
|
|
return (
|
|
<Badge variant="outline" className="text-red-600 dark:text-red-400">
|
|
Location: <span className="line-through">{oldLoc}</span>
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Badge variant="outline" className="text-amber-600 dark:text-amber-400">
|
|
Location{isCreatingNewLocation && ' (New Entity)'}
|
|
{onlyCoordinatesChanged && ' (GPS Refined)'}
|
|
: {oldLoc} → {newLoc}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1 p-2 rounded-md bg-muted/50">
|
|
<div className="text-sm font-medium">
|
|
Location
|
|
{isCreatingNewLocation && (
|
|
<Badge variant="secondary" className="ml-2">New location entity</Badge>
|
|
)}
|
|
{onlyCoordinatesChanged && (
|
|
<Badge variant="outline" className="ml-2">GPS coordinates refined</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
{oldLocation && (
|
|
<span className="text-red-600 dark:text-red-400 line-through">
|
|
{formatLocation(oldLocation)}
|
|
</span>
|
|
)}
|
|
{oldLocation && newLocation && (
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
)}
|
|
{newLocation && (
|
|
<span className="text-green-600 dark:text-green-400">
|
|
{formatLocation(newLocation)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|