feat(web): add /pricing page with subscription tier comparison

Complete public pricing page showing all 4 subscription plans (FREE,
AGENT_PRO, INVESTOR, ENTERPRISE) with billing cycle toggle, feature
comparison table, VND formatting, and Vietnamese/English i18n support.
Also adds pricing link to public navigation header and footer.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-10 22:42:37 +07:00
parent 1aad9b9f95
commit d62eb5f164
4 changed files with 699 additions and 27 deletions

View File

@@ -1,7 +1,9 @@
'use client';
import { Menu, X } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { LanguageSwitcher } from '@/components/ui/language-switcher';
import { Link } from '@/i18n/navigation';
@@ -12,6 +14,25 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
const pathname = usePathname();
const { user } = useAuthStore();
const t = useTranslations();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const navLinks = [
{
href: '/' as const,
label: t('nav.home'),
isActive: pathname === '/' || !!pathname.match(/^\/(vi|en)\/?$/),
},
{
href: '/search' as const,
label: t('nav.search'),
isActive: pathname.includes('/search'),
},
{
href: '/pricing' as const,
label: t('nav.pricing'),
isActive: pathname.includes('/pricing'),
},
];
return (
<div className="min-h-screen bg-background">
@@ -24,29 +45,22 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
<span className="text-lg font-bold text-primary">{t('common.goodgo')}</span>
</Link>
<nav aria-label={t('nav.mainNav')} className="flex items-center space-x-1">
<Link
href="/"
className={cn(
'rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
pathname === '/' || pathname.match(/^\/(vi|en)\/?$/)
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
)}
>
{t('nav.home')}
</Link>
<Link
href="/search"
className={cn(
'rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
pathname.includes('/search')
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
)}
>
{t('nav.search')}
</Link>
{/* Desktop nav */}
<nav aria-label={t('nav.mainNav')} className="hidden items-center space-x-1 sm:flex">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={cn(
'rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
link.isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
)}
>
{link.label}
</Link>
))}
</nav>
<div className="ml-auto flex items-center space-x-2">
@@ -62,18 +76,73 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
</>
) : (
<>
<Link href="/login">
<Link href="/login" className="hidden sm:inline-flex">
<Button variant="ghost" size="sm">
{t('common.login')}
</Button>
</Link>
<Link href="/register">
<Link href="/register" className="hidden sm:inline-flex">
<Button size="sm">{t('common.register')}</Button>
</Link>
</>
)}
{/* Mobile menu toggle */}
<button
aria-label={mobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')}
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<nav
aria-label={t('nav.mainNav')}
className="border-t px-4 pb-4 pt-2 sm:hidden"
>
<div className="space-y-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileMenuOpen(false)}
className={cn(
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
link.isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
)}
>
{link.label}
</Link>
))}
</div>
<div className="mt-3 space-y-2 border-t pt-3">
{user ? (
<>
<p className="px-3 text-sm text-muted-foreground">{user.fullName}</p>
<Link href="/dashboard" onClick={() => setMobileMenuOpen(false)}>
<Button size="sm" className="w-full">{t('common.dashboard')}</Button>
</Link>
</>
) : (
<div className="flex gap-2">
<Link href="/login" className="flex-1" onClick={() => setMobileMenuOpen(false)}>
<Button variant="ghost" size="sm" className="w-full">
{t('common.login')}
</Button>
</Link>
<Link href="/register" className="flex-1" onClick={() => setMobileMenuOpen(false)}>
<Button size="sm" className="w-full">{t('common.register')}</Button>
</Link>
</div>
)}
</div>
</nav>
)}
</header>
<main id="main-content" role="main">
@@ -110,6 +179,7 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
<div>
<h3 className="mb-3 text-sm font-semibold">{t('footer.support')}</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li><Link href="/pricing" className="hover:text-foreground">{t('nav.pricing')}</Link></li>
<li><Link href="/login" className="hover:text-foreground">{t('common.login')}</Link></li>
<li><Link href="/register" className="hover:text-foreground">{t('common.register')}</Link></li>
</ul>