feat: add blocks section to marketing navigation and routes

- Introduce new blocks routes for hero, pricing, features, FAQ, stats, call-to-action, and content components
- Update marketing navigation configuration to include new blocks menu section
- Add corresponding route entries in `routes.ts`
- Create placeholder pages for block components
- Update localization files to support new blocks menu items
- Modify pricing page to include pricing comparator component
- Refactor pricing component styling for consistency
This commit is contained in:
javayhu 2025-03-09 01:11:05 +08:00
parent 63b549212e
commit 416e184a59
11 changed files with 415 additions and 59 deletions

View File

@ -2,7 +2,7 @@
"Common": {
"login": "Login",
"logout": "Log out",
"signUp": "Sign up"
"signUp": "Sign up"
},
"HomePage": {
"title": "next-intl example"
@ -77,7 +77,7 @@
"allPosts": "All Posts"
},
"Marketing": {
"menu": {
"navbar": {
"features": {
"title": "Features"
},
@ -144,6 +144,32 @@
"description": "The legal agreement between you and our company"
}
}
},
"blocks": {
"title": "Blocks",
"items": {
"hero": {
"title": "Hero Blocks"
},
"pricing": {
"title": "Pricing Blocks"
},
"features": {
"title": "Features Blocks"
},
"faq": {
"title": "Faq Blocks"
},
"stats": {
"title": "Stats Blocks"
},
"callToAction": {
"title": "Call to Action Blocks"
},
"content": {
"title": "Content Blocks"
}
}
}
},
"footer": {
@ -185,26 +211,26 @@
"settings": "Settings"
}
},
"mail": {
"common": {
"Mail": {
"common": {
"team": "{name} Team",
"copyright": "Copyright {year} All Rights Reserved."
},
"verifyEmail": {
"title": "Hi, {name}.",
"body": "Please click the link below to verify your email address.",
"confirmEmail": "Confirm email",
"subject": "Verify your email"
},
"forgotPassword": {
"title": "Hi, {name}.",
"body": "Please click the link below to reset your password.",
"resetPassword": "Reset password",
"subject": "Reset your password"
},
"subscribeNewsletter": {
"body": "Thank you for subscribing to the newsletter. We will keep you updated with the latest news and updates.",
"subject": "Thanks for subscribing"
}
}
},
"verifyEmail": {
"title": "Hi, {name}.",
"body": "Please click the link below to verify your email address.",
"confirmEmail": "Confirm email",
"subject": "Verify your email"
},
"forgotPassword": {
"title": "Hi, {name}.",
"body": "Please click the link below to reset your password.",
"resetPassword": "Reset password",
"subject": "Reset your password"
},
"subscribeNewsletter": {
"body": "Thank you for subscribing to the newsletter. We will keep you updated with the latest news and updates.",
"subject": "Thanks for subscribing"
}
}
}

View File

