diff --git a/src/components/upload/UppyPhotoSubmissionUpload.tsx b/src/components/upload/UppyPhotoSubmissionUpload.tsx index 67bd2ff8..e2984de3 100644 --- a/src/components/upload/UppyPhotoSubmissionUpload.tsx +++ b/src/components/upload/UppyPhotoSubmissionUpload.tsx @@ -1,22 +1,22 @@ -import React, { useState } from 'react'; -import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; -import { logger } from '@/lib/logger'; -import { getErrorMessage } from '@/lib/errorHandler'; -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 { UppyPhotoUploadLazy } from './UppyPhotoUploadLazy'; -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'; -import { UppyPhotoSubmissionUploadProps } from '@/types/submissions'; +import React, { useState } from "react"; +import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; +import { logger } from "@/lib/logger"; +import { getErrorMessage } from "@/lib/errorHandler"; +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 { UppyPhotoUploadLazy } from "./UppyPhotoUploadLazy"; +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"; +import { UppyPhotoSubmissionUploadProps } from "@/types/submissions"; export function UppyPhotoSubmissionUpload({ onSubmissionComplete, @@ -24,7 +24,7 @@ export function UppyPhotoSubmissionUpload({ entityType, parentId, }: UppyPhotoSubmissionUploadProps) { - const [title, setTitle] = useState(''); + const [title, setTitle] = useState(""); const [photos, setPhotos] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number } | null>(null); @@ -36,11 +36,11 @@ export function UppyPhotoSubmissionUpload({ const newPhotos: PhotoWithCaption[] = files.map((file, index) => ({ url: URL.createObjectURL(file), // Object URL for preview file, // Store the file for later upload - caption: '', + caption: "", order: photos.length + index, - uploadStatus: 'pending' as const, + uploadStatus: "pending" as const, })); - setPhotos(prev => [...prev, ...newPhotos]); + setPhotos((prev) => [...prev, ...newPhotos]); }; const handlePhotosChange = (updatedPhotos: PhotoWithCaption[]) => { @@ -48,10 +48,10 @@ export function UppyPhotoSubmissionUpload({ }; const handleRemovePhoto = (index: number) => { - setPhotos(prev => { + setPhotos((prev) => { const photo = prev[index]; // Revoke object URL if it exists - if (photo.file && photo.url.startsWith('blob:')) { + if (photo.file && photo.url.startsWith("blob:")) { URL.revokeObjectURL(photo.url); } return prev.filter((_, i) => i !== index); @@ -61,48 +61,45 @@ export function UppyPhotoSubmissionUpload({ const handleSubmit = async () => { if (!user) { toast({ - variant: 'destructive', - title: 'Authentication Required', - description: 'Please sign in to submit photos.', + 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.', + 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); - + 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 - )); + 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 invokeWithTracking( - 'upload-image', - { metadata: { requireSignedURLs: false }, variant: 'public' }, - user?.id + "upload-image", + { metadata: { requireSignedURLs: false }, variant: "public" }, + user?.id, ); if (uploadError) throw uploadError; @@ -111,37 +108,37 @@ export function UppyPhotoSubmissionUpload({ // Upload file to Cloudflare if (!photo.file) { - throw new Error('Photo file is missing'); + throw new Error("Photo file is missing"); } const formData = new FormData(); - formData.append('file', photo.file); + formData.append("file", photo.file); const uploadResponse = await fetch(uploadURL, { - method: 'POST', + method: "POST", body: formData, }); if (!uploadResponse.ok) { - throw new Error('Failed to upload to Cloudflare'); + throw new Error("Failed to upload to Cloudflare"); } // Poll for processing completion let attempts = 0; const maxAttempts = 30; - let cloudflareUrl = ''; + let cloudflareUrl = ""; while (attempts < maxAttempts) { - const { data: { session } } = await supabase.auth.getSession(); - const supabaseUrl = 'https://ydvtmnrszybqnbcqbdcy.supabase.co'; - const statusResponse = await fetch( - `${supabaseUrl}/functions/v1/upload-image?id=${cloudflareId}`, - { - headers: { - 'Authorization': `Bearer ${session?.access_token || ''}`, - 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4', - } - } - ); + const { + data: { session }, + } = await supabase.auth.getSession(); + const supabaseUrl = "https://api.thrillwiki.com"; + const statusResponse = await fetch(`${supabaseUrl}/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(); @@ -151,12 +148,12 @@ export function UppyPhotoSubmissionUpload({ } } - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); attempts++; } if (!cloudflareUrl) { - throw new Error('Upload processing timeout'); + throw new Error("Upload processing timeout"); } // Revoke object URL @@ -165,28 +162,25 @@ export function UppyPhotoSubmissionUpload({ uploadedPhotos.push({ ...photo, url: cloudflareUrl, - uploadStatus: 'uploaded' as const, + uploadStatus: "uploaded" as const, }); // Update status - setPhotos(prev => prev.map(p => - p === photo ? { ...p, url: cloudflareUrl, uploadStatus: 'uploaded' as const } : p - )); - + setPhotos((prev) => + prev.map((p) => (p === photo ? { ...p, url: cloudflareUrl, uploadStatus: "uploaded" as const } : p)), + ); } catch (error: unknown) { const errorMsg = getErrorMessage(error); - logger.error('Photo submission upload failed', { + logger.error("Photo submission upload failed", { photoTitle: photo.title, photoOrder: photo.order, fileName: photo.file?.name, - error: errorMsg + error: errorMsg, }); - - setPhotos(prev => prev.map(p => - p === photo ? { ...p, uploadStatus: 'failed' as const } : p - )); - - throw new Error(`Failed to upload ${photo.title || 'photo'}: ${errorMsg}`); + + setPhotos((prev) => prev.map((p) => (p === photo ? { ...p, uploadStatus: "failed" as const } : p))); + + throw new Error(`Failed to upload ${photo.title || "photo"}: ${errorMsg}`); } } } @@ -195,22 +189,22 @@ export function UppyPhotoSubmissionUpload({ // Create content_submission record first const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') + .from("content_submissions") .insert({ user_id: user.id, - submission_type: 'photo', + submission_type: "photo", content: {}, // Empty content, all data is in relational tables }) .select() .single(); if (submissionError || !submissionData) { - throw submissionError || new Error('Failed to create submission record'); + throw submissionError || new Error("Failed to create submission record"); } // Create photo_submission record const { data: photoSubmissionData, error: photoSubmissionError } = await supabase - .from('photo_submissions') + .from("photo_submissions") .insert({ submission_id: submissionData.id, entity_type: entityType, @@ -222,14 +216,17 @@ export function UppyPhotoSubmissionUpload({ .single(); if (photoSubmissionError || !photoSubmissionData) { - throw photoSubmissionError || new Error('Failed to create photo submission'); + throw photoSubmissionError || new Error("Failed to create photo submission"); } // Insert all photo items const photoItems = photos.map((photo, index) => ({ photo_submission_id: photoSubmissionData.id, - cloudflare_image_id: photo.url.split('/').slice(-2, -1)[0] || '', // Extract ID from URL - cloudflare_image_url: photo.uploadStatus === 'uploaded' ? photo.url : uploadedPhotos.find(p => p.order === photo.order)?.url || photo.url, + cloudflare_image_id: photo.url.split("/").slice(-2, -1)[0] || "", // Extract ID from URL + cloudflare_image_url: + photo.uploadStatus === "uploaded" + ? photo.url + : uploadedPhotos.find((p) => p.order === photo.order)?.url || photo.url, caption: photo.caption.trim() || null, title: photo.title?.trim() || null, filename: photo.file?.name || null, @@ -238,43 +235,41 @@ export function UppyPhotoSubmissionUpload({ mime_type: photo.file?.type || null, })); - const { error: itemsError } = await supabase - .from('photo_submission_items') - .insert(photoItems); + const { error: itemsError } = await supabase.from("photo_submission_items").insert(photoItems); if (itemsError) { throw itemsError; } toast({ - title: 'Submission Successful', - description: 'Your photos have been submitted for review. Thank you for contributing!', + 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:')) { + photos.forEach((photo) => { + if (photo.url.startsWith("blob:")) { URL.revokeObjectURL(photo.url); } }); - setTitle(''); + setTitle(""); setPhotos([]); onSubmissionComplete?.(); } catch (error: unknown) { const errorMsg = getErrorMessage(error); - logger.error('Photo submission failed', { + logger.error("Photo submission failed", { entityType, entityId, photoCount: photos.length, userId: user?.id, - error: errorMsg + error: errorMsg, }); - + toast({ - variant: 'destructive', - title: 'Submission Failed', - description: errorMsg || 'There was an error submitting your photos. Please try again.', + variant: "destructive", + title: "Submission Failed", + description: errorMsg || "There was an error submitting your photos. Please try again.", }); } finally { setIsSubmitting(false); @@ -285,8 +280,8 @@ export function UppyPhotoSubmissionUpload({ // Cleanup on unmount React.useEffect(() => { return () => { - photos.forEach(photo => { - if (photo.url.startsWith('blob:')) { + photos.forEach((photo) => { + if (photo.url.startsWith("blob:")) { URL.revokeObjectURL(photo.url); } }); @@ -294,7 +289,7 @@ export function UppyPhotoSubmissionUpload({ }, []); const metadata = { - submissionType: 'photo', + submissionType: "photo", entityId, entityType, parentId, @@ -308,15 +303,13 @@ export function UppyPhotoSubmissionUpload({
- - Submit Photos - + Submit Photos Share your photos with the community. All submissions will be reviewed before being published.
- +
@@ -348,9 +341,7 @@ export function UppyPhotoSubmissionUpload({ disabled={isSubmitting} className="transition-all duration-200 focus:ring-2 focus:ring-primary/20" /> -

- {title.length}/100 characters -

+

{title.length}/100 characters

@@ -358,7 +349,7 @@ export function UppyPhotoSubmissionUpload({ {photos.length > 0 && ( - {photos.length} photo{photos.length !== 1 ? 's' : ''} selected + {photos.length} photo{photos.length !== 1 ? "s" : ""} selected )}
@@ -403,7 +394,7 @@ export function UppyPhotoSubmissionUpload({
)} - - +
Your submission will be reviewed and published within 24-48 hours @@ -430,4 +421,4 @@ export function UppyPhotoSubmissionUpload({ ); -} \ No newline at end of file +}