learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 106 lines 3.0 kB view raw
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}