@ -74,7 +74,7 @@
"allPosts": "全部文章"
},
"Marketing": {
"menu": {
"navbar": {
"features": {
"title": "功能"
},
@ -141,6 +141,32 @@
"description": "关于您与我们公司之间的法律协议和条款"
}
}
},
"blocks": {
"title": "内置组件",
"items": {
"hero": {
"title": "Hero组件"
},
"pricing": {
"title": "Pricing组件"
},
"features": {
"title": "Features组件"
},
"faq": {
"title": "Faq组件"
},
"stats": {
"title": "Stats组件"
},
"callToAction": {
"title": "Call to Action组件"
},
"content": {
"title": "Content组件"
}
}
}
},
"footer": {
@ -183,7 +209,7 @@
"logout": "退出"
}
},
"mail": {
"Mail": {
"common": {
"team": "{name} 团队",
"copyright": "版权所有 {year} All Rights Reserved."

View File

@ -0,0 +1,29 @@
import Pricing3 from "@/components/nsui/pricing3";
import Pricing4 from "@/components/nsui/pricing4";
import Pricing5 from "@/components/nsui/pricing5";
import PricingComparator from "@/components/pricing-comparator";
import { getTranslations } from 'next-intl/server';
interface PricingPageProps {
params: Promise<{ locale: string }>;
};
export default async function PricingPage(props: PricingPageProps) {
const params = await props.params;
const { locale } = params;
const t = await getTranslations('PricingPage');
return (
<>
<div className="mt-8 flex flex-col gap-16 pb-16">
<Pricing5 />
<Pricing4 />
<Pricing3 />
<PricingComparator />
</div>
</>
);
}

View File

@ -0,0 +1,29 @@
import Pricing3 from "@/components/nsui/pricing3";
import Pricing4 from "@/components/nsui/pricing4";
import Pricing5 from "@/components/nsui/pricing5";
import PricingComparator from "@/components/pricing-comparator";
import { getTranslations } from 'next-intl/server';
interface PricingPageProps {
params: Promise<{ locale: string }>;
};
export default async function PricingPage(props: PricingPageProps) {
const params = await props.params;
const { locale } = params;
const t = await getTranslations('PricingPage');
return (
<>
<div className="mt-8 flex flex-col gap-16 pb-16">
<Pricing5 />
<Pricing4 />
<Pricing3 />
<PricingComparator />
</div>
</>
);
}

View File

@ -1,6 +1,7 @@
import Pricing3 from "@/components/nsui/pricing3";
import Pricing4 from "@/components/nsui/pricing4";
import Pricing5 from "@/components/nsui/pricing5";
import PricingComparator from "@/components/pricing-comparator";
import { getTranslations } from 'next-intl/server';
interface PricingPageProps {
@ -20,6 +21,8 @@ export default async function PricingPage(props: PricingPageProps) {
<Pricing4 />
<Pricing3 />
<PricingComparator />
</div>
</>
);

View File

@ -43,7 +43,7 @@ export default function Pricing3() {
</Card>
<Card className="relative">
<span className="bg-linear-to-br/increasing absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full from-purple-400 to-amber-300 px-3 py-1 text-xs font-medium text-amber-950 ring-1 ring-inset ring-white/20 ring-offset-1 ring-offset-gray-950/5">Popular</span>
<span className="absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full px-3 py-1 text-xs font-medium bg-primary text-primary-foreground">Popular</span>
<CardHeader>
<CardTitle className="font-medium">Pro</CardTitle>

View File

@ -0,0 +1,182 @@
import { Button } from '@/components/ui/button'
import { Cpu, Sparkles } from 'lucide-react'
import Link from 'next/link'
const tableData = [
{
feature: 'Feature 1',
free: true,
pro: true,
startup: true,
},
{
feature: 'Feature 2',
free: true,
pro: true,
startup: true,
},
{
feature: 'Feature 3',
free: false,
pro: true,
startup: true,
},
{
feature: 'Tokens',
free: '',
pro: '20 Users',
startup: 'Unlimited',
},
{
feature: 'Video calls',
free: '',
pro: '12 Weeks',
startup: '56',
},
{
feature: 'Support',
free: '',
pro: 'Secondes',
startup: 'Unlimited',
},
{
feature: 'Security',
free: '',
pro: '20 Users',
startup: 'Unlimited',
},
]
/**
* https://nsui.irung.me/comparator
*/
export default function PricingComparator() {
return (
<section className="py-16">
<div className="mx-auto max-w-5xl px-6">
<div className="w-full overflow-auto lg:overflow-visible">
<table className="w-[200vw] border-separate border-spacing-x-3 md:w-full dark:[--color-muted:var(--color-zinc-900)]">
<thead className="bg-background sticky top-0">
<tr className="*:py-4 *:text-left *:font-medium">
<th className="lg:w-2/5"></th>
<th className="space-y-3">
<span className="block">Free</span>
<Button asChild variant="outline" size="sm">
<Link href="#">Get Started</Link>
</Button>
</th>
<th className="bg-muted rounded-t-(--radius) space-y-3 px-4">
<span className="block">Pro</span>
<Button asChild size="sm">
<Link href="#">Get Started</Link>
</Button>
</th>
<th className="space-y-3">
<span className="block">Startup</span>
<Button asChild variant="outline" size="sm">
<Link href="#">Get Started</Link>
</Button>
</th>
</tr>
</thead>
<tbody className="text-caption text-sm">
<tr className="*:py-3">
<td className="flex items-center gap-2 font-medium">
<Cpu className="size-4" />
<span>Features</span>
</td>
<td></td>
<td className="bg-muted border-none px-4"></td>
<td></td>
</tr>
{tableData.slice(-4).map((row, index) => (
<tr key={index} className="*:border-b *:py-3">
<td className="text-muted-foreground">{row.feature}</td>
<td>
{row.free === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.free
)}
</td>
<td className="bg-muted border-none px-4">
<div className="-mb-3 border-b py-3">
{row.pro === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.pro
)}
</div>
</td>
<td>
{row.startup === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.startup
)}
</td>
</tr>
))}
<tr className="*:pb-3 *:pt-8">
<td className="flex items-center gap-2 font-medium">
<Sparkles className="size-4" />
<span>AI Models</span>
</td>
<td></td>
<td className="bg-muted border-none px-4"></td>
<td></td>
</tr>
{tableData.map((row, index) => (
<tr key={index} className="*:border-b *:py-3">
<td className="text-muted-foreground">{row.feature}</td>
<td>
{row.free === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.free
)}
</td>
<td className="bg-muted border-none px-4">
<div className="-mb-3 border-b py-3">
{row.pro === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.pro
)}
</div>
</td>
<td>
{row.startup === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
</svg>
) : (
row.startup
)}
</td>
</tr>
))}
<tr className="*:py-6">
<td></td>
<td></td>
<td className="bg-muted rounded-b-(--radius) border-none px-4"></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
)
}

