feat: Implement photo submission editing and display enhancements

This commit is contained in:
gpt-engineer-app[bot]
2025-10-15 13:04:12 +00:00
parent 97337ed7a3
commit 0b9a5cc5fb
6 changed files with 323 additions and 19 deletions

View File

@@ -0,0 +1,90 @@
/**
* PhotoGrid Component
* Reusable photo grid display with modal support
*/
import { memo } from 'react';
import { Eye, AlertCircle } from 'lucide-react';
import { useIsMobile } from '@/hooks/use-mobile';
import type { PhotoItem } from '@/types/photos';
import { generatePhotoAlt } from '@/lib/photoHelpers';
interface PhotoGridProps {
photos: PhotoItem[];
onPhotoClick?: (photos: PhotoItem[], index: number) => void;
maxDisplay?: number;
className?: string;
}
export const PhotoGrid = memo(({
photos,
onPhotoClick,
maxDisplay,
className = ''
}: PhotoGridProps) => {
const isMobile = useIsMobile();
const defaultMaxDisplay = isMobile ? 2 : 3;
const maxToShow = maxDisplay ?? defaultMaxDisplay;
const displayPhotos = photos.slice(0, maxToShow);
const remainingCount = Math.max(0, photos.length - maxToShow);
if (photos.length === 0) {
return (
<div className="text-sm text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
No photos available
</div>
);
}
return (
<div className={`grid gap-2 ${isMobile ? 'grid-cols-2' : 'grid-cols-3'} ${className}`}>
{displayPhotos.map((photo, index) => (
<div
key={photo.id}
className="relative cursor-pointer group overflow-hidden rounded-md border bg-muted/30"
onClick={() => onPhotoClick?.(photos, index)}
>
<img
src={photo.url}
alt={generatePhotoAlt(photo)}
className="w-full h-32 object-cover transition-opacity group-hover:opacity-80"
loading="lazy"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const parent = target.parentElement;
if (parent) {
const errorDiv = document.createElement('div');
errorDiv.className = 'absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs p-2';
const icon = document.createElement('div');
icon.textContent = '⚠️';
icon.className = 'text-lg mb-1';
const text = document.createElement('div');
text.textContent = 'Failed to load';
errorDiv.appendChild(icon);
errorDiv.appendChild(text);
parent.appendChild(errorDiv);
}
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 group-hover:opacity-100 transition-opacity">
<Eye className="w-5 h-5" />
</div>
{photo.caption && (
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs p-1 truncate">
{photo.caption}
</div>
)}
</div>
))}
{remainingCount > 0 && (
<div className="flex items-center justify-center bg-muted rounded-md text-sm text-muted-foreground font-medium">
+{remainingCount} more
</div>
)}
</div>
);
});
PhotoGrid.displayName = 'PhotoGrid';

View File

@@ -1,14 +1,14 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useIsMobile } from '@/hooks/use-mobile';
import { PhotoGrid } from '@/components/common/PhotoGrid';
import type { PhotoSubmissionItem } from '@/types/photo-submissions';
import type { PhotoItem } from '@/types/photos';
interface PhotoSubmissionDisplayProps {
submissionId: string;
}
export function PhotoSubmissionDisplay({ submissionId }: PhotoSubmissionDisplayProps) {
const isMobile = useIsMobile();
const [photos, setPhotos] = useState<PhotoSubmissionItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -114,21 +114,15 @@ export function PhotoSubmissionDisplay({ submissionId }: PhotoSubmissionDisplayP
);
}
return (
<div className={`grid gap-2 ${isMobile ? 'grid-cols-2' : 'grid-cols-3'}`}>
{photos.slice(0, isMobile ? 2 : 3).map((photo) => (
<img
key={photo.id}
src={photo.cloudflare_image_url}
alt={photo.title || photo.caption || 'Submitted photo'}
className="w-full h-32 object-cover rounded-md"
/>
))}
{photos.length > (isMobile ? 2 : 3) && (
<div className="text-sm text-muted-foreground flex items-center justify-center bg-muted rounded-md">
+{photos.length - (isMobile ? 2 : 3)} more
</div>
)}
</div>
);
// Convert PhotoSubmissionItem[] to PhotoItem[] for PhotoGrid
const photoItems: PhotoItem[] = photos.map(photo => ({
id: photo.id,
url: photo.cloudflare_image_url,
filename: photo.filename || `Photo ${photo.order_index + 1}`,
caption: photo.caption,
title: photo.title,
date_taken: photo.date_taken,
}));
return <PhotoGrid photos={photoItems} />;
}