feat(ui): mejorar estilos y estructura de componentes en el dashboard
Se ajustan los estilos y la estructura de varios componentes en el dashboard, incluyendo ExportDropdown, ResearcherCard, SwordProfileSelect y SyncButton, para mejorar la presentación y la responsividad. Se añade soporte para clases personalizadas en SyncButton y se integra el nuevo sistema de exportación en GroupResultsPage.
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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,16 +209,6 @@ 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" />
|
||||||
@@ -233,33 +239,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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user