View File

@ -12,17 +12,24 @@ import { DashboardIcon } from '@radix-ui/react-icons';
import {
AudioLinesIcon,
BuildingIcon,
ChartNoAxesCombinedIcon,
CircleDollarSignIcon,
CircleHelpIcon,
CookieIcon,
FileTextIcon,
FilmIcon,
FlameIcon,
ImageIcon,
ListChecksIcon,
MailboxIcon,
MailIcon,
NewspaperIcon,
RocketIcon,
SettingsIcon,
ShieldCheckIcon,
SquareKanbanIcon,
SquarePenIcon
SquarePenIcon,
WandSparklesIcon
} from 'lucide-react';
type TranslationFunction = (key: string, ...args: any[]) => string;
@ -53,47 +60,47 @@ export function createTranslator(t: any): TranslationFunction {
export function getMenuLinks(t: TranslationFunction): NestedMenuItem[] {
return [
{
title: t('Marketing.menu.features.title'),
title: t('Marketing.navbar.features.title'),
href: Routes.Features,
external: false
},
{
title: t('Marketing.menu.pricing.title'),
title: t('Marketing.navbar.pricing.title'),
href: Routes.Pricing,
external: false
},
{
title: t('Marketing.menu.blog.title'),
title: t('Marketing.navbar.blog.title'),
href: Routes.Blog,
external: false
},
{
title: t('Marketing.menu.ai.title'),
title: t('Marketing.navbar.ai.title'),
items: [
{
title: t('Marketing.menu.ai.items.text.title'),
description: t('Marketing.menu.ai.items.text.description'),
title: t('Marketing.navbar.ai.items.text.title'),
description: t('Marketing.navbar.ai.items.text.description'),
icon: <SquarePenIcon className="size-5 shrink-0" />,
href: Routes.AIText,
external: false
},
{
title: t('Marketing.menu.ai.items.image.title'),
description: t('Marketing.menu.ai.items.image.description'),
title: t('Marketing.navbar.ai.items.image.title'),
description: t('Marketing.navbar.ai.items.image.description'),
icon: <ImageIcon className="size-5 shrink-0" />,
href: Routes.AIImage,
external: false
},
{
title: t('Marketing.menu.ai.items.video.title'),
description: t('Marketing.menu.ai.items.video.description'),
title: t('Marketing.navbar.ai.items.video.title'),
description: t('Marketing.navbar.ai.items.video.description'),
icon: <FilmIcon className="size-5 shrink-0" />,
href: Routes.AIVideo,
external: false
},
{
title: t('Marketing.menu.ai.items.audio.title'),
description: t('Marketing.menu.ai.items.audio.description'),
title: t('Marketing.navbar.ai.items.audio.title'),
description: t('Marketing.navbar.ai.items.audio.description'),
icon: <AudioLinesIcon className="size-5 shrink-0" />,
href: Routes.AIAudio,
external: false
@ -101,66 +108,113 @@ export function getMenuLinks(t: TranslationFunction): NestedMenuItem[] {
]
},
{
title: t('Marketing.menu.pages.title'),
title: t('Marketing.navbar.pages.title'),
items: [
{
title: t('Marketing.menu.pages.items.about.title'),
description: t('Marketing.menu.pages.items.about.description'),
title: t('Marketing.navbar.pages.items.about.title'),
description: t('Marketing.navbar.pages.items.about.description'),
icon: <BuildingIcon className="size-5 shrink-0" />,
href: Routes.About,
external: false
},
{
title: t('Marketing.menu.pages.items.contact.title'),
description: t('Marketing.menu.pages.items.contact.description'),
title: t('Marketing.navbar.pages.items.contact.title'),
description: t('Marketing.navbar.pages.items.contact.description'),
icon: <MailIcon className="size-5 shrink-0" />,
href: Routes.Contact,
external: false
},
{
title: t('Marketing.menu.pages.items.waitlist.title'),
description: t('Marketing.menu.pages.items.waitlist.description'),
title: t('Marketing.navbar.pages.items.waitlist.title'),
description: t('Marketing.navbar.pages.items.waitlist.description'),
icon: <MailboxIcon className="size-5 shrink-0" />,
href: Routes.Waitlist,
external: false
},
{
title: t('Marketing.menu.pages.items.changelog.title'),
description: t('Marketing.menu.pages.items.changelog.description'),
title: t('Marketing.navbar.pages.items.changelog.title'),
description: t('Marketing.navbar.pages.items.changelog.description'),
icon: <ListChecksIcon className="size-5 shrink-0" />,
href: Routes.Changelog,
external: false
},
{
title: t('Marketing.menu.pages.items.roadmap.title'),
description: t('Marketing.menu.pages.items.roadmap.description'),
title: t('Marketing.navbar.pages.items.roadmap.title'),
description: t('Marketing.navbar.pages.items.roadmap.description'),
icon: <SquareKanbanIcon className="size-5 shrink-0" />,
href: Routes.Roadmap,
external: true
},
{
title: t('Marketing.menu.pages.items.cookiePolicy.title'),
description: t('Marketing.menu.pages.items.cookiePolicy.description'),
title: t('Marketing.navbar.pages.items.cookiePolicy.title'),
description: t('Marketing.navbar.pages.items.cookiePolicy.description'),
icon: <CookieIcon className="size-5 shrink-0" />,
href: Routes.CookiePolicy,
external: false
},
{
title: t('Marketing.menu.pages.items.privacyPolicy.title'),
description: t('Marketing.menu.pages.items.privacyPolicy.description'),
title: t('Marketing.navbar.pages.items.privacyPolicy.title'),
description: t('Marketing.navbar.pages.items.privacyPolicy.description'),
icon: <ShieldCheckIcon className="size-5 shrink-0" />,
href: Routes.PrivacyPolicy,
external: false
},
{
title: t('Marketing.menu.pages.items.termsOfService.title'),
description: t('Marketing.menu.pages.items.termsOfService.description'),
title: t('Marketing.navbar.pages.items.termsOfService.title'),
description: t('Marketing.navbar.pages.items.termsOfService.description'),
icon: <FileTextIcon className="size-5 shrink-0" />,
href: Routes.TermsOfService,
external: false
}
]
},
{
title: t('Marketing.navbar.blocks.title'),
items: [
{
title: t('Marketing.navbar.blocks.items.hero.title'),
icon: <FlameIcon className="size-5 shrink-0" />,
href: Routes.HeroBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.pricing.title'),
icon: <CircleDollarSignIcon className="size-5 shrink-0" />,
href: Routes.PricingBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.features.title'),
icon: <WandSparklesIcon className="size-5 shrink-0" />,
href: Routes.FeaturesBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.faq.title'),
icon: <CircleHelpIcon className="size-5 shrink-0" />,
href: Routes.FAQBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.stats.title'),
icon: <ChartNoAxesCombinedIcon className="size-5 shrink-0" />,
href: Routes.StatsBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.callToAction.title'),
icon: <RocketIcon className="size-5 shrink-0" />,
href: Routes.CallToActionBlocks,
external: false
},
{
title: t('Marketing.navbar.blocks.items.content.title'),
icon: <NewspaperIcon className="size-5 shrink-0" />,
href: Routes.ContentBlocks,
external: false
}
]
}
];
}

View File

@ -97,8 +97,8 @@ export async function getTemplate<T extends Template>({
// get the subject from the messages
const subject =
"subject" in messages.mail[template as keyof Messages["mail"]]
? messages.mail[template].subject
"subject" in messages.Mail[template as keyof Messages["Mail"]]
? messages.Mail[template].subject
: "";
const html = await render(email);

View File

@ -34,6 +34,14 @@ export enum Routes {
AIImage = '/dashboard/features/ai-image',
AIVideo = '/dashboard/features/ai-video',
AIAudio = '/dashboard/features/ai-audio',
HeroBlocks = '/blocks/hero',
PricingBlocks = '/blocks/pricing',
FeaturesBlocks = '/blocks/features',
FAQBlocks = '/blocks/faq',
StatsBlocks = '/blocks/stats',
CallToActionBlocks = '/blocks/call-to-action',
ContentBlocks = '/blocks/content',
}
/**

View File

@ -2,8 +2,7 @@ import type { Icons } from "@/components/icons/icons";
import type { ReactNode } from "react";
/**
* utm parameters
* https://utmbuilder.com/
* site config
*/
export type SiteConfig = {
name: string;