mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 06:11:15 -05:00
feat: Implement photo submission editing and display enhancements
This commit is contained in:
90
src/components/common/PhotoGrid.tsx
Normal file
90
src/components/common/PhotoGrid.tsx
Normal 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';
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user