DEVELOPER REFERENCE — LIBRARY LOOT
Library Loot
Developer Reference
← Index

Source: components/Navbar.jsx

// src/components/Navbar.jsx
//
// Sticky top navigation. Mobile-friendly hamburger collapses links below the
// nav bar; brand mark stays visible.
//
// Created by Miguel Brown on 5/12/26.
// Copyright (c) 2026 Luckey Logic LLC. All rights reserved.

import React, { useEffect, useRef, useState } from 'react'
import { Link, NavLink, useNavigate }         from 'react-router-dom'

import { useAuth }            from '../context/AuthContext.jsx'

import siteContent            from '../data/siteContent.js'

import styles                 from './Navbar.module.css'

const links = [
  { to: '/',             label: 'Home'        },
  { to: '/books',        label: 'Books'       },
  { to: '/about',        label: 'About'       },
  { to: '/for-parents',  label: 'For Parents' },
  { to: '/donors',       label: 'Donors'      },
  { to: '/faq',          label: 'FAQ'         }
]

/**
 * Navbar — sticky top navigation with brand mark and primary links plus
 * an auth slot on the right (Sign in / Sign up when signed out, display
 * name + Sign out when signed in).
 *
 * @returns {JSX.Element}
 */
export default function Navbar() {

  const { user, isAdmin, signOut } = useAuth()
  const navigate                   = useNavigate()
  const [open, setOpen]            = useState(false)

  // Refs for outside-tap dismiss. The nav element is the dropdown
  // panel itself; the toggle is the hamburger button. A pointerdown
  // outside both should close the menu. We deliberately let taps
  // INSIDE the panel pass through — closing on internal taps would
  // break things like the future "expand a submenu" or "tap inside
  // the panel without choosing a link."
  const navRef                     = useRef(null)
  const toggleRef                  = useRef(null)

  const closeMenu = () => setOpen(false)

  // Outside-tap + Escape dismiss for the mobile menu. Only active
  // when the menu is open — avoids burning event handlers in the
  // common closed-state.
  useEffect(() => {
    if (!open) return

    function handlePointerDown(e) {
      const inNav    = navRef.current    && navRef.current.contains(e.target)
      const inToggle = toggleRef.current && toggleRef.current.contains(e.target)
      if (!inNav && !inToggle) setOpen(false)
    }

    function handleKeyDown(e) {
      if (e.key === 'Escape') setOpen(false)
    }

    document.addEventListener('pointerdown', handlePointerDown)
    document.addEventListener('keydown',     handleKeyDown)
    return () => {
      document.removeEventListener('pointerdown', handlePointerDown)
      document.removeEventListener('keydown',     handleKeyDown)
    }
  }, [open])

  const handleSignOut = async () => {
    closeMenu()
    try {
      await signOut()
      navigate('/', { replace: true })
    } catch (e) {
      // signOut() already surfaced via context error; swallow here.
    }
  }

  // Greeting precedence: displayName first name → email local-part (first
  // segment before any dot) → generic fallback. Never the full email — it's
  // long, sensitive-feeling, and the cap-12-char ellipsis was hiding the @ symbol.
  const greeting = (() => {
    if (!user) return null
    if (user.displayName && user.displayName.trim().length > 0) {
      return user.displayName.trim().split(' ')[0]
    }
    if (user.email) {
      return user.email.split('@')[0].split('.')[0]
    }
    return 'there'
  })()

  return (
    <header className={styles.navWrap}>
      <div className={`container ${styles.navInner}`}>

        <Link to="/" className={styles.brand} onClick={closeMenu}>
          <span className={styles.brandGlyph} aria-hidden="true">⚡</span>
          <span className={styles.brandText}>{siteContent.brand.name}</span>
        </Link>

        <button
          ref          ={toggleRef}
          type         ="button"
          className    ={styles.toggle}
          aria-label   ="Toggle navigation menu"
          aria-expanded={open}
          onClick      ={() => setOpen(prev => !prev)}
        >
          <span className={styles.toggleBar} />
          <span className={styles.toggleBar} />
          <span className={styles.toggleBar} />
        </button>

        <nav
          ref       ={navRef}
          className ={`${styles.links} ${open ? styles.linksOpen : ''}`}
        >
          {links.map(({ to, label }) => (
            <NavLink
              key       ={to}
              to        ={to}
              end       ={to === '/'}
              onClick   ={closeMenu}
              className ={({ isActive }) =>
                `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
              }
            >
              {label}
            </NavLink>
          ))}

          <span className={styles.navDivider} aria-hidden="true" />

          {user ? (
            <>
              <NavLink
                to        ="/account"
                onClick   ={closeMenu}
                className ={({ isActive }) =>
                  `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
                }
              >
                My account
              </NavLink>
              {isAdmin ? (
                <NavLink
                  to        ="/admin"
                  onClick   ={closeMenu}
                  className ={({ isActive }) =>
                    `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
                  }
                >
                  Admin
                </NavLink>
              ) : null}
              <span className={styles.greeting} title={user.email || ''}>
                Hi, {greeting}
              </span>
              <button
                type      ="button"
                onClick   ={handleSignOut}
                className ={styles.navAction}
              >
                Sign out
              </button>
            </>
          ) : (
            <>
              <NavLink
                to        ="/login"
                onClick   ={closeMenu}
                className ={({ isActive }) =>
                  `${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
                }
              >
                Sign in
              </NavLink>
              <Link
                to        ="/signup"
                onClick   ={closeMenu}
                className ={styles.navCta}
              >
                Sign up
              </Link>
            </>
          )}
        </nav>

      </div>
    </header>
  )
}