mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 04:51:13 -05:00
Improve error handling and environment configuration across the application
Enhance input validation, update environment variable usage for Supabase and Turnstile, and refine image upload and auth logic for better robustness and developer experience. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cb061c75-702e-4b89-a8d1-77a96cdcdfbb Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/cb061c75-702e-4b89-a8d1-77a96cdcdfbb/ANdRXVZ
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
@@ -33,6 +33,7 @@ interface UploadedImage {
|
||||
url: string;
|
||||
filename: string;
|
||||
thumbnailUrl: string;
|
||||
previewUrl?: string;
|
||||
}
|
||||
|
||||
export function PhotoUpload({
|
||||
@@ -53,6 +54,7 @@ export function PhotoUpload({
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const objectUrlsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const isAvatar = variant === 'avatar';
|
||||
const isCompact = variant === 'compact';
|
||||
@@ -60,6 +62,36 @@ export function PhotoUpload({
|
||||
const totalImages = uploadedImages.length + existingPhotos.length;
|
||||
const canUploadMore = totalImages < actualMaxFiles;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
objectUrlsRef.current.forEach(url => {
|
||||
try {
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error revoking object URL:', error);
|
||||
}
|
||||
});
|
||||
objectUrlsRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const createObjectUrl = (file: File): string => {
|
||||
const url = URL.createObjectURL(file);
|
||||
objectUrlsRef.current.add(url);
|
||||
return url;
|
||||
};
|
||||
|
||||
const revokeObjectUrl = (url: string) => {
|
||||
if (objectUrlsRef.current.has(url)) {
|
||||
try {
|
||||
URL.revokeObjectURL(url);
|
||||
objectUrlsRef.current.delete(url);
|
||||
} catch (error) {
|
||||
console.error('Error revoking object URL:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = (file: File): string | null => {
|
||||
// Check file size using configurable limit
|
||||
const maxSize = maxSizeMB * 1024 * 1024;
|
||||
@@ -76,85 +108,92 @@ export function PhotoUpload({
|
||||
return null;
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File): Promise<UploadedImage> => {
|
||||
// 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()
|
||||
},
|
||||
variant: isAvatar ? 'avatar' : 'public'
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const uploadResponse = await fetch(uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error('Direct upload to Cloudflare failed');
|
||||
}
|
||||
|
||||
// Step 3: Poll for upload completion and get final URLs
|
||||
const maxAttempts = 60; // 30 seconds maximum wait with faster polling
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
// Use direct fetch with URL parameters instead of supabase.functions.invoke with body
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://ydvtmnrszybqnbcqbdcy.supabase.co';
|
||||
const response = await fetch(`${supabaseUrl}/functions/v1/upload-image?id=${encodeURIComponent(id)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const statusData = await response.json();
|
||||
|
||||
if (statusData?.success && statusData.uploaded && statusData.urls) {
|
||||
const imageUrl = isAvatar ? statusData.urls.avatar : statusData.urls.public;
|
||||
const thumbUrl = isAvatar ? statusData.urls.avatar : statusData.urls.thumbnail;
|
||||
|
||||
return {
|
||||
id: statusData.id,
|
||||
url: imageUrl,
|
||||
filename: file.name,
|
||||
thumbnailUrl: thumbUrl
|
||||
};
|
||||
}
|
||||
const uploadFile = async (file: File, previewUrl: string): Promise<UploadedImage> => {
|
||||
try {
|
||||
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()
|
||||
},
|
||||
variant: isAvatar ? 'avatar' : 'public'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Status poll error:', error);
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error('Upload URL error:', uploadError);
|
||||
revokeObjectUrl(previewUrl);
|
||||
throw new Error(uploadError.message);
|
||||
}
|
||||
|
||||
// Wait 500ms before checking again (faster polling)
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
attempts++;
|
||||
}
|
||||
if (!uploadData?.success) {
|
||||
revokeObjectUrl(previewUrl);
|
||||
throw new Error(uploadData?.error || 'Failed to get upload URL');
|
||||
}
|
||||
|
||||
throw new Error('Upload timeout - image processing took too long');
|
||||
const { uploadURL, id } = uploadData;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const uploadResponse = await fetch(uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
revokeObjectUrl(previewUrl);
|
||||
throw new Error('Direct upload to Cloudflare failed');
|
||||
}
|
||||
|
||||
const maxAttempts = 60;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://ydvtmnrszybqnbcqbdcy.supabase.co';
|
||||
const response = await fetch(`${supabaseUrl}/functions/v1/upload-image?id=${encodeURIComponent(id)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const statusData = await response.json();
|
||||
|
||||
if (statusData?.success && statusData.uploaded && statusData.urls) {
|
||||
const imageUrl = isAvatar ? statusData.urls.avatar : statusData.urls.public;
|
||||
const thumbUrl = isAvatar ? statusData.urls.avatar : statusData.urls.thumbnail;
|
||||
|
||||
revokeObjectUrl(previewUrl);
|
||||
|
||||
return {
|
||||
id: statusData.id,
|
||||
url: imageUrl,
|
||||
filename: file.name,
|
||||
thumbnailUrl: thumbUrl,
|
||||
previewUrl: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Status poll error:', error);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
revokeObjectUrl(previewUrl);
|
||||
throw new Error('Upload timeout - image processing took too long');
|
||||
} catch (error) {
|
||||
revokeObjectUrl(previewUrl);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiles = async (files: FileList) => {
|
||||
@@ -172,7 +211,6 @@ export function PhotoUpload({
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all files first
|
||||
for (const file of filesToUpload) {
|
||||
const validationError = validateFile(file);
|
||||
if (validationError) {
|
||||
@@ -186,8 +224,9 @@ export function PhotoUpload({
|
||||
setError(null);
|
||||
onUploadStart?.();
|
||||
|
||||
const previewUrls: string[] = [];
|
||||
|
||||
try {
|
||||
// Delete old image first if this is an avatar update
|
||||
if (isAvatar && currentImageId) {
|
||||
try {
|
||||
await supabase.functions.invoke('upload-image', {
|
||||
@@ -196,23 +235,22 @@ export function PhotoUpload({
|
||||
});
|
||||
} catch (deleteError) {
|
||||
console.warn('Failed to delete old avatar:', deleteError);
|
||||
// Continue with upload even if deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
const uploadPromises = filesToUpload.map(async (file, index) => {
|
||||
setUploadProgress((index / filesToUpload.length) * 100);
|
||||
return uploadFile(file);
|
||||
const previewUrl = createObjectUrl(file);
|
||||
previewUrls.push(previewUrl);
|
||||
return uploadFile(file, previewUrl);
|
||||
});
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
|
||||
if (isAvatar) {
|
||||
// For avatars, replace all existing images
|
||||
setUploadedImages(results);
|
||||
onUploadComplete?.(results.map(img => img.url), results[0]?.id);
|
||||
} else {
|
||||
// For regular uploads, append to existing images
|
||||
setUploadedImages(prev => [...prev, ...results]);
|
||||
const allUrls = [...existingPhotos, ...uploadedImages.map(img => img.url), ...results.map(img => img.url)];
|
||||
onUploadComplete?.(allUrls);
|
||||
@@ -220,6 +258,7 @@ export function PhotoUpload({
|
||||
|
||||
setUploadProgress(100);
|
||||
} catch (error: any) {
|
||||
previewUrls.forEach(url => revokeObjectUrl(url));
|
||||
const errorMessage = error.message || 'Upload failed';
|
||||
setError(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
@@ -230,6 +269,11 @@ export function PhotoUpload({
|
||||
};
|
||||
|
||||
const removeImage = (imageId: string) => {
|
||||
const imageToRemove = uploadedImages.find(img => img.id === imageId);
|
||||
if (imageToRemove?.previewUrl) {
|
||||
revokeObjectUrl(imageToRemove.previewUrl);
|
||||
}
|
||||
|
||||
setUploadedImages(prev => prev.filter(img => img.id !== imageId));
|
||||
const updatedUrls = [...existingPhotos, ...uploadedImages.filter(img => img.id !== imageId).map(img => img.url)];
|
||||
onUploadComplete?.(updatedUrls);
|
||||
|
||||
Reference in New Issue
Block a user