From b121da09f76c1a98a97ae76340100cc0177e5e20 Mon Sep 17 00:00:00 2001 From: Tyler Stevens Date: Tue, 4 Aug 2026 13:19:12 -0600 Subject: [PATCH] chore: make npm run lint runnable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo had no ESLint config, so `next lint` dropped into its interactive "How would you like to configure ESLint?" setup prompt and never completed. Lint has therefore never run in CI or locally, and the pre-PR check of lint + build + test could not pass. - add eslint.config.mjs — flat config (ESLint 9) spreading the array eslint-config-next already exports. It ships ignores for .next/, out/, build/ and next-env.d.ts, so they are not repeated - switch the lint script from `next lint` to `eslint .`. `next lint` is deprecated and is removed entirely in Next 16 Lint then reported 4 errors from 3 root causes, all pre-existing: - AnnouncementBanner declared its LinkOrA wrapper inside the render, giving the component a new identity every render and remounting the banner contents. Hoisted to module scope as BannerLink - AnnouncementBanner and CountdownTimer both call setState in a mount effect. Both are deliberate hydration guards — localStorage and the client clock are unavailable during SSR — so each gets a targeted eslint-disable-next-line with the reason. The rule stays at error strength for new code - Logo.tsx carried an eslint-disable for a rule that no longer fires on inside . Removed 7 @next/next/no-img-element warnings remain and do not fail the run. Several are intentional (remote avatars, supporter logos); converting them to next/image is a separate call. Verified: lint, build and test all exit 0. AnnouncementBanner and CountdownTimer both render from null data, so both were temporarily activated to exercise them — internal and external banner branches (target/rel correct on external), dismiss writes localStorage and hides the banner, and the countdown ticks after hydration. The temporary data edits were reverted; data/ is untouched. Co-Authored-By: Claude Opus 5 --- components/layout/AnnouncementBanner.tsx | 56 +++++++++++++++++------- components/telehash/CountdownTimer.tsx | 3 ++ components/ui/Logo.tsx | 1 - eslint.config.mjs | 7 +++ package.json | 2 +- 5 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 eslint.config.mjs diff --git a/components/layout/AnnouncementBanner.tsx b/components/layout/AnnouncementBanner.tsx index 29b7a02..2e3c880 100644 --- a/components/layout/AnnouncementBanner.tsx +++ b/components/layout/AnnouncementBanner.tsx @@ -4,7 +4,30 @@ import { useEffect, useState } from 'react' import Link from 'next/link' import { activeAnnouncement } from '@/data/announcements' -type WrapperProps = { children: React.ReactNode; className?: string } +type BannerLinkProps = { + href: string + external?: boolean + children: React.ReactNode + className?: string +} + +/** Declared at module scope — defining this inside the render remounted the + * banner contents on every render and reset their state. */ +function BannerLink({ href, external, children, className }: BannerLinkProps) { + if (external) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} export default function AnnouncementBanner() { const [visible, setVisible] = useState(false) @@ -20,6 +43,9 @@ export default function AnnouncementBanner() { // Check dismiss state if (localStorage.getItem(`banner_dismissed_${id}`)) return + // localStorage does not exist during SSR, so dismissal can only be resolved + // after mount. The banner starts hidden and reveals itself here. + // eslint-disable-next-line react-hooks/set-state-in-effect setVisible(true) }, []) @@ -32,25 +58,17 @@ export default function AnnouncementBanner() { setVisible(false) } - const LinkOrA = external - ? ({ children, className }: WrapperProps) => ( - - {children} - - ) - : ({ children, className }: WrapperProps) => ( - - {children} - - ) - return (
{/* Left: badge + message (clickable) */} - + {/* Badge */} {label} @@ -64,13 +82,17 @@ export default function AnnouncementBanner() { - + {/* Right: "View Event →" + dismiss */}
- + View Fundraising Event → - +