import React, { useState, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import JSZip from 'jszip'; import { toast } from 'react-toastify'; import * as RadixDialog from '@radix-ui/react-dialog'; import { Dialog, DialogTitle, DialogDescription } from '~/components/ui/Dialog'; import { classNames } from '~/utils/classNames'; interface ImportProjectDialogProps { isOpen: boolean; onClose: () => void; onImport?: (files: Map) => void; } interface FileStructure { [path: string]: string | ArrayBuffer; } interface ImportStats { totalFiles: number; totalSize: number; fileTypes: Map; directories: Set; } const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB max per file const MAX_TOTAL_SIZE = 200 * 1024 * 1024; // 200MB max total const IGNORED_PATTERNS = [ /node_modules\//, /\.git\//, /\.next\//, /dist\//, /build\//, /\.cache\//, /\.vscode\//, /\.idea\//, /\.DS_Store$/, /Thumbs\.db$/, /\.env\.local$/, /\.env\.production$/, ]; const BINARY_EXTENSIONS = [ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.pdf', '.zip', '.tar', '.gz', '.rar', '.mp3', '.mp4', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot', ]; export const ImportProjectDialog: React.FC = ({ isOpen, onClose, onImport }) => { const [isDragging, setIsDragging] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [importProgress, setImportProgress] = useState(0); const [importStats, setImportStats] = useState(null); const [selectedFiles, setSelectedFiles] = useState({}); const [errorMessage, setErrorMessage] = useState(null); const fileInputRef = useRef(null); const dropZoneRef = useRef(null); const resetState = useCallback(() => { setSelectedFiles({}); setImportStats(null); setImportProgress(0); setErrorMessage(null); setIsProcessing(false); }, []); const shouldIgnoreFile = (path: string): boolean => { return IGNORED_PATTERNS.some((pattern) => pattern.test(path)); }; const isBinaryFile = (filename: string): boolean => { return BINARY_EXTENSIONS.some((ext) => filename.toLowerCase().endsWith(ext)); }; const formatFileSize = (bytes: number): string => { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(2)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; }; const processZipFile = async (file: File): Promise => { const zip = new JSZip(); const zipData = await zip.loadAsync(file); const files: FileStructure = {}; const stats: ImportStats = { totalFiles: 0, totalSize: 0, fileTypes: new Map(), directories: new Set(), }; const filePromises: Promise[] = []; zipData.forEach((relativePath, zipEntry) => { if (!zipEntry.dir && !shouldIgnoreFile(relativePath)) { const promise = (async () => { try { const content = await zipEntry.async(isBinaryFile(relativePath) ? 'arraybuffer' : 'string'); files[relativePath] = content; stats.totalFiles++; // Use a safe method to get uncompressed size const size = (zipEntry as any)._data?.uncompressedSize || 0; stats.totalSize += size; const ext = relativePath.split('.').pop() || 'unknown'; stats.fileTypes.set(ext, (stats.fileTypes.get(ext) || 0) + 1); const dir = relativePath.substring(0, relativePath.lastIndexOf('/')); if (dir) { stats.directories.add(dir); } setImportProgress((prev) => Math.min(prev + 100 / Object.keys(zipData.files).length, 100)); } catch (err) { console.error(`Failed to process ${relativePath}:`, err); } })(); filePromises.push(promise); } }); await Promise.all(filePromises); setImportStats(stats); return files; }; const processFileList = async (fileList: FileList): Promise => { const files: FileStructure = {}; const stats: ImportStats = { totalFiles: 0, totalSize: 0, fileTypes: new Map(), directories: new Set(), }; let totalSize = 0; for (let i = 0; i < fileList.length; i++) { const file = fileList[i]; const path = (file as any).webkitRelativePath || file.name; if (shouldIgnoreFile(path)) { continue; } if (file.size > MAX_FILE_SIZE) { toast.warning(`Skipping ${file.name}: File too large (${formatFileSize(file.size)})`); continue; } totalSize += file.size; if (totalSize > MAX_TOTAL_SIZE) { toast.error('Total size exceeds 200MB limit'); break; } try { const content = await (isBinaryFile(file.name) ? file.arrayBuffer() : file.text()); files[path] = content; stats.totalFiles++; stats.totalSize += file.size; const ext = file.name.split('.').pop() || 'unknown'; stats.fileTypes.set(ext, (stats.fileTypes.get(ext) || 0) + 1); const dir = path.substring(0, path.lastIndexOf('/')); if (dir) { stats.directories.add(dir); } setImportProgress(((i + 1) / fileList.length) * 100); } catch (err) { console.error(`Failed to read ${file.name}:`, err); } } setImportStats(stats); return files; }; const handleFileSelect = async (event: React.ChangeEvent) => { const files = event.target.files; if (!files || files.length === 0) { return; } setIsProcessing(true); setErrorMessage(null); setImportProgress(0); try { let processedFiles: FileStructure = {}; if (files.length === 1 && files[0].name.endsWith('.zip')) { processedFiles = await processZipFile(files[0]); } else { processedFiles = await processFileList(files); } if (Object.keys(processedFiles).length === 0) { toast.warning('No valid files found to import'); setIsProcessing(false); return; } setSelectedFiles(processedFiles); toast.info(`Ready to import ${Object.keys(processedFiles).length} files`); } catch (error) { console.error('Error processing files:', error); setErrorMessage(error instanceof Error ? error.message : 'Failed to process files'); toast.error('Failed to process files'); } finally { setIsProcessing(false); setImportProgress(0); } }; const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = e.dataTransfer.files; if (files.length > 0) { const input = fileInputRef.current; if (input) { const dataTransfer = new DataTransfer(); Array.from(files).forEach((file) => dataTransfer.items.add(file)); input.files = dataTransfer.files; handleFileSelect({ target: input } as any); } } }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target) { setIsDragging(false); } }, []); const getFileExtension = (filename: string): string => { const parts = filename.split('.'); return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'file'; }; const getFileIcon = (filename: string): string => { const ext = getFileExtension(filename); const iconMap: { [key: string]: string } = { js: 'i-vscode-icons:file-type-js', jsx: 'i-vscode-icons:file-type-reactjs', ts: 'i-vscode-icons:file-type-typescript', tsx: 'i-vscode-icons:file-type-reactts', css: 'i-vscode-icons:file-type-css', scss: 'i-vscode-icons:file-type-scss', html: 'i-vscode-icons:file-type-html', json: 'i-vscode-icons:file-type-json', md: 'i-vscode-icons:file-type-markdown', py: 'i-vscode-icons:file-type-python', vue: 'i-vscode-icons:file-type-vue', svg: 'i-vscode-icons:file-type-svg', git: 'i-vscode-icons:file-type-git', folder: 'i-vscode-icons:default-folder', }; return iconMap[ext] || 'i-vscode-icons:default-file'; }; const handleImportClick = useCallback(async () => { if (Object.keys(selectedFiles).length === 0) { return; } setIsProcessing(true); try { const fileMap = new Map(); for (const [path, content] of Object.entries(selectedFiles)) { if (typeof content === 'string') { fileMap.set(path, content); } else if (content instanceof ArrayBuffer) { // Convert ArrayBuffer to base64 string for binary files const bytes = new Uint8Array(content); const binary = String.fromCharCode(...bytes); const base64 = btoa(binary); fileMap.set(path, base64); } } if (onImport) { // Use the provided onImport callback await onImport(fileMap); } toast.success(`Successfully imported ${importStats?.totalFiles || 0} files`, { position: 'bottom-right', autoClose: 3000, }); resetState(); onClose(); } catch (error) { toast.error('Failed to import project', { position: 'bottom-right' }); setErrorMessage(error instanceof Error ? error.message : 'Import failed'); } finally { setIsProcessing(false); } }, [selectedFiles, importStats, onImport, onClose, resetState]); return ( !open && onClose()}>
Import Existing Project Upload your project files or drag and drop them here. Supports individual files, folders, or ZIP archives.
{!Object.keys(selectedFiles).length ? (

{isDragging ? 'Drop your project here' : 'Drag & Drop your project'}

Support for folders, multiple files, or ZIP archives

{isProcessing && (

Processing files... {Math.round(importProgress)}%

)}
{errorMessage && (

{errorMessage}

)} ) : ( {importStats && (

Total Files

{importStats.totalFiles}

Total Size

{formatFileSize(importStats.totalSize)}

Directories

{importStats.directories.size}

)}

Files to Import

{Object.keys(selectedFiles) .slice(0, 50) .map((path, index) => (
{path}
))} {Object.keys(selectedFiles).length > 50 && (
... and {Object.keys(selectedFiles).length - 50} more files
)}
)}
); };