feat(ui): mejoras dashboard y entorno local con ngrok/ORCID sandbox
- Añade enlace Volver al inicio y márgenes max-w-7xl en dashboard y group - Corrige hora de última sincronización (UTC en formatDate) - Evita scroll horizontal en tabla de publicaciones - Soporta backend/.env.local y compose opcional para sandbox/ngrok - Cookie OAuth Secure en redirects HTTPS; README y .env.example
This commit is contained in:
@@ -2,4 +2,7 @@ VITE_API_URL=https://api.tudominio.com/api
|
||||
VITE_API_PROXY_TARGET=
|
||||
# Debe coincidir con API_KEY_VALUE del backend. La inyecta el proxy (Vite/nginx).
|
||||
VITE_API_KEY=
|
||||
# Producción: https://pub.orcid.org/v3.0 · Sandbox local: https://pub.sandbox.orcid.org/v3.0
|
||||
# (en `npm run dev` puedes ponerlo en `frontend/.env.local` sin tocar el .env del repo)
|
||||
VITE_ORCID_PUBLIC_API_BASE=https://pub.orcid.org/v3.0
|
||||
VITE_USE_MOCKS=false
|
||||
@@ -4,11 +4,11 @@ import { Spinner } from "../ui/Spinner";
|
||||
import { Badge } from "../ui/Badge";
|
||||
|
||||
const COLUMNS = [
|
||||
{ key: "title", label: "Título" },
|
||||
{ key: "journal", label: "Revista / Fuente" },
|
||||
{ key: "publication_year", label: "Año" },
|
||||
{ key: "title", label: "Título", thClass: "w-[35%]" },
|
||||
{ key: "journal", label: "Revista / Fuente", thClass: "w-[28%]", tdClass: "break-words" },
|
||||
{ key: "publication_year", label: "Año", thClass: "w-16" },
|
||||
{ key: "doi", label: "DOI" },
|
||||
{ key: "type", label: "Tipo" },
|
||||
{ key: "type", label: "Tipo", thClass: "w-20" },
|
||||
];
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
@@ -376,7 +376,7 @@ export function PublicationsTable({
|
||||
) : loading ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<table className="w-full min-w-[720px] border-collapse">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-surface-secondary">
|
||||
<th
|
||||
@@ -395,7 +395,7 @@ export function PublicationsTable({
|
||||
<th
|
||||
key={col.key}
|
||||
onClick={() => toggleSort(col.key)}
|
||||
className="select-none whitespace-nowrap border-b border-surface-border/60 px-4 py-2.5 text-left text-xs font-medium tracking-wide text-ink-secondary"
|
||||
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()}
|
||||
@@ -461,7 +461,7 @@ export function PublicationsTable({
|
||||
{pub.title}
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3.5 text-[13px] text-ink-secondary">
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation, useParams, Navigate } from "react-router-dom";
|
||||
import { useLocation, useParams, Navigate, Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AppHeader } from "../components/layout/AppHeader";
|
||||
@@ -9,6 +9,7 @@ import { StatsRow } from "../components/dashboard/StatsRow";
|
||||
import { PublicationsTable } from "../components/dashboard/PublicationsTable";
|
||||
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
|
||||
import { SyncButton } from "../components/dashboard/SyncButton";
|
||||
import { ArrowLeftIcon } from "../components/ui/Icons";
|
||||
import {
|
||||
downloadExport,
|
||||
searchResearcher,
|
||||
@@ -198,7 +199,15 @@ export function DashboardPage() {
|
||||
<div className="flex min-h-screen flex-col bg-surface-tertiary">
|
||||
<AppHeader variant="dashboard" />
|
||||
<main className="flex-1">
|
||||
<div className="mx-auto w-full max-w-[1100px] px-5 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>
|
||||
|
||||
{researcher ? (
|
||||
<ResearcherCard
|
||||
researcher={researcher}
|
||||
|
||||
@@ -190,7 +190,7 @@ export function GroupResultsPage() {
|
||||
<div className="flex min-h-screen flex-col bg-surface-tertiary">
|
||||
<AppHeader variant="group" />
|
||||
<main className="flex-1">
|
||||
<div className="mx-auto w-full max-w-[1100px] px-5 py-7">
|
||||
<div className="mx-auto w-full max-w-7xl px-4 py-7">
|
||||
{/* Page header */}
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
/**
|
||||
* Locale-aware full date + time formatter (used in dashboard headers).
|
||||
*
|
||||
* The backend stores datetimes in UTC but serialises them without a timezone
|
||||
* suffix (e.g. "2026-05-19T08:20:00"). JS Date treats those naive strings as
|
||||
* *local* time, which would show the wrong hour in non-UTC browsers. We
|
||||
* normalise by appending "Z" so the engine always interprets the value as UTC
|
||||
* and toLocaleString then converts it correctly to the user's local timezone.
|
||||
*/
|
||||
export function formatDate(iso) {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
const normalized = /[Zz]|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`;
|
||||
const d = new Date(normalized);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleString("es-ES", {
|
||||
day: "2-digit",
|
||||
|
||||
Reference in New Issue
Block a user