Skip to content

Commit ee9ce70

Browse files
committed
feat(ui): add header component
1 parent fc9ae2b commit ee9ce70

File tree

6 files changed

+133
-10
lines changed

6 files changed

+133
-10
lines changed

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import { ThemeProvider } from "@/components/theme-provider";
4+
import { Header } from "@/components/header";
45
import "./globals.css";
56

67
const geistSans = Geist({
@@ -35,6 +36,7 @@ export default function RootLayout({
3536
enableSystem
3637
disableTransitionOnChange
3738
>
39+
<Header />
3840
{children}
3941
</ThemeProvider>
4042
</body>

app/page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import { ThemeToggle } from "@/components/theme-toggle";
21
import { R2Test } from "@/components/r2-test";
32

43
export default function Home() {
54
return (
65
<div className="min-h-screen p-8">
7-
<header className="mb-8 flex items-center justify-between">
8-
<h1 className="text-3xl font-bold">GhostPaste</h1>
9-
<ThemeToggle />
10-
</header>
116
<main className="mx-auto max-w-4xl">
127
<div className="bg-card text-card-foreground rounded-lg border p-6 shadow-sm">
138
<h2 className="mb-4 text-2xl font-semibold">

components/header.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import { Header } from "./header";
5+
6+
// Mock next-themes used by ThemeToggle
7+
vi.mock("next-themes", () => ({
8+
useTheme: () => ({
9+
theme: "light",
10+
setTheme: vi.fn(),
11+
resolvedTheme: "light",
12+
systemTheme: "light",
13+
themes: ["light", "dark"],
14+
forcedTheme: undefined,
15+
}),
16+
}));
17+
18+
describe("Header", () => {
19+
it("renders logo and navigation links", () => {
20+
render(<Header />);
21+
expect(
22+
screen.getByRole("link", { name: /ghostpaste/i })
23+
).toBeInTheDocument();
24+
expect(
25+
screen.getAllByRole("link", { name: /create/i })[0]
26+
).toBeInTheDocument();
27+
});
28+
29+
it("toggles mobile navigation", async () => {
30+
const user = userEvent.setup();
31+
render(<Header />);
32+
33+
const toggle = screen.getByLabelText(/toggle menu/i);
34+
expect(
35+
screen.queryByLabelText("Mobile navigation")
36+
).not.toBeInTheDocument();
37+
38+
await user.click(toggle);
39+
expect(screen.getByLabelText("Mobile navigation")).toBeInTheDocument();
40+
41+
await user.click(toggle);
42+
expect(
43+
screen.queryByLabelText("Mobile navigation")
44+
).not.toBeInTheDocument();
45+
});
46+
});

components/header.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useState } from "react";
5+
import { Menu } from "lucide-react";
6+
7+
import { Button } from "@/components/ui/button";
8+
import { ThemeToggle } from "@/components/theme-toggle";
9+
10+
export function Header() {
11+
const [open, setOpen] = useState(false);
12+
13+
return (
14+
<header className="bg-background/95 sticky top-0 z-40 w-full border-b backdrop-blur">
15+
<div className="container mx-auto flex h-14 items-center justify-between px-4">
16+
<Link href="/" className="font-semibold">
17+
GhostPaste
18+
</Link>
19+
<nav aria-label="Main navigation" className="hidden gap-4 sm:flex">
20+
<Link href="/create" className="text-sm font-medium hover:underline">
21+
Create
22+
</Link>
23+
<Link href="/about" className="text-sm font-medium hover:underline">
24+
About
25+
</Link>
26+
<a
27+
href="https://github.com/nullcoder/ghostpaste"
28+
target="_blank"
29+
rel="noopener noreferrer"
30+
className="text-sm font-medium hover:underline"
31+
>
32+
GitHub
33+
</a>
34+
</nav>
35+
<div className="flex items-center gap-2">
36+
<ThemeToggle />
37+
<Button
38+
aria-label="Toggle menu"
39+
variant="ghost"
40+
size="icon"
41+
className="sm:hidden"
42+
onClick={() => setOpen((prev) => !prev)}
43+
>
44+
<Menu className="size-4" />
45+
</Button>
46+
</div>
47+
</div>
48+
{open && (
49+
<nav
50+
aria-label="Mobile navigation"
51+
className="bg-background border-t px-4 pt-2 pb-4 sm:hidden"
52+
>
53+
<Link
54+
href="/create"
55+
className="block py-2 text-sm font-medium"
56+
onClick={() => setOpen(false)}
57+
>
58+
Create
59+
</Link>
60+
<Link
61+
href="/about"
62+
className="block py-2 text-sm font-medium"
63+
onClick={() => setOpen(false)}
64+
>
65+
About
66+
</Link>
67+
<a
68+
href="https://github.com/nullcoder/ghostpaste"
69+
target="_blank"
70+
rel="noopener noreferrer"
71+
className="block py-2 text-sm font-medium"
72+
onClick={() => setOpen(false)}
73+
>
74+
GitHub
75+
</a>
76+
</nav>
77+
)}
78+
</header>
79+
);
80+
}

docs/PHASE_4_ISSUE_TRACKING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ All 19 Phase 4 UI component issues have been successfully created on GitHub. Thi
1010

1111
| GitHub # | Component | Priority | Status | Description |
1212
| -------- | ------------- | -------- | ----------- | -------------------------------------------- |
13-
| #53 | Header | HIGH | 🟡 Ready | Main header with navigation and theme toggle |
13+
| #53 | Header | HIGH | 🟢 Complete | Main header with navigation and theme toggle |
1414
| #70 | Footer | LOW | 🟡 Ready | Simple footer with links and copyright |
1515
| #62 | Container | MEDIUM | 🟡 Ready | Reusable container for consistent spacing |
1616
| #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
6363

6464
### Week 2: Essential Components
6565

66-
5. **#53** - Header (Navigation)
66+
5. **#53** - Header (Navigation) ✅ COMPLETE
6767
6. **#61** - GistViewer (View functionality)
6868
7. **#60** - ShareDialog (Sharing flow)
6969
8. **#58** - ErrorBoundary (Error handling)
@@ -122,4 +122,4 @@ gh issue edit [number] --add-label "in progress"
122122
gh pr create --title "feat: implement [component]" --body "Closes #[number]"
123123
```
124124

125-
Last Updated: 2025-01-07
125+
Last Updated: 2025-06-05

docs/TODO.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ This document tracks the implementation progress of GhostPaste. Check off tasks
100100

101101
### Layout Components
102102

103-
- [ ] Create Header component with navigation - [#53](https://github.com/nullcoder/ghostpaste/issues/53)
103+
- [x] Create Header component with navigation - [#53](https://github.com/nullcoder/ghostpaste/issues/53)
104104
- [ ] Create Footer component - [#70](https://github.com/nullcoder/ghostpaste/issues/70)
105105
- [ ] Create Container component for consistent spacing - [#62](https://github.com/nullcoder/ghostpaste/issues/62)
106106
- [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
125125

126126
### UI Features
127127

128-
- [ ] Implement dark/light theme toggle (included in Header #53)
128+
- [x] Implement dark/light theme toggle (included in Header #53)
129129
- [ ] Add toast notifications - [#68](https://github.com/nullcoder/ghostpaste/issues/68)
130130
- [ ] Create keyboard shortcuts - [#72](https://github.com/nullcoder/ghostpaste/issues/72)
131131
- [ ] Add copy-to-clipboard functionality - [#59](https://github.com/nullcoder/ghostpaste/issues/59)

0 commit comments

Comments
 (0)