Files
thrilltrack-explorer/src/components/moderation/FieldComparison.tsx
gpt-engineer-app[bot] d0c613031e Migrate date precision to exact
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').
2025-11-11 22:05:29 +00:00

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>
);
}