mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 17:51:12 -05:00
332 lines
10 KiB
TypeScript
332 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';
|
|
|
|
// Helper to format compact values (truncate long strings)
|
|
function formatCompactValue(value: unknown, precision?: 'day' | 'month' | 'year', 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>
|
|
);
|
|
}
|