fischerX/apps/web/src/components/file/file-uploader.tsx

203 lines
6.1 KiB
TypeScript

'use client';
import { useState, useCallback, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { useUploadFile, useUploadMultipleFiles } from '@/hooks/use-files';
import type { FileUploadOptions } from '@fischerx/types';
interface FileUploaderProps {
onUploadSuccess?: (files: any[]) => void;
accept?: string;
maxSize?: number;
multiple?: boolean;
category?: string;
className?: string;
}
export function FileUploader({
onUploadSuccess,
accept,
maxSize = 10 * 1024 * 1024,
multiple = false,
category,
className,
}: FileUploaderProps) {
const [isDragging, setIsDragging] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
const uploadFile = useUploadFile();
const uploadMultipleFiles = useUploadMultipleFiles();
const validateFile = (file: File): string | null => {
if (maxSize && file.size > maxSize) {
return `File size must be less than ${(maxSize / 1024 / 1024).toFixed(0)}MB`;
}
if (accept) {
const acceptedTypes = accept.split(',');
const fileType = file.type;
const fileName = file.name;
const isAccepted = acceptedTypes.some(type => {
if (type.startsWith('.')) {
return fileName.toLowerCase().endsWith(type.toLowerCase());
}
if (type.endsWith('/*')) {
return fileType.startsWith(type.replace('/*', '/'));
}
return fileType === type;
});
if (!isAccepted) {
return 'File type not accepted';
}
}
return null;
};
const handleFiles = useCallback(
async (selectedFiles: FileList | File[]) => {
const files = Array.from(selectedFiles);
const errors: string[] = [];
files.forEach((file) => {
const error = validateFile(file);
if (error) {
errors.push(`${file.name}: ${error}`);
}
});
if (errors.length > 0) {
alert(errors.join('\n'));
return;
}
const options: FileUploadOptions = category ? { category } : {};
try {
setUploadProgress(0);
let result;
if (multiple && files.length > 1) {
result = await uploadMultipleFiles.mutateAsync({ files, options });
if (result.success && onUploadSuccess) {
onUploadSuccess(result.data);
}
} else if (files.length === 1) {
result = await uploadFile.mutateAsync({ file: files[0], options });
if (result.success && onUploadSuccess) {
onUploadSuccess([result.data]);
}
}
setUploadProgress(100);
setTimeout(() => setUploadProgress(0), 1000);
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed. Please try again.');
}
},
[uploadFile, uploadMultipleFiles, category, multiple, onUploadSuccess, maxSize, accept]
);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
},
[handleFiles]
);
const handleFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
handleFiles(e.target.files);
}
e.target.value = '';
},
[handleFiles]
);
const isUploading = uploadFile.isPending || uploadMultipleFiles.isPending;
return (
<Card className={className}>
<CardHeader>
<CardTitle>Upload Files</CardTitle>
</CardHeader>
<CardContent>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={cn(
'flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-lg cursor-pointer transition-colors',
isDragging
? 'border-primary bg-primary/10'
: 'border-gray-300 hover:border-primary hover:bg-gray-50'
)}
>
<input
ref={fileInputRef}
type="file"
multiple={multiple}
accept={accept}
onChange={handleFileSelect}
className="hidden"
disabled={isUploading}
/>
{isUploading ? (
<div className="text-center">
<div className="text-lg font-medium mb-2">Uploading...</div>
<div className="w-48 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
) : (
<>
<div className="text-4xl mb-4">📁</div>
<div className="text-center">
<p className="text-lg font-medium mb-1">
{isDragging ? 'Drop files here' : 'Drag and drop files here'}
</p>
<p className="text-sm text-muted-foreground mb-4">or click to browse</p>
<Button variant="default" disabled={isUploading}>
Select Files
</Button>
</div>
</>
)}
</div>
{uploadFile.error && (
<div className="mt-4 p-3 bg-destructive/10 text-destructive rounded-md">
Error uploading file: {uploadFile.error.message}
</div>
)}
{uploadMultipleFiles.error && (
<div className="mt-4 p-3 bg-destructive/10 text-destructive rounded-md">
Error uploading files: {uploadMultipleFiles.error.message}
</div>
)}
</CardContent>
</Card>
);
}