From ee9ce70766c779011fbe5471d3ac778df6b61d89 Mon Sep 17 00:00:00 2001 From: Thanan Traiongthawon <95660+nullcoder@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:25:14 -0700 Subject: [PATCH 1/2] feat(ui): add header component --- app/layout.tsx | 2 + app/page.tsx | 5 --- components/header.test.tsx | 46 +++++++++++++++++++ components/header.tsx | 80 ++++++++++++++++++++++++++++++++++ docs/PHASE_4_ISSUE_TRACKING.md | 6 +-- docs/TODO.md | 4 +- 6 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 components/header.test.tsx create mode 100644 components/header.tsx 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..c2eb83c --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,80 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { Menu } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/theme-toggle"; + +export function Header() { + const [open, setOpen] = useState(false); + + return ( +
+
+ + GhostPaste + + +
+ + +
+
+ {open && ( + + )} +
+ ); +} diff --git a/docs/PHASE_4_ISSUE_TRACKING.md b/docs/PHASE_4_ISSUE_TRACKING.md index e01253e..bb3575c 100644 --- a/docs/PHASE_4_ISSUE_TRACKING.md +++ b/docs/PHASE_4_ISSUE_TRACKING.md @@ -10,7 +10,7 @@ All 19 Phase 4 UI component issues have been successfully created on GitHub. Thi | GitHub # | Component | Priority | Status | Description | | -------- | ------------- | -------- | ----------- | -------------------------------------------- | -| #53 | Header | HIGH | 🟡 Ready | Main header with navigation and theme toggle | +| #53 | Header | HIGH | 🟢 Complete | Main header with navigation and theme toggle | | #70 | Footer | LOW | 🟡 Ready | Simple footer with links and copyright | | #62 | Container | MEDIUM | 🟡 Ready | Reusable container for consistent spacing | | #57 | Design Tokens | HIGH | 🟢 Complete | Design system tokens for theming | @@ -63,7 +63,7 @@ Note: Issue #69 appears to be a duplicate of #59 (both for copy to clipboard fun ### Week 2: Essential Components -5. **#53** - Header (Navigation) +5. **#53** - Header (Navigation) ✅ COMPLETE 6. **#61** - GistViewer (View functionality) 7. **#60** - ShareDialog (Sharing flow) 8. **#58** - ErrorBoundary (Error handling) @@ -122,4 +122,4 @@ gh issue edit [number] --add-label "in progress" gh pr create --title "feat: implement [component]" --body "Closes #[number]" ``` -Last Updated: 2025-01-07 +Last Updated: 2025-06-05 diff --git a/docs/TODO.md b/docs/TODO.md index 7bca9ce..3366d07 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -100,7 +100,7 @@ This document tracks the implementation progress of GhostPaste. Check off tasks ### Layout Components -- [ ] Create Header component with navigation - [#53](https://github.com/nullcoder/ghostpaste/issues/53) +- [x] Create Header component with navigation - [#53](https://github.com/nullcoder/ghostpaste/issues/53) - [ ] Create Footer component - [#70](https://github.com/nullcoder/ghostpaste/issues/70) - [ ] Create Container component for consistent spacing - [#62](https://github.com/nullcoder/ghostpaste/issues/62) - [x] Implement responsive design tokens - [#57](https://github.com/nullcoder/ghostpaste/issues/57) @@ -125,7 +125,7 @@ This document tracks the implementation progress of GhostPaste. Check off tasks ### UI Features -- [ ] Implement dark/light theme toggle (included in Header #53) +- [x] Implement dark/light theme toggle (included in Header #53) - [ ] Add toast notifications - [#68](https://github.com/nullcoder/ghostpaste/issues/68) - [ ] Create keyboard shortcuts - [#72](https://github.com/nullcoder/ghostpaste/issues/72) - [ ] Add copy-to-clipboard functionality - [#59](https://github.com/nullcoder/ghostpaste/issues/59) From f6d2aedd6d72165c069a9704d2ebbcd022cf91aa Mon Sep 17 00:00:00 2001 From: Thanan Traiongthawon <95660+nullcoder@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:29:01 -0700 Subject: [PATCH 2/2] feat: enhance header component --- components/header.tsx | 164 +++++++++++++++++++----------- components/ui/navigation-menu.tsx | 47 +++++++++ components/ui/sheet.tsx | 106 +++++++++++++++++++ docs/PHASE_4_ISSUE_TRACKING.md | 2 +- docs/TODO.md | 2 +- 5 files changed, 260 insertions(+), 61 deletions(-) create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/sheet.tsx diff --git a/components/header.tsx b/components/header.tsx index c2eb83c..7130853 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,80 +1,126 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; +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 [open, setOpen] = useState(false); + 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 + + + +
- + + + + + + + +
- {open && ( - - )}
); } 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