Files
thrilltrack-explorer/src-old/components/moderation/FieldComparison.tsx

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