BlueFin / frontend /src /components /FileDropZone.jsx
vedanshmadan21's picture
Deploy Intelli-Credit to HuggingFace Spaces
7f6dd09
import { useState } from "react";
import { Upload, X, Check, FileText, AlertCircle } from "lucide-react";
const SYMBOLS = {
annual_report: { icon: FileText, color: "#f97316" },
gst_3b: { icon: FileText, color: "#22c55e" },
gst_2a: { icon: FileText, color: "#10b981" },
gst_1: { icon: FileText, color: "#34d399" },
gst_filing: { icon: FileText, color: "#22c55e" },
bank_statement: { icon: FileText, color: "#3b82f6" },
itr: { icon: FileText, color: "#eab308" },
mca: { icon: FileText, color: "#8b5cf6" },
};
function formatSize(bytes) {
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
function truncate(str, max) {
return str.length <= max ? str : str.slice(0, max) + "…";
}
export default function FileDropZone({
fileType,
label,
description,
acceptedFormats,
required,
file,
onFileSelect,
}) {
const [isDragging, setIsDragging] = useState(false);
const [extError, setExtError] = useState(null);
const exts = acceptedFormats.split(",").map((e) => e.trim().toLowerCase());
function validate(f) {
const name = f.name.toLowerCase();
return exts.some((ext) => name.endsWith(ext));
}
function handleDragOver(e) {
e.preventDefault();
setIsDragging(true);
}
function handleDragLeave() {
setIsDragging(false);
}
function handleDrop(e) {
e.preventDefault();
setIsDragging(false);
const dropped = e.dataTransfer.files[0];
if (!dropped) return;
if (!validate(dropped)) {
setExtError(`Invalid file. Accepted: ${acceptedFormats}`);
return;
}
setExtError(null);
onFileSelect(dropped);
}
function handleInputChange(e) {
const picked = e.target.files[0];
if (!picked) return;
if (!validate(picked)) {
setExtError(`Invalid file. Accepted: ${acceptedFormats}`);
return;
}
setExtError(null);
onFileSelect(picked);
e.target.value = "";
}
function openPicker() {
document.getElementById("file-input-" + fileType).click();
}
const { icon: Icon, color } = SYMBOLS[fileType] || {
icon: FileText,
color: "#7a7a85",
};
const zoneClass = isDragging
? "dropzone dragging"
: file
? "dropzone uploaded"
: "dropzone";
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div
className={zoneClass}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={openPicker}
style={{ flex: 1, minHeight: "160px", display: "flex", flexDirection: "column", justifyContent: "center", position: "relative" }}
>
<input
id={"file-input-" + fileType}
type="file"
accept={acceptedFormats}
style={{ display: "none" }}
onChange={handleInputChange}
/>
{file ? (
<div className="flex items-center gap-md" style={{ textAlign: "left", width: "100%", position: "absolute", left: 0, padding: "0 16px" }}>
<div
style={{
width: "36px",
height: "36px",
borderRadius: "var(--radius-sm)",
background: "var(--success-subtle)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<Check size={18} style={{ color: "var(--success)" }} />
</div>
<div style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
<p
style={{
fontSize: "13px",
fontWeight: 600,
color: "var(--text-primary)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{file.name}
</p>
<p style={{ fontSize: "11px", color: "var(--text-muted)", marginTop: "2px" }}>
{formatSize(file.size)}
</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
onFileSelect(null);
}}
style={{
width: "28px",
height: "28px",
borderRadius: "var(--radius-full)",
background: "transparent",
border: "none",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--text-muted)",
transition: "all var(--transition-fast)",
flexShrink: 0,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--danger-subtle)";
e.currentTarget.style.color = "var(--danger)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.color = "var(--text-muted)";
}}
>
<X size={14} />
</button>
</div>
) : (
<div className="flex flex-col items-center gap-sm" style={{ padding: "8px 0" }}>
<div
style={{
width: "40px",
height: "40px",
borderRadius: "var(--radius-md)",
background: color + "15",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Icon size={20} style={{ color }} />
</div>
<div style={{ textAlign: "center" }}>
<p
style={{
fontSize: "13px",
fontWeight: 600,
color: "var(--text-primary)",
}}
>
{label}
</p>
<p style={{ fontSize: "11px", color: "var(--text-muted)", marginTop: "2px" }}>
{description}
</p>
<p
className="mono"
style={{ fontSize: "10px", color: "var(--text-muted)", marginTop: "4px" }}
>
{acceptedFormats}
</p>
</div>
<span
className={required ? "badge badge-danger" : "badge badge-neutral"}
style={{ fontSize: "10px" }}
>
{required ? "Required" : "Optional"}
</span>
</div>
)}
</div>
{extError && (
<p
className="flex items-center gap-xs"
style={{
color: "var(--danger)",
fontSize: "11px",
marginTop: "6px",
}}
>
<AlertCircle size={12} /> {extError}
</p>
)}
</div>
);
}