refactor: update marketing types, links, and component rendering

- Add new `MenuItem` and `NestedMenuItem` types for improved type safety
- Update marketing configuration to use new types with `title` instead of `name`
- Modify footer, navbar, and user button components to handle optional icons
- Enhance media query hook with server-side rendering support
- Update Chinese translation with minor punctuation adjustments
- Improve component rendering with null checks for icons
This commit is contained in:
javayhu 2025-03-08 17:23:37 +08:00
parent f7714eeda4
commit f4e51757e5
7 changed files with 98 additions and 86 deletions

View File

@ -4,7 +4,7 @@
},
"NotFoundPage": {
"title": "404",
"message": "抱歉,您正在寻找的页面不存在",
"message": "抱歉,您正在寻找的页面不存在",
"backToHome": "返回首页"
},
"ErrorPage": {
@ -53,7 +53,7 @@
},
"error": {
"title": "哎呀!出错了!",
"tryAgain": "请重试",
"tryAgain": "请重试",
"backToLogin": "返回登录",
"checkEmail": "请检查您的邮箱"
}

View File

@ -1,20 +1,16 @@
"use client";
import { Icons } from "@/components/icons/icons";
import { siteConfig } from "@/config/site";
import { cn } from "@/lib/utils";
import { useTheme } from "next-themes";
import Link from "next/link";
import React from "react";
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 { ThemeSwitcherHorizontal } from "@/components/layout/theme-switcher-horizontal";
import { FOOTER_LINKS, SOCIAL_LINKS } from "@/config/marketing";
import { siteConfig } from "@/config/site";
import { LocaleLink } from "@/i18n/navigation";
import { cn } from "@/lib/utils";
import React from "react";
export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const { theme } = useTheme();
return (
<footer className={cn("border-t", className)}>
<Container className="px-4">
@ -36,16 +32,17 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
<div className="flex items-center gap-4 py-2">
<div className="flex items-center gap-2">
{SOCIAL_LINKS.map((link) => (
<Link
key={link.name}
href={link.href}
<a
key={link.title}
href={link.href || "#"}
target="_blank"
rel="noreferrer"
aria-label={link.name}
aria-label={link.title}
className="border border-border inline-flex h-8 w-8 items-center justify-center rounded-full hover:bg-accent hover:text-accent-foreground"
>
{React.cloneElement(link.icon, { 'aria-hidden': 'true' })}
</Link>
<span className="sr-only">{link.title}</span>
{link.icon ? link.icon : null}
</a>
))}
</div>
</div>
@ -65,17 +62,17 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
{section.title}
</span>
<ul className="mt-4 list-inside space-y-3">
{section.links?.map(
(link) =>
link.href && (
<li key={link.name}>
<Link
href={link.href}
target={link.external ? "_blank" : undefined}
{section.items?.map(
(item) =>
item.href && (
<li key={item.title}>
<LocaleLink
href={item.href || "#"}
target={item.external ? "_blank" : undefined}
className="text-sm text-muted-foreground hover:text-primary"
>
{link.name}
</Link>
{item.title}
</LocaleLink>
</li>
),
)}

View File

@ -189,7 +189,7 @@ function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
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 ? subItem.icon : null}
</div>
<div className="flex-1">
<span className="text-sm font-medium">

View File

@ -101,7 +101,7 @@ 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">
{subItem.icon}
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<div className="text-sm font-medium">

View File

@ -19,30 +19,21 @@ import {
} from "@/components/ui/dropdown-menu";
import { AVATAR_LINKS } from "@/config/marketing";
import { useMediaQuery } from "@/hooks/use-media-query";
import { LocaleLink, useLocaleRouter } from "@/i18n/navigation";
import { authClient } from "@/lib/auth-client";
import { LogOutIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { useState } from "react";
export function UserButton() {
const { data: session, error } = authClient.useSession();
const user = session?.user;
// console.log('UserButton, user:', user);
// if (error) {
// console.error("UserButton, error:", error);
// return (
// <div className="size-8 animate-pulse rounded-full border bg-muted" />
// );
// }
const isAdmin = user?.role === "admin";
const handleSignOut = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
console.log("sign out success");
router.push("/");
localeRouter.push("/");
},
onError: (error) => {
console.error("sign out error:", error);
@ -52,14 +43,14 @@ export function UserButton() {
});
};
const router = useRouter();
const localeRouter = useLocaleRouter();
const [open, setOpen] = useState(false);
const closeDrawer = () => {
setOpen(false);
};
const { isMobile } = useMediaQuery();
// Mobile View, use Drawer
if (isMobile) {
return (
@ -68,7 +59,7 @@ export function UserButton() {
<UserAvatar
name={user?.name || undefined}
image={user?.image || undefined}
className="size-8 border"
className="size-10 border"
/>
</DrawerTrigger>
<DrawerPortal>
@ -81,7 +72,7 @@ export function UserButton() {
<UserAvatar
name={user?.name || undefined}
image={user?.image || undefined}
className="size-8 border"
className="size-10 border"
/>
<div className="flex flex-col">
{user?.name && <p className="font-medium">{user.name}</p>}
@ -96,16 +87,17 @@ export function UserButton() {
<ul className="mb-14 mt-1 w-full text-muted-foreground">
{AVATAR_LINKS.map((item) => (
<li
key={item.name}
key={item.title}
className="rounded-lg text-foreground hover:bg-muted"
>
<a href={item.href}
<LocaleLink
href={item.href || "#"}
onClick={closeDrawer}
className="flex w-full items-center gap-3 px-2.5 py-2"
>
{React.cloneElement(item.icon, { className: "size-4" })}
<p className="text-sm">{item.name}</p>
</a>
{item.icon ? item.icon : null}
<p className="text-sm">{item.title}</p>
</LocaleLink>
</li>
))}
@ -139,7 +131,7 @@ export function UserButton() {
<UserAvatar
name={user?.name || undefined}
image={user?.image || undefined}
className="size-8 border"
className="size-10 border"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@ -157,16 +149,18 @@ export function UserButton() {
{AVATAR_LINKS.map((item) => (
<DropdownMenuItem
key={item.name}
key={item.title}
asChild
className="cursor-pointer"
onClick={() => {
router.push(item.href);
if (item.href) {
localeRouter.push(item.href);
}
}}
>
<div className="flex items-center space-x-2.5">
{React.cloneElement(item.icon, { className: "size-4" })}
<p className="text-sm">{item.name}</p>
{item.icon ? item.icon : null}
<p className="text-sm">{item.title}</p>
</div>
</DropdownMenuItem>
))}

View File

@ -7,6 +7,7 @@ import { TikTokIcon } from '@/components/icons/tiktok';
import { TwitterIcon } from '@/components/icons/twitter';
import { YouTubeIcon } from '@/components/icons/youtube';
import { Routes } from '@/routes';
import { MenuItem, NestedMenuItem } from '@/types';
import { DashboardIcon } from '@radix-ui/react-icons';
import {
AudioLinesIcon,
@ -27,7 +28,7 @@ import {
/**
* list all the menu links here, you can customize the links as you want
*/
export const MENU_LINKS = [
export const MENU_LINKS: NestedMenuItem[] = [
{
title: 'Features',
href: Routes.Pricing,
@ -142,37 +143,37 @@ export const MENU_LINKS = [
/**
* list all the footer links here, you can customize the links as you want
*/
export const FOOTER_LINKS = [
export const FOOTER_LINKS: NestedMenuItem[] = [
{
title: 'Product',
links: [
{ name: 'Features', href: Routes.Features, external: false },
{ name: 'Pricing', href: Routes.Pricing, external: false },
{ name: 'FAQ', href: Routes.FAQ, external: false },
items: [
{ title: 'Features', href: Routes.Features, external: false },
{ title: 'Pricing', href: Routes.Pricing, external: false },
{ title: 'FAQ', href: Routes.FAQ, external: false },
]
},
{
title: 'Resources',
links: [
{ name: 'Blog', href: Routes.Blog, external: false },
{ name: 'Changelog', href: Routes.Changelog, external: false },
{ name: 'Roadmap', href: Routes.Roadmap, external: true },
items: [
{ title: 'Blog', href: Routes.Blog, external: false },
{ title: 'Changelog', href: Routes.Changelog, external: false },
{ title: 'Roadmap', href: Routes.Roadmap, external: true },
]
},
{
title: 'Company',
links: [
{ name: 'About', href: Routes.About, external: false },
{ name: 'Contact', href: Routes.Contact, external: false },
{ name: 'Waitlist', href: Routes.Waitlist, external: false }
items: [
{ title: 'About', href: Routes.About, external: false },
{ title: 'Contact', href: Routes.Contact, external: false },
{ title: 'Waitlist', href: Routes.Waitlist, external: false }
]
},
{
title: 'Legal',
links: [
{ name: 'Cookie Policy', href: Routes.CookiePolicy, external: false },
{ name: 'Privacy Policy', href: Routes.PrivacyPolicy, external: false },
{ name: 'Terms of Service', href: Routes.TermsOfService, external: false },
items: [
{ title: 'Cookie Policy', href: Routes.CookiePolicy, external: false },
{ title: 'Privacy Policy', href: Routes.PrivacyPolicy, external: false },
{ title: 'Terms of Service', href: Routes.TermsOfService, external: false },
]
}
];
@ -180,49 +181,49 @@ export const FOOTER_LINKS = [
/**
* list all the social links here, you can delete the ones that are not needed
*/
export const SOCIAL_LINKS = [
export const SOCIAL_LINKS: MenuItem[] = [
{
name: 'Email',
title: 'Email',
href: 'mailto:mksaas@gmail.com',
icon: <MailIcon className="size-4 shrink-0" />
},
{
name: 'GitHub',
title: 'GitHub',
href: 'https://github.com/MkSaaSHQ',
icon: <GitHubIcon className="size-4 shrink-0" />
},
{
name: 'Twitter',
title: 'Twitter',
href: 'https://twitter.com/mksaas',
icon: <TwitterIcon className="size-4 shrink-0" />
},
{
name: 'Bluesky',
title: 'Bluesky',
href: 'https://bsky.app/profile/mksaas.com',
icon: <BlueskyIcon className="size-4 shrink-0" />
},
{
name: 'YouTube',
title: 'YouTube',
href: 'https://www.youtube.com/@MkSaaSHQ',
icon: <YouTubeIcon className="size-4 shrink-0" />
},
{
name: 'LinkedIn',
title: 'LinkedIn',
href: 'https://www.linkedin.com/company/mksaas',
icon: <LinkedInIcon className="size-4 shrink-0" />
},
{
name: 'Facebook',
title: 'Facebook',
href: 'https://www.facebook.com/mksaas',
icon: <FacebookIcon className="size-4 shrink-0" />
},
{
name: 'Instagram',
title: 'Instagram',
href: 'https://www.instagram.com/mksaas',
icon: <InstagramIcon className="size-4 shrink-0" />
},
{
name: 'TikTok',
title: 'TikTok',
href: 'https://www.tiktok.com/@mksaas',
icon: <TikTokIcon className="size-4 shrink-0" />
}
@ -231,14 +232,14 @@ export const SOCIAL_LINKS = [
/**
* list all the avatar links here, you can customize the links as you want
*/
export const AVATAR_LINKS = [
export const AVATAR_LINKS: MenuItem[] = [
{
name: 'Dashboard',
title: 'Dashboard',
href: Routes.Dashboard,
icon: <DashboardIcon className="size-4 shrink-0" />
},
{
name: 'Settings',
title: 'Settings',
href: Routes.Settings,
icon: <SettingsIcon className="size-4 shrink-0" />
}

22
src/types/index.d.ts vendored
View File

@ -1,4 +1,5 @@
import type { Icons } from "@/components/icons/icons";
import type { ReactNode } from "react";
/**
* utm parameters
@ -16,6 +17,25 @@ export type SiteConfig = {
mail: string;
};
export type MenuItem = {
title: string;
description?: string;
icon?: ReactNode;
href?: string;
external?: boolean;
};
export type NestedMenuItem = {
title: string;
description?: string;
icon?: ReactNode;
href?: string;
external?: boolean;
items?: MenuItem[];
};
// marketing config //
export type HeroConfig = {
title: {
first: string;
@ -153,4 +173,4 @@ export type TestimonialType = {
job: string;
image: string;
review: string;
};
};