From 753efd5f07ec35aa7a127c61a5343757e9f1006f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Sun, 26 Oct 2025 03:19:18 +0300 Subject: [PATCH] =?UTF-8?q?Dosya=20Y=C3=B6neticisi=20eklendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .../Seeds/HostData.json | 61 +++ ui/src/routes/route.constant.ts | 1 + ui/src/services/fileManagement.service.ts | 119 +++++ ui/src/types/fileManagement.ts | 78 +++ ui/src/views/admin/files/FileManager.tsx | 495 ++++++++++++++++++ .../admin/files/components/Breadcrumb.tsx | 45 ++ .../views/admin/files/components/FileItem.tsx | 225 ++++++++ .../admin/files/components/FileModals.tsx | 256 +++++++++ .../files/components/FileUploadModal.tsx | 275 ++++++++++ 10 files changed, 1555 insertions(+), 1 deletion(-) create mode 100644 ui/src/services/fileManagement.service.ts create mode 100644 ui/src/types/fileManagement.ts create mode 100644 ui/src/views/admin/files/FileManager.tsx create mode 100644 ui/src/views/admin/files/components/Breadcrumb.tsx create mode 100644 ui/src/views/admin/files/components/FileItem.tsx create mode 100644 ui/src/views/admin/files/components/FileModals.tsx create mode 100644 ui/src/views/admin/files/components/FileUploadModal.tsx diff --git a/.gitignore b/.gitignore index d73f8935..79c46626 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ configs/**/data/** **/node_modules **/.DS_Store -files/ logs/ \ No newline at end of file diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json index 70fad9dd..874fdc3a 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json @@ -1815,6 +1815,12 @@ "en": "Change Log", "tr": "Versiyon Günlüğü" }, + { + "resourceName": "Platform", + "key": "App.Files", + "en": "Files", + "tr": "Dosyalar" + }, { "resourceName": "Platform", "key": "App.BlogManagement", @@ -12882,6 +12888,15 @@ "App.Reports.Categories" ] }, + { + "key": "admin.fileManagement", + "path": "/admin/files", + "componentPath": "@/views/admin/files/FileManager", + "routeType": "protected", + "authority": [ + "App.Files" + ] + }, { "key": "admin.coordinator.classroom.dashboard", "path": "/admin/coordinator/classroom/dashboard", @@ -14387,6 +14402,16 @@ "RequiredPermissionName": null, "IsDisabled": false }, + { + "ParentCode": "App.Administration", + "Code": "App.Files", + "DisplayName": "App.Files", + "Order": 5, + "Url": "/admin/files", + "Icon": "FcFolder", + "RequiredPermissionName": "App.Files", + "IsDisabled": false + }, { "ParentCode": "App.Definitions", "Code": "App.Definitions.WorkHour", @@ -19951,6 +19976,42 @@ "MultiTenancySide": 3, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Administration", + "Name": "App.Files", + "ParentName": null, + "DisplayName": "App.Files", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Files.Create", + "ParentName": "App.Files", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Files.Update", + "ParentName": "App.Files", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Files.Delete", + "ParentName": "App.Files", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Administration", "Name": "App.Definitions.Sector", diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index dde6f1b6..4d5e1cf3 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -59,6 +59,7 @@ export const ROUTES_ENUM = { activityLog: '/admin/activityLog', changeLog: '/admin/changeLog', settings: '/admin/settings', + files: '/admin/files', identity: { user: { detail: '/admin/users/detail/:userId', diff --git a/ui/src/services/fileManagement.service.ts b/ui/src/services/fileManagement.service.ts new file mode 100644 index 00000000..ab9f1914 --- /dev/null +++ b/ui/src/services/fileManagement.service.ts @@ -0,0 +1,119 @@ +import ApiService from './api.service' +import type { + FileItem, + CreateFolderRequest, + RenameItemRequest, + MoveItemRequest, + DeleteItemRequest, + UploadFileRequest, +} from '@/types/fileManagement' + +class FileManagementService { + // Get files and folders for a specific directory + async getItems(folderId?: string): Promise<{ data: { items: FileItem[] } }> { + const params = folderId ? { parentId: folderId } : {} + + return ApiService.fetchData<{ items: FileItem[] }>({ + url: `/api/files`, + method: 'GET', + params, + }) + } + + // Create a new folder + async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> { + return ApiService.fetchData({ + url: `/api/files/folders`, + method: 'POST', + data: request as any, + }) + } + + // Upload a file + async uploadFile(request: UploadFileRequest): Promise<{ data: FileItem }> { + const formData = new FormData() + formData.append('file', request.file) + if (request.parentId) { + formData.append('parentId', request.parentId) + } + + return ApiService.fetchData({ + url: `/api/files/upload`, + method: 'POST', + data: formData as any, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + } + + // Rename a file or folder + async renameItem(request: RenameItemRequest): Promise<{ data: FileItem }> { + return ApiService.fetchData({ + url: `/api/files/${request.id}/rename`, + method: 'PUT', + data: { name: request.newName }, + }) + } + + // Move a file or folder + async moveItem(request: MoveItemRequest): Promise<{ data: FileItem }> { + return ApiService.fetchData({ + url: `/api/files/${request.itemId}/move`, + method: 'PUT', + data: { targetFolderId: request.targetFolderId }, + }) + } + + // Delete a file or folder + async deleteItem(request: DeleteItemRequest): Promise { + await ApiService.fetchData({ + url: `/api/files/${request.id}`, + method: 'DELETE', + }) + } + + // Download a file + async downloadFile(fileId: string): Promise { + const response = await ApiService.fetchData({ + url: `/api/files/${fileId}/download`, + method: 'GET', + responseType: 'blob', + }) + return response.data + } + + // Get file preview/thumbnail + async getFilePreview(fileId: string): Promise { + const response = await ApiService.fetchData({ + url: `/api/files/${fileId}/preview`, + method: 'GET', + responseType: 'blob', + }) + return URL.createObjectURL(response.data) + } + + // Search files and folders + async searchItems(query: string, folderId?: string): Promise<{ data: { items: FileItem[] } }> { + const params = { + q: query, + ...(folderId && { parentId: folderId }), + } + + return ApiService.fetchData<{ items: FileItem[] }>({ + url: `/api/files/search`, + method: 'GET', + params, + }) + } + + // Get folder breadcrumb path + async getFolderPath(folderId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> { + return ApiService.fetchData<{ path: Array<{ id: string; name: string }> }>({ + url: `/api/files/folders/${folderId || 'root'}/path`, + method: 'GET', + }) + } +} + +export default new FileManagementService() \ No newline at end of file diff --git a/ui/src/types/fileManagement.ts b/ui/src/types/fileManagement.ts new file mode 100644 index 00000000..a96218d5 --- /dev/null +++ b/ui/src/types/fileManagement.ts @@ -0,0 +1,78 @@ +export interface FileItem { + id: string + name: string + type: 'file' | 'folder' + size?: number + mimeType?: string + createdAt: Date + modifiedAt: Date + parentId?: string + path: string + isDirectory: boolean + extension?: string +} + +export interface FolderItem extends Omit { + type: 'folder' + childCount?: number +} + +export interface CreateFolderRequest { + name: string + parentId?: string +} + +export interface RenameItemRequest { + id: string + newName: string +} + +export interface MoveItemRequest { + itemId: string + targetFolderId?: string +} + +export interface DeleteItemRequest { + id: string +} + +export interface UploadFileRequest { + file: File + parentId?: string +} + +export interface FileManagerState { + items: FileItem[] + currentPath: string[] + currentFolderId?: string + loading: boolean + uploading: boolean + selectedItems: string[] +} + +export interface BreadcrumbItem { + id?: string + name: string + path: string +} + +export interface FileActionMenuItem { + key: string + label: string + icon?: string + disabled?: boolean + dangerous?: boolean + onClick: () => void +} + +export type ViewMode = 'grid' | 'list' + +export type SortBy = 'name' | 'size' | 'type' | 'modified' +export type SortOrder = 'asc' | 'desc' + +export interface FileManagerFilters { + searchTerm?: string + fileType?: string + sortBy: SortBy + sortOrder: SortOrder +} \ No newline at end of file diff --git a/ui/src/views/admin/files/FileManager.tsx b/ui/src/views/admin/files/FileManager.tsx new file mode 100644 index 00000000..7d8864f7 --- /dev/null +++ b/ui/src/views/admin/files/FileManager.tsx @@ -0,0 +1,495 @@ +import { useState, useEffect, useCallback } from 'react' +import { Helmet } from 'react-helmet' +import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' +import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa' +import AdaptableCard from '@/components/shared/AdaptableCard' +import Container from '@/components/shared/Container' +import { useLocalization } from '@/utils/hooks/useLocalization' +import fileManagementService from '@/services/fileManagement.service' +import Breadcrumb from './components/Breadcrumb' +import FileItem from './components/FileItem' +import FileUploadModal from './components/FileUploadModal' +import { CreateFolderModal, RenameItemModal, DeleteConfirmModal } from './components/FileModals' +import type { + FileItem as FileItemType, + BreadcrumbItem, + ViewMode, + SortBy, + SortOrder, + FileManagerFilters, +} from '@/types/fileManagement' +import classNames from 'classnames' + +// Select options for sorting + +const FileManager = () => { + const { translate } = useLocalization() + + // State + const [loading, setLoading] = useState(true) + const [items, setItems] = useState([]) + const [filteredItems, setFilteredItems] = useState([]) + const [currentFolderId, setCurrentFolderId] = useState() + const [breadcrumbItems, setBreadcrumbItems] = useState([ + { name: 'Files', path: '', id: undefined }, + ]) + const [selectedItems, setSelectedItems] = useState([]) + const [viewMode, setViewMode] = useState('grid') + const [filters, setFilters] = useState({ + searchTerm: '', + sortBy: 'name', + sortOrder: 'asc', + }) + + // Modal states + const [uploadModalOpen, setUploadModalOpen] = useState(false) + const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false) + const [renameModalOpen, setRenameModalOpen] = useState(false) + const [deleteModalOpen, setDeleteModalOpen] = useState(false) + const [itemToRename, setItemToRename] = useState() + const [itemsToDelete, setItemsToDelete] = useState([]) + + // Loading states + const [uploading, setUploading] = useState(false) + const [creating, setCreating] = useState(false) + const [renaming, setRenaming] = useState(false) + const [deleting, setDeleting] = useState(false) + + // Fetch items from API + const fetchItems = useCallback(async (folderId?: string) => { + try { + setLoading(true) + const response = await fileManagementService.getItems(folderId) + setItems(response.data.items) + } catch (error) { + console.error('Failed to fetch items:', error) + toast.push(Failed to load files and folders) + } finally { + setLoading(false) + } + }, []) + + // Fetch breadcrumb path + const fetchBreadcrumb = useCallback(async (folderId?: string) => { + try { + if (!folderId) { + setBreadcrumbItems([{ name: 'Files', path: '', id: undefined }]) + return + } + + const response = await fileManagementService.getFolderPath(folderId) + const pathItems: BreadcrumbItem[] = [ + { name: 'Files', path: '', id: undefined }, + ...response.data.path.map((item) => ({ + name: item.name, + path: item.id, + id: item.id, + })), + ] + setBreadcrumbItems(pathItems) + } catch (error) { + console.error('Failed to fetch breadcrumb:', error) + } + }, []) + + // Initial load + useEffect(() => { + fetchItems(currentFolderId) + fetchBreadcrumb(currentFolderId) + }, [currentFolderId, fetchItems, fetchBreadcrumb]) + + // Filter and sort items + useEffect(() => { + let filtered = [...items] + + // Apply search filter + if (filters.searchTerm) { + filtered = filtered.filter((item) => + item.name.toLowerCase().includes(filters.searchTerm!.toLowerCase()), + ) + } + + // Apply sorting + filtered.sort((a, b) => { + let comparison = 0 + + switch (filters.sortBy) { + case 'name': + comparison = a.name.localeCompare(b.name) + break + case 'size': + comparison = (a.size || 0) - (b.size || 0) + break + case 'type': + comparison = a.type.localeCompare(b.type) + break + case 'modified': + comparison = new Date(a.modifiedAt).getTime() - new Date(b.modifiedAt).getTime() + break + } + + return filters.sortOrder === 'desc' ? -comparison : comparison + }) + + // Folders first + filtered.sort((a, b) => { + if (a.type === 'folder' && b.type === 'file') return -1 + if (a.type === 'file' && b.type === 'folder') return 1 + return 0 + }) + + setFilteredItems(filtered) + }, [items, filters]) + + // Navigation handlers + const handleBreadcrumbNavigate = (breadcrumb: BreadcrumbItem) => { + setCurrentFolderId(breadcrumb.id) + setSelectedItems([]) + } + + const handleItemSelect = (item: FileItemType) => { + setSelectedItems((prev) => { + if (prev.includes(item.id)) { + return prev.filter((id) => id !== item.id) + } else { + return [...prev, item.id] + } + }) + } + + const handleItemDoubleClick = (item: FileItemType) => { + if (item.type === 'folder') { + setCurrentFolderId(item.id) + setSelectedItems([]) + } + } + + // File operations + const handleUploadFiles = async (files: File[]) => { + try { + setUploading(true) + for (const file of files) { + await fileManagementService.uploadFile({ + file, + parentId: currentFolderId, + }) + } + await fetchItems(currentFolderId) + toast.push(Files uploaded successfully) + } catch (error) { + console.error('Upload failed:', error) + toast.push(Failed to upload files) + throw error + } finally { + setUploading(false) + } + } + + const handleCreateFolder = async (name: string) => { + try { + setCreating(true) + await fileManagementService.createFolder({ + name, + parentId: currentFolderId, + }) + await fetchItems(currentFolderId) + toast.push(Folder created successfully) + } catch (error) { + console.error('Create folder failed:', error) + toast.push(Failed to create folder) + throw error + } finally { + setCreating(false) + } + } + + const handleRenameItem = async (newName: string) => { + if (!itemToRename) return + + try { + setRenaming(true) + await fileManagementService.renameItem({ + id: itemToRename.id, + newName, + }) + await fetchItems(currentFolderId) + toast.push(Item renamed successfully) + } catch (error) { + console.error('Rename failed:', error) + toast.push(Failed to rename item) + throw error + } finally { + setRenaming(false) + } + } + + const handleDeleteItems = async () => { + try { + setDeleting(true) + for (const item of itemsToDelete) { + await fileManagementService.deleteItem({ id: item.id }) + } + await fetchItems(currentFolderId) + setSelectedItems([]) + toast.push(Items deleted successfully) + } catch (error) { + console.error('Delete failed:', error) + toast.push(Failed to delete items) + throw error + } finally { + setDeleting(false) + } + } + + const handleDownload = async (item: FileItemType) => { + try { + const blob = await fileManagementService.downloadFile(item.id) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = item.name + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + console.error('Download failed:', error) + toast.push(Failed to download file) + } + } + + // Action handlers + const openRenameModal = (item: FileItemType) => { + setItemToRename(item) + setRenameModalOpen(true) + } + + const openDeleteModal = (items: FileItemType[]) => { + setItemsToDelete(items) + setDeleteModalOpen(true) + } + + const handleDeleteSelected = () => { + const itemsToDelete = items.filter((item) => selectedItems.includes(item.id)) + openDeleteModal(itemsToDelete) + } + + const goUpOneLevel = () => { + if (breadcrumbItems.length > 1) { + const parentBreadcrumb = breadcrumbItems[breadcrumbItems.length - 2] + handleBreadcrumbNavigate(parentBreadcrumb) + } + } + + return ( + <> + + File Manager + + + {/* Header */} +
+
+

Files

+

+ Upload, organize, and manage files in your application +

+
+
+ + {/* Toolbar */} +
+
+ + + {breadcrumbItems.length > 1 && ( + + )} + {selectedItems.length > 0 && ( + + )} +
+ +
+ {/* Search */} +
+ setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))} + prefix={} + className="w-64" + /> +
+ + {/* Sort */} + setFolderName(e.target.value)} + placeholder="Enter folder name" + autoFocus + className="mt-2" + /> + + + +
+ + +
+
+ + ) + }, +) + +CreateFolderModal.displayName = 'CreateFolderModal' + +// Rename Item Modal +export interface RenameItemModalProps { + isOpen: boolean + onClose: () => void + onRename: (newName: string) => Promise + item?: FileItem + loading?: boolean +} + +export const RenameItemModal = forwardRef((props, ref) => { + const { isOpen, onClose, onRename, item, loading = false } = props + const [newName, setNewName] = useState('') + const [error, setError] = useState('') + + // Update input when item changes + useEffect(() => { + if (item) { + setNewName(item.name) + } + }, [item]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!newName.trim()) { + setError(`${item?.type === 'folder' ? 'Folder' : 'File'} name is required`) + return + } + + if (newName.trim() === item?.name) { + onClose() + return + } + + try { + await onRename(newName.trim()) + setError('') + onClose() + } catch (err) { + setError(`Failed to rename ${item?.type}`) + } + } + + const handleClose = () => { + setNewName('') + setError('') + onClose() + } + + if (!item) return null + + return ( + +
+
+

