From 2c78489e4d385550125be26e3dc1b09b7548001a Mon Sep 17 00:00:00 2001 From: adevopg Date: Wed, 15 Jul 2026 12:03:25 +0000 Subject: [PATCH] fix: 47 errores de lint, y uno de ellos era un bug de verdad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salieron al pasar el lint por todo el proyecto. De 47 a 0 (quedan 15 avisos: vs y el del tema, los dos deliberados). - Footer y Video (42 de los 47, que en realidad eran 7 enlaces: el plugin repite cada uno 6 veces): usaban `` SIN el idioma delante. No estaba roto de milagro: el middleware lo salvaba mirando la cookie NEXT_LOCALE. Pero cada clic se comía un 307 y recargaba la página entera en vez de navegar en cliente, y el idioma lo decidía la cookie en vez de la URL en la que estás. Ahora van con el `Link` de i18n, que ya existía. - Turnstile: escribía una ref (`cb.current = onVerify`) DURANTE el render. Es el patrón de "callback fresca", pero no está permitido: React puede descartar ese render y dejarla mal. Pasa a un efecto. - ActivateClient y ConfirmClient: ponían el estado de error con setState dentro del efecto cuando NO hay hash. Eso se sabe ya al renderizar (es un prop), así que se deriva del estado inicial y se ahorra un render. - BattlepayList: `window.location.href = …` -> `.assign()`, igual que en la tienda. - CookieConsent: aquí el efecto es correcto y la regla no aplica, así que se silencia explicando por qué: el consentimiento vive en una cookie del navegador, en el servidor no existe, y leerlo al renderizar rompería la hidratación (le saldría el banner a quien ya había decidido). Verificado: desde /es/ el pie enlaza a /es/terms-and-conditions y desde /en/ a /en/terms-and-conditions; portada, cookies, tienda y mi-cuenta siguen dando 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../activate-account/ActivateClient.tsx | 12 +++++------- web-next/components/BattlepayList.tsx | 4 +++- web-next/components/ConfirmClient.tsx | 10 ++++------ web-next/components/CookieConsent.tsx | 7 +++++++ web-next/components/Footer.tsx | 17 +++++++++++------ web-next/components/Turnstile.tsx | 7 ++++++- web-next/components/Video.tsx | 6 ++++-- 7 files changed, 40 insertions(+), 23 deletions(-) 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 (