Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions components/layout/AnnouncementBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<a href={href} target="_blank" rel="noopener noreferrer" className={className}>
{children}
</a>
)
}

return (
<Link href={href} className={className}>
{children}
</Link>
)
}

export default function AnnouncementBanner() {
const [visible, setVisible] = useState(false)
Expand All @@ -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)
}, [])

Expand All @@ -32,25 +58,17 @@ export default function AnnouncementBanner() {
setVisible(false)
}

const LinkOrA = external
? ({ children, className }: WrapperProps) => (
<a href={href} target="_blank" rel="noopener noreferrer" className={className}>
{children}
</a>
)
: ({ children, className }: WrapperProps) => (
<Link href={href} className={className}>
{children}
</Link>
)

return (
<div className="w-full bg-[#3b1445] border-b border-[#5c2070]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between gap-3 h-10">

{/* Left: badge + message (clickable) */}
<LinkOrA className="flex items-center gap-3 flex-1 min-w-0 overflow-hidden group">
<BannerLink
href={href}
external={external}
className="flex items-center gap-3 flex-1 min-w-0 overflow-hidden group"
>
{/* Badge */}
<span className="font-mono text-[10px] uppercase tracking-widest text-[#00FF41] border border-[#00FF41] px-1.5 py-0.5 shrink-0 leading-none">
{label}
Expand All @@ -64,13 +82,17 @@ export default function AnnouncementBanner() {
<span className="ml-16 sm:hidden" aria-hidden="true">{message}</span>
</span>
</span>
</LinkOrA>
</BannerLink>

{/* Right: "View Event →" + dismiss */}
<div className="flex items-center gap-3 shrink-0">
<LinkOrA className="font-mono text-xs text-[#c084d8] hover:text-white transition-colors hidden sm:block whitespace-nowrap">
<BannerLink
href={href}
external={external}
className="font-mono text-xs text-[#c084d8] hover:text-white transition-colors hidden sm:block whitespace-nowrap"
>
View Fundraising Event →
</LinkOrA>
</BannerLink>

<button
onClick={dismiss}
Expand Down
3 changes: 3 additions & 0 deletions components/telehash/CountdownTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default function CountdownTimer({ targetDate, endDate }: CountdownTimerPr
const [concluded, setConcluded] = useState(false)

useEffect(() => {
// The countdown reads the client clock, so it stays hidden until after
// hydration to avoid a server/client mismatch.
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true)
if (!targetDate) return

Expand Down
1 change: 0 additions & 1 deletion components/ui/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export default function Logo({
{/* Dark mode: browser picks this source when prefers-color-scheme: dark */}
<source media="(prefers-color-scheme: dark)" srcSet={asset.black} />
{/* Light mode fallback */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={asset.white}
alt={alt}
Expand Down
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import next from "eslint-config-next";

// eslint-config-next ships its own ignores for .next/, out/, build/ and
// next-env.d.ts, so they are not repeated here.
const config = [...next];

export default config;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"test": "node --test tests/"
},
"keywords": [],
Expand Down