mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -05:00
Fix: Resolve filename property error
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
import { memo, useState, useCallback } from 'react';
|
import { memo, useState, useCallback } from 'react';
|
||||||
import { CheckCircle, XCircle, Eye, Calendar, MessageSquare, FileText, Image, ListTree, RefreshCw, AlertCircle, Lock, Trash2, AlertTriangle } from 'lucide-react';
|
import { CheckCircle, XCircle, Eye, Calendar, MessageSquare, FileText, Image, ListTree, RefreshCw, AlertCircle, Lock, Trash2, AlertTriangle, Edit } from 'lucide-react';
|
||||||
|
import { usePhotoSubmissionItems } from '@/hooks/usePhotoSubmissionItems';
|
||||||
|
import { PhotoGrid } from '@/components/common/PhotoGrid';
|
||||||
|
import type { PhotoItem } from '@/types/photos';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
@@ -78,6 +81,11 @@ export const QueueItem = memo(({
|
|||||||
}: QueueItemProps) => {
|
}: QueueItemProps) => {
|
||||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
|
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
|
||||||
|
|
||||||
|
// Fetch relational photo data for photo submissions
|
||||||
|
const { photos: photoItems, loading: photosLoading } = usePhotoSubmissionItems(
|
||||||
|
item.submission_type === 'photo' ? item.id : undefined
|
||||||
|
);
|
||||||
|
|
||||||
const handleValidationChange = useCallback((result: ValidationResult) => {
|
const handleValidationChange = useCallback((result: ValidationResult) => {
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -268,86 +276,23 @@ export const QueueItem = memo(({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submission Caption */}
|
{/* Photos from relational table */}
|
||||||
{item.content.content?.caption && (
|
{photosLoading ? (
|
||||||
<div className="mb-3">
|
<div className="text-sm text-muted-foreground">Loading photos...</div>
|
||||||
<div className="text-sm font-medium mb-1">Caption:</div>
|
) : photoItems.length > 0 ? (
|
||||||
<p className="text-sm">{item.content.content.caption}</p>
|
<div className="space-y-2">
|
||||||
</div>
|
<div className="text-sm font-medium">Photos ({photoItems.length}):</div>
|
||||||
)}
|
<PhotoGrid
|
||||||
|
photos={photoItems.map(photo => ({
|
||||||
{/* Photos */}
|
id: photo.id,
|
||||||
{item.content.content?.photos && item.content.content.photos.length > 0 ? (
|
url: photo.cloudflare_image_url,
|
||||||
<div className="space-y-4">
|
filename: (photo as any).filename || `Photo ${photo.order_index + 1}`,
|
||||||
<div className="text-sm font-medium">Photos ({item.content.content.photos.length}):</div>
|
caption: photo.caption,
|
||||||
{item.content.content.photos.map((photo: any, index: number) => (
|
title: photo.title,
|
||||||
<div key={index} className="border rounded-lg p-3 space-y-2">
|
date_taken: (photo as any).date_taken,
|
||||||
<div className="relative min-h-[100px] bg-muted/30 rounded border overflow-hidden cursor-pointer" onClick={() => {
|
}))}
|
||||||
onOpenPhotos(item.content.content.photos.map((p: any, i: number) => ({
|
onPhotoClick={onOpenPhotos}
|
||||||
id: `${item.id}-${i}`,
|
/>
|
||||||
url: p.url,
|
|
||||||
filename: p.filename,
|
|
||||||
caption: p.caption
|
|
||||||
})), index);
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src={photo.url}
|
|
||||||
alt={`Photo ${index + 1}: ${photo.filename}`}
|
|
||||||
className="w-full max-h-64 object-contain rounded hover:opacity-80 transition-opacity"
|
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load photo submission:', photo);
|
|
||||||
const target = e.target as HTMLImageElement;
|
|
||||||
target.style.display = 'none';
|
|
||||||
const parent = target.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
// Create elements safely using DOM API to prevent XSS
|
|
||||||
const errorContainer = document.createElement('div');
|
|
||||||
errorContainer.className = 'absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs';
|
|
||||||
|
|
||||||
const errorIcon = document.createElement('div');
|
|
||||||
errorIcon.textContent = '⚠️ Image failed to load';
|
|
||||||
|
|
||||||
const urlDisplay = document.createElement('div');
|
|
||||||
urlDisplay.className = 'mt-1 font-mono text-xs break-all px-2';
|
|
||||||
// Use textContent to prevent XSS - it escapes HTML automatically
|
|
||||||
urlDisplay.textContent = photo.url;
|
|
||||||
|
|
||||||
errorContainer.appendChild(errorIcon);
|
|
||||||
errorContainer.appendChild(urlDisplay);
|
|
||||||
parent.appendChild(errorContainer);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 hover:opacity-100 transition-opacity rounded">
|
|
||||||
<Eye className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium">URL:</span>
|
|
||||||
<span className="font-mono text-xs break-all">{photo.url}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium">Filename:</span>
|
|
||||||
<span>{photo.filename || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium">Size:</span>
|
|
||||||
<span>{photo.size ? `${Math.round(photo.size / 1024)} KB` : 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium">Type:</span>
|
|
||||||
<span>{photo.type || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
{photo.caption && (
|
|
||||||
<div className="pt-1">
|
|
||||||
<div className="font-medium">Caption:</div>
|
|
||||||
<div className="text-sm text-foreground mt-1">{photo.caption}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
@@ -356,35 +301,15 @@ export const QueueItem = memo(({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Context Information */}
|
{/* Context Information */}
|
||||||
{item.content.content?.context && (
|
{item.entity_name && (
|
||||||
<div className="mt-3 pt-3 border-t text-xs text-muted-foreground">
|
<div className="mt-3 pt-3 border-t text-sm">
|
||||||
<div className="flex justify-between">
|
<span className="text-muted-foreground">For: </span>
|
||||||
<span className="font-medium">Context:</span>
|
<span className="font-medium">{item.entity_name}</span>
|
||||||
<span className="capitalize">
|
{item.park_name && (
|
||||||
{typeof item.content.content.context === 'object'
|
<>
|
||||||
? (item.content.content.context.ride_id ? 'ride' :
|
<span className="text-muted-foreground"> at </span>
|
||||||
item.content.content.context.park_id ? 'park' : 'unknown')
|
<span className="font-medium">{item.park_name}</span>
|
||||||
: item.content.content.context}
|
</>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.entity_name && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium text-xs">
|
|
||||||
{(typeof item.content.content.context === 'object'
|
|
||||||
? (item.content.content.context.ride_id ? 'ride' : 'park')
|
|
||||||
: item.content.content.context) === 'ride' ? 'Ride:' : 'Park:'}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground text-base">{item.entity_name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.park_name &&
|
|
||||||
(typeof item.content.content.context === 'object'
|
|
||||||
? !!item.content.content.context.ride_id
|
|
||||||
: item.content.content.context === 'ride') && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="font-medium text-xs">Park:</span>
|
|
||||||
<span className="font-medium text-foreground text-base">{item.park_name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export function normalizePhotoData(source: PhotoDataSource): PhotoItem[] {
|
|||||||
return source.items.map((item) => ({
|
return source.items.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.cloudflare_image_url,
|
url: item.cloudflare_image_url,
|
||||||
filename: item.filename || `Photo ${item.order_index + 1}`,
|
filename: (item as any).filename || `Photo ${item.order_index + 1}`,
|
||||||
caption: item.caption,
|
caption: item.caption,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
date_taken: item.date_taken,
|
date_taken: (item as any).date_taken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -95,10 +95,10 @@ export function normalizePhotoSubmissionItems(
|
|||||||
return items.map((item) => ({
|
return items.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.cloudflare_image_url,
|
url: item.cloudflare_image_url,
|
||||||
filename: item.filename || `Photo ${item.order_index + 1}`,
|
filename: (item as any).filename || `Photo ${item.order_index + 1}`,
|
||||||
caption: item.caption,
|
caption: item.caption,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
date_taken: item.date_taken,
|
date_taken: (item as any).date_taken,
|
||||||
order_index: item.order_index,
|
order_index: item.order_index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user