Skip to content

Commit f6d2aed

Browse files
committed
feat: enhance header component
1 parent ee9ce70 commit f6d2aed

File tree

5 files changed

+260
-61
lines changed

5 files changed

+260
-61
lines changed

components/header.tsx

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,126 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { useState } from "react";
4+
import { useEffect, useState } from "react";
55
import { Menu } from "lucide-react";
66

7+
import { cn } from "@/lib/utils";
8+
import {
9+
NavigationMenu,
10+
NavigationMenuItem,
11+
NavigationMenuLink,
12+
NavigationMenuList,
13+
} from "@/components/ui/navigation-menu";
14+
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
715
import { Button } from "@/components/ui/button";
816
import { ThemeToggle } from "@/components/theme-toggle";
917

1018
export function Header() {
11-
const [open, setOpen] = useState(false);
19+
const [mobileOpen, setMobileOpen] = useState(false);
20+
const [scrolled, setScrolled] = useState(false);
21+
22+
useEffect(() => {
23+
function handleScroll() {
24+
setScrolled(window.scrollY > 0);
25+
}
26+
handleScroll();
27+
window.addEventListener("scroll", handleScroll);
28+
return () => window.removeEventListener("scroll", handleScroll);
29+
}, []);
1230

1331
return (
14-
<header className="bg-background/95 sticky top-0 z-40 w-full border-b backdrop-blur">
32+
<header
33+
className={cn(
34+
"bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-40 w-full border-b backdrop-blur",
35+
scrolled && "shadow-sm"
36+
)}
37+
>
38+
<a
39+
href="#main"
40+
className="bg-muted sr-only absolute top-2 left-4 rounded px-2 py-1 text-sm focus:not-sr-only"
41+
>
42+
Skip to main content
43+
</a>
1544
<div className="container mx-auto flex h-14 items-center justify-between px-4">
16-
<Link href="/" className="font-semibold">
45+
<Link href="/" className="flex items-center font-semibold">
46+
<span className="mr-1">👻</span>
1747
GhostPaste
1848
</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>
49+
<NavigationMenu className="hidden sm:block">
50+
<NavigationMenuList className="flex gap-4">
51+
<NavigationMenuItem>
52+
<Link href="/create" legacyBehavior passHref>
53+
<NavigationMenuLink className="text-sm font-medium hover:underline">
54+
Create
55+
</NavigationMenuLink>
56+
</Link>
57+
</NavigationMenuItem>
58+
<NavigationMenuItem>
59+
<Link href="/about" legacyBehavior passHref>
60+
<NavigationMenuLink className="text-sm font-medium hover:underline">
61+
About
62+
</NavigationMenuLink>
63+
</Link>
64+
</NavigationMenuItem>
65+
<NavigationMenuItem>
66+
<NavigationMenuLink
67+
href="https://github.com/nullcoder/ghostpaste"
68+
target="_blank"
69+
rel="noopener noreferrer"
70+
className="text-sm font-medium hover:underline"
71+
>
72+
GitHub
73+
</NavigationMenuLink>
74+
</NavigationMenuItem>
75+
</NavigationMenuList>
76+
</NavigationMenu>
3577
<div className="flex items-center gap-2">
3678
<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>
79+
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
80+
<SheetTrigger asChild>
81+
<Button
82+
aria-label="Toggle menu"
83+
variant="ghost"
84+
size="icon"
85+
className="sm:hidden"
86+
>
87+
<Menu className="size-4" />
88+
</Button>
89+
</SheetTrigger>
90+
<SheetContent
91+
side="left"
92+
aria-label="Mobile navigation"
93+
className="sm:hidden"
94+
>
95+
<nav className="mt-4 grid gap-2 text-base">
96+
<Link
97+
href="/create"
98+
onClick={() => setMobileOpen(false)}
99+
className="hover:underline"
100+
>
101+
Create
102+
</Link>
103+
<Link
104+
href="/about"
105+
onClick={() => setMobileOpen(false)}
106+
className="hover:underline"
107+
>
108+
About
109+
</Link>
110+
<a
111+
href="https://github.com/nullcoder/ghostpaste"
112+
target="_blank"
113+
rel="noopener noreferrer"
114+
className="hover:underline"
115+
onClick={() => setMobileOpen(false)}
116+
>
117+
GitHub
118+
</a>
119+
</nav>
120+
</SheetContent>
121+
</Sheet>
46122
</div>
47123
</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-
)}
78124
</header>
79125
);
80126
}

