Dosya Yöneticisi ana klasör tools düzeltildi

This commit is contained in:
Sedat Öztürk 2025-10-26 11:59:02 +03:00
parent c93007cc07
commit f839d1fec0
4 changed files with 401 additions and 121 deletions

View file

@ -8,7 +8,6 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kurs.Platform.BlobStoring; using Kurs.Platform.BlobStoring;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp; using Volo.Abp;
@ -200,42 +199,50 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
{ {
ValidateFileName(input.Name); ValidateFileName(input.Name);
var items = await GetFolderIndexAsync(input.ParentId); var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath))
if (items.Any(x => x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase)))
{ {
throw new UserFriendlyException("A folder or file with this name already exists"); throw new UserFriendlyException("CDN path is not configured");
} }
var folderId = GenerateFileId(); var tenantId = _currentTenant.Id?.ToString() ?? "host";
var folderPath = string.IsNullOrEmpty(input.ParentId) var parentPath = Path.Combine(cdnBasePath, tenantId);
? input.Name
: $"{input.ParentId}/{input.Name}"; if (!string.IsNullOrEmpty(input.ParentId))
{
parentPath = Path.Combine(parentPath, input.ParentId);
}
var folderPath = Path.Combine(parentPath, input.Name);
// Klasör zaten var mı kontrol et
if (Directory.Exists(folderPath))
{
throw new UserFriendlyException("A folder with this name already exists");
}
// Dosya ile aynı isimde bir şey var mı kontrol et
if (File.Exists(folderPath))
{
throw new UserFriendlyException("A file with this name already exists");
}
// Klasörü oluştur
Directory.CreateDirectory(folderPath);
var metadata = new FileMetadata var metadata = new FileMetadata
{ {
Id = folderId, Id = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
Name = input.Name, Name = input.Name,
Type = "folder", Type = "folder",
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
ModifiedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow,
Path = folderPath, Path = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
ParentId = input.ParentId ?? string.Empty, ParentId = input.ParentId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = _currentTenant.Id?.ToString()
}; };
// Create folder marker blob
var folderMarkerPath = GetTenantPrefix() + folderPath + FolderMarkerSuffix;
await _blobContainer.SaveAsync(folderMarkerPath, Array.Empty<byte>());
// Create folder index
await SaveFolderIndexAsync(new List<FileMetadata>(), folderId);
// Update parent index
items.Add(metadata);
await SaveFolderIndexAsync(items, input.ParentId);
return new FileItemDto return new FileItemDto
{ {
Id = metadata.Id, Id = metadata.Id,
@ -267,18 +274,37 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var blobPath = GetTenantPrefix() + filePath; var blobPath = GetTenantPrefix() + filePath;
// Save file content // Save file content to CDN path
var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath))
{
throw new UserFriendlyException("CDN path is not configured");
}
var tenantId = _currentTenant.Id?.ToString() ?? "host";
var fullCdnPath = Path.Combine(cdnBasePath, tenantId);
if (!string.IsNullOrEmpty(input.ParentId))
{
fullCdnPath = Path.Combine(fullCdnPath, input.ParentId);
}
// Dizini oluştur
Directory.CreateDirectory(fullCdnPath);
var fullFilePath = Path.Combine(fullCdnPath, input.FileName);
long fileSize; long fileSize;
if (input.FileStream != null) if (input.FileStream != null)
{ {
input.FileStream.Position = 0; input.FileStream.Position = 0;
await _blobContainer.SaveAsync(blobPath, input.FileStream); using var fileStream = File.Create(fullFilePath);
await input.FileStream.CopyToAsync(fileStream);
fileSize = input.FileStream.Length; fileSize = input.FileStream.Length;
} }
else if (input.FileContent != null) else if (input.FileContent != null)
{ {
using var stream = new MemoryStream(input.FileContent); await File.WriteAllBytesAsync(fullFilePath, input.FileContent);
await _blobContainer.SaveAsync(blobPath, stream);
fileSize = input.FileContent.Length; fileSize = input.FileContent.Length;
} }
else else
@ -452,42 +478,48 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
public async Task DeleteItemAsync(string id) public async Task DeleteItemAsync(string id)
{ {
var metadata = await FindItemMetadataAsync(id); var cdnBasePath = _configuration["App:CdnPath"];
if (metadata == null) if (string.IsNullOrEmpty(cdnBasePath))
{ {
throw new UserFriendlyException("Item not found"); throw new UserFriendlyException("CDN path is not configured");
} }
var blobPath = GetTenantPrefix() + metadata.Path; var tenantId = _currentTenant.Id?.ToString() ?? "host";
var fullPath = Path.Combine(cdnBasePath, tenantId, id);
if (metadata.Type == "folder") if (Directory.Exists(fullPath))
{ {
// Delete folder marker and index // Klasör sil (içindeki tüm dosyalar ile birlikte)
await _blobContainer.DeleteAsync(blobPath + FolderMarkerSuffix); Directory.Delete(fullPath, recursive: true);
await _blobContainer.DeleteAsync(GetTenantPrefix() + $"{id}/{IndexFileName}"); }
else if (File.Exists(fullPath))
{
// Dosya sil
File.Delete(fullPath);
} }
else else
{ {
// Delete file throw new UserFriendlyException("Item not found");
await _blobContainer.DeleteAsync(blobPath);
} }
// Remove from parent index
var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId);
parentItems.RemoveAll(x => x.Id == id);
await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId);
} }
public async Task<Stream> DownloadFileAsync(string id) public async Task<Stream> DownloadFileAsync(string id)
{ {
var metadata = await FindItemMetadataAsync(id); var cdnBasePath = _configuration["App:CdnPath"];
if (metadata == null || metadata.Type != "file") if (string.IsNullOrEmpty(cdnBasePath))
{
throw new UserFriendlyException("CDN path is not configured");
}
var tenantId = _currentTenant.Id?.ToString() ?? "host";
var fullFilePath = Path.Combine(cdnBasePath, tenantId, id);
if (!File.Exists(fullFilePath))
{ {
throw new UserFriendlyException("File not found"); throw new UserFriendlyException("File not found");
} }
var blobPath = GetTenantPrefix() + metadata.Path; return File.OpenRead(fullFilePath);
return await _blobContainer.GetAsync(blobPath);
} }
public async Task<Stream> GetFilePreviewAsync(string id) public async Task<Stream> GetFilePreviewAsync(string id)

