Last active
May 30, 2025 16:32
-
-
Save ORESoftware/322f76b1dfeca1191995a526d3f4d0d1 to your computer and use it in GitHub Desktop.
file example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// source file path: ./src/components/nav/coach-header.tsx | |
'use client'; | |
import React, { useEffect, useRef, useState } from 'react'; | |
import Link from 'next/link'; | |
import { | |
ChevronDownIcon, | |
ChevronUpIcon, | |
MagnifyingGlassIcon, | |
PersonIcon, | |
} from '@radix-ui/react-icons'; | |
import MobileNavDropdown from '@/components/nav/mobile-dropdown-menu'; | |
import NavSearch, { NavSearchBarHandle } from '../search/nav-search'; | |
import UserInfoButton from './nav-utils/user-info-button'; | |
export default function CoachHeader() { | |
const [showSearch, setShowSearch] = useState(false); | |
const [showMoreDropdown, setShowMoreDropdown] = useState(false); | |
const moreDropdownRef = useRef<HTMLDivElement>(null); | |
const navSearchRef = useRef<NavSearchBarHandle>(null); | |
useEffect(() => { | |
const handleClickOutside = (event: MouseEvent) => { | |
if (moreDropdownRef.current && !moreDropdownRef.current.contains(event.target as Node)) { | |
setShowMoreDropdown(false); | |
} | |
}; | |
document.addEventListener('mousedown', handleClickOutside); | |
return () => { | |
document.removeEventListener('mousedown', handleClickOutside); | |
}; | |
}, []); | |
const handleHeaderSearchButtonClick = () => { | |
navSearchRef.current?.triggerSubmit(); | |
}; | |
return ( | |
<> | |
{/* Main header container: relative positioning for the search overlay context */} | |
<header className='relative top-0 z-50 flex h-[60px] w-full items-center justify-between gap-2 border-b bg-[#03796b] px-4 py-2 text-white sm:px-8 lg:h-[80px] lg:gap-0'> | |
{!showSearch ? ( | |
// Normal header layout | |
<> | |
{/* Desktop Navigation and other elements */} | |
<div className='hidden flex-grow items-center justify-between text-sm font-medium md:flex'> | |
{/* Left-aligned group: Coaching Icon and Main Navigation Links */} | |
<div className='flex items-center gap-6'> | |
<Link href='/u/coach'> | |
<div className='flex items-center font-semibold'> | |
<PersonIcon className='size-6' /> | |
<span className='sr-only md:not-sr-only'>Coaching</span> | |
</div> | |
</Link> | |
<Link href='/u/coach'>Home</Link> | |
<Link href='/u/coach/dashboard'>Dashboard</Link> | |
<Link href='/u/coach/profile'>Profile</Link> | |
<Link href='/u/coach/clients'>Clients</Link> | |
<Link href='/u/coach/schedule'>Schedule</Link> | |
<Link href='/u/coach/events'>Events</Link> | |
</div> | |
{/* Right-aligned group (within the main nav area): Search and More... */} | |
<div className='flex items-center gap-6'> | |
<div | |
className='flex cursor-pointer items-center gap-1' | |
onClick={() => setShowSearch(true)}> | |
<MagnifyingGlassIcon className='size-4' /> | |
<span>Search</span> | |
</div> | |
<div className='relative' ref={moreDropdownRef}> | |
<div | |
className='flex cursor-pointer items-center gap-1' | |
onClick={() => setShowMoreDropdown(!showMoreDropdown)}> | |
<span>More...</span> | |
{showMoreDropdown ? ( | |
<ChevronUpIcon className='size-4' /> | |
) : ( | |
<ChevronDownIcon className='size-4' /> | |
)} | |
</div> | |
{showMoreDropdown && ( | |
<div className='absolute right-0 top-full z-10 mt-2 w-32 rounded-md border bg-white py-1 text-black shadow-lg'> | |
<Link href='/u/coach/item1' className='block px-4 py-2 hover:bg-gray-100'> | |
Item 1 | |
</Link> | |
<Link href='/u/coach/item2' className='block px-4 py-2 hover:bg-gray-100'> | |
Item 2 | |
</Link> | |
<Link href='/u/coach/item3' className='block px-4 py-2 hover:bg-gray-100'> | |
Item 3 | |
</Link> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
{/* Mobile Dropdown - visible on smaller screens */} | |
{/* This div takes full width on mobile, pushing UserInfoButton to a new line if not handled by parent flex */} | |
<div className='flex-grow md:hidden'> | |
{' '} | |
{/* Use flex-grow to allow MobileNavDropdown to take space */} | |
<MobileNavDropdown navType='coach' /> | |
</div> | |
{/* User Info Button - consistently placed to the far right */} | |
{/* ml-auto on this div will push it to the right of other sibling elements in the flex container */} | |
<div className='ml-auto flex-shrink-0'> | |
<UserInfoButton loggedInUser={null!} /> | |
</div> | |
</> | |
) : ( | |
// Search mode layout - Full overlay | |
<div className='absolute inset-0 z-[51] flex h-full w-full items-center justify-between gap-2 bg-[#03796b] px-4 py-2 sm:px-8'> | |
<div className='flex flex-1 items-center gap-2'> | |
<NavSearch ref={navSearchRef} /> | |
<button | |
onClick={handleHeaderSearchButtonClick} | |
className='flex flex-shrink-0 items-center gap-1.5 rounded-md border border-white/40 bg-teal-500 px-3 py-1.5 text-sm text-white transition-colors hover:bg-teal-600 focus:outline-none focus:ring-2 focus:ring-white focus:ring-opacity-75' | |
title='Submit Search'> | |
<MagnifyingGlassIcon className='h-4 w-4' /> | |
Search | |
</button> | |
<button | |
onClick={() => setShowSearch(false)} | |
className='flex-shrink-0 rounded-md border border-white/30 bg-pink-500 px-3 py-1.5 text-sm text-white transition-colors hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-white focus:ring-opacity-75' | |
title='Close Search Bar'> | |
Close | |
</button> | |
</div> | |
<div className='ml-2 flex-shrink-0'> | |
<UserInfoButton loggedInUser={null!} /> | |
</div> | |
</div> | |
)} | |
</header> | |
</> | |
); | |
} | |
// source file path: ./src/components/nav/mobile-dropdown-menu.tsx | |
'use client'; | |
import { useState } from 'react'; | |
import Link from 'next/link'; | |
import { motion } from 'framer-motion'; | |
import { | |
FiCalendar, | |
FiChevronDown, | |
FiClock, | |
FiGrid, | |
FiHome, | |
FiSettings, | |
FiStar, | |
FiUser, | |
FiUsers, | |
} from 'react-icons/fi'; | |
interface MobileNavDropdownProps { | |
navType: 'admin' | 'coach' | 'client' | 'intake' | 'user'; // Accepting a string prop for nav type | |
} | |
const adminNav = [ | |
{ href: '/u/admin', label: 'Home', icon: FiHome }, | |
{ href: '/u/admin/audit', label: 'Audit', icon: FiGrid }, | |
{ href: '/u/admin/dashboard', label: 'Dashboard', icon: FiGrid }, | |
{ href: '/u/admin/users', label: 'Users', icon: FiUsers }, | |
{ href: '/u/admin/sessions', label: 'Sessions', icon: FiCalendar }, | |
{ href: '/u/admin/skills', label: 'Skills', icon: FiStar }, | |
{ href: '/u/admin/blogging', label: 'Blog Mgmt', icon: FiStar }, | |
// { href: '/u/admin/settings', label: 'Settings', icon: FiSettings }, | |
]; | |
const coachNav = [ | |
{ href: '/u/coach', label: 'Home', icon: FiHome }, | |
{ href: '/u/coach/dashboard', label: 'Dashboard', icon: FiGrid }, | |
{ href: '/u/coach/profile', label: 'Profile', icon: FiUser }, | |
{ href: '/u/coach/clients', label: 'Clients', icon: FiUsers }, | |
{ href: '/u/coach/schedule', label: 'Schedule', icon: FiCalendar }, | |
{ href: '/u/coach/events', label: 'Events', icon: FiStar }, | |
]; | |
const clientNav = [ | |
{ href: '/u/client/sessions', label: 'Sessions', icon: FiCalendar }, | |
{ href: '/u/client/schedule', label: 'Schedule', icon: FiClock }, | |
{ href: '/u/client/profile', label: 'Profile', icon: FiUser }, | |
]; | |
const intakeNav = [ | |
{ href: '/u/intake/appointments', label: 'Intake Appointments', icon: FiCalendar }, | |
{ href: '/u/intake/users', label: 'Users', icon: FiUsers }, | |
{ href: '/u/intake/skills', label: 'Skills', icon: FiStar }, | |
{ href: '/u/intake/availability', label: 'Availability', icon: FiClock }, | |
]; | |
const userNav = [ | |
{ href: '/u/intake/appointments', label: 'Intake Appointments', icon: FiCalendar }, | |
{ href: '/u/intake/users', label: 'Users', icon: FiUsers }, | |
{ href: '/u/intake/skills', label: 'Skills', icon: FiStar }, | |
{ href: '/u/intake/availability', label: 'Availability', icon: FiClock }, | |
]; | |
export default function MobileNavDropdown({ navType }: MobileNavDropdownProps) { | |
const [open, setOpen] = useState(false); | |
// Choose which nav to render based on the navType prop | |
const navItems = | |
navType === 'admin' | |
? adminNav | |
: navType === 'coach' | |
? coachNav | |
: navType === 'client' | |
? clientNav | |
: navType === 'user' | |
? userNav | |
: intakeNav; | |
return ( | |
<div className='relative md:hidden'> | |
<button | |
onClick={() => setOpen(!open)} | |
className='text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75'> | |
<FiChevronDown className={`text-2xl transition-transform ${open ? 'rotate-180' : ''}`} /> | |
</button> | |
{open && ( | |
<motion.div | |
className='absolute right-0 z-50 mt-2 w-56 overflow-hidden rounded-md bg-white text-gray-800 shadow-lg' | |
initial={{ opacity: 0, y: -10 }} | |
animate={{ opacity: 1, y: 0 }} | |
exit={{ opacity: 0, y: 10 }} | |
transition={{ duration: 0.2, ease: 'easeInOut' }}> | |
<ul className='py-2'> | |
{navItems.map((item) => { | |
const Icon = item.icon; // Dynamically use the icon component | |
return ( | |
<li key={item.href}> | |
<Link | |
href={item.href} | |
onClick={() => setOpen(false)} | |
className='flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1'> | |
<Icon className='h-5 w-5 text-primary-500' /> | |
{item.label} | |
</Link> | |
</li> | |
); | |
})} | |
<li className='my-1'> | |
<div className='h-px bg-gradient-to-r from-transparent via-gray-200 to-transparent' /> | |
</li> | |
<li> | |
<Link | |
href='/u/user/settings' | |
onClick={() => setOpen(false)} | |
className='flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1'> | |
<FiSettings className='h-5 w-5 text-gray-500' /> | |
Settings | |
</Link> | |
</li> | |
</ul> | |
</motion.div> | |
)} | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment