Files
gridpilot.gg/apps/website/components/shared/UploadDropzone.tsx
2026-01-19 18:01:30 +01:00

175 lines
5.1 KiB
TypeScript

import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { IconButton } from '@/ui/IconButton';
import { ListItem, ListItemActions, ListItemInfo } from '@/ui/ListItem';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { AlertCircle, CheckCircle2, Upload, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
export interface UploadDropzoneProps {
onFilesSelected: (files: File[]) => void;
accept?: string;
multiple?: boolean;
maxSize?: number; // in bytes
isLoading?: boolean;
error?: string;
}
export function UploadDropzone({
onFilesSelected,
accept,
multiple = false,
maxSize,
isLoading,
error,
}: UploadDropzoneProps) {
const [isDragging, setIsDragging] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
validateAndSelectFiles(files);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
validateAndSelectFiles(files);
}
};
const validateAndSelectFiles = (files: File[]) => {
let filteredFiles = files;
if (accept) {
const acceptedTypes = accept.split(',').map(t => t.trim());
filteredFiles = filteredFiles.filter(file => {
return acceptedTypes.some(type => {
if (type.startsWith('.')) {
return file.name.endsWith(type);
}
if (type.endsWith('/*')) {
return file.type.startsWith(type.replace('/*', ''));
}
return file.type === type;
});
});
}
if (maxSize) {
filteredFiles = filteredFiles.filter(file => file.size <= maxSize);
}
if (!multiple) {
filteredFiles = filteredFiles.slice(0, 1);
}
setSelectedFiles(filteredFiles);
onFilesSelected(filteredFiles);
};
const removeFile = (index: number) => {
const newFiles = [...selectedFiles];
newFiles.splice(index, 1);
setSelectedFiles(newFiles);
onFilesSelected(newFiles);
};
return (
<Box
fullWidth
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Surface
variant="muted"
rounded="xl"
padding={8}
onClick={() => fileInputRef.current?.click()}
style={{
border: `2px dashed ${isDragging ? 'var(--ui-color-intent-primary)' : (error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)')}`,
backgroundColor: isDragging ? 'rgba(25, 140, 255, 0.05)' : 'var(--ui-color-bg-surface-muted)',
textAlign: 'center',
cursor: 'pointer'
}}
>
<Box
as="input"
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
accept={accept}
multiple={multiple}
style={{ display: 'none' }}
/>
<Box display="flex" flexDirection="col" alignItems="center" gap={4}>
<Icon
icon={isLoading ? Upload : (selectedFiles.length > 0 ? CheckCircle2 : Upload)}
size={10}
intent={isDragging ? 'primary' : (error ? 'critical' : 'low')}
animate={isLoading ? 'pulse' : 'none'}
/>
<Box>
<Text weight="bold" variant="high" size="lg" block marginBottom={1}>
{isDragging ? 'Drop files here' : 'Click or drag to upload'}
</Text>
<Text size="sm" variant="low" block>
{accept ? `Accepted formats: ${accept}` : 'All file types accepted'}
{maxSize && ` (Max ${Math.round(maxSize / 1024 / 1024)}MB)`}
</Text>
</Box>
{error && (
<Box display="flex" alignItems="center" gap={2} marginTop={2}>
<Icon icon={AlertCircle} size={4} intent="warning" />
<Text size="sm" variant="warning" weight="medium">{error}</Text>
</Box>
)}
</Box>
</Surface>
{selectedFiles.length > 0 && (
<Box marginTop={4} display="flex" flexDirection="col" gap={2}>
{selectedFiles.map((file, index) => (
<ListItem key={`${file.name}-${index}`}>
<ListItemInfo
title={file.name}
description={`${Math.round(file.size / 1024)} KB`}
/>
<ListItemActions>
<IconButton
icon={X}
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
removeFile(index);
}}
title="Remove"
/>
</ListItemActions>
</ListItem>
))}
</Box>
)}
</Box>
);
}