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:
parent
f7714eeda4
commit
f4e51757e5
@ -4,7 +4,7 @@
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"title": "404",
|
||||
"message": "抱歉,您正在寻找的页面不存在。",
|
||||
"message": "抱歉,您正在寻找的页面不存在",
|
||||
"backToHome": "返回首页"
|
||||
},
|
||||
"ErrorPage": {
|
||||
@ -53,7 +53,7 @@
|
||||
},
|
||||
"error": {
|
||||
"title": "哎呀!出错了!",
|
||||
"tryAgain": "请重试。",
|
||||
"tryAgain": "请重试",
|
||||
"backToLogin": "返回登录",
|
||||
"checkEmail": "请检查您的邮箱"
|
||||
}
|
||||
|
@ -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>
|
||||
),
|
||||
)}
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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
22
src/types/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user