203 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|