diff --git a/app/layout.tsx b/app/layout.tsx index 28c2329..be6a703 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { ThemeProvider } from "@/components/theme-provider"; +import { Header } from "@/components/header"; import "./globals.css"; const geistSans = Geist({ @@ -35,6 +36,7 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > +
{children} diff --git a/app/page.tsx b/app/page.tsx index c012f26..32dab9f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,8 @@ -import { ThemeToggle } from "@/components/theme-toggle"; import { R2Test } from "@/components/r2-test"; export default function Home() { return (
-
-

GhostPaste

- -

diff --git a/components/header.test.tsx b/components/header.test.tsx new file mode 100644 index 0000000..7c02062 --- /dev/null +++ b/components/header.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Header } from "./header"; + +// Mock next-themes used by ThemeToggle +vi.mock("next-themes", () => ({ + useTheme: () => ({ + theme: "light", + setTheme: vi.fn(), + resolvedTheme: "light", + systemTheme: "light", + themes: ["light", "dark"], + forcedTheme: undefined, + }), +})); + +describe("Header", () => { + it("renders logo and navigation links", () => { + render(
); + expect( + screen.getByRole("link", { name: /ghostpaste/i }) + ).toBeInTheDocument(); + expect( + screen.getAllByRole("link", { name: /create/i })[0] + ).toBeInTheDocument(); + }); + + it("toggles mobile navigation", async () => { + const user = userEvent.setup(); + render(
); + + const toggle = screen.getByLabelText(/toggle menu/i); + expect( + screen.queryByLabelText("Mobile navigation") + ).not.toBeInTheDocument(); + + await user.click(toggle); + expect(screen.getByLabelText("Mobile navigation")).toBeInTheDocument(); + + await user.click(toggle); + expect( + screen.queryByLabelText("Mobile navigation") + ).not.toBeInTheDocument(); + }); +}); diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..7130853 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,126 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Menu } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, +} from "@/components/ui/navigation-menu"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/theme-toggle"; + +export function Header() { + const [mobileOpen, setMobileOpen] = useState(false); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + function handleScroll() { + setScrolled(window.scrollY > 0); + } + handleScroll(); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( +
+ + Skip to main content + +
+ + 👻 + GhostPaste + + + + + + + Create + + + + + + + About + + + + + + GitHub + + + + +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..eb04716 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -0,0 +1,47 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +function NavigationMenu({ className, ...props }: React.ComponentProps<"nav">) { + return