feat(download-tracking): implementar seguimiento de descargas por usuario en el dashboard

Se añaden funciones para marcar publicaciones y resultados de grupo como descargados en los componentes DashboardPage y GroupResultsPage. Se optimiza la lógica de carga de publicaciones para incluir un control de estado que evita la descarga innecesaria. Además, se actualizan los mocks de publicaciones para reflejar el estado de descarga. Se mejora la presentación del texto en el componente PublicationsTable.
This commit is contained in:
Alexis
2026-06-03 12:27:57 +02:00
parent 2f48dab109
commit 58f164b036
7 changed files with 94 additions and 14 deletions
@@ -444,7 +444,7 @@ export function PublicationsTable({
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
NUEVO
</span>
)}
<p className="text-[14px] font-medium leading-relaxed text-ink-primary">
@@ -570,7 +570,7 @@ export function PublicationsTable({
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
NUEVO
</span>
)}
{pub.title}
+23 -3
View File
@@ -21,6 +21,10 @@ import {
swordXmlFilename,
} from "../utils/exportProfiles";
import { useAuth } from "../contexts/AuthContext";
import {
markPublicationsAsDownloaded,
publicationsNeedDownloadFlags,
} from "../utils/downloadTracking";
const SUCCESS_FLASH_MS = 3000;
/** Minimum gap between sync requests (protects backend + avoids toast spam). */
@@ -48,6 +52,7 @@ export function DashboardPage() {
const { isAuthenticated } = useAuth();
const initialBundleRef = useRef(location.state?.bundle ?? null);
const consumedInitialBundleRef = useRef(false);
const initialBundle = initialBundleRef.current;
const [researcher, setResearcher] = useState(initialBundle?.researcher ?? null);
@@ -115,14 +120,25 @@ export function DashboardPage() {
useEffect(() => {
if (!isValidOrcid(orcid)) return;
if (initialBundleRef.current) {
const cachedBundle = initialBundleRef.current;
if (cachedBundle && !consumedInitialBundleRef.current) {
consumedInitialBundleRef.current = true;
initialBundleRef.current = null;
return;
if (
!publicationsNeedDownloadFlags(
cachedBundle.publications,
isAuthenticated,
)
) {
return;
}
}
const ctrl = new AbortController();
loadBundle(ctrl.signal);
return () => ctrl.abort();
}, [orcid, loadBundle]);
}, [orcid, loadBundle, isAuthenticated]);
useEffect(() => {
return () => {
@@ -270,6 +286,10 @@ export function DashboardPage() {
} else {
scope = "todo el investigador";
}
if (isAuthenticated && ids?.length) {
setPublications((prev) => markPublicationsAsDownloaded(prev, ids));
}
toast.success(`Exportación ${format.toUpperCase()} completada`, {
id: EXPORT_TOAST_ID,
description: scope,
+10 -1
View File
@@ -22,6 +22,7 @@ import {
} from "../utils/exportProfiles";
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
import { useAuth } from "../contexts/AuthContext";
import { markGroupResultsAsDownloaded } from "../utils/downloadTracking";
const EXPORT_COOLDOWN_MS = 5000;
const GLOBAL_EXPORT_TOAST_ID = "group-export-global";
@@ -108,7 +109,7 @@ export function GroupResultsPage() {
})();
return () => ctrl.abort();
}, [orcidIds, navigate]);
}, [orcidIds, navigate, isAuthenticated]);
useEffect(() => {
const cardTimersObj = cardExportCooldownTimerRef.current;
@@ -218,6 +219,10 @@ export function GroupResultsPage() {
anchor.remove();
URL.revokeObjectURL(objectUrl);
}
if (isAuthenticated) {
setResults((prev) => markGroupResultsAsDownloaded(prev, ids));
}
toast.success(`Exportación ${format.toUpperCase()} completada`, {
id: GLOBAL_EXPORT_TOAST_ID,
description: `${ids.length} publicaciones exportadas.`,
@@ -276,6 +281,10 @@ export function GroupResultsPage() {
anchor.remove();
URL.revokeObjectURL(objectUrl);
}
if (isAuthenticated) {
setResults((prev) => markGroupResultsAsDownloaded(prev, ids));
}
toast.success(`Exportación ${format.toUpperCase()} completada`, {
id: `group-export-card-${orcidId}`,
description: `${ids.length} publicaciones de ${orcidId}.`,
+5
View File
@@ -29,6 +29,7 @@ export const MOCK_PUBLICATIONS = [
doi: "10.1038/s41567-025-xxxx",
type: "journal-article",
last_modified: "2025-09-01T10:00:00Z",
downloaded_by_me: false,
},
{
id: "uuid-2",
@@ -40,6 +41,7 @@ export const MOCK_PUBLICATIONS = [
doi: "10.1000/jdr.2024.12",
type: "review",
last_modified: "2024-11-12T09:00:00Z",
downloaded_by_me: false,
},
{
id: "uuid-3",
@@ -50,6 +52,7 @@ export const MOCK_PUBLICATIONS = [
doi: "10.1007/s11192-024-04801-z",
type: "journal-article",
last_modified: "2024-06-20T15:30:00Z",
downloaded_by_me: true,
},
{
id: "uuid-4",
@@ -60,6 +63,7 @@ export const MOCK_PUBLICATIONS = [
doi: "10.1145/3587-dl.2023.09",
type: "conference-paper",
last_modified: "2023-10-05T11:45:00Z",
downloaded_by_me: false,
},
{
id: "uuid-5",
@@ -70,6 +74,7 @@ export const MOCK_PUBLICATIONS = [
doi: "10.1016/j.ijls.2023.03.011",
type: "journal-article",
last_modified: "2023-04-18T08:15:00Z",
downloaded_by_me: true,
},
];
+32
View File
@@ -0,0 +1,32 @@
/**
* Helpers for per-user "new publication" (not yet downloaded) tracking.
* Backend sets `downloaded_by_me` when a Bearer token is present; these
* utilities keep the dashboard in sync after login and export.
*/
/** True when an authenticated user needs a refetch to obtain download flags. */
export function publicationsNeedDownloadFlags(publications, isAuthenticated) {
if (!isAuthenticated || !publications?.length) return false;
return publications.some((p) => p.downloaded_by_me == null);
}
/** Mark the given publication IDs as downloaded in local state. */
export function markPublicationsAsDownloaded(publications, downloadedIds) {
if (!downloadedIds?.length || !publications?.length) return publications;
const ids = new Set(downloadedIds);
return publications.map((p) =>
ids.has(p.id) ? { ...p, downloaded_by_me: true } : p,
);
}
/** Apply download flags across group-search result bundles. */
export function markGroupResultsAsDownloaded(results, downloadedIds) {
if (!downloadedIds?.length || !results?.length) return results;
const ids = new Set(downloadedIds);
return results.map((bundle) => ({
...bundle,
publications: (bundle.publications ?? []).map((p) =>
ids.has(p.id) ? { ...p, downloaded_by_me: true } : p,
),
}));
}