Commits all remaining uncommitted local work: - apps/web: fischerei, verband, modules, members-cms, documents, newsletter, meetings, site-builder, courses, bookings, events, finance pages and components - apps/web: marketing page updates, layout, paths config, next.config.mjs, styles/makerkit.css - apps/web/i18n: documents, fischerei, marketing, verband (de+en) - packages/features: finance, fischerei, member-management, module-builder, newsletter, sitzungsprotokolle, verbandsverwaltung server APIs and components - packages/ui: button.tsx updates - pnpm-lock.yaml
209 lines
6.8 KiB
TypeScript
209 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback } from 'react';
|
|
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
|
|
import { Download, FileIcon } from 'lucide-react';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
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 t = useTranslations('common');
|
|
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="overflow-x-auto rounded-md border">
|
|
<table className="w-full min-w-[640px] text-sm">
|
|
<thead>
|
|
<tr className="bg-muted/50 border-b">
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Dateiname
|
|
</th>
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Typ
|
|
</th>
|
|
<th scope="col" className="p-3 text-right font-medium">
|
|
Größe
|
|
</th>
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Hochgeladen
|
|
</th>
|
|
<th scope="col" 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={t('deleteFile')}
|
|
description={t('deleteFileConfirm')}
|
|
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>
|
|
);
|
|
}
|