learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import clsx from "clsx";
2import { createSignal } from "solid-js";
3
4type FileInputProps = {
5 onFileSelect: (file: File) => void;
6 onError?: (message: string) => void;
7 accept?: string;
8 disabled?: boolean;
9 maxSize?: number; // in bytes
10};
11
12export default function FileDropZone(props: FileInputProps) {
13 const [isDragOver, setIsDragOver] = createSignal(false);
14 let inputRef: HTMLInputElement | undefined;
15
16 const validateAndSelect = (file: File) => {
17 if (props.maxSize && file.size > props.maxSize) {
18 props.onError?.(`File size exceeds limit of ${(props.maxSize / (1024 * 1024)).toFixed(0)}MB`);
19 return;
20 }
21 props.onFileSelect(file);
22 };
23
24 const handleDragOver = (e: DragEvent) => {
25 e.preventDefault();
26 if (!props.disabled) {
27 setIsDragOver(true);
28 }
29 };
30
31 const handleDragLeave = () => {
32 setIsDragOver(false);
33 };
34
35 const handleDrop = (e: DragEvent) => {
36 e.preventDefault();
37 setIsDragOver(false);
38
39 if (props.disabled) return;
40
41 if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
42 validateAndSelect(e.dataTransfer.files[0]);
43 }
44 };
45
46 const handleClick = () => {
47 if (!props.disabled) {
48 inputRef?.click();
49 }
50 };
51
52 const handleInputChange = (e: Event) => {
53 const target = e.target as HTMLInputElement;
54 if (target.files && target.files.length > 0) {
55 validateAndSelect(target.files[0]);
56 }
57 };
58
59 return (
60 <div
61 class={clsx(
62 "border-2 border-dashed rounded-xl p-8 transition-colors cursor-pointer flex flex-col items-center justify-center text-center gap-4",
63 isDragOver()
64 ? "border-accent-500 bg-accent-500/10"
65 : "border-neutral-700 hover:border-neutral-600 bg-neutral-800/50 hover:bg-neutral-800",
66 props.disabled && "opacity-50 cursor-not-allowed",
67 )}
68 onDragOver={handleDragOver}
69 onDragLeave={handleDragLeave}
70 onDrop={handleDrop}
71 onClick={handleClick}>
72 <input
73 type="file"
74 data-testid="file-upload-input"
75 ref={inputRef}
76 class="hidden"
77 accept={props.accept}
78 onChange={handleInputChange}
79 disabled={props.disabled} />
80
81 {/* TODO: replace with i-* icon */}
82 <div class="p-4 rounded-full bg-neutral-700/50">
83 <svg
84 xmlns="http://www.w3.org/2000/svg"
85 width="32"
86 height="32"
87 viewBox="0 0 24 24"
88 fill="none"
89 stroke="currentColor"
90 stroke-width="2"
91 stroke-linecap="round"
92 stroke-linejoin="round"
93 class="text-neutral-400">
94 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
95 <polyline points="17 8 12 3 7 8" />
96 <line x1="12" x2="12" y1="3" y2="15" />
97 </svg>
98 </div>
99
100 <div class="space-y-1">
101 <p class="text-lg font-medium">Click to upload or drag and drop</p>
102 <p class="text-sm text-neutral-400">PDF or DOCX (max 10MB)</p>
103 </div>
104 </div>
105 );
106}