506 lines
16 KiB
TypeScript
506 lines
16 KiB
TypeScript
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 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<FileItemType[]>([])
|
||
const [filteredItems, setFilteredItems] = useState<FileItemType[]>([])
|
||
const [currentFolderId, setCurrentFolderId] = useState<string | undefined>()
|
||
const [breadcrumbItems, setBreadcrumbItems] = useState<BreadcrumbItem[]>([
|
||
{ name: 'Files', path: '', id: undefined },
|
||
])
|
||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
||
const [filters, setFilters] = useState<FileManagerFilters>({
|
||
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<FileItemType | undefined>()
|
||
const [itemsToDelete, setItemsToDelete] = useState<FileItemType[]>([])
|
||
|
||
// 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)
|
||
// Backend returns GetFilesDto which has Items property
|
||
setItems(response.data.items || [])
|
||
} catch (error) {
|
||
console.error('Failed to fetch items:', error)
|
||
toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
|
||
} 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(<Notification type="success">Files uploaded successfully</Notification>)
|
||
} catch (error) {
|
||
console.error('Upload failed:', error)
|
||
toast.push(<Notification type="danger">Failed to upload files</Notification>)
|
||
throw error
|
||
} finally {
|
||
setUploading(false)
|
||
}
|
||
}
|
||
|
||
const handleCreateFolder = async (name: string) => {
|
||
try {
|
||
setCreating(true)
|
||
await fileManagementService.createFolder({
|
||
name,
|
||
parentId: currentFolderId,
|
||
})
|
||
await fetchItems(currentFolderId)
|
||
toast.push(<Notification type="success">Folder created successfully</Notification>)
|
||
} catch (error) {
|
||
console.error('Create folder failed:', error)
|
||
toast.push(<Notification type="danger">Failed to create folder</Notification>)
|
||
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(<Notification type="success">Item renamed successfully</Notification>)
|
||
} catch (error) {
|
||
console.error('Rename failed:', error)
|
||
toast.push(<Notification type="danger">Failed to rename item</Notification>)
|
||
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(<Notification type="success">Items deleted successfully</Notification>)
|
||
} catch (error) {
|
||
console.error('Delete failed:', error)
|
||
toast.push(<Notification type="danger">Failed to delete items</Notification>)
|
||
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(<Notification type="danger">Failed to download file</Notification>)
|
||
}
|
||
}
|
||
|
||
// Action handlers
|
||
const openRenameModal = (item: FileItemType) => {
|
||
setItemToRename(item)
|
||
setRenameModalOpen(true)
|
||
}
|
||
|
||
const openDeleteModal = (items: FileItemType[]) => {
|
||
setItemsToDelete(items)
|
||
setDeleteModalOpen(true)
|
||
}
|
||
|
||
const goUpOneLevel = () => {
|
||
if (breadcrumbItems.length > 1) {
|
||
const parentBreadcrumb = breadcrumbItems[breadcrumbItems.length - 2]
|
||
handleBreadcrumbNavigate(parentBreadcrumb)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Container>
|
||
<Helmet
|
||
titleTemplate="%s | Sözsoft Kurs Platform"
|
||
title={translate('::' + 'App.Files')}
|
||
defaultTitle="Sözsoft Kurs Platform"
|
||
></Helmet>
|
||
|
||
{/* Toolbar */}
|
||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-4 mt-2">
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="solid"
|
||
icon={<FaCloudUploadAlt />}
|
||
onClick={() => setUploadModalOpen(true)}
|
||
>
|
||
Upload Files
|
||
</Button>
|
||
<Button
|
||
variant="default"
|
||
icon={<FaFolder />}
|
||
onClick={() => setCreateFolderModalOpen(true)}
|
||
>
|
||
Create Folder
|
||
</Button>
|
||
{breadcrumbItems.length > 1 && (
|
||
<Button variant="default" icon={<FaArrowUp />} onClick={goUpOneLevel}>
|
||
Go Up
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-4">
|
||
{/* Search */}
|
||
<div className="flex items-center">
|
||
<Input
|
||
size="sm"
|
||
placeholder="Search files..."
|
||
value={filters.searchTerm}
|
||
onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))}
|
||
prefix={<FaSearch className="text-gray-400" />}
|
||
className="w-64"
|
||
/>
|
||
</div>
|
||
|
||
{/* Sort */}
|
||
<Select
|
||
size="sm"
|
||
options={[
|
||
{ value: 'name-asc', label: 'Name (A-Z)' },
|
||
{ value: 'name-desc', label: 'Name (Z-A)' },
|
||
{ value: 'size-asc', label: 'Size (Small to Large)' },
|
||
{ value: 'size-desc', label: 'Size (Large to Small)' },
|
||
{ value: 'modified-desc', label: 'Modified (Newest)' },
|
||
{ value: 'modified-asc', label: 'Modified (Oldest)' },
|
||
]}
|
||
value={{
|
||
value: `${filters.sortBy}-${filters.sortOrder}`,
|
||
label: (() => {
|
||
const sortOptions = {
|
||
'name-asc': 'Name (A-Z)',
|
||
'name-desc': 'Name (Z-A)',
|
||
'size-asc': 'Size (Small to Large)',
|
||
'size-desc': 'Size (Large to Small)',
|
||
'modified-desc': 'Modified (Newest)',
|
||
'modified-asc': 'Modified (Oldest)',
|
||
}
|
||
return sortOptions[
|
||
`${filters.sortBy}-${filters.sortOrder}` as keyof typeof sortOptions
|
||
]
|
||
})(),
|
||
}}
|
||
onChange={(option) => {
|
||
if (option && 'value' in option) {
|
||
const [sortBy, sortOrder] = (option.value as string).split('-') as [
|
||
SortBy,
|
||
SortOrder,
|
||
]
|
||
setFilters((prev) => ({ ...prev, sortBy, sortOrder }))
|
||
}
|
||
}}
|
||
/>
|
||
|
||
{/* View Mode */}
|
||
<div className="flex border border-gray-300 dark:border-gray-600 rounded">
|
||
<Button
|
||
variant="plain"
|
||
size="sm"
|
||
icon={<FaTh />}
|
||
className={classNames(
|
||
'rounded-r-none border-r',
|
||
viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
|
||
)}
|
||
onClick={() => setViewMode('grid')}
|
||
/>
|
||
<Button
|
||
variant="plain"
|
||
size="sm"
|
||
icon={<FaList />}
|
||
className={classNames(
|
||
'rounded-l-none',
|
||
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
|
||
)}
|
||
onClick={() => setViewMode('list')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Breadcrumb */}
|
||
<div className="mb-6">
|
||
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
||
</div>
|
||
|
||
{/* Files Grid/List */}
|
||
{loading ? (
|
||
<div className="flex justify-center items-center py-20">
|
||
<Spinner size="lg" />
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* List View Header */}
|
||
{viewMode === 'list' && (
|
||
<div className="grid grid-cols-12 gap-4 px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 border-b dark:border-gray-700 mb-2">
|
||
<div className="col-span-1"></div> {/* Icon column */}
|
||
<div className="col-span-4">İsim</div>
|
||
<div className="col-span-2">Tür</div>
|
||
<div className="col-span-2">Boyut</div>
|
||
<div className="col-span-2">Değiştirilme</div>
|
||
<div className="col-span-1"></div> {/* Actions column */}
|
||
</div>
|
||
)}
|
||
|
||
<div
|
||
className={classNames(
|
||
viewMode === 'grid'
|
||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
|
||
: 'space-y-1',
|
||
)}
|
||
>
|
||
{filteredItems.length === 0 ? (
|
||
<div className="col-span-full text-center py-20">
|
||
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
|
||
<p className="text-gray-500 dark:text-gray-400">
|
||
{filters.searchTerm ? 'No files match your search' : 'This folder is empty'}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
filteredItems.map((item) => (
|
||
<FileItem
|
||
key={item.id}
|
||
item={item}
|
||
viewMode={viewMode}
|
||
selected={selectedItems.includes(item.id)}
|
||
onSelect={handleItemSelect}
|
||
onDoubleClick={handleItemDoubleClick}
|
||
onCreateFolder={(parentItem) => {
|
||
// Klasör içinde yeni klasör oluşturmak için parent klasörü set et
|
||
setCurrentFolderId(parentItem.id)
|
||
setCreateFolderModalOpen(true)
|
||
}}
|
||
onRename={openRenameModal}
|
||
onMove={(item) => {
|
||
// Move işlevi henüz implement edilmedi
|
||
toast.push(<Notification type="info">Move özelliği yakında eklenecek</Notification>)
|
||
}}
|
||
onDelete={(item) => openDeleteModal([item])}
|
||
onDownload={item.type === 'file' ? handleDownload : undefined}
|
||
onPreview={item.type === 'file' ? (item) => {
|
||
// Preview işlevi - resimler için modal açabiliriz
|
||
if (item.mimeType?.startsWith('image/')) {
|
||
// Resim preview modal'ı açılabilir
|
||
toast.push(<Notification type="info">Resim önizleme özelliği yakında eklenecek</Notification>)
|
||
} else {
|
||
// Diğer dosya tipleri için download
|
||
handleDownload(item)
|
||
}
|
||
} : undefined}
|
||
/>
|
||
))
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Modals */}
|
||
<FileUploadModal
|
||
isOpen={uploadModalOpen}
|
||
onClose={() => setUploadModalOpen(false)}
|
||
onUpload={handleUploadFiles}
|
||
currentFolderId={currentFolderId}
|
||
loading={uploading}
|
||
/>
|
||
|
||
<CreateFolderModal
|
||
isOpen={createFolderModalOpen}
|
||
onClose={() => setCreateFolderModalOpen(false)}
|
||
onCreate={handleCreateFolder}
|
||
loading={creating}
|
||
currentFolderId={currentFolderId}
|
||
/>
|
||
|
||
<RenameItemModal
|
||
isOpen={renameModalOpen}
|
||
onClose={() => {
|
||
setRenameModalOpen(false)
|
||
setItemToRename(undefined)
|
||
}}
|
||
onRename={handleRenameItem}
|
||
item={itemToRename}
|
||
loading={renaming}
|
||
/>
|
||
|
||
<DeleteConfirmModal
|
||
isOpen={deleteModalOpen}
|
||
onClose={() => {
|
||
setDeleteModalOpen(false)
|
||
setItemsToDelete([])
|
||
}}
|
||
onDelete={handleDeleteItems}
|
||
items={itemsToDelete}
|
||
loading={deleting}
|
||
/>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default FileManager
|