bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
162 lines (161 loc) • 7.43 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { FileIcon, UpdateIcon, UploadIcon } from "@radix-ui/react-icons";
import { useCallback, useState } from "react";
import { HEIGHTS } from "../../lib/layout-constants.js";
import { cn } from "../../lib/utils.js";
/**
* DropZone - Standardized drag & drop component with consistent styling
*
* Features:
* - Beautiful dashed border styling with CSS variables
* - Radix icons for consistent theming
* - Drag states with visual feedback
* - File validation and error handling
* - Fully accessible with keyboard support
* - Customizable content and styling
*
* @example
* ```tsx
* // Basic file upload
* <DropZone
* accept=".json,.txt"
* onFileSelect={(file) => processFile(file)}
* title="Import Backup"
* description="Drag & drop your backup file here"
* />
*
* // Custom data drop zone
* <DropZone
* icon={<TokensIcon />}
* title="Add Transaction Data"
* description="Drop your transaction files"
* height={200}
* />
* ```
*/
export function DropZone({ onDrop, onFileSelect, accept, multiple = false, disabled = false, loading = false, className = "", height = HEIGHTS.CARD_MIN, icon, title = "Upload Files", description = "Drag & drop or click to select", loadingText = "Processing...", dragActiveText = "Drop files here", dragActiveDescription = "Release to upload", maxSize, onError, size = "default", }) {
const [dragActive, setDragActive] = useState(false);
const validateFile = useCallback((file) => {
if (maxSize && file.size > maxSize) {
return `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`;
}
if (accept) {
const acceptedTypes = accept.split(",").map((type) => type.trim());
const isAccepted = acceptedTypes.some((type) => {
if (type.startsWith(".")) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
return file.type === type;
});
if (!isAccepted) {
return `Invalid file type. Accepted types: ${accept}`;
}
}
return null;
}, [accept, maxSize]);
const handleFiles = useCallback((fileList) => {
if (disabled || loading)
return;
const filesArray = Array.from(fileList);
// Validate files
for (const file of filesArray) {
const error = validateFile(file);
if (error) {
onError?.(error);
return;
}
}
// Handle files
if (onDrop) {
onDrop(fileList);
}
else if (onFileSelect && filesArray.length > 0) {
const firstFile = filesArray[0];
if (firstFile) {
onFileSelect(firstFile);
}
}
}, [disabled, loading, validateFile, onDrop, onFileSelect, onError]);
const handleFileSelect = useCallback((e) => {
const files = e.target.files;
if (files) {
handleFiles(files);
}
// Reset input value so same file can be selected again
e.target.value = "";
}, [handleFiles]);
const handleDrop = useCallback((e) => {
e.preventDefault();
setDragActive(false);
if (disabled || loading)
return;
const files = e.dataTransfer.files;
if (files) {
handleFiles(files);
}
}, [disabled, loading, handleFiles]);
const handleDragOver = useCallback((e) => {
e.preventDefault();
}, []);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
if (!disabled && !loading) {
setDragActive(true);
}
}, [disabled, loading]);
const handleDragLeave = useCallback((e) => {
e.preventDefault();
// Only hide drag state if leaving the entire drop zone
if (!e.currentTarget.contains(e.relatedTarget)) {
setDragActive(false);
}
}, []);
const handleKeyDown = useCallback((e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
// Trigger file input click
const input = e.currentTarget.querySelector('input[type="file"]');
if (input && !disabled && !loading) {
input.click();
}
}
}, [disabled, loading]);
const isDisabled = disabled || loading;
const currentIcon = loading ? (_jsx(UpdateIcon, { className: "animate-spin" })) : (icon || _jsx(UploadIcon, {}));
const currentTitle = loading
? loadingText
: dragActive
? dragActiveText
: title;
const currentDescription = loading
? ""
: dragActive
? dragActiveDescription
: description;
const iconSizeClasses = {
sm: "h-8 w-8",
default: "h-12 w-12",
lg: "h-16 w-16",
};
const textSizeClasses = {
sm: "text-sm",
default: "text-base",
lg: "text-lg",
};
const descriptionSizeClasses = {
sm: "text-xs",
default: "text-sm",
lg: "text-base",
};
return (_jsxs("div", { className: cn("relative grid place-items-center rounded-lg border-2 border-dashed transition-all duration-200", dragActive
? "border-primary bg-primary/10 shadow-lg shadow-primary/25"
: "border-muted-foreground/25 bg-muted/50 hover:border-muted-foreground/50", isDisabled && "cursor-not-allowed opacity-50", !isDisabled && "cursor-pointer", className), style: {
height: typeof height === "number" ? `${height}px` : height,
}, tabIndex: isDisabled ? -1 : 0,
// biome-ignore lint/a11y/useSemanticElements: Drop zone requires complex drag/drop handling that native buttons don't support
role: "button", "aria-label": `${title}. ${description}`, onDrop: handleDrop, onDragOver: handleDragOver, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onKeyDown: handleKeyDown, children: [_jsx("input", { type: "file", accept: accept, multiple: multiple, onChange: handleFileSelect, disabled: isDisabled, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" }), _jsxs("div", { className: "pointer-events-none flex flex-col items-center gap-3", children: [_jsx("div", { className: cn(iconSizeClasses[size], dragActive || loading ? "text-primary" : "text-muted-foreground"), children: currentIcon }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("p", { className: cn("font-medium", textSizeClasses[size], dragActive || loading ? "text-primary" : "text-foreground"), children: currentTitle }), currentDescription && (_jsx("p", { className: cn(descriptionSizeClasses[size], "text-muted-foreground"), children: currentDescription }))] }), accept && !loading && (_jsx("p", { className: "text-xs text-muted-foreground", children: accept }))] })] }));
}
// Preset variants for common use cases
export const FileDropZone = (props) => (_jsx(DropZone, { icon: _jsx(FileIcon, {}), title: "Select Files", description: "Drag & drop or click to browse", ...props }));
export const BackupDropZone = (props) => (_jsx(DropZone, { icon: _jsx(UploadIcon, {}), title: "Import Backup", description: "Drag & drop your backup file", accept: ".json,.txt", ...props }));