mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 14:51:13 -05:00
Fix photo uploads
This commit is contained in:
@@ -1,16 +1,12 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import Uppy from '@uppy/core';
|
|
||||||
import Dashboard from '@uppy/dashboard';
|
|
||||||
import XHRUpload from '@uppy/xhr-upload';
|
|
||||||
import ImageEditor from '@uppy/image-editor';
|
|
||||||
import { DashboardModal } from '@uppy/react';
|
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Upload, X, Eye, Loader2 } from 'lucide-react';
|
import { Upload, X, Eye, Loader2, CheckCircle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { DragDropZone } from './DragDropZone';
|
import { DragDropZone } from './DragDropZone';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
|
||||||
interface UppyPhotoUploadProps {
|
interface UppyPhotoUploadProps {
|
||||||
onUploadComplete?: (urls: string[]) => void;
|
onUploadComplete?: (urls: string[]) => void;
|
||||||
@@ -63,282 +59,170 @@ export function UppyPhotoUpload({
|
|||||||
showPreview = true,
|
showPreview = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
enableDragDrop = true,
|
enableDragDrop = true,
|
||||||
showUploadModal = true,
|
|
||||||
}: UppyPhotoUploadProps) {
|
}: UppyPhotoUploadProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
|
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const uppyRef = useRef<Uppy | null>(null);
|
const [currentFileName, setCurrentFileName] = useState('');
|
||||||
const uploadedImagesRef = useRef<string[]>([]);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
const validateFile = (file: File): string | null => {
|
||||||
// Initialize Uppy
|
const maxSize = maxSizeMB * 1024 * 1024;
|
||||||
const uppy = new Uppy({
|
if (file.size > maxSize) {
|
||||||
restrictions: {
|
return `File "${file.name}" exceeds ${maxSizeMB}MB limit`;
|
||||||
maxNumberOfFiles: maxFiles,
|
}
|
||||||
allowedFileTypes,
|
|
||||||
maxFileSize: maxSizeMB * 1024 * 1024,
|
|
||||||
},
|
|
||||||
autoProceed: false,
|
|
||||||
allowMultipleUploadBatches: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Dashboard plugin
|
const allowedTypes = allowedFileTypes.map(type =>
|
||||||
uppy.use(Dashboard, {
|
type.replace('*', '').replace('/', '')
|
||||||
inline: false,
|
);
|
||||||
trigger: null,
|
|
||||||
hideProgressDetails: false,
|
|
||||||
proudlyDisplayPoweredByUppy: false,
|
|
||||||
showRemoveButtonAfterComplete: true,
|
|
||||||
showSelectedFiles: true,
|
|
||||||
note: `Images up to ${maxSizeMB}MB, max ${maxFiles} files`,
|
|
||||||
theme: 'auto',
|
|
||||||
closeAfterFinish: true,
|
|
||||||
closeModalOnClickOutside: true,
|
|
||||||
disablePageScrollWhenModalOpen: true,
|
|
||||||
animateOpenClose: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Image Editor plugin
|
if (!allowedTypes.includes('image') && !allowedFileTypes.includes('image/*')) {
|
||||||
uppy.use(ImageEditor, {
|
const fileType = file.type.split('/')[0];
|
||||||
quality: 0.8,
|
if (!allowedTypes.includes(fileType)) {
|
||||||
cropperOptions: {
|
return `File type "${file.type}" is not allowed`;
|
||||||
viewMode: 1,
|
|
||||||
background: false,
|
|
||||||
autoCropArea: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add XHR Upload plugin (configured per file in beforeUpload)
|
|
||||||
uppy.use(XHRUpload, {
|
|
||||||
endpoint: '', // Will be set dynamically
|
|
||||||
method: 'POST',
|
|
||||||
formData: true,
|
|
||||||
bundle: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Before upload hook - fetch upload URL for each file
|
|
||||||
uppy.on('files-added', async (files) => {
|
|
||||||
if (disabled) {
|
|
||||||
uppy.cancelAll();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setIsUploading(true);
|
return null;
|
||||||
setUploadProgress(0);
|
};
|
||||||
onUploadStart?.();
|
|
||||||
|
|
||||||
// Process all files in parallel to get upload URLs
|
const uploadSingleFile = async (file: File): Promise<string> => {
|
||||||
const uploadPromises = files.map(async (file) => {
|
setCurrentFileName(file.name);
|
||||||
try {
|
|
||||||
const response = await supabase.functions.invoke('upload-image', {
|
|
||||||
body: { metadata, variant },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.error) {
|
// Step 1: Get upload URL from Supabase edge function
|
||||||
throw new Error(response.error.message);
|
const urlResponse = await supabase.functions.invoke('upload-image', {
|
||||||
}
|
body: { metadata, variant },
|
||||||
|
});
|
||||||
|
|
||||||
const result: CloudflareResponse = response.data;
|
if (urlResponse.error) {
|
||||||
|
throw new Error(`Failed to get upload URL: ${urlResponse.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Store Cloudflare ID in file meta
|
const { uploadURL, id: cloudflareId }: CloudflareResponse = urlResponse.data;
|
||||||
uppy.setFileMeta(file.id, {
|
|
||||||
...file.meta,
|
|
||||||
cloudflareId: result.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure XHR upload endpoint for this file
|
// Step 2: Upload file directly to Cloudflare
|
||||||
const xhrPlugin = uppy.getPlugin('XHRUpload') as any;
|
const formData = new FormData();
|
||||||
if (xhrPlugin) {
|
formData.append('file', file);
|
||||||
xhrPlugin.setOptions({
|
|
||||||
endpoint: result.uploadURL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, fileId: file.id };
|
const uploadResponse = await fetch(uploadURL, {
|
||||||
} catch (error) {
|
method: 'POST',
|
||||||
console.error('Failed to get upload URL:', error);
|
body: formData,
|
||||||
uppy.removeFile(file.id);
|
});
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
if (!uploadResponse.ok) {
|
||||||
title: 'Upload Error',
|
throw new Error(`Cloudflare upload failed: ${uploadResponse.statusText}`);
|
||||||
description: `Failed to prepare upload for ${file.name}`,
|
}
|
||||||
});
|
|
||||||
onUploadError?.(error as Error);
|
// Step 3: Poll for processing completion
|
||||||
return { success: false, fileId: file.id };
|
let attempts = 0;
|
||||||
}
|
const maxAttempts = 30;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
const statusResponse = await supabase.functions.invoke('upload-image', {
|
||||||
|
body: { id: cloudflareId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all URL fetching to complete
|
if (!statusResponse.error && statusResponse.data) {
|
||||||
const results = await Promise.all(uploadPromises);
|
const status: UploadSuccessResponse = statusResponse.data;
|
||||||
|
if (status.uploaded && status.urls) {
|
||||||
// Check if any files are ready to upload
|
return status.urls.public;
|
||||||
const successfulFiles = results.filter(r => r.success);
|
|
||||||
if (successfulFiles.length > 0) {
|
|
||||||
// All URLs are fetched, now trigger the upload
|
|
||||||
uppy.upload();
|
|
||||||
} else {
|
|
||||||
// No files ready, reset state
|
|
||||||
setIsUploading(false);
|
|
||||||
setUploadProgress(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle upload progress
|
|
||||||
uppy.on('progress', (progress) => {
|
|
||||||
setUploadProgress(Math.round(progress));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle upload success
|
|
||||||
uppy.on('upload-success', async (file, response) => {
|
|
||||||
try {
|
|
||||||
const cloudflareId = file?.meta?.cloudflareId as string;
|
|
||||||
if (!cloudflareId) {
|
|
||||||
throw new Error('Missing Cloudflare ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll for upload completion with the correct ID
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 30; // 30 seconds max
|
|
||||||
let imageData: UploadSuccessResponse | null = null;
|
|
||||||
|
|
||||||
while (attempts < maxAttempts) {
|
|
||||||
const statusResponse = await supabase.functions.invoke('upload-image', {
|
|
||||||
body: { id: cloudflareId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!statusResponse.error && statusResponse.data) {
|
|
||||||
const status: UploadSuccessResponse = statusResponse.data;
|
|
||||||
if (status.uploaded && status.urls) {
|
|
||||||
imageData = status;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
attempts++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageData?.urls) {
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
const newUrl = imageData.urls.public;
|
attempts++;
|
||||||
uploadedImagesRef.current = [...uploadedImagesRef.current, newUrl];
|
|
||||||
setUploadedImages(prev => [...prev, newUrl]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload post-processing failed:', error);
|
|
||||||
onUploadError?.(error as Error);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Handle upload error
|
throw new Error('Upload processing timeout');
|
||||||
uppy.on('upload-error', (file, error, response) => {
|
};
|
||||||
console.error('Upload error:', error);
|
|
||||||
setIsUploading(false);
|
|
||||||
|
|
||||||
// Check if it's an expired URL error and retry
|
const handleFiles = async (files: File[]) => {
|
||||||
if (error.message?.includes('expired') || response?.status === 400) {
|
if (disabled || isUploading) return;
|
||||||
|
|
||||||
|
// Validate file count
|
||||||
|
const remainingSlots = maxFiles - uploadedImages.length;
|
||||||
|
if (files.length > remainingSlots) {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'Too Many Files',
|
||||||
|
description: `You can only upload ${remainingSlots} more file(s). Maximum is ${maxFiles}.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each file
|
||||||
|
for (const file of files) {
|
||||||
|
const error = validateFile(file);
|
||||||
|
if (error) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Upload URL Expired',
|
variant: 'destructive',
|
||||||
description: 'Retrying upload...',
|
title: 'Invalid File',
|
||||||
|
description: error,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Retry the upload by re-adding the file
|
setIsUploading(true);
|
||||||
if (file) {
|
setUploadProgress(0);
|
||||||
uppy.removeFile(file.id);
|
onUploadStart?.();
|
||||||
// Re-add the file to trigger new URL fetch
|
|
||||||
uppy.addFile({
|
const newUrls: string[] = [];
|
||||||
source: 'retry',
|
const totalFiles = files.length;
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
for (let i = 0; i < files.length; i++) {
|
||||||
data: file.data,
|
const file = files[i];
|
||||||
meta: { ...file.meta, cloudflareId: undefined },
|
try {
|
||||||
});
|
const url = await uploadSingleFile(file);
|
||||||
}
|
newUrls.push(url);
|
||||||
} else {
|
setUploadProgress(Math.round(((i + 1) / totalFiles) * 100));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Upload failed for ${file.name}:`, error);
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: 'Upload Failed',
|
title: 'Upload Failed',
|
||||||
description: error.message || 'An error occurred during upload',
|
description: `Failed to upload "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
});
|
});
|
||||||
onUploadError?.(error);
|
onUploadError?.(error as Error);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Handle upload complete
|
if (newUrls.length > 0) {
|
||||||
uppy.on('complete', (result) => {
|
const allUrls = [...uploadedImages, ...newUrls];
|
||||||
setIsUploading(false);
|
setUploadedImages(allUrls);
|
||||||
setUploadProgress(0);
|
onUploadComplete?.(allUrls);
|
||||||
|
|
||||||
if (result.successful.length > 0) {
|
|
||||||
// Use ref to get current uploaded images
|
|
||||||
onUploadComplete?.(uploadedImagesRef.current);
|
|
||||||
toast({
|
|
||||||
title: 'Upload Complete',
|
|
||||||
description: `Successfully uploaded ${result.successful.length} image(s)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear Uppy's file list for next batch
|
|
||||||
uppy.cancelAll();
|
|
||||||
}
|
|
||||||
setIsModalOpen(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
uppyRef.current = uppy;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
uppy.destroy();
|
|
||||||
};
|
|
||||||
}, [maxFiles, maxSizeMB, allowedFileTypes, metadata, variant, disabled, onUploadStart, onUploadComplete, onUploadError, toast, size]);
|
|
||||||
|
|
||||||
const handleDragDropFiles = async (files: File[]) => {
|
|
||||||
if (!uppyRef.current || disabled) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Add files to Uppy - the files-added event will handle URL fetching and upload
|
|
||||||
files.forEach((file) => {
|
|
||||||
try {
|
|
||||||
uppyRef.current?.addFile({
|
|
||||||
source: 'drag-drop',
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
data: file,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle duplicate file error gracefully
|
|
||||||
if (error.message?.includes('duplicate')) {
|
|
||||||
console.log(`Skipping duplicate file: ${file.name}`);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't call upload() here - the files-added handler will trigger it after URLs are ready
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding files:', error);
|
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
title: 'Upload Complete',
|
||||||
title: 'Error',
|
description: `Successfully uploaded ${newUrls.length} image(s)`,
|
||||||
description: 'Failed to add files for upload',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setCurrentFileName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleFiles(files);
|
||||||
|
}
|
||||||
|
// Reset input
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerFileSelect = () => {
|
||||||
|
if (!disabled && !isUploading) {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeImage = (index: number) => {
|
const removeImage = (index: number) => {
|
||||||
const newUrls = uploadedImages.filter((_, i) => i !== index);
|
const newUrls = uploadedImages.filter((_, i) => i !== index);
|
||||||
uploadedImagesRef.current = newUrls;
|
|
||||||
setUploadedImages(newUrls);
|
setUploadedImages(newUrls);
|
||||||
if (onUploadComplete) {
|
onUploadComplete?.(newUrls);
|
||||||
onUploadComplete(newUrls);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
|
||||||
if (!disabled) {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSizeClasses = () => {
|
const getSizeClasses = () => {
|
||||||
@@ -355,7 +239,7 @@ export function UppyPhotoUpload({
|
|||||||
const renderUploadTrigger = () => {
|
const renderUploadTrigger = () => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return (
|
return (
|
||||||
<div onClick={handleOpenModal} className="cursor-pointer">
|
<div onClick={triggerFileSelect} className="cursor-pointer">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -367,7 +251,7 @@ export function UppyPhotoUpload({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenModal}
|
onClick={triggerFileSelect}
|
||||||
disabled={disabled || isUploading}
|
disabled={disabled || isUploading}
|
||||||
className={cn(baseClasses, sizeClasses, disabledClasses)}
|
className={cn(baseClasses, sizeClasses, disabledClasses)}
|
||||||
size={size === 'compact' ? 'sm' : size === 'large' ? 'lg' : 'default'}
|
size={size === 'compact' ? 'sm' : size === 'large' ? 'lg' : 'default'}
|
||||||
@@ -431,18 +315,43 @@ export function UppyPhotoUpload({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderLoadingState = () => {
|
||||||
|
if (!isUploading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 p-4 bg-muted/50 rounded-lg border border-border space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||||
|
<span className="text-sm font-medium">Uploading...</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{uploadProgress}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{currentFileName && (
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{currentFileName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Progress value={uploadProgress} className="h-2" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (enableDragDrop && !children) {
|
if (enableDragDrop && !children) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<DragDropZone
|
<DragDropZone
|
||||||
onFilesAdded={handleDragDropFiles}
|
onFilesAdded={handleFiles}
|
||||||
maxFiles={maxFiles}
|
maxFiles={maxFiles}
|
||||||
maxSizeMB={maxSizeMB}
|
maxSizeMB={maxSizeMB}
|
||||||
allowedFileTypes={allowedFileTypes}
|
allowedFileTypes={allowedFileTypes}
|
||||||
disabled={disabled || isUploading}
|
disabled={disabled || isUploading}
|
||||||
className="min-h-[200px]"
|
className="min-h-[200px]"
|
||||||
/>
|
/>
|
||||||
|
{renderLoadingState()}
|
||||||
{renderPreview()}
|
{renderPreview()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -452,17 +361,18 @@ export function UppyPhotoUpload({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{enableDragDrop && children ? (
|
{enableDragDrop && children ? (
|
||||||
<DragDropZone
|
<DragDropZone
|
||||||
onFilesAdded={handleDragDropFiles}
|
onFilesAdded={handleFiles}
|
||||||
maxFiles={maxFiles}
|
maxFiles={maxFiles}
|
||||||
maxSizeMB={maxSizeMB}
|
maxSizeMB={maxSizeMB}
|
||||||
allowedFileTypes={allowedFileTypes}
|
allowedFileTypes={allowedFileTypes}
|
||||||
disabled={disabled}
|
disabled={disabled || isUploading}
|
||||||
>
|
>
|
||||||
{renderUploadTrigger()}
|
{renderUploadTrigger()}
|
||||||
</DragDropZone>
|
</DragDropZone>
|
||||||
) : (
|
) : (
|
||||||
renderUploadTrigger()
|
renderUploadTrigger()
|
||||||
)}
|
)}
|
||||||
|
{renderLoadingState()}
|
||||||
{renderPreview()}
|
{renderPreview()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -470,18 +380,15 @@ export function UppyPhotoUpload({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept={allowedFileTypes.join(',')}
|
||||||
|
multiple={maxFiles > 1}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|
||||||
{uppyRef.current && showUploadModal && (
|
|
||||||
<DashboardModal
|
|
||||||
uppy={uppyRef.current}
|
|
||||||
open={isModalOpen}
|
|
||||||
onRequestClose={() => setIsModalOpen(false)}
|
|
||||||
closeModalOnClickOutside
|
|
||||||
animateOpenClose
|
|
||||||
browserBackButtonClose
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user