anydrop/web/src/components/DropZone.tsx
ordinarthur 9d6e4da4ae
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
feat: initial commit with full deployment setup
Includes React PWA frontend, WebSocket signaling server, shared types,
K8s manifests, Gitea CI/CD workflow, nginx config, and Dockerfiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 10:30:45 +02:00

102 lines
2.7 KiB
TypeScript

import { useState, useRef, useCallback } from "react";
interface DropZoneProps {
onFilesSelected: (files: File[]) => void;
disabled?: boolean;
}
export default function DropZone({ onFilesSelected, disabled }: DropZoneProps) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const dragCounter = useRef(0);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current++;
if (e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
dragCounter.current = 0;
if (disabled) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
onFilesSelected(files);
}
},
[onFilesSelected, disabled],
);
const handleClick = () => {
if (!disabled) {
inputRef.current?.click();
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
onFilesSelected(files);
}
// Reset to allow re-selecting the same file
e.target.value = "";
};
return (
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleClick}
className={`
border-2 border-dashed rounded-2xl p-8
flex flex-col items-center justify-center gap-3
transition-all duration-200 cursor-pointer
min-h-[160px]
${isDragging
? "border-brand-400 bg-brand-500/10 scale-[1.02]"
: disabled
? "border-slate-700 bg-slate-900/50 cursor-not-allowed opacity-50"
: "border-slate-700 hover:border-slate-500 bg-slate-900/30 hover:bg-slate-900/50"
}
`}
>
<div className="text-4xl">{isDragging ? "📥" : "📁"}</div>
<p className="text-slate-400 text-sm text-center">
{disabled
? "Sélectionnez d'abord un appareil"
: isDragging
? "Déposez vos fichiers ici"
: "Glissez des fichiers ici ou cliquez pour sélectionner"
}
</p>
<input
ref={inputRef}
type="file"
multiple
className="hidden"
onChange={handleFileChange}
/>
</div>
);
}