diff --git a/src/components/upload/PhotoUpload.tsx b/src/components/upload/PhotoUpload.tsx index 49daf986..1338396d 100644 --- a/src/components/upload/PhotoUpload.tsx +++ b/src/components/upload/PhotoUpload.tsx @@ -72,33 +72,75 @@ export function PhotoUpload({ }; const uploadFile = async (file: File): Promise => { + // Step 1: Get direct upload URL from our edge function + const { data: uploadData, error: uploadError } = await supabase.functions.invoke('upload-image', { + body: { + metadata: { + filename: file.name, + size: file.size, + type: file.type, + uploadedAt: new Date().toISOString() + } + } + }); + + if (uploadError) { + console.error('Upload URL error:', uploadError); + throw new Error(uploadError.message); + } + + if (!uploadData?.success) { + throw new Error(uploadData?.error || 'Failed to get upload URL'); + } + + const { uploadURL, id } = uploadData; + + // Step 2: Upload file directly to Cloudflare const formData = new FormData(); formData.append('file', file); - formData.append('metadata', JSON.stringify({ - filename: file.name, - size: file.size, - type: file.type, - uploadedAt: new Date().toISOString() - })); - const { data, error } = await supabase.functions.invoke('upload-image', { + const uploadResponse = await fetch(uploadURL, { + method: 'POST', body: formData, }); - if (error) { - throw new Error(error.message || 'Upload failed'); + if (!uploadResponse.ok) { + throw new Error('Direct upload to Cloudflare failed'); } - if (!data.success) { - throw new Error(data.error || 'Upload failed'); + // Step 3: Poll for upload completion and get final URLs + const maxAttempts = 30; // 30 seconds maximum wait + let attempts = 0; + + while (attempts < maxAttempts) { + const statusUrl = `https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/upload-image?id=${id}`; + const statusResponse = await fetch(statusUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4`, + 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4' + } + }); + + if (statusResponse.ok) { + const statusData = await statusResponse.json(); + + if (statusData?.success && !statusData.draft && statusData.urls) { + return { + id: statusData.id, + url: statusData.urls.original, + filename: file.name, + thumbnailUrl: statusData.urls.thumbnail + }; + } + } + + // Wait 1 second before checking again + await new Promise(resolve => setTimeout(resolve, 1000)); + attempts++; } - return { - id: data.id, - url: data.urls.original, - filename: data.filename, - thumbnailUrl: data.urls.thumbnail - }; + throw new Error('Upload timeout - image processing took too long'); }; const handleFiles = async (files: FileList) => { diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index 82da770e..b9cf8e13 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -19,108 +19,121 @@ serve(async (req) => { throw new Error('Missing Cloudflare credentials') } - if (req.method !== 'POST') { - return new Response( - JSON.stringify({ error: 'Method not allowed' }), - { - status: 405, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } + if (req.method === 'POST') { + // Request a direct upload URL from Cloudflare + const { metadata = {} } = await req.json().catch(() => ({})) + + const directUploadResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + requireSignedURLs: false, + metadata: metadata + }), } ) - } - const formData = await req.formData() - const file = formData.get('file') as File - - if (!file) { - return new Response( - JSON.stringify({ error: 'No file provided' }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - } - ) - } + const directUploadResult = await directUploadResponse.json() - // Validate file size (10MB limit) - const maxSize = 10 * 1024 * 1024 // 10MB - if (file.size > maxSize) { - return new Response( - JSON.stringify({ error: 'File size exceeds 10MB limit' }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - } - ) - } - - // Validate file type - const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'] - if (!allowedTypes.includes(file.type)) { - return new Response( - JSON.stringify({ error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - } - ) - } - - // Create FormData for Cloudflare Images API - const cloudflareFormData = new FormData() - cloudflareFormData.append('file', file) - - // Optional metadata - const metadata = formData.get('metadata') - if (metadata) { - cloudflareFormData.append('metadata', metadata.toString()) - } - - // Upload to Cloudflare Images - const uploadResponse = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, - }, - body: cloudflareFormData, + if (!directUploadResponse.ok) { + console.error('Cloudflare direct upload error:', directUploadResult) + return new Response( + JSON.stringify({ + error: 'Failed to get upload URL', + details: directUploadResult.errors || directUploadResult.error + }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) } - ) - const uploadResult = await uploadResponse.json() - - if (!uploadResponse.ok) { - console.error('Cloudflare upload error:', uploadResult) + // Return the upload URL and image ID to the client return new Response( - JSON.stringify({ - error: 'Failed to upload image', - details: uploadResult.errors || uploadResult.error + JSON.stringify({ + success: true, + uploadURL: directUploadResult.result.uploadURL, + id: directUploadResult.result.id, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + + if (req.method === 'GET') { + // Check image status endpoint + const url = new URL(req.url) + const imageId = url.searchParams.get('id') + + if (!imageId) { + return new Response( + JSON.stringify({ error: 'Image ID is required' }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + + const imageResponse = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, + { + headers: { + 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, + }, + } + ) + + const imageResult = await imageResponse.json() + + if (!imageResponse.ok) { + console.error('Cloudflare image status error:', imageResult) + return new Response( + JSON.stringify({ + error: 'Failed to get image status', + details: imageResult.errors || imageResult.error + }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ) + } + + // Return the image details with convenient URLs + const result = imageResult.result + return new Response( + JSON.stringify({ + success: true, + id: result.id, + uploaded: result.uploaded, + variants: result.variants, + draft: result.draft, + // Provide convenient URLs for different sizes if not draft + urls: result.variants && result.variants.length > 0 ? { + original: result.variants[0], + thumbnail: `${result.variants[0]}/w=400,h=400,fit=crop`, + medium: `${result.variants[0]}/w=800,h=600,fit=cover`, + large: `${result.variants[0]}/w=1200,h=900,fit=cover`, + } : null }), { - status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } - // Return the upload result with image URLs return new Response( - JSON.stringify({ - success: true, - id: uploadResult.result.id, - filename: uploadResult.result.filename, - uploaded: uploadResult.result.uploaded, - variants: uploadResult.result.variants, - // Provide convenient URLs for different sizes - urls: { - original: uploadResult.result.variants[0], // First variant is usually the original - thumbnail: `${uploadResult.result.variants[0]}/w=400,h=400,fit=crop`, // 400x400 thumbnail - medium: `${uploadResult.result.variants[0]}/w=800,h=600,fit=cover`, // 800x600 medium - large: `${uploadResult.result.variants[0]}/w=1200,h=900,fit=cover`, // 1200x900 large - } - }), + JSON.stringify({ error: 'Method not allowed' }), { + status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } )