diff --git a/src/components/rides/RidePhotoGallery.tsx b/src/components/rides/RidePhotoGallery.tsx index c3c0999b..97017038 100644 --- a/src/components/rides/RidePhotoGallery.tsx +++ b/src/components/rides/RidePhotoGallery.tsx @@ -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([]); - 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 ( -
-
-

Upload Photos for {rideName}

- -
- -
- ); - } - - if (loading) { - return ( -
-
- - Loading photos... -
-
- ); - } - - return ( -
- {/* Upload Button */} -
-
-

Photo Gallery

-

- Share your photos of {rideName} -

-
- -
- - {/* Photo Grid */} - {photos.length > 0 ? ( -
- {photos.map((photo) => ( - - - {photo.title - {(photo.title || photo.caption) && ( -
- {photo.title && ( -

{photo.title}

- )} - {photo.caption && ( -

- {photo.caption} -

- )} -
- )} -
-
- ))} -
- ) : ( -
- -

No Photos Yet

-

- Be the first to share photos of {rideName}! -

- -
- )} -
- ); -} \ No newline at end of file +/** + * @deprecated Use EntityPhotoGallery directly or import from RidePhotoGalleryWrapper + * This file is kept for backwards compatibility + */ +export { RidePhotoGallery } from './RidePhotoGalleryWrapper'; \ No newline at end of file diff --git a/src/components/rides/RidePhotoGalleryWrapper.tsx b/src/components/rides/RidePhotoGalleryWrapper.tsx new file mode 100644 index 00000000..1645061a --- /dev/null +++ b/src/components/rides/RidePhotoGalleryWrapper.tsx @@ -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 ( + + ); +} diff --git a/src/components/upload/EntityPhotoGallery.tsx b/src/components/upload/EntityPhotoGallery.tsx new file mode 100644 index 00000000..39aff500 --- /dev/null +++ b/src/components/upload/EntityPhotoGallery.tsx @@ -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([]); + 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 ( +
+
+

Upload Photos for {entityName}

+ +
+ +
+ ); + } + + if (loading) { + return ( +
+
+ + Loading photos... +
+
+ ); + } + + return ( +
+ {/* Upload Button */} +
+
+

Photo Gallery

+

+ Share your photos of {entityName} +

+
+ +
+ + {/* Photo Grid */} + {photos.length > 0 ? ( +
+ {photos.map((photo) => ( + + + {photo.title + {(photo.title || photo.caption) && ( +
+ {photo.title && ( +

{photo.title}

+ )} + {photo.caption && ( +

+ {photo.caption} +

+ )} +
+ )} +
+
+ ))} +
+ ) : ( +
+ +

No Photos Yet

+

+ Be the first to share photos of {entityName}! +

+ +
+ )} +
+ ); +} diff --git a/src/components/upload/UppyPhotoSubmissionUpload.tsx b/src/components/upload/UppyPhotoSubmissionUpload.tsx index 44281eff..618bccfe 100644 --- a/src/components/upload/UppyPhotoSubmissionUpload.tsx +++ b/src/components/upload/UppyPhotoSubmissionUpload.tsx @@ -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([]); 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, }; diff --git a/src/types/submissions.ts b/src/types/submissions.ts new file mode 100644 index 00000000..086d81fb --- /dev/null +++ b/src/types/submissions.ts @@ -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; +} diff --git a/supabase/migrations/20250929193317_8e815f59-2c41-45d5-9792-95d91a476ebd.sql b/supabase/migrations/20250929193317_8e815f59-2c41-45d5-9792-95d91a476ebd.sql new file mode 100644 index 00000000..06e42513 --- /dev/null +++ b/supabase/migrations/20250929193317_8e815f59-2c41-45d5-9792-95d91a476ebd.sql @@ -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) }'; \ No newline at end of file