Refactor: Make photo upload reusable

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 19:34:54 +00:00
parent 4ea5da9f10
commit 63fb0a61aa
6 changed files with 308 additions and 178 deletions

View File

@@ -1,166 +1,5 @@
import { useState, useEffect } from 'react';
import { Camera, Upload, LogIn } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useAuth } from '@/hooks/useAuth';
import { useNavigate } from 'react-router-dom';
import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload';
import { supabase } from '@/integrations/supabase/client';
interface RidePhoto {
id: string;
url: string;
caption?: string;
title?: string;
user_id: string;
created_at: string;
}
interface RidePhotoGalleryProps {
rideId: string;
rideName: string;
parkId?: string;
}
export function RidePhotoGallery({ rideId, rideName, parkId }: RidePhotoGalleryProps) {
const { user } = useAuth();
const navigate = useNavigate();
const [photos, setPhotos] = useState<RidePhoto[]>([]);
const [showUpload, setShowUpload] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPhotos();
}, [rideId]);
const fetchPhotos = async () => {
try {
// For now, we'll show a placeholder since approved photos would come from content_submissions
// In a real implementation, you'd fetch approved photo submissions
setPhotos([]); // Placeholder - no photos yet
} catch (error) {
console.error('Error fetching photos:', error);
} finally {
setLoading(false);
}
};
const handleUploadClick = () => {
if (!user) {
navigate('/auth');
return;
}
setShowUpload(true);
};
const handleSubmissionComplete = () => {
setShowUpload(false);
fetchPhotos(); // Refresh photos after submission
};
if (showUpload) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Upload Photos for {rideName}</h3>
<Button variant="ghost" onClick={() => setShowUpload(false)}>
Back to Gallery
</Button>
</div>
<UppyPhotoSubmissionUpload
rideId={rideId}
parkId={parkId}
onSubmissionComplete={handleSubmissionComplete}
/>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-pulse flex items-center gap-3">
<Camera className="w-8 h-8 text-muted-foreground" />
<span className="text-muted-foreground">Loading photos...</span>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Upload Button */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Photo Gallery</h3>
<p className="text-sm text-muted-foreground">
Share your photos of {rideName}
</p>
</div>
<Button onClick={handleUploadClick} className="gap-2">
{user ? (
<>
<Upload className="w-4 h-4" />
Upload Photos
</>
) : (
<>
<LogIn className="w-4 h-4" />
Sign in to Upload
</>
)}
</Button>
</div>
{/* Photo Grid */}
{photos.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{photos.map((photo) => (
<Card key={photo.id} className="overflow-hidden">
<CardContent className="p-0">
<img
src={photo.url}
alt={photo.title || photo.caption || 'Ride photo'}
className="w-full h-48 object-cover hover:scale-105 transition-transform cursor-pointer"
/>
{(photo.title || photo.caption) && (
<div className="p-3">
{photo.title && (
<h4 className="font-medium text-sm truncate">{photo.title}</h4>
)}
{photo.caption && (
<p className="text-xs text-muted-foreground truncate mt-1">
{photo.caption}
</p>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<div className="text-center py-12">
<Camera className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No Photos Yet</h3>
<p className="text-muted-foreground mb-4">
Be the first to share photos of {rideName}!
</p>
<Button onClick={handleUploadClick} className="gap-2">
{user ? (
<>
<Upload className="w-4 h-4" />
Upload First Photo
</>
) : (
<>
<LogIn className="w-4 h-4" />
Sign in to Upload
</>
)}
</Button>
</div>
)}
</div>
);
}
/**
* @deprecated Use EntityPhotoGallery directly or import from RidePhotoGalleryWrapper
* This file is kept for backwards compatibility
*/
export { RidePhotoGallery } from './RidePhotoGalleryWrapper';

View File

@@ -0,0 +1,22 @@
import { EntityPhotoGallery } from '@/components/upload/EntityPhotoGallery';
interface RidePhotoGalleryProps {
rideId: string;
rideName: string;
parkId?: string;
}
/**
* Backwards-compatible wrapper for RidePhotoGallery
* Uses the generic EntityPhotoGallery component internally
*/
export function RidePhotoGallery({ rideId, rideName, parkId }: RidePhotoGalleryProps) {
return (
<EntityPhotoGallery
entityId={rideId}
entityType="ride"
entityName={rideName}
parentId={parkId}
/>
);
}

View File

@@ -0,0 +1,196 @@
import { useState, useEffect } from 'react';
import { Camera, Upload, LogIn } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useAuth } from '@/hooks/useAuth';
import { useNavigate } from 'react-router-dom';
import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload';
import { supabase } from '@/integrations/supabase/client';
import { EntityPhotoGalleryProps } from '@/types/submissions';
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 [photos, setPhotos] = useState<Photo[]>([]);
const [showUpload, setShowUpload] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPhotos();
}, [entityId, entityType]);
const fetchPhotos = async () => {
try {
// Fetch approved photo submissions for this entity
// Support both new (entity_id) and legacy (park_id/ride_id) formats
const { data: submissions, error } = await supabase
.from('content_submissions')
.select('id, content, created_at, user_id')
.eq('status', 'approved')
.eq('submission_type', 'photo')
.or(`content->entity_id.eq.${entityId},content->park_id.eq.${entityId},content->ride_id.eq.${entityId},content->company_id.eq.${entityId}`)
.order('created_at', { ascending: false })
.limit(50);
if (error) throw error;
// Extract photos from submissions
const extractedPhotos: Photo[] = [];
submissions?.forEach((submission) => {
const content = submission.content as any;
if (content.photos && Array.isArray(content.photos)) {
content.photos.forEach((photo: any) => {
extractedPhotos.push({
id: `${submission.id}-${photo.order}`,
url: photo.url,
caption: photo.caption,
title: photo.title,
user_id: submission.user_id,
created_at: submission.created_at,
});
});
}
});
setPhotos(extractedPhotos);
} catch (error) {
console.error('Error fetching photos:', error);
} finally {
setLoading(false);
}
};
const handleUploadClick = () => {
if (!user) {
navigate('/auth');
return;
}
setShowUpload(true);
};
const handleSubmissionComplete = () => {
setShowUpload(false);
fetchPhotos(); // Refresh photos after submission
};
if (showUpload) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Upload Photos for {entityName}</h3>
<Button variant="ghost" onClick={() => setShowUpload(false)}>
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-12">
<div className="animate-pulse flex items-center gap-3">
<Camera className="w-8 h-8 text-muted-foreground" />
<span className="text-muted-foreground">Loading photos...</span>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Upload Button */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Photo Gallery</h3>
<p className="text-sm text-muted-foreground">
Share your photos of {entityName}
</p>
</div>
<Button onClick={handleUploadClick} className="gap-2">
{user ? (
<>
<Upload className="w-4 h-4" />
Upload Photos
</>
) : (
<>
<LogIn className="w-4 h-4" />
Sign in to Upload
</>
)}
</Button>
</div>
{/* Photo Grid */}
{photos.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{photos.map((photo) => (
<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 object-cover hover:scale-105 transition-transform cursor-pointer"
/>
{(photo.title || photo.caption) && (
<div className="p-3">
{photo.title && (
<h4 className="font-medium text-sm truncate">{photo.title}</h4>
)}
{photo.caption && (
<p className="text-xs text-muted-foreground truncate mt-1">
{photo.caption}
</p>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<div className="text-center py-12">
<Camera className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No Photos Yet</h3>
<p className="text-muted-foreground mb-4">
Be the first to share photos of {entityName}!
</p>
<Button onClick={handleUploadClick} className="gap-2">
{user ? (
<>
<Upload className="w-4 h-4" />
Upload First Photo
</>
) : (
<>
<LogIn className="w-4 h-4" />
Sign in to Upload
</>
)}
</Button>
</div>
)}
</div>
);
}

View File

@@ -13,18 +13,21 @@ import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/hooks/use-toast';
import { Camera, CheckCircle, AlertCircle, Info } from 'lucide-react';
interface UppyPhotoSubmissionUploadProps {
onSubmissionComplete?: () => void;
parkId?: string;
rideId?: string;
}
import { UppyPhotoSubmissionUploadProps } from '@/types/submissions';
export function UppyPhotoSubmissionUpload({
onSubmissionComplete,
entityId,
entityType,
parentId,
// Legacy props (deprecated)
parkId,
rideId,
}: UppyPhotoSubmissionUploadProps) {
// Support legacy props
const finalEntityId = entityId || rideId || parkId || '';
const finalEntityType = entityType || (rideId ? 'ride' : parkId ? 'park' : 'ride');
const finalParentId = parentId || (rideId ? parkId : undefined);
const [title, setTitle] = useState('');
const [photos, setPhotos] = useState<PhotoWithCaption[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -193,10 +196,14 @@ export function UppyPhotoSubmissionUpload({
date: photo.date?.toISOString(),
order: index,
})),
// NEW STRUCTURE: context as string, IDs at top level
context: rideId ? 'ride' : parkId ? 'park' : undefined,
ride_id: rideId,
park_id: parkId,
// NEW STRUCTURE: Generic entity references
context: finalEntityType,
entity_id: finalEntityId,
// Legacy structure for backwards compatibility
...(finalEntityType === 'ride' && { ride_id: finalEntityId }),
...(finalEntityType === 'park' && { park_id: finalEntityId }),
...(finalParentId && finalEntityType === 'ride' && { park_id: finalParentId }),
...(['manufacturer', 'operator', 'designer', 'property_owner'].includes(finalEntityType) && { company_id: finalEntityId }),
},
};
@@ -249,8 +256,12 @@ export function UppyPhotoSubmissionUpload({
const metadata = {
submissionType: 'photo',
parkId,
rideId,
entityId: finalEntityId,
entityType: finalEntityType,
parentId: finalParentId,
// Legacy support
parkId: finalEntityType === 'park' ? finalEntityId : finalParentId,
rideId: finalEntityType === 'ride' ? finalEntityId : undefined,
userId: user?.id,
};

44
src/types/submissions.ts Normal file
View File

@@ -0,0 +1,44 @@
export type EntityType =
| 'park'
| 'ride'
| 'manufacturer'
| 'operator'
| 'designer'
| 'property_owner';
export interface PhotoSubmission {
url: string;
caption?: string;
title?: string;
date?: string;
order: number;
}
export interface PhotoSubmissionContent {
title?: string;
photos: PhotoSubmission[];
context: EntityType;
entity_id: string;
// Legacy support
park_id?: string;
ride_id?: string;
company_id?: string;
}
export interface EntityPhotoGalleryProps {
entityId: string;
entityType: EntityType;
entityName: string;
parentId?: string; // e.g., parkId for a ride
}
export interface UppyPhotoSubmissionUploadProps {
onSubmissionComplete?: () => void;
entityId: string;
entityType: EntityType;
parentId?: string; // Optional parent (e.g., parkId for rides)
// Deprecated (kept for backwards compatibility)
parkId?: string;
rideId?: string;
}

View File

@@ -0,0 +1,18 @@
-- Update content_submissions submission_type to support all entity types
ALTER TABLE public.content_submissions
DROP CONSTRAINT IF EXISTS content_submissions_submission_type_check;
ALTER TABLE public.content_submissions
ADD CONSTRAINT content_submissions_submission_type_check
CHECK (submission_type IN (
'park',
'ride',
'review',
'photo',
'manufacturer',
'operator',
'designer',
'property_owner'
));
COMMENT ON COLUMN public.content_submissions.content IS 'JSONB structure: { title?, photos: Array<{url, caption?, title?, date?, order}>, context: "park"|"ride"|"manufacturer"|"operator"|"designer"|"property_owner", entity_id: string, park_id?: string (legacy), ride_id?: string (legacy), company_id?: string (legacy) }';