Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/files/files-table.tsx
T. Zehetbauer bbb33aa63d
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 5m43s
Workflow / ⚫️ Test (push) Has been skipped
feat: add file upload and management features; enhance pagination and permissions handling
2026-04-01 20:13:15 +02:00

197 lines
6.5 KiB
TypeScript

'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>
);
}