Files
thrilltrack-explorer/src/components/moderation/SubmissionChangesDisplay.tsx
2025-10-06 18:07:19 +00:00

234 lines
8.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
import { PhotoAdditionPreview, PhotoEditPreview, PhotoDeletionPreview } from './PhotoComparison';
import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection';
import type { SubmissionItemData } from '@/types/submissions';
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
interface SubmissionChangesDisplayProps {
item: SubmissionItemData | SubmissionItemWithDeps | { item_data?: any; original_data?: any; item_type: string };
view?: 'summary' | 'detailed';
showImages?: boolean;
submissionId?: string;
}
// Helper to determine change magnitude
function getChangeMagnitude(totalChanges: number, hasImages: boolean, action: string) {
if (action === 'delete') return { label: 'Deletion', variant: 'destructive' as const, icon: AlertTriangle };
if (action === 'create') return { label: 'New', variant: 'default' as const, icon: Plus };
if (hasImages) return { label: 'Major', variant: 'default' as const, icon: Edit };
if (totalChanges >= 5) return { label: 'Major', variant: 'default' as const, icon: Edit };
if (totalChanges >= 3) return { label: 'Moderate', variant: 'secondary' as const, icon: Edit };
return { label: 'Minor', variant: 'outline' as const, icon: Edit };
}
export function SubmissionChangesDisplay({
item,
view = 'summary',
showImages = true,
submissionId
}: SubmissionChangesDisplayProps) {
const [changes, setChanges] = useState<ChangesSummary | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadChanges = async () => {
setLoading(true);
const detectedChanges = await detectChanges(item, submissionId);
setChanges(detectedChanges);
setLoading(false);
};
loadChanges();
}, [item, submissionId]);
if (loading || !changes) {
return <Skeleton className="h-16 w-full" />;
}
// Get appropriate icon for entity type
const getEntityIcon = () => {
const iconClass = "h-4 w-4";
switch (item.item_type) {
case 'park': return <Building2 className={iconClass} />;
case 'ride': return <Train className={iconClass} />;
case 'ride_model': return <Train className={iconClass} />;
case 'manufacturer':
case 'operator':
case 'property_owner':
case 'designer': return <Building className={iconClass} />;
case 'photo':
case 'photo_edit':
case 'photo_delete': return <ImageIcon className={iconClass} />;
default: return <MapPin className={iconClass} />;
}
};
// Get action badge
const getActionBadge = () => {
switch (changes.action) {
case 'create':
return <Badge className="bg-green-600"><Plus className="h-3 w-3 mr-1" />New</Badge>;
case 'edit':
return <Badge className="bg-amber-600"><Edit className="h-3 w-3 mr-1" />Edit</Badge>;
case 'delete':
return <Badge variant="destructive"><Trash2 className="h-3 w-3 mr-1" />Delete</Badge>;
}
};
const magnitude = getChangeMagnitude(
changes.totalChanges,
changes.imageChanges.length > 0,
changes.action
);
if (view === 'summary') {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 flex-wrap">
{getEntityIcon()}
<span className="font-medium">{changes.entityName}</span>
{getActionBadge()}
{changes.action === 'edit' && (
<Badge variant={magnitude.variant} className="text-xs">
{magnitude.label} Change
</Badge>
)}
</div>
{changes.action === 'edit' && changes.totalChanges > 0 && (
<div className="flex flex-wrap gap-1">
{changes.fieldChanges.slice(0, 5).map((change, idx) => (
<FieldDiff key={idx} change={change} compact />
))}
{changes.imageChanges.map((change, idx) => (
<ImageDiff key={`img-${idx}`} change={change} compact />
))}
{changes.photoChanges.map((change, idx) => {
if (change.type === 'added' && change.photos) {
return <PhotoAdditionPreview key={`photo-${idx}`} photos={change.photos} compact />;
}
if (change.type === 'edited' && change.photo) {
return <PhotoEditPreview key={`photo-${idx}`} photo={change.photo} compact />;
}
if (change.type === 'deleted' && change.photo) {
return <PhotoDeletionPreview key={`photo-${idx}`} photo={change.photo} compact />;
}
return null;
})}
{changes.hasLocationChange && (
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
Location
</Badge>
)}
{changes.totalChanges > 5 && (
<Badge variant="outline">
+{changes.totalChanges - 5} more
</Badge>
)}
</div>
)}
{changes.action === 'create' && item.item_data?.description && (
<div className="text-sm text-muted-foreground line-clamp-2">
{item.item_data.description}
</div>
)}
{changes.action === 'delete' && (
<div className="text-sm text-destructive">
Marked for deletion
</div>
)}
</div>
);
}
// Detailed view
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
{getEntityIcon()}
<h3 className="text-lg font-semibold">{changes.entityName}</h3>
{getActionBadge()}
</div>
{changes.action === 'create' && (
<div className="text-sm text-muted-foreground">
Creating new {item.item_type}
</div>
)}
{changes.action === 'delete' && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
This {item.item_type} will be deleted
</div>
)}
{changes.action === 'edit' && changes.totalChanges > 0 && (
<>
{changes.fieldChanges.length > 0 && (
<div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Field Changes ({changes.fieldChanges.length})</h4>
<div className="grid gap-2">
{changes.fieldChanges.map((change, idx) => (
<FieldDiff key={idx} change={change} />
))}
</div>
</div>
)}
{showImages && changes.imageChanges.length > 0 && (
<div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Image Changes</h4>
<div className="grid gap-2">
{changes.imageChanges.map((change, idx) => (
<ImageDiff key={idx} change={change} />
))}
</div>
</div>
)}
{showImages && changes.photoChanges.length > 0 && (
<div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Photo Changes</h4>
<div className="grid gap-2">
{changes.photoChanges.map((change, idx) => {
if (change.type === 'added' && change.photos) {
return <PhotoAdditionPreview key={idx} photos={change.photos} compact={false} />;
}
if (change.type === 'edited' && change.photo) {
return <PhotoEditPreview key={idx} photo={change.photo} compact={false} />;
}
if (change.type === 'deleted' && change.photo) {
return <PhotoDeletionPreview key={idx} photo={change.photo} compact={false} />;
}
return null;
})}
</div>
</div>
)}
{changes.hasLocationChange && (
<div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Location Change</h4>
<LocationDiff
oldLocation={item.original_data?.location || item.original_data?.location_id}
newLocation={item.item_data?.location || item.item_data?.location_id}
/>
</div>
)}
</>
)}
{changes.action === 'edit' && changes.totalChanges === 0 && (
<div className="text-sm text-muted-foreground">
No changes detected
</div>
)}
</div>
);
}