- {/* Page header */}
-
-
-
-
-
-
-
- Búsqueda grupal
-
- {!loading && (
-
- {results.length} investigador{results.length !== 1 ? "es" : ""} encontrado{results.length !== 1 ? "s" : ""}
- {errors.length > 0 && (
-
- · {errors.length} con error
-
- )}
-
- )}
-
+ {/* Global export buttons */}
+ {!loading && results.length > 0 && (
+
+ {["xml", "zip"].map((fmt) => (
+ handleGlobalExport(fmt)}
+ disabled={globalDisabled}
+ className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-4 py-2 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-60"
+ >
+ {globalExporting === fmt ? (
+
+ ) : isAuthenticated && allNewIds.length > 0 ? (
+
+ ) : (
+
+ )}
+ {globalExporting === fmt
+ ? `Exportando ${fmt.toUpperCase()}...`
+ : `${fmt.toUpperCase()} · ${globalLabel}`}
+
+ ))}
+
+ )}
- {/* Global export buttons */}
+ {/* Loading state */}
+ {loading && (
+
+
+
+ Sincronizando {orcidIds?.length ?? "?"} investigadores con ORCID...
+
+
+ Esto puede tardar unos segundos si hay muchos perfiles nuevos.
+
+
+ )}
+
+ {/* Results grid */}
{!loading && results.length > 0 && (
-
- {["xml", "zip"].map((fmt) => (
-
handleGlobalExport(fmt)}
- disabled={globalDisabled}
- className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-4 py-2 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-60"
- >
- {globalExporting === fmt ? (
-
- ) : isAuthenticated && allNewIds.length > 0 ? (
-
- ) : (
-
- )}
- {globalExporting === fmt
- ? `Exportando ${fmt.toUpperCase()}...`
- : `${fmt.toUpperCase()} · ${globalLabel}`}
-
+
+ {results.map((bundle) => (
+
+ handleCardExport(
+ bundle.researcher?.orcid_id,
+ fmt,
+ newIds,
+ totalIds,
+ )
+ }
+ />
))}
)}
-
- {/* Loading state */}
- {loading && (
-
-
-
- Sincronizando {orcidIds?.length ?? "?"} investigadores con ORCID...
-
-
- Esto puede tardar unos segundos si hay muchos perfiles nuevos.
-
-
- )}
-
- {/* Results grid */}
- {!loading && results.length > 0 && (
-
- {results.map((bundle) => (
-
- handleCardExport(
- bundle.researcher?.orcid_id,
- fmt,
- newIds,
- totalIds,
- )
- }
- />
- ))}
-
- )}
-
- {/* Errors */}
- {!loading && errors.length > 0 && (
-
-
- ORCID iDs que no pudieron cargarse
-
-
- {errors.map((e) => (
-
-
-
-
- {e.orcid_id}
-
-
- {e.detail ?? "No se pudo obtener información de este ORCID."}
-
+ {/* Errors */}
+ {!loading && errors.length > 0 && (
+
+
+ ORCID iDs que no pudieron cargarse
+
+
+ {errors.map((e) => (
+
+
+
+
+ {e.orcid_id}
+
+
+ {e.detail ?? "No se pudo obtener información de este ORCID."}
+
+
-
- ))}
+ ))}
+
-
- )}
+ )}
- {/* Empty state */}
- {!loading && results.length === 0 && errors.length === 0 && (
-
-
-
No se encontraron resultados.
-
-
- Volver al inicio
-
-
- )}
-
+ {/* Empty state */}
+ {!loading && results.length === 0 && errors.length === 0 && (
+
+
+
No se encontraron resultados.
+
+
+ Volver al inicio
+
+
+ )}
+
+
+
);
}
diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx
index 6793a2c..8a9ce27 100644
--- a/frontend/src/pages/LandingPage.jsx
+++ b/frontend/src/pages/LandingPage.jsx
@@ -1,15 +1,17 @@
import { useEffect, useRef, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { AppHeader } from "../components/layout/AppHeader";
-import { DocumentIcon, UsersIcon } from "../components/ui/Icons";
+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 +
@@ -21,7 +23,22 @@ import { AUTH_MESSAGE_TYPE, AUTH_ERROR_TYPE } from "../contexts/AuthContext";
*/
export function LandingPage() {
const navigate = useNavigate();
- const { isAuthenticated } = useAuth();
+ 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("");
@@ -29,9 +46,11 @@ export function LandingPage() {
const [loginLoading, setLoginLoading] = useState(false);
// Group search state
- const [groupInput, setGroupInput] = useState("");
+ 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);
@@ -126,28 +145,60 @@ export function LandingPage() {
}
}
- function parseGroupOrcids(raw) {
- return raw
- .split(/[\s,\n]+/)
- .map((s) => s.trim())
- .filter(Boolean);
+ /**
+ * 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() {
- const ids = parseGroupOrcids(groupInput);
- if (ids.length === 0) {
- setGroupError("Introduce al menos un ORCID iD.");
- return;
- }
- const invalid = ids.filter((id) => !isValidOrcid(id));
- if (invalid.length > 0) {
- setGroupError(`ORCID iDs con formato incorrecto: ${invalid.join(", ")}`);
+ if (groupTags.length === 0) {
+ setGroupError("Introduce al menos un ORCID iD válido.");
return;
}
setGroupError("");
setGroupLoading(true);
try {
- navigate("/group", { state: { orcidIds: ids } });
+ navigate("/group", { state: { orcidIds: groupTags } });
} finally {
setGroupLoading(false);
}
@@ -157,32 +208,20 @@ export function LandingPage() {
if (event.key === "Enter") handleValidate();
}
- function handleGroupKeyDown(event) {
- if (event.key === "Enter" && !event.shiftKey) {
- event.preventDefault();
- handleGroupSearch();
- }
- }
-
return (
-
+
{/* ── Hero ── */}
-
-
-
-
- Tu producción científica,{" "}
- siempre al día.
+
+ Tus publicaciones, listas para depositar.
- Sincroniza tu perfil ORCID y deposita tus publicaciones
- automáticamente vía SWORD.
+ Conecta tu ORCID y descárgalas en XML cuando quieras.
@@ -190,26 +229,45 @@ export function LandingPage() {
{/* ── Left: individual search + login ── */}
-
+
{isAuthenticated ? (
-
- Sesión activa
-
- Verás publicaciones nuevas marcadas en el dashboard
-
-
- ) : (
-
- {loginLoading ? : }
- {loginLoading
- ? "Abriendo ventana de ORCID..."
- : "Iniciar sesión con ORCID"}
-
+
+ {getInitials(displayName ?? userOrcidId ?? "?")}
+
+
+
+ {displayName ?? "Mi Perfil"}
+
+
+
+ {userOrcidId ?? "—"}
+
+
+ Verás publicaciones nuevas marcadas en el dashboard
+
+
+
+ ) : (
+ <>
+
+ {loginLoading ? : }
+ {loginLoading
+ ? "Abriendo ventana de ORCID..."
+ : "Iniciar sesión con ORCID"}
+
+
+ Actualizamos tus publicaciones automáticamente cada mes.
+
+ >
)}
@@ -272,10 +330,10 @@ export function LandingPage() {
{/* ── Right: group search ── */}
-
+
-
-
+
+
Búsqueda grupal de investigadores
@@ -283,55 +341,79 @@ export function LandingPage() {
Pega varios ORCID iDs separados por comas, espacios o saltos de
línea para buscar y comparar varios investigadores a la vez.
-
- {/* ── Info chips ── */}
-
- {["ORCID OAuth 2.0", "SWORD v2", "DSpace · EPrints"].map((label) => (
-
- {label}
-
- ))}
-
-
+
);
}