import React, { useState, useRef, useEffect } from "react";
import {
IconFile as FileType, IconMap as Map, IconDownload as Download,
IconAlertCircle as AlertCircle, IconCheck as Check, IconX as X,
IconSettings as Settings, IconCloudUpload as CloudUpload
} from "@tabler/icons-react";
import { cn } from "../lib/utils";
// --- Types ---
interface FileState {
file: File | null;
error: string | null;
}
// --- Components ---
const FileCard = ({
file,
onRemove,
icon: Icon
}: {
file: File;
onRemove: () => void;
icon: React.ElementType
}) => (
{file.name}
{(file.size / 1024 / 1024).toFixed(2)} MB
);
const Dropzone = ({
accept,
label,
sublabel,
fileState,
onFileSelect,
icon: Icon,
color = "indigo"
}: {
accept: string;
label: string;
sublabel: string;
fileState: FileState;
onFileSelect: (file: File) => void;
icon: React.ElementType;
color?: "indigo" | "purple";
}) => {
const inputRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setIsDragging(true);
} else if (e.type === "dragleave") {
setIsDragging(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
onFileSelect(e.dataTransfer.files[0]);
}
};
const bgStyles = {
indigo: "from-indigo-500/5 via-blue-500/5 to-transparent",
purple: "from-purple-500/5 via-pink-500/5 to-transparent"
};
const borderStyles = {
indigo: "group-hover:border-indigo-500/30",
purple: "group-hover:border-purple-500/30"
};
return (
!fileState.file && inputRef.current?.click()}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={cn(
"relative h-56 rounded-3xl border-2 border-dashed transition-all duration-500 flex flex-col items-center justify-center p-8 text-center cursor-pointer overflow-hidden",
isDragging
? "border-indigo-400 bg-indigo-500/10 scale-[1.02] shadow-2xl"
: "border-white/10 hover:bg-white/[0.02]",
fileState.file ? "border-solid bg-white/5 cursor-default border-white/5" : bgStyles[color],
!fileState.file && borderStyles[color]
)}
>
e.target.files?.[0] && onFileSelect(e.target.files[0])}
/>
{fileState.file ? (
onFileSelect(null as any)}
icon={Icon}
/>
Archivo cargado
) : (
<>
{label}
{sublabel}
>
)}
);
};
// --- Main App ---
export default function ClimateMapApp() {
const [pdf, setPdf] = useState(null);
const [shp, setShp] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [resultImage, setResultImage] = useState(null);
const [errorDetails, setErrorDetails] = useState(null);
const [showConfig, setShowConfig] = useState(false);
const [backendUrl, setBackendUrl] = useState(import.meta.env.PUBLIC_BACKEND_URL || "");
const resultRef = useRef(null);
// Auto-scroll to results
useEffect(() => {
if (resultImage && resultRef.current) {
resultRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, [resultImage]);
const handleProcess = async () => {
if (!pdf || !shp) return;
setIsProcessing(true);
setResultImage(null);
setErrorDetails(null);
const formData = new FormData();
formData.append("pdf", pdf);
formData.append("shp", shp);
const baseUrl = backendUrl || (typeof window !== "undefined" ? window.location.origin : "");
const endpoint = `${baseUrl.replace(/\/$/, "")}/api/procesar/`;
try {
const response = await fetch(endpoint, {
method: "POST",
body: formData,
});
if (!response.ok) {
const errJson = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(errJson.detail || "Error processing files");
}
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
// Cleanup previous ObjectURL to prevent memory leak
if (resultImage) {
URL.revokeObjectURL(resultImage);
}
setResultImage(imageUrl);
} catch (err: any) {
setErrorDetails(`${err.message} (URL: ${endpoint})`);
} finally {
setIsProcessing(false);
}
};
const handleReset = () => {
// Cleanup ObjectURL when resetting
if (resultImage) {
URL.revokeObjectURL(resultImage);
}
setPdf(null);
setShp(null);
setResultImage(null);
setErrorDetails(null);
};
return (
{/* Main Processor Card */}
{/* Toolbar */}
{/* Header */}
Generador Inteligente
Sube el informe PDF y los archivos geográficos SHP. Nuestro algoritmo unificará los datos automáticamente.
{showConfig && (
setBackendUrl(e.target.value)}
className="w-full bg-[#0b0d13] border-none rounded-lg px-4 py-3 text-white/80 placeholder:text-white/20 focus:ring-1 focus:ring-indigo-500 font-mono text-sm text-center"
placeholder="http://localhost:8000"
/>
)}
{/* Upload Grid */}
setPdf(f)}
icon={FileType}
color="indigo"
/>
setShp(f)}
icon={Map}
color="purple"
/>
{/* Action Area */}
{(!pdf || !shp) && (
Sube ambos archivos para continuar
)}
{/* Error Message */}
{errorDetails && (
Error en el proceso
{errorDetails}
)}
{/* Results View */}
{resultImage && (
{/* Result Header */}
Mapa Generado con Éxito
Zonificación completada
{/* Image Container */}
)}
);
}