diff --git a/frontend/public/uja-logo.png b/frontend/public/uja-logo.png new file mode 100644 index 0000000..77d17fd Binary files /dev/null and b/frontend/public/uja-logo.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 17b6d82..76683e6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,11 +24,21 @@ export default function App() { ); diff --git a/frontend/src/components/layout/AppHeader.jsx b/frontend/src/components/layout/AppHeader.jsx index 029e66c..a846ae6 100644 --- a/frontend/src/components/layout/AppHeader.jsx +++ b/frontend/src/components/layout/AppHeader.jsx @@ -30,7 +30,7 @@ export function AppHeader({ variant = "landing" }) { {/* Brand — always navigates home */} ORCID2SWORD diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx new file mode 100644 index 0000000..c8b7654 --- /dev/null +++ b/frontend/src/components/layout/Footer.jsx @@ -0,0 +1,86 @@ +export default function Footer() { + const technologies = ["ORCID OAuth 2.0", "SWORD v2", "DSpace", "EPrints", "Dublin Core"]; + + return ( + + ); + } \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 32818c8..704dfbe 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -55,6 +55,12 @@ --color-tag-default-text: #5F5E5A; --color-tag-default-border: #D3D1C7; + /* Error (hue-0° mirrors of the ORCID green palette — same HSL lightness & saturation) */ + --color-error-vivid: #CE3939; + --color-error-soft: #F3DDDD; + --color-error-border: #DD9797; + --color-error-text: #6E1111; + /* Fonts */ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index cdd9a17..276d7e9 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -3,6 +3,7 @@ import { useLocation, useParams, Navigate } from "react-router-dom"; import { toast } from "sonner"; import { AppHeader } from "../components/layout/AppHeader"; +import Footer from "../components/layout/Footer"; import { ResearcherCard } from "../components/dashboard/ResearcherCard"; import { StatsRow } from "../components/dashboard/StatsRow"; import { PublicationsTable } from "../components/dashboard/PublicationsTable"; @@ -196,53 +197,42 @@ export function DashboardPage() { return (
+
+
+ {researcher ? ( + + + + + } + /> + ) : ( + + )} -
- {researcher ? ( - - - - - } + + + loadBundle()} + selectedIds={selectedIds} + onSelectedIdsChange={setSelectedIds} + isAuthenticated={isAuthenticated} /> - ) : ( - - )} - - - - loadBundle()} - selectedIds={selectedIds} - onSelectedIdsChange={setSelectedIds} - isAuthenticated={isAuthenticated} - /> - -
- - Datos obtenidos vía ORCID Public API v3.0 - -
- {["ORCID OAuth 2.0", "SWORD v2", "Dublin Core"].map((t) => ( - - {t} - - ))} -
-
-
+
+
+
); } diff --git a/frontend/src/pages/GroupResultsPage.jsx b/frontend/src/pages/GroupResultsPage.jsx index 8a29ad4..5991b95 100644 --- a/frontend/src/pages/GroupResultsPage.jsx +++ b/frontend/src/pages/GroupResultsPage.jsx @@ -3,6 +3,7 @@ import { useLocation, useNavigate, Link } from "react-router-dom"; import { toast } from "sonner"; import { AppHeader } from "../components/layout/AppHeader"; +import Footer from "../components/layout/Footer"; import { Spinner } from "../components/ui/Spinner"; import { OrcidLogo } from "../components/ui/OrcidLogo"; import { @@ -188,135 +189,137 @@ export function GroupResultsPage() { return (
+
+
+ {/* 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 + + )} +

+ )} +
+
-
- {/* 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) => ( + + ))} +
+ )}
- {/* 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) => ( - +
+ {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 - -
- ) : ( - +
+ {getInitials(displayName ?? userOrcidId ?? "?")} +
+
+

+ {displayName ?? "Mi Perfil"} +

+
+ + {userOrcidId ?? "—"} +
+

+ Verás publicaciones nuevas marcadas en el dashboard +

+
+ + ) : ( + <> + +

+ 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.

-