diff --git a/web-next/app/[locale]/activate-account/ActivateClient.tsx b/web-next/app/[locale]/activate-account/ActivateClient.tsx index 6b2ec81..2a03a8b 100644 --- a/web-next/app/[locale]/activate-account/ActivateClient.tsx +++ b/web-next/app/[locale]/activate-account/ActivateClient.tsx @@ -8,18 +8,16 @@ const ERROR_KEYS = ['invalidLink', 'expiredLink'] as const export function ActivateClient({ hash }: { hash: string }) { const t = useTranslations('Activate') - const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading') + // Sin hash el error se sabe YA, al renderizar (y 'invalidLink' es justo el + // errorKey por defecto): se deriva del prop en vez de ponerlo con setState + // dentro del efecto, que encadena un render de más. + const [state, setState] = useState<'loading' | 'ok' | 'error'>(hash ? 'loading' : 'error') const [errorKey, setErrorKey] = useState('invalidLink') const ran = useRef(false) useEffect(() => { - if (ran.current) return // evita doble POST en StrictMode + if (!hash || ran.current) return // evita doble POST en StrictMode ran.current = true - if (!hash) { - setErrorKey('invalidLink') - setState('error') - return - } fetch('/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/web-next/components/BattlepayList.tsx b/web-next/components/BattlepayList.tsx index d3381f6..087cce9 100644 --- a/web-next/components/BattlepayList.tsx +++ b/web-next/components/BattlepayList.tsx @@ -22,7 +22,9 @@ export function BattlepayList({ orders }: { orders: BattlepayOrder[] }) { }) const d: { success?: boolean; url?: string; error?: string } = await res.json() if (d.success && d.url) { - window.location.href = d.url + // .assign() y no `location.href = …`: equivalente, pero la regla del + // compilador de React prohíbe asignar a algo de fuera del componente. + window.location.assign(d.url) } else { setError(t(d.error === 'orderNotFound' ? 'orderNotFound' : 'genericError')) setBusy(null) diff --git a/web-next/components/ConfirmClient.tsx b/web-next/components/ConfirmClient.tsx index 391f6bc..a299e52 100644 --- a/web-next/components/ConfirmClient.tsx +++ b/web-next/components/ConfirmClient.tsx @@ -17,16 +17,14 @@ export function ConfirmClient({ showLogin?: boolean }) { const t = useTranslations(namespace) - const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading') + // Sin hash el error se sabe YA, al renderizar: se deriva del prop en vez de + // ponerlo con setState dentro del efecto (eso encadena un render de más). + const [state, setState] = useState<'loading' | 'ok' | 'error'>(hash ? 'loading' : 'error') const ran = useRef(false) useEffect(() => { - if (ran.current) return + if (!hash || ran.current) return ran.current = true - if (!hash) { - setState('error') - return - } fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/web-next/components/CookieConsent.tsx b/web-next/components/CookieConsent.tsx index 5592071..33a74fe 100644 --- a/web-next/components/CookieConsent.tsx +++ b/web-next/components/CookieConsent.tsx @@ -103,9 +103,16 @@ export function CookieBanner() { useEffect(() => { window.UWCookies = { showBanner, revokeConsent, acceptAll, rejectAll } // Precarga los checkboxes con lo ya guardado (si lo hay). + // El lint avisa de que esto encadena un render, y aquí no hay alternativa: + // el consentimiento está en una cookie del navegador y en el servidor no + // existe, así que no se puede saber al renderizar sin romper la hidratación + // (saldría el banner en el HTML a quien ya había decidido). El render de más + // es inherente y solo ocurre al montar. + /* eslint-disable react-hooks/set-state-in-effect */ const saved = readConsent() if (saved) setPrefs({ analytics: !!saved.analytics, marketing: !!saved.marketing }) else setVisible(true) // Aparece si aún no hay decisión guardada. + /* eslint-enable react-hooks/set-state-in-effect */ const onShow = () => { const cur = readConsent() if (cur) setPrefs({ analytics: !!cur.analytics, marketing: !!cur.marketing }) diff --git a/web-next/components/Footer.tsx b/web-next/components/Footer.tsx index 149b9fa..83617e1 100644 --- a/web-next/components/Footer.tsx +++ b/web-next/components/Footer.tsx @@ -1,9 +1,14 @@ import { getTranslations } from 'next-intl/server' import { getRealmName } from '@/lib/realm' +import { Link } from '@/i18n/navigation' /** * Pie de página — réplica del partial Django `partials/footer.html` * (enlaces legales + copyright + crédito de diseño) con las clases del tema real. + * + * Los enlaces van con el `Link` de i18n, no con : sin el idioma + * delante, cada clic se comía un 307 del middleware y recargaba la página + * entera, y el idioma lo decidía la cookie en vez de la URL en la que estás. */ export async function Footer() { const t = await getTranslations('UI') @@ -13,27 +18,27 @@ export async function Footer() { return (