import { useEffect, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { AppHeader } from "../components/layout/AppHeader"; import { UsersIcon } from "../components/ui/Icons"; import { OrcidLogo } from "../components/ui/OrcidLogo"; import { Spinner } from "../components/ui/Spinner"; import { getInitials } from "../utils/formatters"; import { formatOrcidInput, isValidOrcid } from "../utils/orcid"; import { getOrcidAuthorizeUrl, searchResearcher } from "../services/api"; import { useAuth } from "../contexts/AuthContext"; import { AUTH_MESSAGE_TYPE, AUTH_ERROR_TYPE } from "../contexts/AuthContext"; import Footer from "../components/layout/Footer"; /** * Entry view: login con ORCID iD + búsqueda individual anónima + * buscador grupal para múltiples investigadores. * * Flujo de login: * - abre popup OAuth → sandbox.orcid.org → /callback * - recibe JWT → cierra popup → estado actualizado aquí. */ export function LandingPage() { const navigate = useNavigate(); const { isAuthenticated, userName, userOrcidId } = useAuth(); // If the JWT doesn't carry the name (ORCID sandbox omits it), fetch it // lazily from the public researcher endpoint so the identity block is correct. const [resolvedName, setResolvedName] = useState(null); useEffect(() => { if (!isAuthenticated || !userOrcidId) { setResolvedName(null); return; } if (userName) { setResolvedName(userName); return; } let cancelled = false; searchResearcher(userOrcidId) .then((bundle) => { if (!cancelled) setResolvedName(bundle.researcher?.name ?? null); }) .catch(() => {}); return () => { cancelled = true; }; }, [isAuthenticated, userOrcidId, userName]); const displayName = resolvedName ?? userName; const [orcidInput, setOrcidInput] = useState(""); const [error, setError] = useState(""); const [validating, setValidating] = useState(false); const [loginLoading, setLoginLoading] = useState(false); // Group search state const [groupTags, setGroupTags] = useState([]); const [groupRawInput, setGroupRawInput] = useState(""); const [groupError, setGroupError] = useState(""); const [groupLoading, setGroupLoading] = useState(false); const groupInputRef = useRef(null); // Cleanup refs for popup polling interval const popupRef = useRef(null); const popupTimerRef = useRef(null); // Clean up popup polling on unmount useEffect(() => { return () => { if (popupTimerRef.current) clearInterval(popupTimerRef.current); }; }, []); function handleOrcidChange(event) { setOrcidInput(formatOrcidInput(event.target.value)); if (error) setError(""); } async function handleValidate() { if (!isValidOrcid(orcidInput)) { setError( "Formato inválido. El ORCID iD debe tener la estructura: 0000-0002-1234-5678", ); return; } setValidating(true); try { const bundle = await searchResearcher(orcidInput); navigate(`/dashboard/${orcidInput}`, { state: { bundle } }); } catch (err) { toast.error("No se pudo buscar el ORCID iD", { description: err?.message ?? "Inténtalo de nuevo en unos segundos.", }); } finally { setValidating(false); } } function handleOrcidLogin() { setLoginLoading(true); const authorizeUrl = getOrcidAuthorizeUrl(); const popup = window.open( authorizeUrl, "orcid_oauth", "width=600,height=700,scrollbars=yes,resizable=yes", ); if (!popup || popup.closed) { // El navegador bloqueó el popup → hacemos redirect completo setLoginLoading(false); window.location.href = authorizeUrl; return; } popupRef.current = popup; // Escuchamos el postMessage que AuthCallbackPage envía al completar function handleMessage(event) { if (event.origin !== window.location.origin) return; if (event.data?.type === AUTH_MESSAGE_TYPE) { cleanup(); setLoginLoading(false); toast.success("Sesión iniciada con ORCID", { description: "Ya puedes ver qué publicaciones son nuevas para ti.", }); } else if (event.data?.type === AUTH_ERROR_TYPE) { cleanup(); setLoginLoading(false); toast.error("No se pudo iniciar sesión", { description: event.data.error ?? "Inténtalo de nuevo.", }); } } window.addEventListener("message", handleMessage); // Detectamos si el usuario cierra el popup manualmente antes de autenticar popupTimerRef.current = setInterval(() => { if (popup.closed) { cleanup(); setLoginLoading(false); } }, 500); function cleanup() { window.removeEventListener("message", handleMessage); if (popupTimerRef.current) { clearInterval(popupTimerRef.current); popupTimerRef.current = null; } } } /** * Splits a raw string on comma/space/newline separators, promotes valid * ORCIDs to tags (deduplicating against existing ones), and returns any * leftover invalid tokens joined by a space so the user can correct them. */ function commitRawInput(raw) { const parts = raw.split(/[\s,\n]+/).map((s) => s.trim()).filter(Boolean); const valid = parts.filter(isValidOrcid); const invalid = parts.filter((p) => !isValidOrcid(p)); if (valid.length > 0) { setGroupTags((prev) => [...new Set([...prev, ...valid])]); if (groupError) setGroupError(""); } return invalid.join(" "); } function handleGroupTagKeyDown(event) { const { key } = event; if (key === "Enter" || key === "," || key === " ") { event.preventDefault(); const leftover = commitRawInput(groupRawInput); setGroupRawInput(leftover); } else if (key === "Backspace" && groupRawInput === "" && groupTags.length > 0) { setGroupTags((prev) => prev.slice(0, -1)); } } function handleGroupRawChange(event) { setGroupRawInput(event.target.value); if (groupError) setGroupError(""); } function handleGroupPaste(event) { event.preventDefault(); const pasted = event.clipboardData.getData("text"); const combined = groupRawInput ? `${groupRawInput} ${pasted}` : pasted; const leftover = commitRawInput(combined); setGroupRawInput(leftover); } function removeGroupTag(tag) { setGroupTags((prev) => prev.filter((t) => t !== tag)); if (groupError) setGroupError(""); } async function handleGroupSearch() { if (groupTags.length === 0) { setGroupError("Introduce al menos un ORCID iD válido."); return; } setGroupError(""); setGroupLoading(true); try { navigate("/group", { state: { orcidIds: groupTags } }); } finally { setGroupLoading(false); } } function handleKeyDown(event) { if (event.key === "Enter") handleValidate(); } return (
{/* ── Hero ── */}

Tus publicaciones, listas para depositar.

Conecta tu ORCID y descárgalas en XML cuando quieras.

{/* ── Two-column grid ── */}
{/* ── Left: individual search + login ── */}
{isAuthenticated ? (
{getInitials(displayName ?? userOrcidId ?? "?")}

{displayName ?? "Mi Perfil"}

{userOrcidId ?? "—"}

Verás publicaciones nuevas marcadas en el dashboard

) : ( <>

Actualizamos tus publicaciones automáticamente cada mes.

)}
{isAuthenticated ? "TU ORCID iD" : "O INTRODUCE TU ORCID iD"}
{error && (

{error}

)}

{isAuthenticated ? "Busca un investigador o usa «Cerrar sesión» arriba." : "Pulsa «Iniciar sesión» para autenticarte, o «Buscar» de forma anónima."}

{/* ── Right: group search ── */}

Búsqueda grupal de investigadores

Pega varios ORCID iDs separados por comas, espacios o saltos de línea para buscar y comparar varios investigadores a la vez.

{/* ── Tag input area ── */}
groupInputRef.current?.focus()} className={`flex min-h-[120px] cursor-text flex-wrap content-start gap-1.5 overflow-y-auto rounded-lg border px-3 py-2.5 transition-colors ${ groupError ? "border-border-danger" : "border-surface-border-strong focus-within:border-brand-accent" }`} > {groupTags.map((tag) => ( {tag} ))}
{groupError && (

{groupError}

)}
); } export default LandingPage;