+ Rename {item.type === 'folder' ? 'Folder' : 'File'} +

+
+ +
+ + setNewName(e.target.value)} + placeholder={`Enter ${item.type} name`} + autoFocus + /> + +
+ +
+ + +
+
+
+ ) +}) + +RenameItemModal.displayName = 'RenameItemModal' + +// Delete Confirmation Modal +export interface DeleteConfirmModalProps { + isOpen: boolean + onClose: () => void + onDelete: () => Promise + items: FileItem[] + loading?: boolean +} + +export const DeleteConfirmModal = forwardRef( + (props, ref) => { + const { isOpen, onClose, onDelete, items, loading = false } = props + + const handleDelete = async () => { + try { + await onDelete() + onClose() + } catch (err) { + // Error handling is done in parent component + } + } + + const folderCount = items.filter((item) => item.type === 'folder').length + const fileCount = items.filter((item) => item.type === 'file').length + + return ( + +
+
+

Delete Items

+
+ +
+

+ Are you sure you want to delete the following items? This action cannot be undone. +

+ +
+ {folderCount > 0 && ( +

+ {folderCount} folder{folderCount > 1 ? 's' : ''} +

+ )} + {fileCount > 0 && ( +

+ {fileCount} file{fileCount > 1 ? 's' : ''} +

+ )} +
+ + {items.length === 1 && ( +
+

+ {items[0].name} +

+
+ )} +
+ +
+ + +
+
+
+ ) + }, +) + +DeleteConfirmModal.displayName = 'DeleteConfirmModal' diff --git a/ui/src/views/admin/files/components/FileUploadModal.tsx b/ui/src/views/admin/files/components/FileUploadModal.tsx new file mode 100644 index 00000000..13f87a5a --- /dev/null +++ b/ui/src/views/admin/files/components/FileUploadModal.tsx @@ -0,0 +1,275 @@ +import { forwardRef, useState, useCallback, useRef } from 'react' +import { Dialog, Button, Progress } from '@/components/ui' +import { HiCloudArrowUp, HiXMark } from 'react-icons/hi2' +import classNames from 'classnames' + +export interface FileUploadModalProps { + isOpen: boolean + onClose: () => void + onUpload: (files: File[]) => Promise + currentFolderId?: string + loading?: boolean + className?: string +} + +interface UploadFileWithProgress extends File { + id: string + progress: number + status: 'pending' | 'uploading' | 'completed' | 'error' + error?: string +} + +const FileUploadModal = forwardRef((props, ref) => { + const { isOpen, onClose, onUpload, currentFolderId, loading = false, className } = props + + const [uploadFiles, setUploadFiles] = useState([]) + const [isDragOver, setIsDragOver] = useState(false) + const [uploading, setUploading] = useState(false) + const fileInputRef = useRef(null) + + const generateFileId = () => Math.random().toString(36).substr(2, 9) + + const formatFileSize = (bytes: number): string => { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + if (bytes === 0) return '0 B' + + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i] + } + + const handleFilesSelect = useCallback((files: FileList | File[]) => { + const fileArray = Array.from(files) + const newFiles: UploadFileWithProgress[] = fileArray.map((file) => ({ + ...file, + id: generateFileId(), + progress: 0, + status: 'pending' as const, + })) + + setUploadFiles((prev) => [...prev, ...newFiles]) + }, []) + + const handleFileInputChange = (e: React.ChangeEvent) => { + if (e.target.files) { + handleFilesSelect(e.target.files) + } + } + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + + if (e.dataTransfer.files) { + handleFilesSelect(e.dataTransfer.files) + } + }, + [handleFilesSelect], + ) + + const removeFile = (fileId: string) => { + setUploadFiles((prev) => prev.filter((f) => f.id !== fileId)) + } + + const handleUpload = async () => { + if (uploadFiles.length === 0) return + + setUploading(true) + const filesToUpload = uploadFiles.filter((f) => f.status === 'pending') + + try { + // Simulate upload progress for demo - replace with actual upload logic + for (const file of filesToUpload) { + setUploadFiles((prev) => + prev.map((f) => (f.id === file.id ? { ...f, status: 'uploading' as const } : f)), + ) + + // Simulate progress + for (let progress = 0; progress <= 100; progress += 10) { + await new Promise((resolve) => setTimeout(resolve, 100)) + setUploadFiles((prev) => prev.map((f) => (f.id === file.id ? { ...f, progress } : f))) + } + + setUploadFiles((prev) => + prev.map((f) => (f.id === file.id ? { ...f, status: 'completed' as const } : f)), + ) + } + + // Call the actual upload function + await onUpload(filesToUpload) + + // Close modal after successful upload + setTimeout(() => { + onClose() + setUploadFiles([]) + }, 1000) + } catch (error) { + console.error('Upload failed:', error) + setUploadFiles((prev) => + prev.map((f) => + f.status === 'uploading' ? { ...f, status: 'error' as const, error: 'Upload failed' } : f, + ), + ) + } finally { + setUploading(false) + } + } + + const handleClose = () => { + if (!uploading) { + setUploadFiles([]) + onClose() + } + } + + const totalFiles = uploadFiles.length + const completedFiles = uploadFiles.filter((f) => f.status === 'completed').length + const hasError = uploadFiles.some((f) => f.status === 'error') + + return ( + +
+
+

Upload Files

+
+ +
+ {/* Upload Area */} +
+ + + +

+ Choose files or drag and drop here +

+

+ Select one or more files to upload +

+ +
+ + {/* File List */} + {uploadFiles.length > 0 && ( +
+

+ Files to upload ({totalFiles}) +

+
+ {uploadFiles.map((file) => ( +
+
+

+ {file.name} +

+

+ {formatFileSize(file.size)} +

+ + {/* Progress Bar */} + {file.status === 'uploading' && ( +
+ +
+ )} + + {/* Status Messages */} + {file.status === 'completed' && ( +

+ Upload completed +

+ )} + {file.status === 'error' && ( +

+ {file.error || 'Upload failed'} +

+ )} +
+ + {file.status === 'pending' && ( +
+ ))} +
+
+ )} +
+ + {/* Footer */} +
+
+ {uploading && ( + + Uploading {completedFiles}/{totalFiles} files... + + )} + {!uploading && completedFiles > 0 && !hasError && ( + All files uploaded successfully! + )} + {hasError && Some files failed to upload} +
+ +
+ + +
+
+
+
+ ) +}) + +FileUploadModal.displayName = 'FileUploadModal' + +export default FileUploadModal