Skip to content

Commit d9f19d9

Browse files
committed
feat(nav): add nested navigation with dropdown menus for About Us section
1 parent f965532 commit d9f19d9

File tree

8 files changed

+148
-39
lines changed

8 files changed

+148
-39
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Metadata } from "next";
2+
import { ComingSoon, comingSoonText } from "@/components/sections/coming-soon";
3+
import { buildPageMetadata } from "@/lib/config";
4+
5+
const description = comingSoonText;
6+
export const metadata: Metadata = buildPageMetadata("/about-us/annual-report", { description });
7+
8+
export default function AnnualReport() {
9+
return <ComingSoon />;
10+
}

src/app/about-us/exco/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Metadata } from "next";
2+
import { ComingSoon, comingSoonText } from "@/components/sections/coming-soon";
3+
import { buildPageMetadata } from "@/lib/config";
4+
5+
const description = comingSoonText;
6+
export const metadata: Metadata = buildPageMetadata("/about-us/exco", { description });
7+
8+
export default function ExecutiveCommittee() {
9+
return <ComingSoon />;
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Metadata } from "next";
2+
import { ComingSoon, comingSoonText } from "@/components/sections/coming-soon";
3+
import { buildPageMetadata } from "@/lib/config";
4+
5+
const description = comingSoonText;
6+
export const metadata: Metadata = buildPageMetadata("/about-us/our-story", { description });
7+
8+
export default function OurStory() {
9+
return <ComingSoon />;
10+
}

src/app/about-us/partners/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Metadata } from "next";
2+
import { ComingSoon, comingSoonText } from "@/components/sections/coming-soon";
3+
import { buildPageMetadata } from "@/lib/config";
4+
5+
const description = comingSoonText;
6+
export const metadata: Metadata = buildPageMetadata("/about-us/partners", { description });
7+
8+
export default function Partners() {
9+
return <ComingSoon />;
10+
}

src/app/sitemap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { MetadataRoute } from "next";
22
import { siteConfig } from "@/lib/config";
3-
import { isInternalHref } from "@/lib/utils";
3+
import { isInternalHref, flattenByChildren } from "@/lib/utils";
44

55
export const dynamic = "force-static";
66

77
export default function sitemap(): MetadataRoute.Sitemap {
88
const rawSiteUrl = process.env.SITE_URL || "";
99
const siteUrl = rawSiteUrl.replace(/\/$/, ""); // remove trailing slash
1010

11-
const allNavs = [...siteConfig.mainNav, ...siteConfig.utilityNav];
11+
const allNavs = [...flattenByChildren(siteConfig.mainNav), ...flattenByChildren(siteConfig.utilityNav)];
1212

1313
return allNavs
1414
.filter((route) => isInternalHref(route.href))

src/components/sections/header.tsx

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
NavigationMenuItem,
88
NavigationMenuLink,
99
NavigationMenuList,
10+
NavigationMenuTrigger,
11+
NavigationMenuContent,
1012
} from "@/components/ui/navigation-menu";
1113
import { ModeToggle } from "@/components/primitives/mode-toggle";
1214
import { Menu, Mail } from "lucide-react";
@@ -18,9 +20,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
1820
import { Button } from "@/components/ui/button";
1921
import { AnimatePresence, motion } from "framer-motion";
2022
import { useState } from "react";
23+
import { useRouter } from "next/navigation";
2124

