mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -05:00
feat: Implement photo change detection
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
|
import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
|
||||||
import { detectChanges } from '@/lib/submissionChangeDetection';
|
import { PhotoAdditionPreview, PhotoEditPreview, PhotoDeletionPreview } from './PhotoComparison';
|
||||||
|
import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection';
|
||||||
import type { SubmissionItemData } from '@/types/submissions';
|
import type { SubmissionItemData } from '@/types/submissions';
|
||||||
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||||
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
|
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
|
||||||
@@ -9,6 +12,7 @@ interface SubmissionChangesDisplayProps {
|
|||||||
item: SubmissionItemData | SubmissionItemWithDeps;
|
item: SubmissionItemData | SubmissionItemWithDeps;
|
||||||
view?: 'summary' | 'detailed';
|
view?: 'summary' | 'detailed';
|
||||||
showImages?: boolean;
|
showImages?: boolean;
|
||||||
|
submissionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to determine change magnitude
|
// Helper to determine change magnitude
|
||||||
@@ -24,9 +28,25 @@ function getChangeMagnitude(totalChanges: number, hasImages: boolean, action: st
|
|||||||
export function SubmissionChangesDisplay({
|
export function SubmissionChangesDisplay({
|
||||||
item,
|
item,
|
||||||
view = 'summary',
|
view = 'summary',
|
||||||
showImages = true
|
showImages = true,
|
||||||
|
submissionId
|
||||||
}: SubmissionChangesDisplayProps) {
|
}: SubmissionChangesDisplayProps) {
|
||||||
const changes = detectChanges(item);
|
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
|
// Get appropriate icon for entity type
|
||||||
const getEntityIcon = () => {
|
const getEntityIcon = () => {
|
||||||
@@ -83,6 +103,18 @@ export function SubmissionChangesDisplay({
|
|||||||
{changes.imageChanges.map((change, idx) => (
|
{changes.imageChanges.map((change, idx) => (
|
||||||
<ImageDiff key={`img-${idx}`} change={change} compact />
|
<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 && (
|
{changes.hasLocationChange && (
|
||||||
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
|
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
|
||||||
Location
|
Location
|
||||||
@@ -156,6 +188,26 @@ export function SubmissionChangesDisplay({
|
|||||||
</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 && (
|
{changes.hasLocationChange && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h4 className="text-sm font-medium">Location Change</h4>
|
<h4 className="text-sm font-medium">Location Change</h4>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export function SubmissionItemsList({
|
|||||||
item={item}
|
item={item}
|
||||||
view={view}
|
view={view}
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { SubmissionItemData } from '@/types/submissions';
|
import type { SubmissionItemData } from '@/types/submissions';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
export interface FieldChange {
|
export interface FieldChange {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -17,11 +18,16 @@ export interface ImageChange {
|
|||||||
|
|
||||||
export interface PhotoChange {
|
export interface PhotoChange {
|
||||||
type: 'added' | 'edited' | 'deleted';
|
type: 'added' | 'edited' | 'deleted';
|
||||||
photoUrl: string;
|
photos?: Array<{ url: string; title?: string; caption?: string }>;
|
||||||
|
photo?: {
|
||||||
|
url: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
oldCaption?: string;
|
oldCaption?: string;
|
||||||
newCaption?: string;
|
newCaption?: string;
|
||||||
|
oldTitle?: string;
|
||||||
|
newTitle?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangesSummary {
|
export interface ChangesSummary {
|
||||||
@@ -35,10 +41,54 @@ export interface ChangesSummary {
|
|||||||
totalChanges: number;
|
totalChanges: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects photo changes for a submission
|
||||||
|
*/
|
||||||
|
async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]> {
|
||||||
|
const changes: PhotoChange[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch photo submission with items
|
||||||
|
const { data: photoSubmission, error } = await supabase
|
||||||
|
.from('photo_submissions')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
items:photo_submission_items(*)
|
||||||
|
`)
|
||||||
|
.eq('submission_id', submissionId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching photo submissions:', error);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photoSubmission?.items && photoSubmission.items.length > 0) {
|
||||||
|
// For now, treat all photos as additions
|
||||||
|
// TODO: Implement edit/delete detection by comparing with existing entity photos
|
||||||
|
changes.push({
|
||||||
|
type: 'added',
|
||||||
|
photos: photoSubmission.items.map((item: any) => ({
|
||||||
|
url: item.cloudflare_image_url,
|
||||||
|
title: item.title,
|
||||||
|
caption: item.caption
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error detecting photo changes:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects what changed between original_data and item_data
|
* Detects what changed between original_data and item_data
|
||||||
*/
|
*/
|
||||||
export function detectChanges(item: { item_data?: any; original_data?: any; item_type: string }): ChangesSummary {
|
export async function detectChanges(
|
||||||
|
item: { item_data?: any; original_data?: any; item_type: string },
|
||||||
|
submissionId?: string
|
||||||
|
): Promise<ChangesSummary> {
|
||||||
const itemData = item.item_data || {};
|
const itemData = item.item_data || {};
|
||||||
const originalData = item.original_data || {};
|
const originalData = item.original_data || {};
|
||||||
|
|
||||||
@@ -124,15 +174,18 @@ export function detectChanges(item: { item_data?: any; original_data?: any; item
|
|||||||
// Get entity name
|
// Get entity name
|
||||||
const entityName = itemData.name || originalData?.name || 'Unknown';
|
const entityName = itemData.name || originalData?.name || 'Unknown';
|
||||||
|
|
||||||
|
// Detect photo changes if submissionId provided
|
||||||
|
const photoChanges = submissionId ? await detectPhotoChanges(submissionId) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
entityType: item.item_type,
|
entityType: item.item_type,
|
||||||
entityName,
|
entityName,
|
||||||
fieldChanges,
|
fieldChanges,
|
||||||
imageChanges,
|
imageChanges,
|
||||||
photoChanges: [], // Will be populated by component with submissionId
|
photoChanges,
|
||||||
hasLocationChange,
|
hasLocationChange,
|
||||||
totalChanges: fieldChanges.length + imageChanges.length + (hasLocationChange ? 1 : 0)
|
totalChanges: fieldChanges.length + imageChanges.length + photoChanges.length + (hasLocationChange ? 1 : 0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user