diff --git a/src/components/moderation/SubmissionChangesDisplay.tsx b/src/components/moderation/SubmissionChangesDisplay.tsx
new file mode 100644
index 00000000..6a518208
--- /dev/null
+++ b/src/components/moderation/SubmissionChangesDisplay.tsx
@@ -0,0 +1,157 @@
+import { Badge } from '@/components/ui/badge';
+import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
+import { detectChanges } 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 } from 'lucide-react';
+
+interface SubmissionChangesDisplayProps {
+ item: SubmissionItemData | SubmissionItemWithDeps;
+ view?: 'summary' | 'detailed';
+ showImages?: boolean;
+}
+
+export function SubmissionChangesDisplay({
+ item,
+ view = 'summary',
+ showImages = true
+}: SubmissionChangesDisplayProps) {
+ const changes = detectChanges(item);
+
+ // Get appropriate icon for entity type
+ const getEntityIcon = () => {
+ const iconClass = "h-4 w-4";
+ switch (item.item_type) {
+ case 'park': return
;
+ case 'ride': return
;
+ case 'manufacturer':
+ case 'operator':
+ case 'property_owner':
+ case 'designer': return
;
+ case 'photo': return
;
+ default: return
;
+ }
+ };
+
+ // Get action badge
+ const getActionBadge = () => {
+ switch (changes.action) {
+ case 'create':
+ return
New;
+ case 'edit':
+ return
Edit;
+ case 'delete':
+ return
Delete;
+ }
+ };
+
+ if (view === 'summary') {
+ return (
+
+
+ {getEntityIcon()}
+ {changes.entityName}
+ {getActionBadge()}
+
+
+ {changes.action === 'edit' && changes.totalChanges > 0 && (
+
+ {changes.fieldChanges.slice(0, 5).map((change, idx) => (
+
+ ))}
+ {changes.imageChanges.map((change, idx) => (
+
+ ))}
+ {changes.hasLocationChange && (
+
+ Location
+
+ )}
+ {changes.totalChanges > 5 && (
+
+ +{changes.totalChanges - 5} more
+
+ )}
+
+ )}
+
+ {changes.action === 'create' && (
+
+ New {item.item_type}
+
+ )}
+
+ {changes.action === 'delete' && (
+
+ Marked for deletion
+
+ )}
+
+ );
+ }
+
+ // Detailed view
+ return (
+
+
+ {getEntityIcon()}
+
{changes.entityName}
+ {getActionBadge()}
+
+
+ {changes.action === 'create' && (
+
+ Creating new {item.item_type}
+
+ )}
+
+ {changes.action === 'delete' && (
+
+ This {item.item_type} will be deleted
+
+ )}
+
+ {changes.action === 'edit' && changes.totalChanges > 0 && (
+ <>
+ {changes.fieldChanges.length > 0 && (
+
+
Field Changes ({changes.fieldChanges.length})
+
+ {changes.fieldChanges.map((change, idx) => (
+
+ ))}
+
+
+ )}
+
+ {showImages && changes.imageChanges.length > 0 && (
+
+
Image Changes
+
+ {changes.imageChanges.map((change, idx) => (
+
+ ))}
+
+
+ )}
+
+ {changes.hasLocationChange && (
+
+
Location Change
+
+
+ )}
+ >
+ )}
+
+ {changes.action === 'edit' && changes.totalChanges === 0 && (
+
+ No changes detected
+
+ )}
+
+ );
+}
diff --git a/src/lib/submissionChangeDetection.ts b/src/lib/submissionChangeDetection.ts
new file mode 100644
index 00000000..5b5c9dda
--- /dev/null
+++ b/src/lib/submissionChangeDetection.ts
@@ -0,0 +1,226 @@
+import type { SubmissionItemData } from '@/types/submissions';
+
+export interface FieldChange {
+ field: string;
+ oldValue: any;
+ newValue: any;
+ changeType: 'added' | 'removed' | 'modified';
+}
+
+export interface ImageChange {
+ type: 'banner' | 'card';
+ oldUrl?: string;
+ newUrl?: string;
+ oldId?: string;
+ newId?: string;
+}
+
+export interface ChangesSummary {
+ action: 'create' | 'edit' | 'delete';
+ entityType: string;
+ entityName?: string;
+ fieldChanges: FieldChange[];
+ imageChanges: ImageChange[];
+ hasLocationChange: boolean;
+ totalChanges: number;
+}
+
+/**
+ * Detects what changed between original_data and item_data
+ */
+export function detectChanges(item: { item_data?: any; original_data?: any; item_type: string }): ChangesSummary {
+ const itemData = item.item_data || {};
+ const originalData = item.original_data || {};
+
+ // Determine action type
+ const action: 'create' | 'edit' | 'delete' =
+ !originalData || Object.keys(originalData).length === 0 ? 'create' :
+ itemData.deleted ? 'delete' : 'edit';
+
+ const fieldChanges: FieldChange[] = [];
+ const imageChanges: ImageChange[] = [];
+ let hasLocationChange = false;
+
+ if (action === 'create') {
+ // For creates, all fields are "added"
+ Object.entries(itemData).forEach(([key, value]) => {
+ if (shouldTrackField(key) && value !== null && value !== undefined && value !== '') {
+ fieldChanges.push({
+ field: key,
+ oldValue: null,
+ newValue: value,
+ changeType: 'added',
+ });
+ }
+ });
+ } else if (action === 'edit') {
+ // Compare each field
+ const allKeys = new Set([
+ ...Object.keys(itemData),
+ ...Object.keys(originalData)
+ ]);
+
+ allKeys.forEach(key => {
+ if (!shouldTrackField(key)) return;
+
+ const oldValue = originalData[key];
+ const newValue = itemData[key];
+
+ // Handle location changes specially
+ if (key === 'location' || key === 'location_id') {
+ if (!isEqual(oldValue, newValue)) {
+ hasLocationChange = true;
+ fieldChanges.push({
+ field: key,
+ oldValue,
+ newValue,
+ changeType: 'modified',
+ });
+ }
+ return;
+ }
+
+ // Check for changes
+ if (!isEqual(oldValue, newValue)) {
+ if ((oldValue === null || oldValue === undefined || oldValue === '') && newValue) {
+ fieldChanges.push({
+ field: key,
+ oldValue,
+ newValue,
+ changeType: 'added',
+ });
+ } else if ((newValue === null || newValue === undefined || newValue === '') && oldValue) {
+ fieldChanges.push({
+ field: key,
+ oldValue,
+ newValue,
+ changeType: 'removed',
+ });
+ } else {
+ fieldChanges.push({
+ field: key,
+ oldValue,
+ newValue,
+ changeType: 'modified',
+ });
+ }
+ }
+ });
+
+ // Detect image changes
+ detectImageChanges(originalData, itemData, imageChanges);
+ }
+
+ // Get entity name
+ const entityName = itemData.name || originalData?.name || 'Unknown';
+
+ return {
+ action,
+ entityType: item.item_type,
+ entityName,
+ fieldChanges,
+ imageChanges,
+ hasLocationChange,
+ totalChanges: fieldChanges.length + imageChanges.length,
+ };
+}
+
+/**
+ * Determines if a field should be tracked for changes
+ */
+function shouldTrackField(key: string): boolean {
+ const excludedFields = [
+ 'id',
+ 'created_at',
+ 'updated_at',
+ 'slug',
+ 'image_assignments',
+ 'banner_image_url',
+ 'banner_image_id',
+ 'card_image_url',
+ 'card_image_id',
+ ];
+ return !excludedFields.includes(key);
+}
+
+/**
+ * Deep equality check for values
+ */
+function isEqual(a: any, b: any): boolean {
+ if (a === b) return true;
+ if (a == null || b == null) return a === b;
+ if (typeof a !== typeof b) return false;
+
+ if (typeof a === 'object') {
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) return false;
+ return a.every((item, i) => isEqual(item, b[i]));
+ }
+
+ const keysA = Object.keys(a);
+ const keysB = Object.keys(b);
+ if (keysA.length !== keysB.length) return false;
+
+ return keysA.every(key => isEqual(a[key], b[key]));
+ }
+
+ return false;
+}
+
+/**
+ * Detects changes in banner/card images
+ */
+function detectImageChanges(
+ originalData: any,
+ itemData: any,
+ imageChanges: ImageChange[]
+): void {
+ // Check banner image
+ if (originalData.banner_image_id !== itemData.banner_image_id ||
+ originalData.banner_image_url !== itemData.banner_image_url) {
+ imageChanges.push({
+ type: 'banner',
+ oldUrl: originalData.banner_image_url,
+ newUrl: itemData.banner_image_url,
+ oldId: originalData.banner_image_id,
+ newId: itemData.banner_image_id,
+ });
+ }
+
+ // Check card image
+ if (originalData.card_image_id !== itemData.card_image_id ||
+ originalData.card_image_url !== itemData.card_image_url) {
+ imageChanges.push({
+ type: 'card',
+ oldUrl: originalData.card_image_url,
+ newUrl: itemData.card_image_url,
+ oldId: originalData.card_image_id,
+ newId: itemData.card_image_id,
+ });
+ }
+}
+
+/**
+ * Format field name for display
+ */
+export function formatFieldName(field: string): string {
+ return field
+ .replace(/_/g, ' ')
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, str => str.toUpperCase())
+ .trim();
+}
+
+/**
+ * Format field value for display
+ */
+export function formatFieldValue(value: any): string {
+ if (value === null || value === undefined) return 'None';
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
+ if (typeof value === 'object') {
+ if (Array.isArray(value)) return `${value.length} items`;
+ return JSON.stringify(value, null, 2);
+ }
+ if (typeof value === 'number') return value.toLocaleString();
+ return String(value);
+}