555 lines
21 KiB
TypeScript
555 lines
21 KiB
TypeScript
"use client"
|
|
|
|
import { useReducer } from "react"
|
|
import Link from "next/link"
|
|
import Image from "next/image"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Menu, X, Phone, ChevronDown } from "lucide-react"
|
|
import { GetFreeMachineModal } from "@/components/get-free-machine-modal"
|
|
import { Cart } from "@/components/cart"
|
|
import { CartButton } from "@/components/cart-button"
|
|
import { MobileCartButton } from "@/components/mobile-cart-button"
|
|
|
|
// Header state types following Vercel React Best Practices
|
|
type HeaderState = {
|
|
isMenuOpen: boolean
|
|
isWhoWeServeOpen: boolean
|
|
isVendingMachinesOpen: boolean
|
|
isFoodBeverageOpen: boolean
|
|
isServicesOpen: boolean
|
|
isBlogPostsOpen: boolean
|
|
isAboutOpen: boolean
|
|
isModalOpen: boolean
|
|
isCartOpen: boolean
|
|
}
|
|
|
|
type HeaderAction =
|
|
| { type: "TOGGLE_MENU" }
|
|
| { type: "TOGGLE_WHO_WE_SERVE" }
|
|
| { type: "TOGGLE_VENDING_MACHINES" }
|
|
| { type: "TOGGLE_FOOD_BEVERAGE" }
|
|
| { type: "TOGGLE_SERVICES" }
|
|
| { type: "TOGGLE_BLOG_POSTS" }
|
|
| { type: "TOGGLE_ABOUT" }
|
|
| { type: "SET_MODAL"; value: boolean }
|
|
| { type: "SET_CART"; value: boolean }
|
|
|
|
// Reducer for header state - consolidates related mobile menu states
|
|
function headerReducer(state: HeaderState, action: HeaderAction): HeaderState {
|
|
switch (action.type) {
|
|
case "TOGGLE_MENU":
|
|
return { ...state, isMenuOpen: !state.isMenuOpen }
|
|
case "TOGGLE_WHO_WE_SERVE":
|
|
return { ...state, isWhoWeServeOpen: !state.isWhoWeServeOpen }
|
|
case "TOGGLE_VENDING_MACHINES":
|
|
return { ...state, isVendingMachinesOpen: !state.isVendingMachinesOpen }
|
|
case "TOGGLE_FOOD_BEVERAGE":
|
|
return { ...state, isFoodBeverageOpen: !state.isFoodBeverageOpen }
|
|
case "TOGGLE_SERVICES":
|
|
return { ...state, isServicesOpen: !state.isServicesOpen }
|
|
case "TOGGLE_BLOG_POSTS":
|
|
return { ...state, isBlogPostsOpen: !state.isBlogPostsOpen }
|
|
case "TOGGLE_ABOUT":
|
|
return { ...state, isAboutOpen: !state.isAboutOpen }
|
|
case "SET_MODAL":
|
|
return { ...state, isModalOpen: action.value }
|
|
case "SET_CART":
|
|
return { ...state, isCartOpen: action.value }
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
export function Header() {
|
|
const [state, dispatch] = useReducer(headerReducer, {
|
|
isMenuOpen: false,
|
|
isWhoWeServeOpen: false,
|
|
isVendingMachinesOpen: false,
|
|
isFoodBeverageOpen: false,
|
|
isServicesOpen: false,
|
|
isBlogPostsOpen: false,
|
|
isAboutOpen: false,
|
|
isModalOpen: false,
|
|
isCartOpen: false,
|
|
})
|
|
|
|
const whoWeServeItems = [
|
|
{ label: "Warehouses", href: "/warehouses" },
|
|
{ label: "Auto Repair", href: "/auto-repair" },
|
|
{ label: "Gyms", href: "/gyms" },
|
|
{ label: "Community Centers", href: "/community-centers" },
|
|
{ label: "Dance Studios", href: "/dance-studios" },
|
|
{ label: "Car Washes", href: "/car-washes" },
|
|
]
|
|
|
|
const vendingMachinesItems = [
|
|
{ label: "Machines We Use", href: "/vending-machines/machines-we-use" },
|
|
{
|
|
label: "Machines for Sale in Utah",
|
|
href: "/vending-machines/machines-for-sale",
|
|
},
|
|
{ label: "Vending Machine Manuals", href: "/manuals" },
|
|
]
|
|
|
|
const foodBeverageItems = [
|
|
{ label: "Healthy Options", href: "/food-and-beverage/healthy-options" },
|
|
{
|
|
label: "Traditional Options",
|
|
href: "/food-and-beverage/traditional-options",
|
|
},
|
|
{
|
|
label: "Food & Beverage Suppliers",
|
|
href: "/food-and-beverage/suppliers",
|
|
},
|
|
]
|
|
|
|
const servicesItems = [
|
|
{ label: "Vending Machine Repairs", href: "/services/repairs" },
|
|
{ label: "Vending Machine Moving Services", href: "/services/moving" },
|
|
{ label: "Vending Machine Parts", href: "/services/parts" },
|
|
{ label: "Vending Machine Manuals", href: "/manuals" },
|
|
]
|
|
|
|
const blogPostsItems = [
|
|
{ label: "Seaga HY900 Support", href: "/seaga-hy900-support" },
|
|
]
|
|
|
|
const aboutItems = [
|
|
{ label: "About Us", href: "/about-us" },
|
|
{ label: "Reviews", href: "/reviews" },
|
|
{ label: "FAQs", href: "/about/faqs" },
|
|
]
|
|
|
|
return (
|
|
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/90 shadow-sm">
|
|
<div className="w-full px-4 lg:px-6 xl:px-8 2xl:px-12">
|
|
<div className="flex h-20 items-center justify-between gap-4 lg:gap-6">
|
|
{/* Logo */}
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2 flex-shrink-0 min-w-0"
|
|
>
|
|
<Image
|
|
src="/rmv-logo.png"
|
|
alt="Rocky Mountain Vending"
|
|
width={220}
|
|
height={55}
|
|
className="h-12 sm:h-14 w-auto object-contain"
|
|
priority
|
|
/>
|
|
</Link>
|
|
|
|
{/* Desktop Navigation */}
|
|
<nav className="hidden items-center gap-4 lg:gap-6 md:flex flex-1 justify-center">
|
|
<Link href="/" className="text-sm font-medium transition-colors">
|
|
Home
|
|
</Link>
|
|
|
|
{/* Who We Serve Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand outline-none data-[state=open]:text-primary">
|
|
Who We Serve
|
|
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-52">
|
|
{whoWeServeItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Vending Machines Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand outline-none data-[state=open]:text-primary">
|
|
Vending Machines
|
|
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-60">
|
|
{vendingMachinesItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Food and Beverage Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand whitespace-nowrap outline-none data-[state=open]:text-primary">
|
|
Food & Beverage
|
|
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-60">
|
|
{foodBeverageItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Services Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand outline-none data-[state=open]:text-primary">
|
|
Services
|
|
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-72">
|
|
{servicesItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Blog Posts Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand outline-none data-[state=open]:text-primary">
|
|
Blog Posts
|
|
<ChevronDown className="h-4 w-4" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-52">
|
|
{blogPostsItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* About Us Dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm font-medium hover-brand outline-none data-[state=open]:text-primary">
|
|
About Us
|
|
<ChevronDown className="h-4 w-4" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-52">
|
|
{aboutItems.map((item) => (
|
|
<DropdownMenuItem key={item.href} asChild>
|
|
<Link href={item.href}>{item.label}</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Link
|
|
href="/products"
|
|
className="text-sm font-medium transition-colors"
|
|
>
|
|
Products
|
|
</Link>
|
|
<Link
|
|
href="/service-areas"
|
|
className="text-sm font-medium transition-colors"
|
|
>
|
|
Service Areas
|
|
</Link>
|
|
<Link
|
|
href="/contact-us"
|
|
className="text-sm font-medium transition-colors"
|
|
>
|
|
Contact Us
|
|
</Link>
|
|
</nav>
|
|
|
|
{/* Desktop CTA */}
|
|
<div className="hidden items-center gap-2 lg:gap-3 md:flex flex-shrink-0">
|
|
<CartButton
|
|
onClick={() => dispatch({ type: "SET_CART", value: true })}
|
|
/>
|
|
<a
|
|
href="tel:+14352339668"
|
|
className="flex items-center gap-2 text-sm font-medium transition-colors whitespace-nowrap"
|
|
>
|
|
<Phone className="h-4 w-4 flex-shrink-0" />
|
|
<span className="hidden lg:inline">(435) 233-9668</span>
|
|
</a>
|
|
<Button
|
|
onClick={() => dispatch({ type: "SET_MODAL", value: true })}
|
|
className="bg-primary hover:bg-primary/90 whitespace-nowrap"
|
|
size="sm"
|
|
>
|
|
Get Free Machine
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
className="md:hidden"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
aria-label="Toggle menu"
|
|
>
|
|
{state.isMenuOpen ? (
|
|
<X className="h-6 w-6" />
|
|
) : (
|
|
<Menu className="h-6 w-6" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mobile Navigation */}
|
|
{state.isMenuOpen && (
|
|
<nav className="flex flex-col gap-5 py-6 md:hidden border-t border-border/40">
|
|
<Link
|
|
href="/"
|
|
className="text-sm font-medium py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
Home
|
|
</Link>
|
|
|
|
{/* Who We Serve Mobile Section */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_WHO_WE_SERVE" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="Who We Serve menu"
|
|
aria-expanded={state.isWhoWeServeOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
Who We Serve
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isWhoWeServeOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isWhoWeServeOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{whoWeServeItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Vending Machines Mobile */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_VENDING_MACHINES" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="Vending Machines menu"
|
|
aria-expanded={state.isVendingMachinesOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
Vending Machines
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isVendingMachinesOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isVendingMachinesOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{vendingMachinesItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Food and Beverage Mobile */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_FOOD_BEVERAGE" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="Food & Beverage menu"
|
|
aria-expanded={state.isFoodBeverageOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
Food & Beverage
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isFoodBeverageOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isFoodBeverageOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{foodBeverageItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Services Mobile */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_SERVICES" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="Services menu"
|
|
aria-expanded={state.isServicesOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
Services
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isServicesOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isServicesOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{servicesItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Blog Posts Mobile */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_BLOG_POSTS" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="Blog Posts menu"
|
|
aria-expanded={state.isBlogPostsOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
Blog Posts
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isBlogPostsOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isBlogPostsOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{blogPostsItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* About Us Mobile */}
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={() => dispatch({ type: "TOGGLE_ABOUT" })}
|
|
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
|
aria-label="About menu"
|
|
aria-expanded={state.isAboutOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
About Us
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${state.isAboutOpen ? "rotate-180" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
{state.isAboutOpen && (
|
|
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
|
{aboutItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className="text-sm py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Link
|
|
href="/products"
|
|
className="text-sm font-medium py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
Products
|
|
</Link>
|
|
<Link
|
|
href="/service-areas"
|
|
className="text-sm font-medium py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
Service Areas
|
|
</Link>
|
|
<Link
|
|
href="/contact-us"
|
|
className="text-sm font-medium py-1 transition-colors"
|
|
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
|
>
|
|
Contact Us
|
|
</Link>
|
|
|
|
<div className="flex flex-col gap-4 pt-4 border-t border-border/40">
|
|
<MobileCartButton
|
|
onClick={() => {
|
|
dispatch({ type: "TOGGLE_MENU" })
|
|
dispatch({ type: "SET_CART", value: true })
|
|
}}
|
|
/>
|
|
<a
|
|
href="tel:+14352339668"
|
|
className="flex items-center gap-2 text-sm font-medium transition-colors"
|
|
>
|
|
<Phone className="h-4 w-4" />
|
|
<span>(435) 233-9668</span>
|
|
</a>
|
|
<Button
|
|
onClick={() => {
|
|
dispatch({ type: "TOGGLE_MENU" })
|
|
dispatch({ type: "SET_MODAL", value: true })
|
|
}}
|
|
className="bg-primary hover:bg-primary/90 w-full"
|
|
>
|
|
Get Free Machine
|
|
</Button>
|
|
</div>
|
|
</nav>
|
|
)}
|
|
</div>
|
|
<GetFreeMachineModal
|
|
open={state.isModalOpen}
|
|
onOpenChange={(value) => dispatch({ type: "SET_MODAL", value })}
|
|
/>
|
|
<Cart
|
|
isOpen={state.isCartOpen}
|
|
onClose={() => dispatch({ type: "SET_CART", value: false })}
|
|
/>
|
|
</header>
|
|
)
|
|
}
|