feat: add file upload and management features; enhance pagination and permissions handling
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Upload } from 'lucide-react';
|
||||
|
||||
import { uploadFile } from '@kit/module-builder/actions/file-actions';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@kit/ui/dialog';
|
||||
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||
|
||||
interface FileUploadDialogProps {
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
const ACCEPTED_TYPES = [
|
||||
'image/*',
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/csv',
|
||||
'text/plain',
|
||||
'application/zip',
|
||||
].join(',');
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
export function FileUploadDialog({ accountId }: FileUploadDialogProps) {
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<{
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
base64: string;
|
||||
} | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { execute, isPending } = useActionWithToast(uploadFile, {
|
||||
successMessage: 'Datei hochgeladen',
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
setSelectedFile(null);
|
||||
setError(null);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setError(null);
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
setSelectedFile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setError('Die Datei darf maximal 10 MB groß sein.');
|
||||
setSelectedFile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
// Remove the data:...;base64, prefix
|
||||
const base64 = result.split(',')[1] ?? '';
|
||||
|
||||
setSelectedFile({
|
||||
name: file.name,
|
||||
type: file.type || 'application/octet-stream',
|
||||
size: file.size,
|
||||
base64,
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
setError('Fehler beim Lesen der Datei.');
|
||||
setSelectedFile(null);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleUpload = useCallback(() => {
|
||||
if (!selectedFile) return;
|
||||
|
||||
execute({
|
||||
accountId,
|
||||
fileName: selectedFile.name,
|
||||
fileType: selectedFile.type,
|
||||
fileSize: selectedFile.size,
|
||||
base64: selectedFile.base64,
|
||||
});
|
||||
}, [accountId, execute, selectedFile]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isPending) {
|
||||
setOpen(isOpen);
|
||||
|
||||
if (!isOpen) {
|
||||
setSelectedFile(null);
|
||||
setError(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isPending],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button size="sm" data-test="file-upload-btn">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Datei hochladen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent showCloseButton={!isPending}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Datei hochladen</DialogTitle>
|
||||
<DialogDescription>
|
||||
Wählen Sie eine Datei aus (max. 10 MB).
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={ACCEPTED_TYPES}
|
||||
onChange={handleFileSelect}
|
||||
className="file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 block w-full text-sm file:mr-4 file:rounded-md file:border-0 file:px-4 file:py-2 file:text-sm file:font-medium"
|
||||
data-test="file-upload-input"
|
||||
/>
|
||||
|
||||
{error && <p className="text-destructive text-sm">{error}</p>}
|
||||
|
||||
{selectedFile && (
|
||||
<div className="bg-muted rounded-md p-3">
|
||||
<p className="text-sm font-medium">{selectedFile.name}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{selectedFile.type} · {formatFileSize(selectedFile.size)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!selectedFile || isPending}
|
||||
data-test="file-upload-submit"
|
||||
>
|
||||
{isPending ? 'Wird hochgeladen...' : 'Hochladen'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user