Skip to content
Merged
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
229 changes: 173 additions & 56 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,205 @@
"use client";

import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

// Extracted icon components
const GitHubIcon = () => (
<svg
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
);

const MenuIcon = () => (
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
);

const CloseIcon = () => (
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);

// Navigation links configuration
const navLinks = [
{ href: "/", label: "Home" },
{ href: "/lessons", label: "Lessons" },
{ href: "/sandbox", label: "Sandbox" },
{ href: "/visualizer", label: "Visualizer" },
];

export default function Header() {
const pathname = usePathname();
const [isMenuOpen, setIsMenuOpen] = useState(false);

// Close mobile menu when navigating between pages
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);

// Close menu when clicking outside or pressing Escape
useEffect(() => {
if (!isMenuOpen) return;

const handleOutsideClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (
!target.closest("#mobile-menu-button") &&
!target.closest("#mobile-menu")
) {
setIsMenuOpen(false);
}
};

const handleEsc = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsMenuOpen(false);
}
};

// Prevent scrolling when mobile menu is open
document.body.style.overflow = "hidden";

document.addEventListener("mousedown", handleOutsideClick);
document.addEventListener("keydown", handleEsc);

return () => {
document.body.style.overflow = "";
document.removeEventListener("mousedown", handleOutsideClick);
document.removeEventListener("keydown", handleEsc);
};
}, [isMenuOpen]);

const isActive = (path: string) => {
if (path === "/") {
return pathname === path;
}
return pathname === path || pathname.startsWith(`${path}/`);
};

return (
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<Link href="/" className="font-bold text-xl text-blue-600">
SQL Playground
</Link>
</div>
<nav className="ml-6 flex space-x-8">
<Link
href="/"
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
pathname === "/"
? "border-blue-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
Home
</Link>
<Link
href="/lessons"
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
pathname === "/lessons" || pathname.startsWith("/lessons/")
? "border-blue-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
Lessons
</Link>
<Link
href="/sandbox"
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
pathname === "/sandbox"
? "border-blue-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
Sandbox
</Link>
{/* Logo */}
<div className="flex items-center">
<Link href="/" className="font-bold text-xl text-blue-600">
SQL Playground
</Link>
</div>

{/* Desktop navigation */}
<nav className="hidden md:flex items-center space-x-8">
{navLinks.map((link) => (
<Link
href="/visualizer"
key={link.href}
href={link.href}
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
pathname === "/visualizer"
isActive(link.href)
? "border-blue-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
Visualizer
{link.label}
</Link>
</nav>
</div>
<div className="flex items-center">
<a
))}
<Link
href="https://github.com/Scc33/BuddySQL"
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:text-gray-700"
>
<span className="sr-only">GitHub</span>
<svg
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
<GitHubIcon />
</Link>
</nav>

{/* Mobile menu button */}
<div className="flex items-center md:hidden">
<a
href="https://github.com/Scc33/BuddySQL"
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:text-gray-700 mr-4"
>
<span className="sr-only">GitHub</span>
<GitHubIcon />
</a>
<button
id="mobile-menu-button"
type="button"
className="inline-flex items-center justify-center p-2 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 cursor-pointer"
aria-controls="mobile-menu"
aria-expanded={isMenuOpen}
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<span className="sr-only">
{isMenuOpen ? "Close menu" : "Open menu"}
</span>
{isMenuOpen ? <CloseIcon /> : <MenuIcon />}
</button>
</div>
</div>
</div>

{/* Mobile menu */}
<div
id="mobile-menu"
className={`${isMenuOpen ? "block" : "hidden"} md:hidden`}
aria-labelledby="mobile-menu-button"
>
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-white shadow-lg border-t">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={`block px-3 py-2 rounded-md text-base font-medium ${
isActive(link.href)
? "bg-blue-50 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
{link.label}
</Link>
))}
</div>
</div>
</header>
);
}