Improve component stability and user experience with safety checks

Implement robust error handling, safety checks for data structures, and state management improvements across various components to prevent runtime errors and enhance user experience.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: a71e826a-1d38-4b6e-a34f-fbf5ba1f1b25
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-10-08 19:27:31 +00:00
parent f21602b992
commit bfba3baf7e
12 changed files with 224 additions and 110 deletions

View File

@@ -33,3 +33,7 @@ outputType = "webview"
[[ports]] [[ports]]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 45171
externalPort = 3000

View File

@@ -0,0 +1,47 @@
🔴 Critical Issues
1. Hot Module Reload (HMR) Failures
Your browser console shows HMR errors for several components:
ManufacturerRides.tsx and ManufacturerModels.tsx
Version-related components (VersionIndicator, VersionComparisonDialog, EntityVersionHistory)
Impact: During development, changes to these files won't refresh automatically, requiring full page reloads.
2. Fast Refresh Incompatibility
Two exports are breaking React Fast Refresh:
useSidebar in src/components/ui/sidebar.tsx
uploadPendingImages in src/components/upload/EntityMultiImageUploader.tsx
Impact: Components using these will cause full page reloads on changes instead of hot updates.
⚠️ High Priority Issues
3. Potential Race Conditions
useEntityVersions hook: Uses a request counter but could still have timing issues when multiple requests are in flight
useModerationQueue hook: Lacks explicit request cancellation, could show stale data if network is slow
Impact: Users might see outdated information or experience data inconsistencies.
4. Memory Leaks
PhotoUpload component: Object URLs might not be revoked if upload errors occur
useEntityVersions hook: Supabase realtime subscriptions might not cleanup properly on unmount
useAuth hook: Timeouts for Novu updates might not be cleared when component unmounts
Impact: Memory usage grows over time, eventually slowing down or crashing the browser.
5. Missing Null/Undefined Safety Checks
PhotoModal: Accessing currentPhoto without checking if photos array is empty
EntityHistoryTimeline: Accessing event config without verifying it exists
useEntityVersions: Assumes Supabase data is always an array
EntityEditPreview: Accessing nested image data without structure validation
Impact: Runtime errors that crash components when data is missing.
🟡 Medium Priority Issues
6. Inconsistent Error Handling
Error handling varies across components:
Some use console.error() (silent for users)
Others use toast.error() (visible notifications)
File upload errors don't show which specific file failed
Impact: Users don't always know when something went wrong.
7. State Management Issues
useSearch hook: Uses JSON.stringify() for memoization which fails with functions/Symbols
AutocompleteSearch: Search dropdown state not properly tied to loading state
EntityEditPreview: Deep object comparison issues for detecting changes

View File

