mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:11:12 -05:00
261 lines
8.8 KiB
TypeScript
261 lines
8.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Camera, Upload, LogIn, Settings, ArrowUpDown } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload';
|
|
import { PhotoManagementDialog } from '@/components/upload/PhotoManagementDialog';
|
|
import { PhotoModal } from '@/components/moderation/PhotoModal';
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
import { EntityPhotoGalleryProps } from '@/types/submissions';
|
|
import { useUserRole } from '@/hooks/useUserRole';
|
|
import { getErrorMessage } from '@/lib/errorHandler';
|
|
|
|
interface Photo {
|
|
id: string;
|
|
url: string;
|
|
caption?: string;
|
|
title?: string;
|
|
user_id: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export function EntityPhotoGallery({
|
|
entityId,
|
|
entityType,
|
|
entityName,
|
|
parentId
|
|
}: EntityPhotoGalleryProps) {
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
const { isModerator } = useUserRole();
|
|
const [photos, setPhotos] = useState<Photo[]>([]);
|
|
const [showUpload, setShowUpload] = useState(false);
|
|
const [showManagement, setShowManagement] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [sortBy, setSortBy] = useState<'newest' | 'oldest'>('newest');
|
|
|
|
useEffect(() => {
|
|
fetchPhotos();
|
|
}, [entityId, entityType, sortBy]);
|
|
|
|
const fetchPhotos = async () => {
|
|
try {
|
|
// Fetch photos directly from the photos table
|
|
const { data: photoData, error } = await supabase
|
|
.from('photos')
|
|
.select('id, cloudflare_image_url, title, caption, submitted_by, created_at, order_index')
|
|
.eq('entity_type', entityType)
|
|
.eq('entity_id', entityId)
|
|
.order('created_at', { ascending: sortBy === 'oldest' });
|
|
|
|
if (error) throw error;
|
|
|
|
// Map to Photo interface
|
|
const mappedPhotos: Photo[] = photoData?.map((photo) => ({
|
|
id: photo.id,
|
|
url: photo.cloudflare_image_url,
|
|
caption: photo.caption || undefined,
|
|
title: photo.title || undefined,
|
|
user_id: photo.submitted_by || '',
|
|
created_at: photo.created_at,
|
|
})) || [];
|
|
|
|
setPhotos(mappedPhotos);
|
|
} catch (error: unknown) {
|
|
// Photo fetch failed - display empty gallery
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUploadClick = () => {
|
|
if (!user) {
|
|
navigate('/auth');
|
|
return;
|
|
}
|
|
setShowUpload(true);
|
|
};
|
|
|
|
const handleSubmissionComplete = () => {
|
|
setShowUpload(false);
|
|
fetchPhotos(); // Refresh photos after submission
|
|
};
|
|
|
|
const handlePhotoClick = (index: number) => {
|
|
setSelectedPhotoIndex(index);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setIsModalOpen(false);
|
|
setSelectedPhotoIndex(null);
|
|
};
|
|
|
|
if (showUpload) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-0">
|
|
<div>
|
|
<h3 className="text-base sm:text-lg font-semibold">Submit Additional Photos for {entityName}</h3>
|
|
<p className="text-sm text-muted-foreground">Photos submitted here go through a separate review process</p>
|
|
</div>
|
|
<Button variant="ghost" onClick={() => setShowUpload(false)} className="w-full sm:w-auto">
|
|
Back to Gallery
|
|
</Button>
|
|
</div>
|
|
<UppyPhotoSubmissionUpload
|
|
entityId={entityId}
|
|
entityType={entityType}
|
|
parentId={parentId}
|
|
onSubmissionComplete={handleSubmissionComplete}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-8 sm:py-12">
|
|
<div className="animate-pulse flex items-center gap-3">
|
|
<Camera className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground" />
|
|
<span className="text-sm sm:text-base text-muted-foreground">Loading photos...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header with Upload and Management Buttons */}
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
<div>
|
|
<h3 className="text-base sm:text-lg font-semibold">Photo Gallery</h3>
|
|
<p className="text-sm text-muted-foreground hidden sm:block">
|
|
Share your photos of {entityName}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
|
{isModerator() && photos.length > 0 && (
|
|
<Button onClick={() => setShowManagement(true)} variant="outline" className="gap-2 w-full sm:w-auto">
|
|
<Settings className="w-4 h-4" />
|
|
<span className="sm:inline">Manage</span>
|
|
</Button>
|
|
)}
|
|
<Button onClick={handleUploadClick} className="gap-2 w-full sm:w-auto">
|
|
{user ? (
|
|
<>
|
|
<Upload className="w-4 h-4" />
|
|
<span className="sm:inline">Submit Additional Photos</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<LogIn className="w-4 h-4" />
|
|
<span className="sm:inline">Sign in to Upload</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sort Dropdown */}
|
|
{photos.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<ArrowUpDown className="w-4 h-4 text-muted-foreground" />
|
|
<Select value={sortBy} onValueChange={(value: 'newest' | 'oldest') => setSortBy(value)}>
|
|
<SelectTrigger className="w-full sm:w-[180px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="newest">Newest First</SelectItem>
|
|
<SelectItem value="oldest">Oldest First</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Photo Management Dialog */}
|
|
<PhotoManagementDialog
|
|
entityId={entityId}
|
|
entityType={entityType}
|
|
open={showManagement}
|
|
onOpenChange={setShowManagement}
|
|
onUpdate={fetchPhotos}
|
|
/>
|
|
|
|
{/* Photo Grid */}
|
|
{photos.length > 0 ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
{photos.map((photo, index) => (
|
|
<Card key={photo.id} className="overflow-hidden">
|
|
<CardContent className="p-0">
|
|
<img
|
|
src={photo.url}
|
|
alt={photo.title || photo.caption || `${entityName} photo`}
|
|
className="w-full h-48 sm:h-56 md:h-48 object-cover transition-transform cursor-pointer touch-manipulation active:opacity-80 sm:hover:scale-105"
|
|
onClick={() => handlePhotoClick(index)}
|
|
/>
|
|
{photo.caption && (
|
|
<div className="p-2 sm:p-3">
|
|
<p className="text-[10px] sm:text-xs text-muted-foreground line-clamp-2">
|
|
{photo.caption}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 px-4">
|
|
<Camera className="w-12 h-12 sm:w-16 sm:h-16 text-muted-foreground mx-auto mb-4" />
|
|
<h3 className="text-lg sm:text-xl font-semibold mb-2">No Photos Yet</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Be the first to share photos of {entityName}!
|
|
</p>
|
|
<Button onClick={handleUploadClick} className="gap-2 w-full sm:w-auto">
|
|
{user ? (
|
|
<>
|
|
<Upload className="w-4 h-4" />
|
|
<span>Upload First Photo</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<LogIn className="w-4 h-4" />
|
|
<span>Sign in to Upload</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Photo Lightbox Modal */}
|
|
{selectedPhotoIndex !== null && (
|
|
<PhotoModal
|
|
photos={photos.map(photo => ({
|
|
id: photo.id,
|
|
url: photo.url,
|
|
caption: photo.caption,
|
|
filename: photo.title,
|
|
}))}
|
|
initialIndex={selectedPhotoIndex}
|
|
isOpen={isModalOpen}
|
|
onClose={handleCloseModal}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|