feat: enhance authentication flow and UI components

- Updated .env.example to include OAuth authentication details and bypass mode for development.
- Integrated AuthProvider in App component to manage authentication state.
- Added AuthCallbackPage for handling OAuth callback.
- Enhanced ExportDropdown and PublicationsTable components to display new publication indicators for authenticated users.
- Updated AppHeader to show authentication status and logout functionality.
- Improved LandingPage to support group search and simulate login in bypass mode.
- Refactored DashboardPage to conditionally handle publication exports based on user authentication status.
This commit is contained in:
Alexis
2026-04-29 12:19:47 +02:00
parent d743afd446
commit 25dfeec3f7
12 changed files with 1211 additions and 85 deletions
@@ -4,6 +4,7 @@ import {
DocumentIcon,
DownloadIcon,
PackageIcon,
SparkleIcon,
} from "../ui/Icons";
import { Spinner } from "../ui/Spinner";
@@ -23,17 +24,20 @@ const FORMATS = [
];
/**
* SWORD export dropdown. Delegates the actual download to `onExport(format)`
* so it can be wired up either to the real API or to a mock layer from the
* parent page.
* SWORD export dropdown. Delegatea the actual download to `onExport(format)`.
*
* `exportingFormat` (optional) lets the parent keep the button in a loading
* state between clicks (e.g. while waiting for the backend blob).
* Props:
* - `isAuthenticated` → cambia el texto del botón principal.
* - `newPublicationsCount` → cuántas publicaciones tiene downloaded_by_me=false.
* - `selectedCount` → publicaciones seleccionadas manualmente.
* - `exportingFormat` → formato en curso (pone el botón en loading).
*/
export function ExportDropdown({
onExport,
exportingFormat = null,
selectedCount = 0,
isAuthenticated = false,
newPublicationsCount = 0,
}) {
const [open, setOpen] = useState(false);
const rootRef = useRef(null);
@@ -56,19 +60,40 @@ export function ExportDropdown({
onExport(format);
}
const idleLabel = hasSelection
? `Exportar seleccionadas (${selectedCount})`
: "Exportar todas";
// Label logic:
// manual selection → always "Exportar seleccionadas (N)"
// logged in, no selection → "Descargar lo nuevo (N)" or "Todo descargado"
// not logged in, no selection → "Descargar todo"
let idleLabel;
let showSparkle = false;
if (hasSelection) {
idleLabel = `Exportar seleccionadas (${selectedCount})`;
} else if (isAuthenticated) {
if (newPublicationsCount > 0) {
idleLabel = `Descargar lo nuevo (${newPublicationsCount})`;
showSparkle = true;
} else {
idleLabel = "Todo descargado";
}
} else {
idleLabel = "Descargar todo";
}
return (
<div className="relative" ref={rootRef}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
disabled={isBusy}
disabled={isBusy || (isAuthenticated && !hasSelection && newPublicationsCount === 0)}
className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-[18px] py-2.5 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-70"
>
{isBusy ? <Spinner size={15} /> : <DownloadIcon />}
{isBusy ? (
<Spinner size={15} />
) : showSparkle ? (
<SparkleIcon size={15} className="text-brand-accent" />
) : (
<DownloadIcon />
)}
{isBusy
? `Exportando ${exportingFormat.toUpperCase()}...`
: idleLabel}
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { AlertIcon, ChevronDownIcon, FilterIcon, SearchIcon } from "../ui/Icons";
import { AlertIcon, ChevronDownIcon, FilterIcon, SearchIcon, SparkleIcon } from "../ui/Icons";
import { Spinner } from "../ui/Spinner";
import { Badge } from "../ui/Badge";
@@ -83,6 +83,7 @@ export function PublicationsTable({
onRetry,
selectedIds = EMPTY_SELECTION,
onSelectedIdsChange,
isAuthenticated = false,
}) {
const [filter, setFilter] = useState("");
const [sortKey, setSortKey] = useState("publication_year");
@@ -447,7 +448,18 @@ export function PublicationsTable({
/>
</td>
<td className="max-w-[280px] px-4 py-3.5 text-[13px] font-medium leading-relaxed text-ink-primary">
{pub.title}
<span className="flex flex-wrap items-start gap-1.5">
{isAuthenticated && pub.downloaded_by_me === false && (
<span
title="No descargada aún por ti"
className="mt-0.5 inline-flex shrink-0 items-center gap-0.5 rounded-full bg-brand-accent/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-brand-accent"
>
<SparkleIcon size={9} />
Nuevo
</span>
)}
{pub.title}
</span>
</td>
<td className="whitespace-nowrap px-4 py-3.5 text-[13px] text-ink-secondary">
{pub.journal || "—"}