View file

@ -69,7 +69,7 @@ public class BlobManager : DomainService
// Default container methods (for FileManagement and other general purposes) // Default container methods (for FileManagement and other general purposes)
private IBlobContainer GetDefaultContainer() private IBlobContainer GetDefaultContainer()
{ {
return _blobContainerFactory.Create(""); return _blobContainerFactory.Create("default");
} }
public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true) public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true)

View file

@ -390,19 +390,32 @@ const FileManager = () => {
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} /> <Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
</div> </div>
{/* Content */} {/* Files Grid/List */}
{loading ? ( {loading ? (
<div className="flex justify-center items-center py-20"> <div className="flex justify-center items-center py-20">
<Spinner size={40} /> <Spinner size="lg" />
</div> </div>
) : ( ) : (
<div <>
className={classNames( {/* List View Header */}
viewMode === 'grid' {viewMode === 'list' && (
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4' <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">
: 'space-y-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 ? ( {filteredItems.length === 0 ? (
<div className="col-span-full text-center py-20"> <div className="col-span-full text-center py-20">
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" /> <FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
@ -415,17 +428,37 @@ const FileManager = () => {
<FileItem <FileItem
key={item.id} key={item.id}
item={item} item={item}
viewMode={viewMode}
selected={selectedItems.includes(item.id)} selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect} onSelect={handleItemSelect}
onDoubleClick={handleItemDoubleClick} 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} 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])} onDelete={(item) => openDeleteModal([item])}
onDownload={item.type === 'file' ? handleDownload : undefined} onDownload={item.type === 'file' ? handleDownload : undefined}
className={viewMode === 'list' ? 'flex items-center p-3 space-x-4' : undefined} onPreview={item.type === 'file' ? (item) => {
// Preview işlevi - resimler için modal açabiliriz
if (item.mimeType?.startsWith('image/')) {
// Resim preview modal'ıı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> </div>
</>
)} )}
{/* Modals */} {/* Modals */}

View file

@ -1,7 +1,8 @@
import { forwardRef, useState } from 'react' import { forwardRef, useState, useEffect } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { import {
HiFolder, HiFolder,
HiFolderPlus,
HiDocument, HiDocument,
HiPhoto, HiPhoto,
HiFilm, HiFilm,
@ -14,14 +15,16 @@ import {
HiArrowDownTray, HiArrowDownTray,
HiEye, HiEye,
} from 'react-icons/hi2' } from 'react-icons/hi2'
import { Dropdown } from '@/components/ui' // import { Dropdown } from '@/components/ui' // Artık kullanmıyoruz
import type { FileItem as FileItemType, FileActionMenuItem } from '@/types/fileManagement' import type { FileItem as FileItemType, FileActionMenuItem } from '@/types/fileManagement'
export interface FileItemProps { export interface FileItemProps {
item: FileItemType item: FileItemType
selected?: boolean selected?: boolean
viewMode?: 'grid' | 'list'
onSelect?: (item: FileItemType) => void onSelect?: (item: FileItemType) => void
onDoubleClick?: (item: FileItemType) => void onDoubleClick?: (item: FileItemType) => void
onCreateFolder?: (parentItem: FileItemType) => void
onRename?: (item: FileItemType) => void onRename?: (item: FileItemType) => void
onMove?: (item: FileItemType) => void onMove?: (item: FileItemType) => void
onDelete?: (item: FileItemType) => void onDelete?: (item: FileItemType) => void
@ -30,31 +33,33 @@ export interface FileItemProps {
className?: string className?: string
} }
const getFileIcon = (item: FileItemType) => { const getFileIcon = (item: FileItemType, large: boolean = false) => {
const iconSize = large ? "h-12 w-12" : "h-8 w-8"
if (item.type === 'folder') { if (item.type === 'folder') {
return <HiFolder className="h-8 w-8 text-blue-500" /> return <HiFolder className={`${iconSize} text-blue-500`} />
} }
const extension = item.extension?.toLowerCase() const extension = item.extension?.toLowerCase()
const mimeType = item.mimeType?.toLowerCase() const mimeType = item.mimeType?.toLowerCase()
if (mimeType?.startsWith('image/')) { if (mimeType?.startsWith('image/')) {
return <HiPhoto className="h-8 w-8 text-green-500" /> return <HiPhoto className={`${iconSize} text-green-500`} />
} }
if (mimeType?.startsWith('video/')) { if (mimeType?.startsWith('video/')) {
return <HiFilm className="h-8 w-8 text-purple-500" /> return <HiFilm className={`${iconSize} text-purple-500`} />
} }
if (mimeType?.startsWith('audio/')) { if (mimeType?.startsWith('audio/')) {
return <HiMusicalNote className="h-8 w-8 text-pink-500" /> return <HiMusicalNote className={`${iconSize} text-pink-500`} />
} }
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) { if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) {
return <HiArchiveBox className="h-8 w-8 text-orange-500" /> return <HiArchiveBox className={`${iconSize} text-orange-500`} />
} }
return <HiDocument className="h-8 w-8 text-gray-500" /> return <HiDocument className={`${iconSize} text-gray-500`} />
} }
const formatFileSize = (bytes?: number): string => { const formatFileSize = (bytes?: number): string => {
@ -67,12 +72,51 @@ const formatFileSize = (bytes?: number): string => {
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i] return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i]
} }
const formatDate = (date?: string | Date): string => {
if (!date) return ''
try {
const dateObj = typeof date === 'string' ? new Date(date) : date
return dateObj.toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
} catch {
return ''
}
}
const getFileTypeLabel = (item: FileItemType): string => {
if (item.type === 'folder') return 'Klasör'
const extension = item.extension?.toLowerCase()
const mimeType = item.mimeType?.toLowerCase()
if (mimeType?.startsWith('image/')) return 'Resim'
if (mimeType?.startsWith('video/')) return 'Video'
if (mimeType?.startsWith('audio/')) return 'Ses'
if (mimeType?.includes('pdf')) return 'PDF'
if (mimeType?.includes('word')) return 'Word'
if (mimeType?.includes('excel') || mimeType?.includes('spreadsheet')) return 'Excel'
if (mimeType?.includes('powerpoint') || mimeType?.includes('presentation')) return 'PowerPoint'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) return 'Arşiv'
if (['txt', 'md'].includes(extension || '')) return 'Metin'
if (['json', 'xml', 'css', 'js', 'ts', 'html'].includes(extension || '')) return 'Kod'
return extension?.toUpperCase() || 'Dosya'
}
const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => { const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
const { const {
item, item,
selected = false, selected = false,
viewMode = 'grid',
onSelect, onSelect,
onDoubleClick, onDoubleClick,
onCreateFolder,
onRename, onRename,
onMove, onMove,
onDelete, onDelete,
@ -92,119 +136,290 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
} }
const actionMenuItems: FileActionMenuItem[] = [ const actionMenuItems: FileActionMenuItem[] = [
// Preview - sadece dosyalar için
...(item.type === 'file' && onPreview ...(item.type === 'file' && onPreview
? [ ? [
{ {
key: 'preview', key: 'preview',
label: 'Preview', label: 'Önizle',
icon: 'HiEye', icon: 'HiEye',
onClick: () => onPreview(item), onClick: () => onPreview(item),
}, },
] ]
: []), : []),
// Download - sadece dosyalar için
...(item.type === 'file' && onDownload ...(item.type === 'file' && onDownload
? [ ? [
{ {
key: 'download', key: 'download',
label: 'Download', label: 'İndir',
icon: 'HiArrowDownTray', icon: 'HiArrowDownTray',
onClick: () => onDownload(item), onClick: () => onDownload(item),
}, },
] ]
: []), : []),
{ // Create Folder - sadece klasörler için
key: 'rename', ...(item.type === 'folder' && onCreateFolder
label: 'Rename', ? [
icon: 'HiPencil', {
onClick: () => onRename?.(item), key: 'createFolder',
}, label: 'Yeni Klasör',
{ icon: 'HiFolderPlus',
key: 'move', onClick: () => onCreateFolder(item),
label: 'Move', },
icon: 'HiArrowRightOnRectangle', ]
onClick: () => onMove?.(item), : []),
}, // Rename - her şey için
{ ...(onRename
key: 'delete', ? [
label: 'Delete', {
icon: 'HiTrash', key: 'rename',
dangerous: true, label: 'Yeniden Adlandır',
onClick: () => onDelete?.(item), icon: 'HiPencil',
}, onClick: () => onRename(item),
},
]
: []),
// Move - her şey için
...(onMove
? [
{
key: 'move',
label: 'Taşı',
icon: 'HiArrowRightOnRectangle',
onClick: () => onMove(item),
},
]
: []),
// Delete - her şey için
...(onDelete
? [
{
key: 'delete',
label: 'Sil',
icon: 'HiTrash',
dangerous: true,
onClick: () => onDelete(item),
},
]
: []),
] ]
// Debug için
useEffect(() => {
console.log('Action menu items for', item.name, ':', actionMenuItems)
console.log('Props:', { onCreateFolder: !!onCreateFolder, onRename: !!onRename, onMove: !!onMove, onDelete: !!onDelete })
}, [actionMenuItems, item.name])
const dropdownList = ( const dropdownList = (
<div className="py-1"> <div className="py-1 min-w-36">
{actionMenuItems.map((menuItem) => ( {actionMenuItems.map((menuItem) => (
<div <div
key={menuItem.key} key={menuItem.key}
className={classNames( className={classNames(
'flex items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700', 'flex items-center px-2 py-1 text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
menuItem.dangerous && 'text-red-600 dark:text-red-400', menuItem.dangerous && 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20',
)} )}
onClick={() => { onClick={() => {
menuItem.onClick() menuItem.onClick()
setDropdownOpen(false) setDropdownOpen(false)
}} }}
> >
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-2" />} {menuItem.icon === 'HiFolderPlus' && <HiFolderPlus className="h-4 w-4 mr-3 text-blue-500" />}
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-2" />} {menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-3 text-gray-500" />}
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-2" />} {menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-3 text-green-500" />}
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-3 text-orange-500" />}
{menuItem.icon === 'HiArrowRightOnRectangle' && ( {menuItem.icon === 'HiArrowRightOnRectangle' && (
<HiArrowRightOnRectangle className="h-4 w-4 mr-2" /> <HiArrowRightOnRectangle className="h-4 w-4 mr-3 text-purple-500" />
)} )}
{menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-2" />} {menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-3" />}
{menuItem.label} <span className="flex-1">{menuItem.label}</span>
</div> </div>
))} ))}
</div> </div>
) )
// Resim preview komponenti
const ImagePreview = ({ src, alt }: { src: string; alt: string }) => {
const [imageError, setImageError] = useState(false)
return (
<div className="w-full h-full bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
{!imageError ? (
<img
src={src}
alt={alt}
className="max-w-full max-h-full object-cover"
onError={() => setImageError(true)}
/>
) : (
<div className="flex items-center justify-center w-full h-full">
{getFileIcon(item, false)}
</div>
)}
</div>
)
}
if (viewMode === 'list') {
return (
<div
ref={ref}
className={classNames(
'relative group grid grid-cols-12 gap-4 p-3 border rounded-lg cursor-pointer transition-all duration-200',
'hover:border-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700/50',
selected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800',
className,
)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{/* File Icon or Preview */}
<div className="col-span-1 flex items-center">
<div className="w-8 h-8">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
) : (
<div className="w-full h-full flex items-center justify-center">
{getFileIcon(item, false)}
</div>
)}
</div>
</div>
{/* File Name */}
<div className="col-span-4 flex items-center min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{item.name}
</p>
</div>
{/* File Type */}
<div className="col-span-2 flex items-center">
<span className="text-sm text-gray-500 dark:text-gray-400">
{getFileTypeLabel(item)}
</span>
</div>
{/* File Size */}
<div className="col-span-2 flex items-center">
{item.type === 'file' && item.size ? (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formatFileSize(item.size)}
</span>
) : (
<span className="text-sm text-gray-500 dark:text-gray-400">-</span>
)}
</div>
{/* Modified Date */}
<div className="col-span-2 flex items-center">
<span className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(item.modifiedAt)}
</span>
</div>
{/* Action Menu */}
<div className="col-span-1 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity relative">
<button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-1 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
</div>
</div>
)
}
// Grid view (varsayılan)
return ( return (
<div <div
ref={ref} ref={ref}
className={classNames( className={classNames(
'relative group p-4 border rounded-lg cursor-pointer transition-all duration-200', 'relative group p-4 border rounded-xl cursor-pointer transition-all duration-300',
'hover:border-blue-300 hover:shadow-md', 'hover:border-blue-400 hover:shadow-lg hover:-translate-y-1',
selected selected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-lg'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800', : 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm',
className, className,
)} )}
onClick={handleClick} onClick={handleClick}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
> >
{/* Action Menu */} {/* Action Menu */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
<Dropdown <button
onToggle={(open) => setDropdownOpen(open || false)} className="p-1.5 rounded-full bg-white dark:bg-gray-700 shadow-md hover:shadow-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-all duration-200"
renderTitle={ onClick={(e) => {
<button e.stopPropagation()
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600" setDropdownOpen(!dropdownOpen)
onClick={(e) => { }}
e.stopPropagation()
}}
>
<HiEllipsisVertical className="h-4 w-4" />
</button>
}
> >
{dropdownList} <HiEllipsisVertical className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</Dropdown> </button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-2 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
</div> </div>
{/* File/Folder Icon */} {/* File/Folder Icon or Preview */}
<div className="flex justify-center mb-3">{getFileIcon(item)}</div> <div className="flex justify-center mb-3">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<div className="w-16 h-16">
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
</div>
) : (
<div className="flex justify-center">
{getFileIcon(item, true)}
</div>
)}
</div>
{/* File/Folder Name */} {/* File/Folder Name and Details */}
<div className="text-center"> <div className="text-center">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1">
{item.name} {item.name}
</p> </p>
{/* File Size */} {/* File Size and Type */}
{item.type === 'file' && item.size && ( {item.type === 'file' && (
<p className="text-xs text-gray-500 dark:text-gray-400">{formatFileSize(item.size)}</p> <div className="space-y-1">
<p className="text-xs text-gray-500 dark:text-gray-400">
{getFileTypeLabel(item)}
</p>
{item.size && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(item.size)}
</p>
)}
</div>
)} )}
{/* Folder Child Count */} {/* Folder Child Count */}
@ -212,7 +427,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
'childCount' in item && 'childCount' in item &&
typeof (item as any).childCount === 'number' && ( typeof (item as any).childCount === 'number' && (
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{(item as any).childCount} items {(item as any).childCount} öğe
</p> </p>
)} )}
</div> </div>