2225
export function Header() {
2326
const [isOpen, setIsOpen] = useState(false);
27+
const router = useRouter();
2428

2529
return (
2630
<Collapsible
@@ -44,18 +48,45 @@ export function Header() {
4448
/>
4549
<span className="font-semibold">ALPHA HKU</span>
4650
</Link>
47-
<nav className="hidden lg:flex items-center gap-6 text-sm">
48-
<NavigationMenu>
49-
<NavigationMenuList>
51+
<nav className="hidden lg:flex">
52+
<NavigationMenu viewport={false}>
53+
<NavigationMenuList className="gap-0">
5054
{siteConfig.mainNav.map((link) => (
5155
<NavigationMenuItem key={link.href}>
52-
<NavigationMenuLink asChild>
53-
{isInternalHref(link.href) ? (
54-
<Link href={link.href}>{link.label}</Link>
55-
) : (
56-
<a href={link.href}>{link.label}</a>
57-
)}
58-
</NavigationMenuLink>
56+
{link.children && link.children.length > 0 ? (
57+
<>
58+
<NavigationMenuTrigger
59+
className="p-2 font-normal bg-transparent"
60+
onClick={() => router.push(link.href)}
61+
>
62+
{link.label}
63+
</NavigationMenuTrigger>
64+
<NavigationMenuContent>
65+
<div className="w-28">
66+
{link.children.map((child) => (
67+
<NavigationMenuLink
68+
key={child.href}
69+
asChild
70+
>
71+
{isInternalHref(child.href) ? (
72+
<Link href={child.href}>{child.label}</Link>
73+
) : (
74+
<a href={child.href}>{child.label}</a>
75+
)}
76+
</NavigationMenuLink>
77+
))}
78+
</div>
79+
</NavigationMenuContent>
80+
</>
81+
) : (
82+
<NavigationMenuLink asChild>
83+
{isInternalHref(link.href) ? (
84+
<Link href={link.href}>{link.label}</Link>
85+
) : (
86+
<a href={link.href}>{link.label}</a>
87+
)}
88+
</NavigationMenuLink>
89+
)}
5990
</NavigationMenuItem>
6091
))}
6192
</NavigationMenuList>
@@ -126,25 +157,51 @@ export function Header() {
126157
<div className="border-b border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
127158
<div className="container px-4 lg:px-6 py-4">
128159
<nav className="grid gap-4 text-sm">
129-
{siteConfig.mainNav.map((link) =>
130-
isInternalHref(link.href) ? (
131-
<Link
132-
key={link.href}
133-
href={link.href}
134-
className="hover:text-foreground/80"
135-
>
136-
{link.label}
137-
</Link>
138-
) : (
139-
<a
140-
key={link.href}
141-
href={link.href}
142-
className="hover:text-foreground/80"
143-
>
144-
{link.label}
145-
</a>
146-
)
147-
)}
160+
{siteConfig.mainNav.map((link) => (
161+
<div
162+
key={link.href}
163+
className="grid gap-4"
164+
>
165+
{isInternalHref(link.href) ? (
166+
<Link
167+
href={link.href}
168+
className="hover:text-foreground/80"
169+
>
170+
{link.label}
171+
</Link>
172+
) : (
173+
<a
174+
href={link.href}
175+
className="hover:text-foreground/80"
176+
>
177+
{link.label}
178+
</a>
179+
)}
180+
{link.children && link.children.length > 0 && (
181+
<div className="ml-4 grid gap-2">
182+
{link.children.map((child) =>
183+
isInternalHref(child.href) ? (
184+
<Link
185+
key={child.href}
186+
href={child.href}
187+
className="hover:text-foreground/80"
188+
>
189+
{child.label}
190+
</Link>
191+
) : (
192+
<a
193+
key={child.href}
194+
href={child.href}
195+
className="hover:text-foreground/80"
196+
>
197+
{child.label}
198+
</a>
199+
)
200+
)}
201+
</div>
202+
)}
203+
</div>
204+
))}
148205
</nav>
149206
</div>
150207
</div>

src/lib/config.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { MetadataRoute } from "next";
2-
import { isInternalHref } from "@/lib/utils";
2+
import { isInternalHref, flattenByChildren } from "@/lib/utils";
33
import type { Metadata } from "next";
44

55
type NavItem = {
66
href: string;
77
label: string;
88
priority?: MetadataRoute.Sitemap[0]["priority"];
99
changeFrequency?: MetadataRoute.Sitemap[0]["changeFrequency"];
10+
children?: NavItem[];
1011
};
1112

1213
const donateLink = "mailto:alphahku1213@gmail.com?subject=Donation%20Inquiry";
1314

1415
const mainNav: NavItem[] = [
1516
{ href: "/", label: "Home" },
16-
{ href: "/about-us", label: "About Us" },
17+
{
18+
href: "/about-us",
19+
label: "About Us",
20+
children: [
21+
{ href: "/about-us/our-story", label: "Our Story" },
22+
{ href: "/about-us/annual-report", label: "Annual Report" },
23+
{ href: "/about-us/exco", label: "Executive Committee" },
24+
{ href: "/about-us/partners", label: "Partners" },
25+
],
26+
},
1727
{ href: "/upcoming-event", label: "Upcoming Event" },
1828
{ href: "/our-work", label: "Our Work" },
1929
{ href: "/blog", label: "Blog" },
@@ -39,17 +49,15 @@ export const siteConfig = {
3949
seoImageHeight: 802,
4050
mainNav,
4151
utilityNav,
42-
staticRoutes: [
43-
...mainNav
44-
.filter((item) => isInternalHref(item.href))
45-
.map((item) => (item.href === "/" ? "" : item.href)),
46-
],
52+
staticRoutes: flattenByChildren(mainNav)
53+
.filter((item) => isInternalHref(item.href))
54+
.map((item) => (item.href === "/" ? "" : item.href)),
4755
};
4856

4957
export function getNavLabel(path: string): string | undefined {
5058
// normalize trailing slash
5159
const normalized = path === "/" ? "/" : path.replace(/\/$/, "");
52-
return siteConfig.mainNav.find((i) => i.href === normalized)?.label;
60+
return flattenByChildren(siteConfig.mainNav).find((i) => i.href === normalized)?.label;
5361
}
5462

5563
export function buildPageMetadata(

src/lib/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ export function isInternalHref(href: string): boolean {
2525
if (href.startsWith("//")) return false; // protocol-relative
2626
return true;
2727
}
28+
29+
export function flattenByChildren<T extends { children?: T[] }>(items: T[]): T[] {
30+
return items.flatMap((item) => [item, ...(item.children ? flattenByChildren(item.children) : [])]);
31+
}

0 commit comments

Comments
 (0)