import { useCallback, useEffect, useState } from "react"; import { useParams, Navigate } from "react-router-dom"; import { toast } from "sonner"; import { AppHeader } from "../components/layout/AppHeader"; import { ResearcherCard } from "../components/dashboard/ResearcherCard"; import { StatsRow } from "../components/dashboard/StatsRow"; import { PublicationsTable } from "../components/dashboard/PublicationsTable"; import { ExportDropdown } from "../components/dashboard/ExportDropdown"; import { SyncButton } from "../components/dashboard/SyncButton"; import { downloadExport, getExportUrl, getPublications, syncResearcher, validateOrcid, } from "../services/api"; import { isValidOrcid } from "../utils/orcid"; const SUCCESS_FLASH_MS = 3000; /** * Researcher detail page. Owns: * - Initial researcher lookup (validate + publications fetch on mount). * - Sync workflow (POST + refresh + success toast). * - Export workflow (download blob + success/error toast). */ export function DashboardPage() { const { orcid } = useParams(); const [researcher, setResearcher] = useState(null); const [publications, setPublications] = useState([]); const [pubsLoading, setPubsLoading] = useState(true); const [pubsError, setPubsError] = useState(null); const [syncStatus, setSyncStatus] = useState("idle"); // idle | loading | success const [exportingFormat, setExportingFormat] = useState(null); const loadResearcher = useCallback( async (signal) => { try { const data = await validateOrcid(orcid, { signal }); if (!signal?.aborted) setResearcher(data); } catch (err) { if (signal?.aborted) return; toast.error("No se pudo cargar el investigador", { description: err?.message ?? "Error desconocido.", }); } }, [orcid], ); const loadPublications = useCallback( async (signal) => { setPubsLoading(true); setPubsError(null); try { const data = await getPublications(orcid, { signal }); if (!signal?.aborted) setPublications(data); } catch (err) { if (signal?.aborted) return; setPubsError(err); } finally { if (!signal?.aborted) setPubsLoading(false); } }, [orcid], ); useEffect(() => { if (!isValidOrcid(orcid)) return; const ctrl = new AbortController(); loadResearcher(ctrl.signal); loadPublications(ctrl.signal); return () => ctrl.abort(); }, [orcid, loadResearcher, loadPublications]); if (!isValidOrcid(orcid)) { return ; } async function handleSync() { setSyncStatus("loading"); try { const updated = await syncResearcher(orcid); if (updated) setResearcher(updated); await loadPublications(); setSyncStatus("success"); toast.success("Sincronización completada", { description: "Las publicaciones se han actualizado desde ORCID.", }); setTimeout(() => setSyncStatus("idle"), SUCCESS_FLASH_MS); } catch (err) { setSyncStatus("idle"); toast.error("Error al sincronizar con ORCID", { description: err?.message ?? "Inténtalo de nuevo más tarde.", }); } } async function handleExport(format) { setExportingFormat(format); try { const { blob, url } = await downloadExport(orcid, format); if (blob) { const objectUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = objectUrl; anchor.download = `sword-${orcid}.${format}`; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(objectUrl); } toast.success(`Exportación ${format.toUpperCase()} completada`, { description: url ?? getExportUrl(orcid, format), }); } catch (err) { toast.error(`Error al exportar ${format.toUpperCase()}`, { description: err?.message ?? "No se pudo generar el fichero.", }); } finally { setExportingFormat(null); } } return (
{researcher ? ( } /> ) : ( )} loadPublications()} />
Datos obtenidos vía ORCID Public API v3.0
{["ORCID OAuth 2.0", "SWORD v2", "Dublin Core"].map((t) => ( {t} ))}
); } function ResearcherSkeleton() { return (
); } export default DashboardPage;