Skip to content
3 changes: 3 additions & 0 deletions web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { Geist, Geist_Mono, Averia_Serif_Libre } from 'next/font/google'
import './globals.css'
import Navbar from '@/components/Navbar'

const geistSans = Geist({
variable: '--font-geist-sans',
Expand Down Expand Up @@ -32,7 +33,9 @@ export default function RootLayout({
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} ${averiaSerif.variable} antialiased`}
style={{ paddingTop: '88px' }}
>
<Navbar />
{children}
</body>
</html>
Expand Down
172 changes: 172 additions & 0 deletions web/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { usePathname } from 'next/navigation'

const navLinks = [
{ label: 'Home', href: '/' },
{ label: 'About Us', href: '/about' },
{ label: 'Events', href: '/events' },
{ label: 'Sponsors', href: '/sponsors' },
]

const ctaLink = { label: 'Join SSA!', href: '/contact' }

export default function Navbar() {
const [hidden, setHidden] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const pathname = usePathname()
const menuOpenRef = useRef(false)

// Keep ref in sync with menuOpen state
useEffect(() => {
menuOpenRef.current = menuOpen
}, [menuOpen])

// Half-sticky scroll behaviour
useEffect(() => {
Comment thread
GladwynChua marked this conversation as resolved.
let lastY = window.scrollY

const handleScroll = () => {
if (menuOpenRef.current) return
const y = window.scrollY
if (y > lastY && y > 80) {
setHidden(true)
} else if (y <= lastY) {
setHidden(false)
}
lastY = y
}

handleScroll()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this! Unfortunately I don't think it quite fixes the bug yet, let me walk through why so it makes sense.

When the page first loads, you set lastY = window.scrollY on the line just above. Then handleScroll() runs straight away, and inside it y = window.scrollY too. So y and lastY are the same number at this point.

That means:

  • y > lastY → false (they're equal), so the navbar doesn't get hidden
  • y <= lastY → true, so we run setHidden(false) and the navbar stays visible

The problem is that on first load there's no "scroll direction" to detect yet (the user hasn't moved). So checking direction here can't tell us whether to hide the navbar.

The simplest fix is to just look at where the user is on the page (not which way they're moving). Something like:

setHidden(window.scrollY > 80)

at the top of the effect. That way, if someone refreshes while already scrolled down, the navbar starts hidden like it should. Then your existing scroll handler takes over from there.

Try it out by scrolling down the page and refreshing, you should see the navbar stay hidden until you scroll up. Let me know if anything's unclear!!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for the feedback Joe. You are most certainly right, let me try implementing it this way 👍

window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])

// Close menu on desktop resize
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) setMenuOpen(false)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])

// Lock body scroll when menu is open
useEffect(() => {
if (menuOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [menuOpen])

return (
<>
{/* ── NAVBAR ── */}
<nav
className={`fixed top-0 left-0 right-0 z-50 h-[88px] bg-ssa-red border-b border-white/20 flex items-center px-4 sm:px-6 lg:px-10 transition-all duration-300 ${hidden ? '-translate-y-full' : 'translate-y-0 shadow-lg'}`}
>
<div className="w-full flex items-center gap-4">
{/* Logo */}
<Link href="/" className="shrink-0" aria-label="SSA Home">
<Image
src="/mascot.png"
alt="SSA Mascot"
width={56}
height={56}
className="object-contain w-[46px] h-[46px] sm:w-[56px] sm:h-[56px]"
/>
</Link>

{/* Desktop links */}
<ul className="hidden md:flex items-center gap-1">
{navLinks.map(({ label, href }) => {
const isActive = pathname === href
return (
<li key={href}>
<Link
href={href}
aria-current={isActive ? 'page' : undefined}
className={`group relative font-averia font-bold text-xl px-4 py-2 transition-colors whitespace-nowrap hover:text-ssa-yellow ${isActive ? 'text-ssa-yellow' : 'text-ssa-black'}`}
>
{label}
<span
className={`absolute bottom-0 left-4 right-4 h-[2px] bg-ssa-yellow transition-transform duration-200 ${isActive ? 'scale-x-100' : 'scale-x-0 group-hover:scale-x-100'}`}
/>
</Link>
</li>
)
})}
</ul>

{/* Desktop CTA */}
<div className="hidden md:flex items-center ml-auto">
<Link
href={ctaLink.href}
className="font-averia font-bold text-xl text-ssa-black bg-ssa-yellow-light px-5 py-2 rounded-full hover:bg-ssa-yellow transition-colors shrink-0"
>
{ctaLink.label}
</Link>
</div>

{/* Hamburger */}
<button
Comment thread
GladwynChua marked this conversation as resolved.
className="md:hidden ml-auto flex flex-col justify-center gap-[5px] w-10 h-10 p-1 shrink-0"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
Comment thread
GladwynChua marked this conversation as resolved.
<span
className={`block h-[3px] w-6 bg-ssa-black rounded transition-all duration-300 ${menuOpen ? 'translate-y-[8px] rotate-45' : ''}`}
/>
<span
className={`block h-[3px] w-6 bg-ssa-black rounded transition-all duration-300 ${menuOpen ? 'opacity-0 scale-x-0' : ''}`}
/>
<span
className={`block h-[3px] w-6 bg-ssa-black rounded transition-all duration-300 ${menuOpen ? '-translate-y-[8px] -rotate-45' : ''}`}
/>
</button>
</div>
</nav>

{/* ── MOBILE MENU ── */}
<div
Comment thread
GladwynChua marked this conversation as resolved.
id="mobile-menu"
className={`fixed top-[88px] left-0 right-0 z-40 bg-ssa-red border-t border-white/20 transition-all duration-300 ease-in-out md:hidden ${menuOpen ? 'opacity-100 translate-y-0 pointer-events-auto' : 'opacity-0 -translate-y-2 pointer-events-none'}`}
>
<ul className="flex flex-col">
{[...navLinks, ctaLink].map(({ label, href }) => {
const isActive = pathname === href
return (
<li key={href}>
<Link
href={href}
onClick={() => setMenuOpen(false)}
aria-current={isActive ? 'page' : undefined}
className={`block font-averia font-bold text-lg px-6 py-4 border-b border-white/10 border-l-4 hover:text-ssa-yellow transition-colors ${isActive ? 'text-ssa-yellow border-l-ssa-yellow' : 'text-ssa-black border-l-transparent'}`}
>
{label}
</Link>
</li>
)
})}
</ul>
</div>

{/* Overlay */}
{menuOpen && (
<div
className="fixed inset-0 z-30 md:hidden"
onClick={() => setMenuOpen(false)}
/>
)}
</>
)
}
Loading