feat: add file upload and management features; enhance pagination and permissions handling
This commit is contained in:
196
apps/web/app/[locale]/home/[account]/files/files-table.tsx
Normal file
196
apps/web/app/[locale]/home/[account]/files/files-table.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Download, FileIcon } from 'lucide-react';
|
||||
|
||||
import { deleteFile } from '@kit/module-builder/actions/file-actions';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||
|
||||
import { DeleteConfirmButton } from './delete-confirm-button';
|
||||
|
||||
interface FileRecord {
|
||||
id: string;
|
||||
file_name: string;
|
||||
original_name: string;
|
||||
mime_type: string;
|
||||
file_size: number;
|
||||
created_at: string;
|
||||
storage_path: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
interface FilesTableProps {
|
||||
files: FileRecord[];
|
||||
pagination: { total: number; page: number; pageSize: number };
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString('de-AT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function getMimeLabel(mimeType: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'application/pdf': 'PDF',
|
||||
'image/jpeg': 'JPEG',
|
||||
'image/png': 'PNG',
|
||||
'image/gif': 'GIF',
|
||||
'image/webp': 'WebP',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
'DOCX',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
|
||||
'text/csv': 'CSV',
|
||||
'text/plain': 'TXT',
|
||||
'application/zip': 'ZIP',
|
||||
};
|
||||
|
||||
return map[mimeType] ?? mimeType.split('/').pop()?.toUpperCase() ?? 'Datei';
|
||||
}
|
||||
|
||||
export function FilesTable({ files, pagination }: FilesTableProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { total, page, pageSize } = pagination;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
|
||||
const { execute: executeDelete, isPending: isDeleting } = useActionWithToast(
|
||||
deleteFile,
|
||||
{
|
||||
successMessage: 'Datei gelöscht',
|
||||
onSuccess: () => router.refresh(),
|
||||
},
|
||||
);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('page', String(newPage));
|
||||
router.push(`?${params.toString()}`);
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dateien ({total})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{files.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<FileIcon className="text-muted-foreground mb-4 h-12 w-12" />
|
||||
<h3 className="text-lg font-semibold">Keine Dateien vorhanden</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Laden Sie Ihre erste Datei hoch.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Dateiname</th>
|
||||
<th className="p-3 text-left font-medium">Typ</th>
|
||||
<th className="p-3 text-right font-medium">Größe</th>
|
||||
<th className="p-3 text-left font-medium">Hochgeladen</th>
|
||||
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr key={file.id} className="hover:bg-muted/30 border-b">
|
||||
<td className="max-w-[300px] truncate p-3 font-medium">
|
||||
{file.original_name}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge variant="secondary">
|
||||
{getMimeLabel(file.mime_type)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="text-muted-foreground p-3 text-right">
|
||||
{formatFileSize(file.file_size)}
|
||||
</td>
|
||||
<td className="text-muted-foreground p-3">
|
||||
{formatDate(file.created_at)}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<a
|
||||
href={file.publicUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download={file.original_name}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-test="file-download-btn"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</a>
|
||||
<DeleteConfirmButton
|
||||
title="Datei löschen"
|
||||
description="Möchten Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
isPending={isDeleting}
|
||||
onConfirm={() => executeDelete({ fileId: file.id })}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
data-test="files-prev-page"
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
data-test="files-next-page"
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user