import React, { useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Progress } from '@/components/ui/progress'; import { UppyPhotoUpload } from './UppyPhotoUpload'; import { PhotoCaptionEditor, PhotoWithCaption } from './PhotoCaptionEditor'; 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; } export function UppyPhotoSubmissionUpload({ onSubmissionComplete, parkId, rideId, }: UppyPhotoSubmissionUploadProps) { const [title, setTitle] = useState(''); const [photos, setPhotos] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number } | null>(null); const { user } = useAuth(); const { toast } = useToast(); const handleFilesSelected = (files: File[]) => { // Convert files to photo objects with object URLs for preview const newPhotos: PhotoWithCaption[] = files.map((file, index) => ({ url: URL.createObjectURL(file), // Object URL for preview file, // Store the file for later upload caption: '', order: photos.length + index, uploadStatus: 'pending' as const, })); setPhotos(prev => [...prev, ...newPhotos]); }; const handlePhotosChange = (updatedPhotos: PhotoWithCaption[]) => { setPhotos(updatedPhotos); }; const handleRemovePhoto = (index: number) => { setPhotos(prev => { const photo = prev[index]; // Revoke object URL if it exists if (photo.file && photo.url.startsWith('blob:')) { URL.revokeObjectURL(photo.url); } return prev.filter((_, i) => i !== index); }); }; const handleSubmit = async () => { if (!user) { toast({ variant: 'destructive', title: 'Authentication Required', description: 'Please sign in to submit photos.', }); return; } if (photos.length === 0) { toast({ variant: 'destructive', title: 'No Photos', description: 'Please upload at least one photo before submitting.', }); return; } setIsSubmitting(true); try { // Upload all photos that haven't been uploaded yet const uploadedPhotos: PhotoWithCaption[] = []; const photosToUpload = photos.filter(p => p.file); if (photosToUpload.length > 0) { setUploadProgress({ current: 0, total: photosToUpload.length }); for (let i = 0; i < photosToUpload.length; i++) { const photo = photosToUpload[i]; setUploadProgress({ current: i + 1, total: photosToUpload.length }); // Update status setPhotos(prev => prev.map(p => p === photo ? { ...p, uploadStatus: 'uploading' as const } : p )); try { // Get upload URL from edge function const { data: uploadData, error: uploadError } = await supabase.functions.invoke('upload-image', { body: { metadata: { requireSignedURLs: false }, variant: 'public' } }); if (uploadError) throw uploadError; const { uploadURL, id: cloudflareId } = uploadData; // Upload file to Cloudflare const formData = new FormData(); formData.append('file', photo.file); const uploadResponse = await fetch(uploadURL, { method: 'POST', body: formData, }); if (!uploadResponse.ok) { throw new Error('Failed to upload to Cloudflare'); } // Poll for processing completion let attempts = 0; const maxAttempts = 30; let cloudflareUrl = ''; while (attempts < maxAttempts) { const { data: { session } } = await supabase.auth.getSession(); const statusResponse = await fetch( `https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/upload-image?id=${cloudflareId}`, { headers: { 'Authorization': `Bearer ${session?.access_token || ''}`, 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4', } } ); if (statusResponse.ok) { const status = await statusResponse.json(); if (status.uploaded && status.urls) { cloudflareUrl = status.urls.public; break; } } await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; } if (!cloudflareUrl) { throw new Error('Upload processing timeout'); } // Revoke object URL URL.revokeObjectURL(photo.url); uploadedPhotos.push({ ...photo, url: cloudflareUrl, uploadStatus: 'uploaded' as const, }); // Update status setPhotos(prev => prev.map(p => p === photo ? { ...p, url: cloudflareUrl, uploadStatus: 'uploaded' as const } : p )); } catch (error) { console.error('Upload error:', error); setPhotos(prev => prev.map(p => p === photo ? { ...p, uploadStatus: 'failed' as const } : p )); throw new Error(`Failed to upload ${photo.title || 'photo'}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } setUploadProgress(null); // Submit to database with Cloudflare URLs const submissionData = { user_id: user.id, submission_type: 'photo', content: { title: title.trim() || undefined, photos: photos.map((photo, index) => ({ url: photo.uploadStatus === 'uploaded' ? photo.url : uploadedPhotos.find(p => p.order === photo.order)?.url || photo.url, caption: photo.caption.trim(), title: photo.title?.trim(), 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, }, }; const { error } = await supabase .from('content_submissions') .insert(submissionData); if (error) { throw error; } toast({ title: 'Submission Successful', description: 'Your photos have been submitted for review. Thank you for contributing!', }); // Cleanup and reset form photos.forEach(photo => { if (photo.url.startsWith('blob:')) { URL.revokeObjectURL(photo.url); } }); setTitle(''); setPhotos([]); onSubmissionComplete?.(); } catch (error) { console.error('Submission error:', error); toast({ variant: 'destructive', title: 'Submission Failed', description: error instanceof Error ? error.message : 'There was an error submitting your photos. Please try again.', }); } finally { setIsSubmitting(false); setUploadProgress(null); } }; // Cleanup on unmount React.useEffect(() => { return () => { photos.forEach(photo => { if (photo.url.startsWith('blob:')) { URL.revokeObjectURL(photo.url); } }); }; }, []); const metadata = { submissionType: 'photo', parkId, rideId, userId: user?.id, }; return (
Submit Photos Share your photos with the community. All submissions will be reviewed before being published.

Submission Guidelines:

  • • Photos should be clear and well-lit
  • • Maximum 10 images per submission
  • • Each image up to 25MB in size
  • • Review process takes 24-48 hours
setTitle(e.target.value)} placeholder="Give your photos a descriptive title (optional)" maxLength={100} disabled={isSubmitting} className="transition-all duration-200 focus:ring-2 focus:ring-primary/20" />

{title.length}/100 characters

{photos.length > 0 && ( {photos.length} photo{photos.length !== 1 ? 's' : ''} selected )}
{photos.length > 0 && ( <> )}
{uploadProgress && (
Uploading photos... {uploadProgress.current} of {uploadProgress.total}
)}
Your submission will be reviewed and published within 24-48 hours
); }