@@ -26,6 +26,9 @@ const eventTypeConfig: Record<HistoryEventType, { icon: typeof Tag; color: strin
milestone: { icon: Milestone, color: 'text-pink-500', label: 'Milestone' }, milestone: { icon: Milestone, color: 'text-pink-500', label: 'Milestone' },
}; };
// Fallback config for unknown event types
const defaultEventConfig = { icon: Tag, color: 'text-gray-500', label: 'Event' };
export function EntityHistoryTimeline({ events, entityName }: EntityHistoryTimelineProps) { export function EntityHistoryTimeline({ events, entityName }: EntityHistoryTimelineProps) {
if (events.length === 0) { if (events.length === 0) {
return ( return (
@@ -54,7 +57,10 @@ export function EntityHistoryTimeline({ events, entityName }: EntityHistoryTimel
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-border" /> <div className="absolute left-6 top-0 bottom-0 w-0.5 bg-border" />
{sortedEvents.map((event, index) => { {sortedEvents.map((event, index) => {
const config = eventTypeConfig[event.type]; // Safety check: verify event.type exists in eventTypeConfig, use fallback if not
const config = event.type && eventTypeConfig[event.type]
? eventTypeConfig[event.type]
: defaultEventConfig;
const Icon = config.icon; const Icon = config.icon;
return ( return (
@@ -105,14 +111,25 @@ export function EntityHistoryTimeline({ events, entityName }: EntityHistoryTimel
} }
function formatEventDate(dateString: string): string { function formatEventDate(dateString: string): string {
// Safety check: validate dateString exists and is a string
if (!dateString || typeof dateString !== 'string') {
return 'Unknown date';
}
try { try {
// Handle year-only dates // Handle year-only dates
if (/^\d{4}$/.test(dateString)) { if (/^\d{4}$/.test(dateString)) {
return dateString; return dateString;
} }
// Handle full dates // Validate date string before creating Date object
const date = new Date(dateString); const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) {
return dateString;
}
return format(date, 'MMMM d, yyyy'); return format(date, 'MMMM d, yyyy');
} catch { } catch {
return dateString; return dateString;

View File

@@ -11,6 +11,36 @@ interface EntityEditPreviewProps {
entityName?: string; entityName?: string;
} }
/**
* Deep equality check for detecting changes in nested objects/arrays
*/
const deepEqual = (a: any, b: any): boolean => {
// Handle null/undefined cases
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
// Handle primitives and functions
if (typeof a !== 'object') return a === b;
// Handle arrays
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, index) => deepEqual(item, b[index]));
}
// One is array, other is not
if (Array.isArray(a) !== Array.isArray(b)) return false;
// Handle objects
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => deepEqual(a[key], b[key]));
};
interface ImageAssignments { interface ImageAssignments {
uploaded: Array<{ uploaded: Array<{
url: string; url: string;
@@ -51,7 +81,10 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
.eq('submission_id', submissionId) .eq('submission_id', submissionId)
.order('order_index', { ascending: true }); .order('order_index', { ascending: true });
if (error) throw error; if (error) {
console.error('EntityEditPreview.fetchSubmissionItems: Failed to fetch submission items:', error);
throw error;
}
if (items && items.length > 0) { if (items && items.length > 0) {
const firstItem = items[0]; const firstItem = items[0];
@@ -75,21 +108,35 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
if (data.images) { if (data.images) {
const images: ImageAssignments = data.images; const images: ImageAssignments = data.images;
// Safety check: verify uploaded array exists and is valid
if (!images.uploaded || !Array.isArray(images.uploaded)) {
// Invalid images data structure, skip image processing
return;
}
// Extract banner image // Extract banner image
if (images.banner_assignment !== null && images.banner_assignment !== undefined) { if (images.banner_assignment !== null && images.banner_assignment !== undefined) {
const bannerImg = images.uploaded[images.banner_assignment]; // Safety check: verify index is within bounds
if (bannerImg) { if (images.banner_assignment >= 0 && images.banner_assignment < images.uploaded.length) {
setBannerImageUrl(bannerImg.url); const bannerImg = images.uploaded[images.banner_assignment];
changed.push('banner_image'); // Validate nested image data
if (bannerImg && bannerImg.url) {
setBannerImageUrl(bannerImg.url);
changed.push('banner_image');
}
} }
} }
// Extract card image // Extract card image
if (images.card_assignment !== null && images.card_assignment !== undefined) { if (images.card_assignment !== null && images.card_assignment !== undefined) {
const cardImg = images.uploaded[images.card_assignment]; // Safety check: verify index is within bounds
if (cardImg) { if (images.card_assignment >= 0 && images.card_assignment < images.uploaded.length) {
setCardImageUrl(cardImg.url); const cardImg = images.uploaded[images.card_assignment];
changed.push('card_image'); // Validate nested image data
if (cardImg && cardImg.url) {
setCardImageUrl(cardImg.url);
changed.push('card_image');
}
} }
} }
} }
@@ -99,9 +146,12 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
const originalData = firstItem.original_data as any; const originalData = firstItem.original_data as any;
const excludeFields = ['images', 'updated_at', 'created_at']; const excludeFields = ['images', 'updated_at', 'created_at'];
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
if (!excludeFields.includes(key) && if (!excludeFields.includes(key)) {
data[key] !== originalData[key]) { // Use deep equality check for objects and arrays
changed.push(key); const isEqual = deepEqual(data[key], originalData[key]);
if (!isEqual) {
changed.push(key);
}
} }
}); });
} }
@@ -109,7 +159,7 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
setChangedFields(changed); setChangedFields(changed);
} }
} catch (error) { } catch (error) {
console.error('Error fetching submission items:', error); console.error('EntityEditPreview.fetchSubmissionItems: Error fetching submission items:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -23,7 +23,19 @@ export function PhotoModal({ photos, initialIndex, isOpen, onClose }: PhotoModal
const [touchEnd, setTouchEnd] = useState<number | null>(null); const [touchEnd, setTouchEnd] = useState<number | null>(null);
const imageRef = useRef<HTMLImageElement>(null); const imageRef = useRef<HTMLImageElement>(null);
const currentPhoto = photos[currentIndex]; // Safety check: ensure photos array exists and is not empty
if (!photos || photos.length === 0) {
return null;
}
// Clamp currentIndex to valid bounds
const safeIndex = Math.max(0, Math.min(currentIndex, photos.length - 1));
const currentPhoto = photos[safeIndex];
// Early return if currentPhoto is undefined
if (!currentPhoto) {
return null;
}
// Minimum swipe distance (in px) // Minimum swipe distance (in px)
const minSwipeDistance = 50; const minSwipeDistance = 50;
@@ -100,7 +112,7 @@ export function PhotoModal({ photos, initialIndex, isOpen, onClose }: PhotoModal
)} )}
{photos.length > 1 && ( {photos.length > 1 && (
<p className={`text-white/70 ${isMobile ? 'text-xs' : 'text-sm'}`}> <p className={`text-white/70 ${isMobile ? 'text-xs' : 'text-sm'}`}>
{currentIndex + 1} of {photos.length} {safeIndex + 1} of {photos.length}
</p> </p>
)} )}
</div> </div>
@@ -111,7 +123,7 @@ export function PhotoModal({ photos, initialIndex, isOpen, onClose }: PhotoModal
<img <img
ref={imageRef} ref={imageRef}
src={currentPhoto?.url} src={currentPhoto?.url}
alt={currentPhoto?.caption || `Photo ${currentIndex + 1}`} alt={currentPhoto?.caption || `Photo ${safeIndex + 1}`}
className="max-w-full max-h-full object-contain select-none" className="max-w-full max-h-full object-contain select-none"
loading="lazy" loading="lazy"
draggable={false} draggable={false}

View File

@@ -296,7 +296,7 @@ export function AutocompleteSearch({
)} )}
</div> </div>
{isOpen && displayItems.length > 0 && ( {isOpen && (displayItems.length > 0 || loading) && (
<div className={`absolute top-full mt-1 left-0 right-0 bg-popover border border-border rounded-lg shadow-xl z-[100] max-h-96 overflow-y-auto ${isHero ? 'max-w-2xl mx-auto' : ''}`}> <div className={`absolute top-full mt-1 left-0 right-0 bg-popover border border-border rounded-lg shadow-xl z-[100] max-h-96 overflow-y-auto ${isHero ? 'max-w-2xl mx-auto' : ''}`}>
<div className="p-2"> <div className="p-2">
{query.length === 0 && showRecentSearches && suggestions.length > 0 && ( {query.length === 0 && showRecentSearches && suggestions.length > 0 && (
@@ -316,7 +316,7 @@ export function AutocompleteSearch({
</> </>
)} )}
{displayItems.map((item, index) => ( {displayItems.length > 0 && displayItems.map((item, index) => (
<div <div
key={item.id} key={item.id}
onClick={() => handleResultClick(item)} onClick={() => handleResultClick(item)}
@@ -364,10 +364,11 @@ export function AutocompleteSearch({
{loading && ( {loading && (
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span className="text-sm text-muted-foreground ml-2">Searching...</span>
</div> </div>
)} )}
{query.length > 0 && results.length > 0 && ( {query.length > 0 && results.length > 0 && !loading && (
<> <>
<Separator className="my-2" /> <Separator className="my-2" />
<Button <Button

View File

@@ -0,0 +1,13 @@
import { createContext } from "react";
export type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
export const SidebarContext = createContext<SidebarContext | null>(null);

View File

@@ -11,6 +11,8 @@ import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent } from "@/components/ui/sheet"; import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { SidebarContext, type SidebarContext as SidebarContextType } from "@/components/ui/sidebar-context";
import { useSidebar } from "@/hooks/useSidebar";
const SIDEBAR_COOKIE_NAME = "sidebar:state"; const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
@@ -19,27 +21,6 @@ const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b"; const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef< const SidebarProvider = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
@@ -92,7 +73,7 @@ const SidebarProvider = React.forwardRef<
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"; const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>( const contextValue = React.useMemo<SidebarContextType>(
() => ({ () => ({
state, state,
open, open,
@@ -633,5 +614,7 @@ export {
SidebarRail, SidebarRail,
SidebarSeparator, SidebarSeparator,
SidebarTrigger, SidebarTrigger,
useSidebar,
}; };
// Re-export useSidebar from the hooks file for backwards compatibility
export { useSidebar } from "@/hooks/useSidebar";

View File

@@ -393,53 +393,3 @@ export function EntityMultiImageUploader({
</div> </div>
); );
} }
// Helper function to upload all local files
export async function uploadPendingImages(images: UploadedImage[]): Promise<UploadedImage[]> {
const uploadedImages: UploadedImage[] = [];
for (const image of images) {
if (image.isLocal && image.file) {
try {
// Get upload URL from Cloudflare
const { data: uploadData, error: uploadError } = await fetch('/api/upload-image', {
method: 'POST',
}).then(res => res.json());
if (uploadError) {
throw new Error('Failed to get upload URL');
}
// Upload to Cloudflare
const formData = new FormData();
formData.append('file', image.file);
const uploadResponse = await fetch(uploadData.uploadURL, {
method: 'POST',
body: formData,
});
if (!uploadResponse.ok) {
throw new Error('Failed to upload image');
}
const result = await uploadResponse.json();
// Return uploaded image with Cloudflare data
uploadedImages.push({
url: result.result.variants[0],
cloudflare_id: result.result.id,
caption: image.caption,
});
} catch (error) {
console.error('Error uploading image:', error);
throw error;
}
} else {
// Already uploaded, keep as is
uploadedImages.push(image);
}
}
return uploadedImages;
}

View File

@@ -67,8 +67,18 @@ export function useEntityVersions(entityType: string, entityId: string) {
// Only continue if this is still the latest request // Only continue if this is still the latest request
if (currentRequestId !== requestCounterRef.current) return; if (currentRequestId !== requestCounterRef.current) return;
// Safety check: verify data is an array before processing
if (!Array.isArray(data)) {
if (isMountedRef.current && currentRequestId === requestCounterRef.current) {
setVersions([]);
setCurrentVersion(null);
setLoading(false);
}
return;
}
// Fetch profiles separately // Fetch profiles separately
const userIds = [...new Set(data?.map(v => v.changed_by).filter(Boolean) || [])]; const userIds = [...new Set(data.map(v => v.changed_by).filter(Boolean))];
const { data: profiles } = await supabase const { data: profiles } = await supabase
.from('profiles') .from('profiles')
.select('user_id, username, avatar_url') .select('user_id, username, avatar_url')
@@ -77,8 +87,11 @@ export function useEntityVersions(entityType: string, entityId: string) {
// Check again if this is still the latest request // Check again if this is still the latest request
if (currentRequestId !== requestCounterRef.current) return; if (currentRequestId !== requestCounterRef.current) return;
const versionsWithProfiles = data?.map(v => { // Safety check: verify profiles array exists before filtering
const profile = profiles?.find(p => p.user_id === v.changed_by); const profilesArray = Array.isArray(profiles) ? profiles : [];
const versionsWithProfiles = data.map(v => {
const profile = profilesArray.find(p => p.user_id === v.changed_by);
return { return {
...v, ...v,
changer_profile: profile || { changer_profile: profile || {
@@ -90,14 +103,16 @@ export function useEntityVersions(entityType: string, entityId: string) {
// Only update state if component is still mounted and this is still the latest request // Only update state if component is still mounted and this is still the latest request
if (isMountedRef.current && currentRequestId === requestCounterRef.current) { if (isMountedRef.current && currentRequestId === requestCounterRef.current) {
setVersions(versionsWithProfiles || []); setVersions(versionsWithProfiles);
setCurrentVersion(versionsWithProfiles?.find(v => v.is_current) || null); setCurrentVersion(versionsWithProfiles.find(v => v.is_current) || null);
setLoading(false); setLoading(false);
} }
} catch (error: any) { } catch (error: any) {
console.error('Error fetching versions:', error); console.error('Error fetching versions:', error);
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error('Failed to load version history'); // Safe error message access with fallback
const errorMessage = error?.message || 'Failed to load version history';
toast.error(errorMessage);
setLoading(false); setLoading(false);
} }
} }
@@ -114,12 +129,15 @@ export function useEntityVersions(entityType: string, entityId: string) {
if (error) throw error; if (error) throw error;
if (isMountedRef.current) { if (isMountedRef.current) {
setFieldHistory(data as FieldChange[] || []); // Safety check: ensure data is an array
const fieldChanges = Array.isArray(data) ? data as FieldChange[] : [];
setFieldHistory(fieldChanges);
} }
} catch (error: any) { } catch (error: any) {
console.error('Error fetching field history:', error); console.error('Error fetching field history:', error);
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error('Failed to load field history'); const errorMessage = error?.message || 'Failed to load field history';
toast.error(errorMessage);
} }
} }
}; };
@@ -137,7 +155,8 @@ export function useEntityVersions(entityType: string, entityId: string) {
} catch (error: any) { } catch (error: any) {
console.error('Error comparing versions:', error); console.error('Error comparing versions:', error);
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error('Failed to compare versions'); const errorMessage = error?.message || 'Failed to compare versions';
toast.error(errorMessage);
} }
return null; return null;
} }
@@ -166,7 +185,8 @@ export function useEntityVersions(entityType: string, entityId: string) {
} catch (error: any) { } catch (error: any) {
console.error('Error rolling back version:', error); console.error('Error rolling back version:', error);
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error('Failed to rollback version'); const errorMessage = error?.message || 'Failed to rollback version';
toast.error(errorMessage);
} }
return null; return null;
} }
@@ -195,7 +215,8 @@ export function useEntityVersions(entityType: string, entityId: string) {
} catch (error: any) { } catch (error: any) {
console.error('Error creating version:', error); console.error('Error creating version:', error);
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error('Failed to create version'); const errorMessage = error?.message || 'Failed to create version';
toast.error(errorMessage);
} }
return null; return null;
} }

11
src/hooks/useSidebar.ts Normal file
View File

@@ -0,0 +1,11 @@
import { useContext } from "react";
import { SidebarContext } from "@/components/ui/sidebar-context";
export function useSidebar() {
const context = useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}

View File

@@ -23,13 +23,16 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// Process all images in parallel for better performance using allSettled // Process all images in parallel for better performance using allSettled
const uploadPromises = images.map(async (image, index): Promise<UploadedImageWithFlag> => { const uploadPromises = images.map(async (image, index): Promise<UploadedImageWithFlag> => {
if (image.isLocal && image.file) { if (image.isLocal && image.file) {
const fileName = image.file.name;
// Step 1: Get upload URL from our Supabase Edge Function // Step 1: Get upload URL from our Supabase Edge Function
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', { const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', {
body: { action: 'get-upload-url' } body: { action: 'get-upload-url' }
}); });
if (urlError || !uploadUrlData?.uploadURL) { if (urlError || !uploadUrlData?.uploadURL) {
throw new Error(`Failed to get upload URL for image ${index + 1}: ${urlError?.message || 'Unknown error'}`); console.error(`imageUploadHelper.uploadPendingImages: Failed to get upload URL for "${fileName}":`, urlError);
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
} }
// Step 2: Upload file directly to Cloudflare // Step 2: Upload file directly to Cloudflare
@@ -43,13 +46,15 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
const errorText = await uploadResponse.text(); const errorText = await uploadResponse.text();
throw new Error(`Upload failed for image ${index + 1} (status ${uploadResponse.status}): ${errorText}`); console.error(`imageUploadHelper.uploadPendingImages: Upload failed for "${fileName}" (status ${uploadResponse.status}):`, errorText);
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
} }
const result: CloudflareUploadResponse = await uploadResponse.json(); const result: CloudflareUploadResponse = await uploadResponse.json();
if (!result.success || !result.result) { if (!result.success || !result.result) {
throw new Error(`Cloudflare upload returned unsuccessful response for image ${index + 1}`); console.error(`imageUploadHelper.uploadPendingImages: Cloudflare upload unsuccessful for "${fileName}"`);
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
} }
// Clean up object URL // Clean up object URL
@@ -99,7 +104,7 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
// If any uploads failed, clean up ONLY newly uploaded images and throw error // If any uploads failed, clean up ONLY newly uploaded images and throw error
if (errors.length > 0) { if (errors.length > 0) {
if (newlyUploadedImageIds.length > 0) { if (newlyUploadedImageIds.length > 0) {
console.error(`Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`); console.error(`imageUploadHelper.uploadPendingImages: Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`);
// Attempt cleanup in parallel but don't throw if it fails // Attempt cleanup in parallel but don't throw if it fails
await Promise.allSettled( await Promise.allSettled(
@@ -107,7 +112,7 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
supabase.functions.invoke('upload-image', { supabase.functions.invoke('upload-image', {
body: { action: 'delete', imageId } body: { action: 'delete', imageId }
}).catch(cleanupError => { }).catch(cleanupError => {
console.error(`Failed to cleanup image ${imageId}:`, cleanupError); console.error(`imageUploadHelper.uploadPendingImages: Failed to cleanup image ${imageId}:`, cleanupError);
}) })
) )
); );