Merge branch 'style/responsive-design' into 'main'
feat(ui): mejorar estilos y estructura de componentes en el dashboard See merge request fjmimbre/orcid_system!3
This commit is contained in:
@@ -49,19 +49,20 @@ export function ExportDropdown({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
|
<div className="mx-auto flex w-full max-w-[440px] flex-col items-stretch gap-2 sm:mx-0 sm:w-auto sm:max-w-none sm:flex-row sm:items-center sm:justify-end">
|
||||||
<SwordProfileSelect
|
<SwordProfileSelect
|
||||||
id="dashboard-export-destination"
|
id="dashboard-export-destination"
|
||||||
value={exportDestination}
|
value={exportDestination}
|
||||||
onChange={onExportDestinationChange}
|
onChange={onExportDestinationChange}
|
||||||
includeZip
|
includeZip
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isBusy || nothingToDownload}
|
disabled={isBusy || nothingToDownload}
|
||||||
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"
|
className="inline-flex w-full items-center justify-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 sm:w-auto"
|
||||||
>
|
>
|
||||||
{isBusy ? (
|
{isBusy ? (
|
||||||
<Spinner size={15} />
|
<Spinner size={15} />
|
||||||
|
|||||||
@@ -258,20 +258,21 @@ export function PublicationsTable({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFiltersOpen((o) => !o)}
|
onClick={() => setFiltersOpen((o) => !o)}
|
||||||
aria-expanded={filtersOpen}
|
aria-expanded={filtersOpen}
|
||||||
aria-controls="pubs-advanced-filters"
|
aria-controls="pubs-advanced-filters"
|
||||||
className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-[13px] font-medium transition-colors ${
|
className={`order-2 inline-flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-[13px] font-medium transition-colors sm:order-1 sm:w-auto sm:justify-start ${
|
||||||
filtersOpen || hasYearFilter
|
filtersOpen || hasYearFilter
|
||||||
? "border-brand-accent/50 bg-brand-accent/10 text-brand-accent"
|
? "border-brand-accent/50 bg-brand-accent/10 text-brand-accent"
|
||||||
: "border-surface-border-strong bg-surface-secondary text-ink-secondary hover:bg-surface-primary"
|
: "border-surface-border-strong bg-surface-secondary text-ink-secondary hover:bg-surface-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FilterIcon />
|
<FilterIcon />
|
||||||
Filtros
|
<span className="sm:hidden">Filtros</span>
|
||||||
|
<span className="hidden sm:inline">Filtros avanzados</span>
|
||||||
{hasYearFilter && (
|
{hasYearFilter && (
|
||||||
<span
|
<span
|
||||||
className="ml-0.5 inline-block h-1.5 w-1.5 rounded-full bg-brand-accent"
|
className="ml-0.5 inline-block h-1.5 w-1.5 rounded-full bg-brand-accent"
|
||||||
@@ -282,7 +283,7 @@ export function PublicationsTable({
|
|||||||
className={`transition-transform ${filtersOpen ? "rotate-180" : ""}`}
|
className={`transition-transform ${filtersOpen ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="relative">
|
<div className="relative order-1 w-full sm:order-2 sm:w-auto">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filtrar publicaciones..."
|
placeholder="Filtrar publicaciones..."
|
||||||
@@ -291,7 +292,7 @@ export function PublicationsTable({
|
|||||||
setFilter(e.target.value);
|
setFilter(e.target.value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
className="w-[220px] rounded-lg border border-surface-border-strong bg-surface-secondary py-2 pl-9 pr-3.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent"
|
className="w-full rounded-lg border border-surface-border-strong bg-surface-secondary py-2 pl-9 pr-3.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent sm:w-[220px]"
|
||||||
/>
|
/>
|
||||||
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-tertiary/70">
|
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-tertiary/70">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
@@ -303,49 +304,80 @@ export function PublicationsTable({
|
|||||||
{filtersOpen && (
|
{filtersOpen && (
|
||||||
<div
|
<div
|
||||||
id="pubs-advanced-filters"
|
id="pubs-advanced-filters"
|
||||||
className="flex flex-wrap items-end gap-4 border-t border-surface-border/60 bg-surface-secondary/40 px-5 py-3"
|
className="border-t border-surface-border/60 bg-surface-secondary/40 px-5 py-3"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||||
<label
|
<p className="text-xs font-medium text-ink-secondary">
|
||||||
htmlFor="year-from"
|
Filtrar por año de publicación
|
||||||
className="text-[11px] font-medium uppercase tracking-wide text-ink-tertiary"
|
</p>
|
||||||
>
|
{yearFilterSummary && (
|
||||||
Desde año
|
<span className="rounded-full border border-brand-accent/30 bg-brand-accent/10 px-2 py-0.5 text-[11px] font-medium text-brand-accent">
|
||||||
</label>
|
{yearFilterSummary}
|
||||||
<CustomSelect
|
</span>
|
||||||
id="year-from"
|
)}
|
||||||
value={yearFrom}
|
|
||||||
onChange={handleYearFromChange}
|
|
||||||
disabled={availableYears.length === 0}
|
|
||||||
options={availableYears.map((y) => ({
|
|
||||||
value: String(y),
|
|
||||||
label: String(y),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
<div className="grid grid-cols-1 items-end gap-3 sm:grid-cols-[1fr_auto_1fr_auto]">
|
||||||
htmlFor="year-to"
|
<div className="flex min-w-[140px] flex-1 flex-col gap-1">
|
||||||
className="text-[11px] font-medium uppercase tracking-wide text-ink-tertiary"
|
<label
|
||||||
>
|
htmlFor="year-from"
|
||||||
Hasta año
|
className="text-[11px] font-medium uppercase tracking-wide text-ink-tertiary"
|
||||||
</label>
|
>
|
||||||
<CustomSelect
|
Desde
|
||||||
id="year-to"
|
</label>
|
||||||
value={yearTo}
|
<CustomSelect
|
||||||
onChange={handleYearToChange}
|
id="year-from"
|
||||||
disabled={availableYears.length === 0}
|
value={yearFrom}
|
||||||
options={availableYears.map((y) => ({
|
onChange={handleYearFromChange}
|
||||||
value: String(y),
|
disabled={availableYears.length === 0}
|
||||||
label: String(y),
|
options={availableYears.map((y) => ({
|
||||||
}))}
|
value: String(y),
|
||||||
/>
|
label: String(y),
|
||||||
|
}))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden pb-2 text-center text-sm text-ink-tertiary sm:block">
|
||||||
|
—
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-[140px] flex-1 flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="year-to"
|
||||||
|
className="text-[11px] font-medium uppercase tracking-wide text-ink-tertiary"
|
||||||
|
>
|
||||||
|
Hasta
|
||||||
|
</label>
|
||||||
|
<CustomSelect
|
||||||
|
id="year-to"
|
||||||
|
value={yearTo}
|
||||||
|
onChange={handleYearToChange}
|
||||||
|
disabled={availableYears.length === 0}
|
||||||
|
options={availableYears.map((y) => ({
|
||||||
|
value: String(y),
|
||||||
|
label: String(y),
|
||||||
|
}))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasYearFilter && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearYearFilter}
|
||||||
|
className="rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2 text-[13px] font-medium text-ink-secondary transition-colors hover:bg-surface-secondary"
|
||||||
|
>
|
||||||
|
Limpiar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasYearFilter && (
|
{hasYearFilter && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearYearFilter}
|
onClick={clearYearFilter}
|
||||||
className="mb-[2px] rounded-md px-2.5 py-1.5 text-xs font-medium text-ink-tertiary transition-colors hover:bg-surface-primary hover:text-ink-primary"
|
className="sr-only"
|
||||||
>
|
>
|
||||||
Limpiar rango
|
Limpiar rango
|
||||||
</button>
|
</button>
|
||||||
@@ -366,123 +398,217 @@ export function PublicationsTable({
|
|||||||
) : loading ? (
|
) : loading ? (
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full border-collapse">
|
<>
|
||||||
<thead>
|
<div className="md:hidden">
|
||||||
<tr className="bg-surface-secondary">
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="w-10 border-b border-surface-border/60 px-4 py-2.5 text-left"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<TriStateCheckbox
|
|
||||||
checked={pageSelectionStats.allChecked}
|
|
||||||
indeterminate={pageSelectionStats.anyChecked}
|
|
||||||
onChange={toggleCurrentPage}
|
|
||||||
ariaLabel="Seleccionar todas las publicaciones de esta página"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
{COLUMNS.map((col) => (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
onClick={() => toggleSort(col.key)}
|
|
||||||
className={`select-none border-b border-surface-border/60 px-4 py-2.5 text-left text-xs font-medium tracking-wide text-ink-secondary${col.thClass ? ` ${col.thClass}` : ""}`}
|
|
||||||
>
|
|
||||||
<span className="flex cursor-pointer items-center">
|
|
||||||
{col.label.toUpperCase()}
|
|
||||||
<SortIcon
|
|
||||||
active={sortKey === col.key}
|
|
||||||
direction={sortDir}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<tr>
|
<p className="p-8 text-center text-sm text-ink-tertiary">
|
||||||
<td
|
No se encontraron publicaciones con los filtros aplicados.
|
||||||
colSpan={COLUMNS.length + 1}
|
</p>
|
||||||
className="p-10 text-center text-sm text-ink-tertiary"
|
|
||||||
>
|
|
||||||
No se encontraron publicaciones con los filtros aplicados.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
) : (
|
||||||
pageRows.map((pub, i) => {
|
<>
|
||||||
const isSelected = selectedIds.has(pub.id);
|
<div className="border-b border-surface-border/60 bg-surface-secondary px-4 py-2.5">
|
||||||
return (
|
<TriStateCheckbox
|
||||||
<tr
|
checked={pageSelectionStats.allChecked}
|
||||||
key={pub.id}
|
indeterminate={pageSelectionStats.anyChecked}
|
||||||
className={`transition-colors ${
|
onChange={toggleCurrentPage}
|
||||||
isSelected
|
ariaLabel="Seleccionar todas las publicaciones de esta página"
|
||||||
? "bg-tag-article-bg/70 hover:bg-tag-article-bg"
|
/>
|
||||||
: "hover:bg-surface-secondary/70"
|
</div>
|
||||||
} ${
|
<div>
|
||||||
i < pageRows.length - 1
|
{pageRows.map((pub, i) => {
|
||||||
? "border-b border-surface-border/60"
|
const isSelected = selectedIds.has(pub.id);
|
||||||
: ""
|
return (
|
||||||
}`}
|
<article
|
||||||
|
key={pub.id}
|
||||||
|
className={`px-4 py-3.5 transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "bg-tag-article-bg/70 hover:bg-tag-article-bg"
|
||||||
|
: "hover:bg-surface-secondary/70"
|
||||||
|
} ${
|
||||||
|
i < pageRows.length - 1
|
||||||
|
? "border-b border-surface-border/60"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex items-start gap-2.5">
|
||||||
|
<TriStateCheckbox
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => toggleRow(pub.id)}
|
||||||
|
ariaLabel={`Seleccionar publicación ${pub.title}`}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div 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>
|
||||||
|
)}
|
||||||
|
<p className="text-[14px] font-medium leading-relaxed text-ink-primary">
|
||||||
|
{pub.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 pl-6.5 text-[12px] text-ink-secondary">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium text-ink-primary">Revista:</span>{" "}
|
||||||
|
{pub.journal || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium text-ink-primary">Año:</span>{" "}
|
||||||
|
{pub.publication_year ?? "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium text-ink-primary">DOI:</span>{" "}
|
||||||
|
{pub.doi ? (
|
||||||
|
<a
|
||||||
|
href={`https://doi.org/${pub.doi}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="break-all font-mono text-[11px] text-brand-accent hover:underline"
|
||||||
|
>
|
||||||
|
{pub.doi}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-mono text-[11px] text-ink-tertiary">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<Badge type={pub.type} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className="hidden w-full border-collapse md:table">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-surface-secondary">
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="w-10 border-b border-surface-border/60 px-4 py-2.5 text-left"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<TriStateCheckbox
|
||||||
|
checked={pageSelectionStats.allChecked}
|
||||||
|
indeterminate={pageSelectionStats.anyChecked}
|
||||||
|
onChange={toggleCurrentPage}
|
||||||
|
ariaLabel="Seleccionar todas las publicaciones de esta página"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{COLUMNS.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
onClick={() => toggleSort(col.key)}
|
||||||
|
className={`select-none border-b border-surface-border/60 px-4 py-2.5 text-left text-xs font-medium tracking-wide text-ink-secondary${col.thClass ? ` ${col.thClass}` : ""}`}
|
||||||
>
|
>
|
||||||
<td
|
<span className="flex cursor-pointer items-center">
|
||||||
className="w-10 cursor-pointer px-4 py-3.5"
|
{col.label.toUpperCase()}
|
||||||
onClick={(e) => {
|
<SortIcon
|
||||||
e.stopPropagation();
|
active={sortKey === col.key}
|
||||||
toggleRow(pub.id);
|
direction={sortDir}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TriStateCheckbox
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => toggleRow(pub.id)}
|
|
||||||
ariaLabel={`Seleccionar publicación ${pub.title}`}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</span>
|
||||||
<td className="max-w-[280px] px-4 py-3.5 text-[13px] font-medium leading-relaxed text-ink-primary">
|
</th>
|
||||||
<span className="flex flex-wrap items-start gap-1.5">
|
))}
|
||||||
{isAuthenticated && pub.downloaded_by_me === false && (
|
</tr>
|
||||||
<span
|
</thead>
|
||||||
title="No descargada aún por ti"
|
<tbody>
|
||||||
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"
|
{filtered.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={COLUMNS.length + 1}
|
||||||
|
className="p-10 text-center text-sm text-ink-tertiary"
|
||||||
|
>
|
||||||
|
No se encontraron publicaciones con los filtros aplicados.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
pageRows.map((pub, i) => {
|
||||||
|
const isSelected = selectedIds.has(pub.id);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={pub.id}
|
||||||
|
className={`transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "bg-tag-article-bg/70 hover:bg-tag-article-bg"
|
||||||
|
: "hover:bg-surface-secondary/70"
|
||||||
|
} ${
|
||||||
|
i < pageRows.length - 1
|
||||||
|
? "border-b border-surface-border/60"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="w-10 cursor-pointer px-4 py-3.5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleRow(pub.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TriStateCheckbox
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => toggleRow(pub.id)}
|
||||||
|
ariaLabel={`Seleccionar publicación ${pub.title}`}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="max-w-[280px] px-4 py-3.5 text-[13px] font-medium leading-relaxed text-ink-primary">
|
||||||
|
<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="px-4 py-3.5 text-[13px] text-ink-secondary">
|
||||||
|
{pub.journal || "—"}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-4 py-3.5 text-[13px] font-medium text-ink-primary">
|
||||||
|
{pub.publication_year ?? "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5">
|
||||||
|
{pub.doi ? (
|
||||||
|
<a
|
||||||
|
href={`https://doi.org/${pub.doi}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="whitespace-nowrap font-mono text-xs text-brand-accent hover:underline"
|
||||||
>
|
>
|
||||||
<SparkleIcon size={9} />
|
{pub.doi}
|
||||||
Nuevo
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="whitespace-nowrap font-mono text-xs text-ink-tertiary">
|
||||||
|
—
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{pub.title}
|
</td>
|
||||||
</span>
|
<td className="px-4 py-3.5">
|
||||||
</td>
|
<Badge type={pub.type} />
|
||||||
<td className="px-4 py-3.5 text-[13px] text-ink-secondary">
|
</td>
|
||||||
{pub.journal || "—"}
|
</tr>
|
||||||
</td>
|
);
|
||||||
<td className="whitespace-nowrap px-4 py-3.5 text-[13px] font-medium text-ink-primary">
|
})
|
||||||
{pub.publication_year ?? "—"}
|
)}
|
||||||
</td>
|
</tbody>
|
||||||
<td className="px-4 py-3.5">
|
</table>
|
||||||
{pub.doi ? (
|
</>
|
||||||
<a
|
|
||||||
href={`https://doi.org/${pub.doi}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="whitespace-nowrap font-mono text-xs text-brand-accent hover:underline"
|
|
||||||
>
|
|
||||||
{pub.doi}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap font-mono text-xs text-ink-tertiary">
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3.5">
|
|
||||||
<Badge type={pub.type} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function ResearcherCard({ researcher, actions = null }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actions && (
|
{actions && (
|
||||||
<div className="ml-auto flex shrink-0 flex-col items-end gap-2.5">
|
<div className="flex w-full flex-col items-stretch gap-2.5 md:ml-auto md:w-auto md:shrink-0 md:items-end">
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -48,28 +48,32 @@ export function SwordProfileSelect({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={`flex items-center gap-2 text-sm ${className}`.trim()}
|
className={`flex w-full flex-col items-start gap-1.5 text-sm sm:w-auto sm:flex-row sm:items-center sm:gap-2 ${className}`.trim()}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap text-ink-tertiary">Destino:</span>
|
<span className="w-full whitespace-nowrap text-center text-ink-tertiary sm:w-auto sm:text-left">
|
||||||
<div className="relative">
|
Destino:
|
||||||
|
</span>
|
||||||
|
<div className="relative w-full sm:w-auto sm:flex-none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id={id}
|
id={id}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
className="inline-flex min-w-44 items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2.5 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary"
|
className="relative inline-flex w-full min-w-44 items-center justify-center rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2.5 pr-10 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary sm:w-auto"
|
||||||
>
|
>
|
||||||
<ExportProfileIcon profile={selected.value} size={20} />
|
<span className="inline-flex min-w-0 items-center gap-2">
|
||||||
<span className="truncate">{selected.label}</span>
|
<ExportProfileIcon profile={selected.value} size={20} />
|
||||||
<ChevronDownIcon className="ml-auto shrink-0" />
|
<span className="truncate text-center">{selected.label}</span>
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="pointer-events-none absolute right-3 top-1/2 shrink-0 -translate-y-1/2" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-labelledby={id}
|
aria-labelledby={id}
|
||||||
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-80 overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
|
className="absolute left-0 top-[calc(100%+6px)] z-50 w-full min-w-0 overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg sm:min-w-80"
|
||||||
>
|
>
|
||||||
{options.map(({ value: optionValue, label, desc }, idx) => (
|
{options.map(({ value: optionValue, label, desc }, idx) => (
|
||||||
<button
|
<button
|
||||||
@@ -91,7 +95,7 @@ export function SwordProfileSelect({
|
|||||||
<div className="text-sm font-medium text-ink-primary">
|
<div className="text-sm font-medium text-ink-primary">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap text-xs text-ink-tertiary">
|
<div className="text-xs text-ink-tertiary sm:whitespace-nowrap">
|
||||||
{desc}
|
{desc}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Spinner } from "../ui/Spinner";
|
|||||||
* Primary action button on the dashboard. Swaps icon + colour scheme
|
* Primary action button on the dashboard. Swaps icon + colour scheme
|
||||||
* depending on the sync lifecycle (idle → loading → success flash).
|
* depending on the sync lifecycle (idle → loading → success flash).
|
||||||
*/
|
*/
|
||||||
export function SyncButton({ onClick, status = "idle" }) {
|
export function SyncButton({ onClick, status = "idle", className = "" }) {
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
const isSuccess = status === "success";
|
const isSuccess = status === "success";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export function SyncButton({ onClick, status = "idle" }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`inline-flex items-center gap-2 rounded-lg px-[18px] py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${palette}`}
|
className={`inline-flex items-center justify-center gap-2 rounded-lg px-[18px] py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${palette} ${className}`.trim()}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner size={15} />
|
<Spinner size={15} />
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export default function Footer() {
|
|||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
{/* Main row */}
|
{/* Main row */}
|
||||||
<div className="flex flex-col gap-8 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-7 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="flex flex-col gap-2 lg:max-w-xs">
|
<div className="flex flex-col gap-2 text-center lg:max-w-xs lg:text-left">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||||
<span className="text-base font-extrabold tracking-tight text-ink-primary">
|
<span className="text-base font-extrabold tracking-tight text-ink-primary">
|
||||||
ORCID<span className="text-orcid-green">2</span>SWORD
|
ORCID<span className="text-orcid-green">2</span>SWORD
|
||||||
</span>
|
</span>
|
||||||
@@ -19,16 +19,16 @@ export default function Footer() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-relaxed text-ink-secondary">
|
<p className="text-sm leading-relaxed text-ink-secondary">
|
||||||
Sincronización de publicaciones ORCID al repositorio institucional.
|
Extracción y preparación de publicaciones ORCID para repositorios académicos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compatible con */}
|
{/* Compatible con */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-ink-tertiary">
|
<span className="text-center text-[10px] font-black uppercase tracking-[0.2em] text-ink-tertiary lg:text-left">
|
||||||
Compatible con
|
Compatible con
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap justify-center gap-1.5 lg:justify-start">
|
||||||
{technologies.map((tech) => (
|
{technologies.map((tech) => (
|
||||||
<span
|
<span
|
||||||
key={tech}
|
key={tech}
|
||||||
@@ -41,13 +41,13 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Institutional links */}
|
{/* Institutional links */}
|
||||||
<div className="flex flex-row gap-6 sm:gap-8">
|
<div className="grid grid-cols-2 gap-2.5 sm:gap-3">
|
||||||
|
|
||||||
{/* Universidad de Jaén */}
|
{/* Universidad de Jaén */}
|
||||||
<a
|
<a
|
||||||
href="https://www.ujaen.es/"
|
href="https://www.ujaen.es/"
|
||||||
target="_blank" rel="noopener noreferrer"
|
target="_blank" rel="noopener noreferrer"
|
||||||
className="group flex items-center gap-2.5"
|
className="group flex items-center justify-center gap-2.5 rounded-lg border border-surface-border bg-surface-secondary/30 px-3 py-2 transition-colors hover:bg-surface-secondary/60"
|
||||||
title="Ir a la web oficial de la Universidad de Jaén"
|
title="Ir a la web oficial de la Universidad de Jaén"
|
||||||
>
|
>
|
||||||
<div className="flex h-8 flex-col justify-center border-r-2 border-surface-border-strong pr-2.5 text-right transition-colors group-hover:border-brand-accent">
|
<div className="flex h-8 flex-col justify-center border-r-2 border-surface-border-strong pr-2.5 text-right transition-colors group-hover:border-brand-accent">
|
||||||
@@ -65,7 +65,7 @@ export default function Footer() {
|
|||||||
<a
|
<a
|
||||||
href="https://github.com/uja-dev-practices/orcid_system"
|
href="https://github.com/uja-dev-practices/orcid_system"
|
||||||
target="_blank" rel="noopener noreferrer"
|
target="_blank" rel="noopener noreferrer"
|
||||||
className="group flex items-center gap-2.5"
|
className="group flex items-center justify-center gap-2.5 rounded-lg border border-surface-border bg-surface-secondary/30 px-3 py-2 transition-colors hover:bg-surface-secondary/60"
|
||||||
title="Ver repositorio oficial"
|
title="Ver repositorio oficial"
|
||||||
>
|
>
|
||||||
<div className="flex h-8 flex-col justify-center border-r-2 border-surface-border-strong pr-2.5 text-right transition-colors group-hover:border-brand-primary">
|
<div className="flex h-8 flex-col justify-center border-r-2 border-surface-border-strong pr-2.5 text-right transition-colors group-hover:border-brand-primary">
|
||||||
|
|||||||
@@ -224,7 +224,11 @@ export function DashboardPage() {
|
|||||||
researcher={researcher}
|
researcher={researcher}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<SyncButton onClick={handleSync} status={syncStatus} />
|
<SyncButton
|
||||||
|
onClick={handleSync}
|
||||||
|
status={syncStatus}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
/>
|
||||||
<ExportDropdown
|
<ExportDropdown
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
exportingFormat={exportingFormat}
|
exportingFormat={exportingFormat}
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "../components/ui/Icons";
|
} from "../components/ui/Icons";
|
||||||
import { downloadExport, searchResearchersBulk } from "../services/api";
|
import { downloadExport, searchResearchersBulk } from "../services/api";
|
||||||
import { DEFAULT_EXPORT_PROFILE, swordXmlFilename } from "../utils/exportProfiles";
|
import {
|
||||||
import { SwordProfileSelect } from "../components/dashboard/SwordProfileSelect";
|
DEFAULT_EXPORT_DESTINATION,
|
||||||
|
DEFAULT_EXPORT_PROFILE,
|
||||||
|
EXPORT_ZIP_DESTINATION,
|
||||||
|
swordXmlFilename,
|
||||||
|
} from "../utils/exportProfiles";
|
||||||
|
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +41,9 @@ export function GroupResultsPage() {
|
|||||||
const [errors, setErrors] = useState([]);
|
const [errors, setErrors] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [globalExporting, setGlobalExporting] = useState(null); // format | null
|
const [globalExporting, setGlobalExporting] = useState(null); // format | null
|
||||||
|
const [globalExportDestination, setGlobalExportDestination] = useState(
|
||||||
|
DEFAULT_EXPORT_DESTINATION,
|
||||||
|
);
|
||||||
const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE);
|
const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE);
|
||||||
|
|
||||||
// Track per-researcher export state (format | null)
|
// Track per-researcher export state (format | null)
|
||||||
@@ -102,6 +110,14 @@ export function GroupResultsPage() {
|
|||||||
[results],
|
[results],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleGlobalExportDestinationChange(nextDestination) {
|
||||||
|
setGlobalExportDestination(nextDestination);
|
||||||
|
// Keep last XML profile for card-level exports.
|
||||||
|
if (nextDestination !== EXPORT_ZIP_DESTINATION) {
|
||||||
|
setSwordProfile(nextDestination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGlobalExport(format, profile = DEFAULT_EXPORT_PROFILE) {
|
async function handleGlobalExport(format, profile = DEFAULT_EXPORT_PROFILE) {
|
||||||
const ids = isAuthenticated ? allNewIds : allIds;
|
const ids = isAuthenticated ? allNewIds : allIds;
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
@@ -193,21 +209,19 @@ export function GroupResultsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalLabel = isAuthenticated
|
|
||||||
? allNewIds.length > 0
|
|
||||||
? `Descargar lo nuevo de todos (${allNewIds.length})`
|
|
||||||
: "Todo descargado"
|
|
||||||
: `Descargar todo (${allIds.length})`;
|
|
||||||
|
|
||||||
const globalDisabled =
|
|
||||||
Boolean(globalExporting) ||
|
|
||||||
(isAuthenticated ? allNewIds.length === 0 : allIds.length === 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-surface-tertiary">
|
<div className="flex min-h-screen flex-col bg-surface-tertiary">
|
||||||
<AppHeader variant="group" />
|
<AppHeader variant="group" />
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
<div className="mx-auto w-full max-w-7xl px-4 py-7">
|
<div className="mx-auto w-full max-w-7xl px-4 py-7">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="mb-5 inline-flex items-center gap-1.5 text-sm text-ink-tertiary transition-colors hover:text-ink-primary"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon size={14} />
|
||||||
|
Volver al inicio
|
||||||
|
</Link>
|
||||||
|
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
|
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -233,33 +247,15 @@ export function GroupResultsPage() {
|
|||||||
|
|
||||||
{/* Global export buttons */}
|
{/* Global export buttons */}
|
||||||
{!loading && results.length > 0 && (
|
{!loading && results.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
<ExportDropdown
|
||||||
<SwordProfileSelect
|
onExport={handleGlobalExport}
|
||||||
id="group-sword-profile"
|
exportingFormat={globalExporting}
|
||||||
value={swordProfile}
|
selectedCount={0}
|
||||||
onChange={setSwordProfile}
|
isAuthenticated={isAuthenticated}
|
||||||
/>
|
newPublicationsCount={allNewIds.length}
|
||||||
{["xml", "zip"].map((fmt) => (
|
exportDestination={globalExportDestination}
|
||||||
<button
|
onExportDestinationChange={handleGlobalExportDestinationChange}
|
||||||
key={fmt}
|
/>
|
||||||
type="button"
|
|
||||||
onClick={() => handleGlobalExport(fmt, swordProfile)}
|
|
||||||
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 ? (
|
|
||||||
<Spinner size={14} />
|
|
||||||
) : isAuthenticated && allNewIds.length > 0 ? (
|
|
||||||
<SparkleIcon size={13} className="text-brand-accent" />
|
|
||||||
) : (
|
|
||||||
<DownloadIcon size={14} />
|
|
||||||
)}
|
|
||||||
{globalExporting === fmt
|
|
||||||
? `Exportando ${fmt.toUpperCase()}...`
|
|
||||||
: `${fmt.toUpperCase()} · ${globalLabel}`}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -294,7 +290,6 @@ export function GroupResultsPage() {
|
|||||||
swordProfile,
|
swordProfile,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
swordProfile={swordProfile}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -355,7 +350,6 @@ function ResearcherResultCard({
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
exporting,
|
exporting,
|
||||||
onExport,
|
onExport,
|
||||||
swordProfile,
|
|
||||||
}) {
|
}) {
|
||||||
const researcher = bundle.researcher ?? {};
|
const researcher = bundle.researcher ?? {};
|
||||||
const publications = bundle.publications ?? [];
|
const publications = bundle.publications ?? [];
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export function LandingPage() {
|
|||||||
Tus publicaciones, listas para depositar.
|
Tus publicaciones, listas para depositar.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto max-w-xl text-[16px] leading-relaxed text-ink-secondary">
|
<p className="mx-auto max-w-xl text-[16px] leading-relaxed text-ink-secondary">
|
||||||
Conecta tu ORCID y descárgalas en XML cuando quieras.
|
Conecta tu ORCID y descárgalas cuando quieras.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user