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:
parent
711537c732
commit
92d7513e3d
@ -106,7 +106,7 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"title": "演示页面",
|
||||
"title": "内置页面",
|
||||
"items": {
|
||||
"about": {
|
||||
"title": "关于我们",
|
||||
|
@ -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"
|
||||
|
@ -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 */}
|
||||
|
@ -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")}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user