diff --git a/src/components/moderation/PhotoComparison.tsx b/src/components/moderation/PhotoComparison.tsx
new file mode 100644
index 00000000..746c8acc
--- /dev/null
+++ b/src/components/moderation/PhotoComparison.tsx
@@ -0,0 +1,159 @@
+import { Badge } from '@/components/ui/badge';
+import { ImageIcon, Trash2, Edit } from 'lucide-react';
+
+interface PhotoAdditionPreviewProps {
+ photos: Array<{
+ url: string;
+ title?: string;
+ caption?: string;
+ }>;
+ compact?: boolean;
+}
+
+export function PhotoAdditionPreview({ photos, compact = false }: PhotoAdditionPreviewProps) {
+ if (compact) {
+ return (
+
+
+ +{photos.length} Photo{photos.length > 1 ? 's' : ''}
+
+ );
+ }
+
+ return (
+
+
+
+ Adding {photos.length} Photo{photos.length > 1 ? 's' : ''}
+
+
+
+ {photos.slice(0, 6).map((photo, idx) => (
+
+

+ {(photo.title || photo.caption) && (
+
+ {photo.title || photo.caption}
+
+ )}
+
+ ))}
+ {photos.length > 6 && (
+
+
+ +{photos.length - 6} more
+
+
+ )}
+
+
+ );
+}
+
+interface PhotoEditPreviewProps {
+ photo: {
+ url: string;
+ oldCaption?: string;
+ newCaption?: string;
+ oldTitle?: string;
+ newTitle?: string;
+ };
+ compact?: boolean;
+}
+
+export function PhotoEditPreview({ photo, compact = false }: PhotoEditPreviewProps) {
+ if (compact) {
+ return (
+
+
+ Photo Edit
+
+ );
+ }
+
+ return (
+
+
+
+ Photo Metadata Edit
+
+
+
+

+
+
+ {photo.oldTitle !== photo.newTitle && (
+
+
Title:
+
{photo.oldTitle || 'None'}
+
{photo.newTitle || 'None'}
+
+ )}
+
+ {photo.oldCaption !== photo.newCaption && (
+
+
Caption:
+
{photo.oldCaption || 'None'}
+
{photo.newCaption || 'None'}
+
+ )}
+
+
+
+ );
+}
+
+interface PhotoDeletionPreviewProps {
+ photo: {
+ url: string;
+ title?: string;
+ caption?: string;
+ };
+ compact?: boolean;
+}
+
+export function PhotoDeletionPreview({ photo, compact = false }: PhotoDeletionPreviewProps) {
+ if (compact) {
+ return (
+
+
+ Delete Photo
+
+ );
+ }
+
+ return (
+
+
+
+ Deleting Photo
+
+
+
+

+
+ {(photo.title || photo.caption) && (
+
+ {photo.title &&
{photo.title}
}
+ {photo.caption &&
{photo.caption}
}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/moderation/SubmissionChangesDisplay.tsx b/src/components/moderation/SubmissionChangesDisplay.tsx
index 6a518208..3cdab6bd 100644
--- a/src/components/moderation/SubmissionChangesDisplay.tsx
+++ b/src/components/moderation/SubmissionChangesDisplay.tsx
@@ -3,7 +3,7 @@ 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';
+import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
interface SubmissionChangesDisplayProps {
item: SubmissionItemData | SubmissionItemWithDeps;
@@ -11,6 +11,16 @@ interface SubmissionChangesDisplayProps {
showImages?: boolean;
}
+// 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',
@@ -45,13 +55,24 @@ export function SubmissionChangesDisplay({
}
};
+ const magnitude = getChangeMagnitude(
+ changes.totalChanges,
+ changes.imageChanges.length > 0,
+ changes.action
+ );
+
if (view === 'summary') {
return (
-
+
{getEntityIcon()}
{changes.entityName}
{getActionBadge()}
+ {changes.action === 'edit' && (
+
+ {magnitude.label} Change
+
+ )}
{changes.action === 'edit' && changes.totalChanges > 0 && (
@@ -75,9 +96,9 @@ export function SubmissionChangesDisplay({
)}
- {changes.action === 'create' && (
-
- New {item.item_type}
+ {changes.action === 'create' && item.item_data?.description && (
+
+ {item.item_data.description}
)}
diff --git a/src/components/moderation/SubmissionItemsList.tsx b/src/components/moderation/SubmissionItemsList.tsx
new file mode 100644
index 00000000..9f63b20f
--- /dev/null
+++ b/src/components/moderation/SubmissionItemsList.tsx
@@ -0,0 +1,112 @@
+import { useState, useEffect } from 'react';
+import { supabase } from '@/integrations/supabase/client';
+import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
+import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay';
+import { Skeleton } from '@/components/ui/skeleton';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { AlertCircle } from 'lucide-react';
+import type { SubmissionItemData } from '@/types/submissions';
+
+interface SubmissionItemsListProps {
+ submissionId: string;
+ view?: 'summary' | 'detailed';
+ showImages?: boolean;
+}
+
+export function SubmissionItemsList({
+ submissionId,
+ view = 'summary',
+ showImages = true
+}: SubmissionItemsListProps) {
+ const [items, setItems] = useState
([]);
+ const [hasPhotos, setHasPhotos] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchSubmissionItems();
+ }, [submissionId]);
+
+ const fetchSubmissionItems = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Fetch submission items
+ const { data: itemsData, error: itemsError } = await supabase
+ .from('submission_items')
+ .select('*')
+ .eq('submission_id', submissionId)
+ .order('order_index');
+
+ if (itemsError) throw itemsError;
+
+ // Check for photo submissions
+ const { data: photoData, error: photoError } = await supabase
+ .from('photo_submissions')
+ .select('id')
+ .eq('submission_id', submissionId)
+ .single();
+
+ if (photoError && photoError.code !== 'PGRST116') {
+ console.warn('Error checking photo submissions:', photoError);
+ }
+
+ setItems((itemsData || []) as SubmissionItemData[]);
+ setHasPhotos(!!photoData);
+ } catch (err) {
+ console.error('Error fetching submission items:', err);
+ setError('Failed to load submission details');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+ {view === 'detailed' && }
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {error}
+
+ );
+ }
+
+ if (items.length === 0 && !hasPhotos) {
+ return (
+
+ No items found for this submission
+
+ );
+ }
+
+ return (
+
+ {/* Show regular submission items */}
+ {items.map((item) => (
+
+
+
+ ))}
+
+ {/* Show photo submission if exists */}
+ {hasPhotos && (
+
+ )}
+
+ );
+}
diff --git a/src/lib/submissionChangeDetection.ts b/src/lib/submissionChangeDetection.ts
index 5b5c9dda..c4e6be93 100644
--- a/src/lib/submissionChangeDetection.ts
+++ b/src/lib/submissionChangeDetection.ts
@@ -15,12 +15,22 @@ export interface ImageChange {
newId?: string;
}
+export interface PhotoChange {
+ type: 'added' | 'edited' | 'deleted';
+ photoUrl: string;
+ title?: string;
+ caption?: string;
+ oldCaption?: string;
+ newCaption?: string;
+}
+
export interface ChangesSummary {
action: 'create' | 'edit' | 'delete';
entityType: string;
entityName?: string;
fieldChanges: FieldChange[];
imageChanges: ImageChange[];
+ photoChanges: PhotoChange[];
hasLocationChange: boolean;
totalChanges: number;
}
@@ -120,8 +130,9 @@ export function detectChanges(item: { item_data?: any; original_data?: any; item
entityName,
fieldChanges,
imageChanges,
+ photoChanges: [], // Will be populated by component with submissionId
hasLocationChange,
- totalChanges: fieldChanges.length + imageChanges.length,
+ totalChanges: fieldChanges.length + imageChanges.length + (hasLocationChange ? 1 : 0)
};
}
@@ -217,10 +228,48 @@ export function formatFieldName(field: string): string {
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);
+
+ // Handle dates
+ if (value instanceof Date || (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value))) {
+ try {
+ const date = new Date(value);
+ return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
+ } catch {
+ return String(value);
+ }
}
+
+ // Handle arrays - show actual items
+ if (Array.isArray(value)) {
+ if (value.length === 0) return 'None';
+ if (value.length <= 3) return value.map(v => String(v)).join(', ');
+ return `${value.slice(0, 3).map(v => String(v)).join(', ')}... +${value.length - 3} more`;
+ }
+
+ // Handle objects - create readable summary
+ if (typeof value === 'object') {
+ // Location object
+ if (value.city || value.state_province || value.country) {
+ const parts = [value.city, value.state_province, value.country].filter(Boolean);
+ return parts.join(', ');
+ }
+
+ // Generic object - show key-value pairs
+ const entries = Object.entries(value).slice(0, 3);
+ if (entries.length === 0) return 'Empty';
+ return entries.map(([k, v]) => `${k}: ${v}`).join(', ');
+ }
+
+ // Handle URLs
+ if (typeof value === 'string' && value.startsWith('http')) {
+ try {
+ const url = new URL(value);
+ return url.hostname + (url.pathname !== '/' ? url.pathname.slice(0, 30) : '');
+ } catch {
+ return value;
+ }
+ }
+
if (typeof value === 'number') return value.toLocaleString();
return String(value);
}