refactor: enhance navigation and link rendering with active state and dynamic imports

- Update footer, navbar, and mobile navbar to use dynamic social and menu links
- Implement active state highlighting for navigation items using locale pathname
- Replace static social links with a dynamic function in marketing configuration
- Improve icon and styling consistency across navigation components
- Add null checks for menu and social links to prevent rendering errors
- Update icon for About page in menu links
This commit is contained in:
javayhu 2025-03-08 21:45:07 +08:00
parent 711537c732
commit 92d7513e3d
6 changed files with 150 additions and 129 deletions

View File

@ -106,7 +106,7 @@
}
},
"pages": {
"title": "演示页面",
"title": "内置页面",
"items": {
"about": {
"title": "关于我们",

View File

@ -4,7 +4,7 @@ import Container from "@/components/container";
import { ThemeSwitcherHorizontal } from "@/components/layout/theme-switcher-horizontal";
import { Logo } from "@/components/logo";
import BuiltWithButton from "@/components/shared/built-with-button";
import { createTranslator, getFooterLinks, SOCIAL_LINKS } from "@/config/marketing";
import { createTranslator, getFooterLinks, getSocialLinks } from "@/config/marketing";
import { siteConfig } from "@/config/site";
import { LocaleLink } from "@/i18n/navigation";
import { cn } from "@/lib/utils";
@ -15,7 +15,8 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const t = useTranslations();
const translator = createTranslator(t);
const footerLinks = getFooterLinks(translator);
const socialLinks = getSocialLinks();
return (
<footer className={cn("border-t", className)}>
<Container className="px-4">
@ -36,7 +37,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
{/* social links */}
<div className="flex items-center gap-4 py-2">
<div className="flex items-center gap-2">
{SOCIAL_LINKS.map((link) => (
{socialLinks && socialLinks.map((link) => (
<a
key={link.title}
href={link.href || "#"}
@ -58,7 +59,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
</div>
{/* footer links */}
{footerLinks.map((section) => (
{footerLinks && footerLinks.map((section) => (
<div
key={section.title}
className="col-span-1 md:col-span-1 items-start"

View File

@ -11,14 +11,13 @@ import {
} from '@/components/ui/collapsible';
import { createTranslator, getMenuLinks } from '@/config/marketing';
import { siteConfig } from '@/config/site';
import { LocaleLink } from '@/i18n/navigation';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Portal } from '@radix-ui/react-portal';
import { ArrowUpRightIcon, ChevronDownIcon, ChevronUpIcon, MenuIcon, XIcon } from 'lucide-react';
import { useTranslations } from "next-intl";
import { usePathname } from 'next/navigation';
import * as React from 'react';
import { RemoveScroll } from 'react-remove-scroll';
import { UserButton } from './user-button';
@ -28,7 +27,7 @@ export function NavbarMobile({
...other
}: React.HTMLAttributes<HTMLDivElement>) {
const [open, setOpen] = React.useState<boolean>(false);
const pathname = usePathname();
const localePathname = useLocalePathname();
const { data: session, error } = authClient.useSession();
const user = session?.user;
@ -42,7 +41,7 @@ export function NavbarMobile({
};
handleRouteChangeStart();
}, [pathname]);
}, [localePathname]);
const handleChange = () => {
const mediaQueryList = window.matchMedia('(min-width: 1024px)');
@ -116,6 +115,8 @@ function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
const translator = createTranslator(t);
const menuLinks = getMenuLinks(translator);
const commonTranslations = useTranslations("Common");
const localePathname = useLocalePathname();
return (
<div className="fixed inset-0 z-50 mt-[72px] overflow-y-auto bg-background backdrop-blur-md animate-in fade-in-0">
<div className="flex size-full flex-col items-start space-y-4 p-4">
@ -151,90 +152,111 @@ function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
{/* main menu */}
<ul className="w-full">
{menuLinks.map((item) => (
<li key={item.title} className="py-2">
{item.items ? (
<Collapsible
open={expanded[item.title.toLowerCase()]}
onOpenChange={(isOpen) =>
setExpanded((prev) => ({
...prev,
[item.title.toLowerCase()]: isOpen
}))
}
>
<CollapsibleTrigger asChild>
<Button
type="button"
variant="ghost"
className="flex w-full items-center justify-between text-left"
>
<span className="text-base font-medium">
{item.title}
</span>
{expanded[item.title.toLowerCase()] ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="mt-2 pl-4 space-y-2">
{item.items.map((subItem) => (
<li key={subItem.title}>
<LocaleLink
href={subItem.href || '#'}
target={subItem.external ? '_blank' : undefined}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group h-auto w-full justify-start gap-4 p-2'
)}
onClick={onLinkClicked}
>
<div className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors group-hover:text-foreground">
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<span className="text-sm font-medium">
{subItem.title}
</span>
{subItem.description && (
<p className="text-xs text-muted-foreground">
{subItem.description}
</p>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
)}
</LocaleLink>
</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
) : (
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className={cn(
buttonVariants({ variant: 'ghost' }),
'w-full justify-start'
)}
onClick={onLinkClicked}
>
<span className="text-base">{item.title}</span>
</LocaleLink>
)}
</li>
))}
{menuLinks && menuLinks.map((item) => {
const isActive = item.href ? localePathname.startsWith(item.href) :
item.items?.some(subItem => subItem.href && localePathname.startsWith(subItem.href));
return (
<li key={item.title} className="py-2">
{item.items ? (
<Collapsible
open={expanded[item.title.toLowerCase()]}
onOpenChange={(isOpen) =>
setExpanded((prev) => ({
...prev,
[item.title.toLowerCase()]: isOpen
}))
}
>
<CollapsibleTrigger asChild>
<Button
type="button"
variant="ghost"
className={cn(
"flex w-full items-center justify-between text-left",
"text-muted-foreground hover:text-primary",
isActive && "font-bold text-primary dark:text-primary-foreground"
)}
>
<span className="text-base">
{item.title}
</span>
{expanded[item.title.toLowerCase()] ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="mt-2 pl-4 space-y-2">
{item.items.map((subItem) => {
const isSubItemActive = subItem.href && localePathname.startsWith(subItem.href);
return (
<li key={subItem.title}>
<LocaleLink
href={subItem.href || '#'}
target={subItem.external ? '_blank' : undefined}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group h-auto w-full justify-start gap-4 p-2',
"text-muted-foreground hover:text-primary",
isSubItemActive && "font-bold text-primary dark:text-primary-foreground"
)}
onClick={onLinkClicked}
>
<div className={cn(
"flex size-8 shrink-0 items-center justify-center transition-colors",
"text-muted-foreground group-hover:text-primary",
isSubItemActive && "text-primary dark:text-primary-foreground"
)}>
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<span className="text-sm font-medium">
{subItem.title}
</span>
{subItem.description && (
<p className="text-xs text-muted-foreground">
{subItem.description}
</p>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary" />
)}
</LocaleLink>
</li>
);
})}
</ul>
</CollapsibleContent>
</Collapsible>
) : (
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className={cn(
buttonVariants({ variant: 'ghost' }),
'w-full justify-start',
"text-muted-foreground hover:text-primary",
isActive && "font-bold text-primary dark:text-primary-foreground"
)}
onClick={onLinkClicked}
>
<span className="text-base">{item.title}</span>
</LocaleLink>
)}
</li>
);
})}
</ul>
{/* bottom buttons */}

View File

@ -20,13 +20,12 @@ import {
import { createTranslator, getMenuLinks } from '@/config/marketing';
import { siteConfig } from '@/config/site';
import { useScroll } from "@/hooks/use-scroll";
import { LocaleLink } from '@/i18n/navigation';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { ArrowUpRightIcon } from 'lucide-react';
import { useTranslations } from "next-intl";
import { usePathname } from 'next/navigation';
interface NavBarProps {
scroll?: boolean;
@ -34,7 +33,11 @@ interface NavBarProps {
const customNavigationMenuTriggerStyle = cn(
navigationMenuTriggerStyle(),
"bg-transparent hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary data-[active]:bg-transparent data-[active]:text-primary data-[state=open]:bg-transparent data-[state=open]:text-primary relative data-[active]:font-bold dark:text-gray-400 dark:hover:text-gray-300 dark:data-[active]:text-white"
"relative bg-transparent text-muted-foreground",
"hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary",
"data-[active]:font-bold data-[active]:bg-transparent data-[active]:text-primary",
"data-[state=open]:bg-transparent data-[state=open]:text-primary",
"dark:hover:text-primary dark:data-[active]:text-primary-foreground"
);
export function Navbar({ scroll }: NavBarProps) {
@ -45,9 +48,9 @@ export function Navbar({ scroll }: NavBarProps) {
const translator = createTranslator(t);
const menuLinks = getMenuLinks(translator);
const commonTranslations = useTranslations("Common");
// console.log(`Navbar, user:`, user);
const localePathname = useLocalePathname();
const pathname = usePathname();
// console.log(`Navbar, user:`, user);
return (
<section className={cn(
@ -73,25 +76,22 @@ export function Navbar({ scroll }: NavBarProps) {
<div className="flex-1 flex items-center justify-center space-x-2">
<NavigationMenu className="relative">
<NavigationMenuList className="flex items-center">
{menuLinks.map((item, index) =>
{menuLinks && menuLinks.map((item, index) =>
item.items ? (
<NavigationMenuItem key={index} className="relative">
<NavigationMenuTrigger
data-active={
item.items.some((subItem) =>
subItem.href && pathname.startsWith(subItem.href)
subItem.href ? localePathname.startsWith(subItem.href) : false
) ? "true" : undefined
}
className={cn(
customNavigationMenuTriggerStyle,
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
)}
className={customNavigationMenuTriggerStyle}
>
{item.title}
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-4 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
{item.items.map((subItem, subIndex) => (
{item.items && item.items.map((subItem, subIndex) => (
<li key={subIndex}>
<NavigationMenuLink asChild>
<LocaleLink
@ -104,11 +104,11 @@ export function Navbar({ scroll }: NavBarProps) {
}
className="group flex select-none flex-row items-center gap-4 rounded-md p-2 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors group-hover:text-foreground">
<div className="flex size-8 shrink-0 items-center justify-center text-muted-foreground group-hover:text-primary">
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<div className="text-sm font-medium">
<div className="text-sm font-medium group-hover:text-primary">
{subItem.title}
</div>
{subItem.description && (
@ -118,7 +118,7 @@ export function Navbar({ scroll }: NavBarProps) {
)}
</div>
{subItem.external && (
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground hover:text-primary-foreground" />
)}
</LocaleLink>
</NavigationMenuLink>
@ -131,11 +131,8 @@ export function Navbar({ scroll }: NavBarProps) {
<NavigationMenuItem key={index}>
<NavigationMenuLink
asChild
active={item.href && pathname.startsWith(item.href) ? true : undefined}
className={cn(
customNavigationMenuTriggerStyle,
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
)}
active={item.href ? localePathname.startsWith(item.href) : false}
className={customNavigationMenuTriggerStyle}
>
<LocaleLink
href={item.href || '#'}
@ -171,10 +168,9 @@ export function Navbar({ scroll }: NavBarProps) {
</Button>
</LoginWrapper>
<Button
variant="default"
<Button asChild
size="sm"
asChild
variant="default"
>
<LocaleLink href={Routes.Register}>
{commonTranslations("signUp")}

View File

@ -91,7 +91,7 @@ export function UserButton() {
</div>
<ul className="mb-14 mt-1 w-full text-muted-foreground">
{avatarLinks.map((item) => (
{avatarLinks && avatarLinks.map((item) => (
<li
key={item.title}
className="rounded-lg text-foreground hover:bg-muted"

View File

@ -11,11 +11,11 @@ import { MenuItem, NestedMenuItem } from '@/types';
import { DashboardIcon } from '@radix-ui/react-icons';
import {
AudioLinesIcon,
BuildingIcon,
CookieIcon,
FileTextIcon,
FilmIcon,
ImageIcon,
InfoIcon,
ListChecksIcon,
MailboxIcon,
MailIcon,
@ -105,7 +105,7 @@ export function getMenuLinks(t: TranslationFunction): NestedMenuItem[] {
{
title: t('Marketing.menu.pages.items.about.title'),
description: t('Marketing.menu.pages.items.about.description'),
icon: <InfoIcon className="size-5 shrink-0" />,
icon: <BuildingIcon className="size-5 shrink-0" />,
href: Routes.About,
external: false
},
@ -208,11 +208,12 @@ export function getFooterLinks(t: TranslationFunction): NestedMenuItem[] {
/**
* list all the social links here, you can delete the ones that are not needed
*/
export const SOCIAL_LINKS: MenuItem[] = [
{
title: 'Email',
href: 'mailto:mksaas@gmail.com',
icon: <MailIcon className="size-4 shrink-0" />
export function getSocialLinks(): MenuItem[] {
return [
{
title: 'Email',
href: 'mailto:mksaas@gmail.com',
icon: <MailIcon className="size-4 shrink-0" />
},
{
title: 'GitHub',
@ -250,11 +251,12 @@ export const SOCIAL_LINKS: MenuItem[] = [
icon: <InstagramIcon className="size-4 shrink-0" />
},
{
title: 'TikTok',
href: 'https://www.tiktok.com/@mksaas',
icon: <TikTokIcon className="size-4 shrink-0" />
}
];
title: 'TikTok',
href: 'https://www.tiktok.com/@mksaas',
icon: <TikTokIcon className="size-4 shrink-0" />
}
];
}
/**
* Get avatar links with translations