components/ui/navigation-menu.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { cn } from "@/lib/utils";
5+
6+
function NavigationMenu({ className, ...props }: React.ComponentProps<"nav">) {
7+
return <nav className={cn(className)} {...props} />;
8+
}
9+
10+
const NavigationMenuList = React.forwardRef<
11+
HTMLUListElement,
12+
React.HTMLAttributes<HTMLUListElement>
13+
>(({ className, ...props }, ref) => (
14+
<ul
15+
ref={ref}
16+
className={cn("flex items-center gap-4", className)}
17+
{...props}
18+
/>
19+
));
20+
NavigationMenuList.displayName = "NavigationMenuList";
21+
22+
const NavigationMenuItem = React.forwardRef<
23+
HTMLLIElement,
24+
React.HTMLAttributes<HTMLLIElement>
25+
>(({ className, ...props }, ref) => (
26+
<li ref={ref} className={cn(className)} {...props} />
27+
));
28+
NavigationMenuItem.displayName = "NavigationMenuItem";
29+
30+
const NavigationMenuLink = React.forwardRef<
31+
HTMLAnchorElement,
32+
React.ComponentProps<"a">
33+
>(({ className, ...props }, ref) => (
34+
<a
35+
ref={ref}
36+
className={cn("text-sm font-medium hover:underline", className)}
37+
{...props}
38+
/>
39+
));
40+
NavigationMenuLink.displayName = "NavigationMenuLink";
41+
42+
export {
43+
NavigationMenu,
44+
NavigationMenuList,
45+
NavigationMenuItem,
46+
NavigationMenuLink,
47+
};

components/ui/sheet.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { createPortal } from "react-dom";
5+
import { X } from "lucide-react";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
interface SheetContextValue {
10+
open: boolean;
11+
setOpen: (open: boolean) => void;
12+
}
13+
14+
const SheetContext = React.createContext<SheetContextValue | null>(null);
15+
16+
interface SheetProps {
17+
open?: boolean;
18+
onOpenChange?: (open: boolean) => void;
19+
children: React.ReactNode;
20+
}
21+
22+
function Sheet({ open, onOpenChange, children }: SheetProps) {
23+
const [internalOpen, setInternalOpen] = React.useState(false);
24+
const isControlled = open !== undefined;
25+
const currentOpen = isControlled ? open : internalOpen;
26+
const setOpen = isControlled
27+
? (onOpenChange as (o: boolean) => void)
28+
: setInternalOpen;
29+
return (
30+
<SheetContext.Provider value={{ open: currentOpen, setOpen }}>
31+
{children}
32+
</SheetContext.Provider>
33+
);
34+
}
35+
36+
function SheetTrigger({
37+
asChild = false,
38+
children,
39+
...props
40+
}: { asChild?: boolean } & React.ComponentProps<"button">) {
41+
const ctx = React.useContext(SheetContext)!;
42+
const handleClick = () => ctx.setOpen(!ctx.open);
43+
if (asChild && React.isValidElement(children)) {
44+
return React.cloneElement(children as React.ReactElement<any>, {
45+
onClick: handleClick,
46+
});
47+
}
48+
return (
49+
<button onClick={handleClick} {...props}>
50+
{children}
51+
</button>
52+
);
53+
}
54+
55+
interface SheetContentProps extends React.HTMLAttributes<HTMLDivElement> {
56+
side?: "top" | "bottom" | "left" | "right";
57+
}
58+
59+
function SheetContent({
60+
side = "right",
61+
className,
62+
children,
63+
...props
64+
}: SheetContentProps) {
65+
const ctx = React.useContext(SheetContext)!;
66+
React.useEffect(() => {
67+
function onKey(e: KeyboardEvent) {
68+
if (e.key === "Escape") ctx.setOpen(false);
69+
}
70+
if (ctx.open) document.addEventListener("keydown", onKey);
71+
return () => document.removeEventListener("keydown", onKey);
72+
}, [ctx]);
73+
74+
if (!ctx.open) return null;
75+
return createPortal(
76+
<div className="fixed inset-0 z-50">
77+
<div
78+
className="absolute inset-0 bg-black/50"
79+
onClick={() => ctx.setOpen(false)}
80+
/>
81+
<div
82+
className={cn(
83+
"bg-background fixed p-6 shadow-lg transition-transform",
84+
side === "left" && "inset-y-0 left-0 w-3/4 max-w-sm border-r",
85+
side === "right" && "inset-y-0 right-0 w-3/4 max-w-sm border-l",
86+
side === "top" && "inset-x-0 top-0 h-3/4 border-b",
87+
side === "bottom" && "inset-x-0 bottom-0 h-3/4 border-t",
88+
className
89+
)}
90+
{...props}
91+
>
92+
{children}
93+
<button
94+
onClick={() => ctx.setOpen(false)}
95+
className="focus:ring-ring absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:outline-none"
96+
>
97+
<X className="size-4" />
98+
<span className="sr-only">Close</span>
99+
</button>
100+
</div>
101+
</div>,
102+
document.body
103+
);
104+
}
105+
106+
export { Sheet, SheetTrigger, SheetContent };

docs/PHASE_4_ISSUE_TRACKING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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-06-05
125+
Last Updated: 2025-06-07

docs/TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ This document tracks the implementation progress of GhostPaste. Check off tasks
337337
- [ ] Phase 8: Deployment
338338
- [ ] Phase 9: Documentation & Polish
339339

340-
**Last Updated:** 2025-06-05
340+
**Last Updated:** 2025-06-07
341341

342342
---
343343

0 commit comments

Comments
 (0)