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>

View File

@@ -0,0 +1,498 @@
'use client';
import { Check, Crown, Rocket, Shield, X, Zap } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Link } from '@/i18n/navigation';
import { usePlans } from '@/lib/hooks/use-subscription';
import type { PlanDto } from '@/lib/subscription-api';
import { cn } from '@/lib/utils';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const PLAN_TIER_ORDER = ['FREE', 'AGENT_PRO', 'INVESTOR', 'ENTERPRISE'];
const TIER_ICONS: Record<string, React.ReactNode> = {
FREE: <Zap className="h-6 w-6" />,
AGENT_PRO: <Rocket className="h-6 w-6" />,
INVESTOR: <Shield className="h-6 w-6" />,
ENTERPRISE: <Crown className="h-6 w-6" />,
};
const TIER_COLORS: Record<string, string> = {
FREE: 'text-muted-foreground',
AGENT_PRO: 'text-blue-600',
INVESTOR: 'text-purple-600',
ENTERPRISE: 'text-amber-600',
};
/** Fallback data when API is unavailable */
const FALLBACK_PLANS: PlanDto[] = [
{
id: 'fallback-free',
tier: 'FREE',
name: 'Miễn phí',
priceMonthlyVND: '0',
priceYearlyVND: '0',
maxListings: 3,
maxSavedSearches: 5,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 5,
analytics: false,
prioritySupport: false,
aiValuation: false,
featuredListing: false,
},
isActive: true,
},
{
id: 'fallback-agent',
tier: 'AGENT_PRO',
name: 'Agent Pro',
priceMonthlyVND: '499000',
priceYearlyVND: '4990000',
maxListings: 50,
maxSavedSearches: 30,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 30,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
},
isActive: true,
},
{
id: 'fallback-investor',
tier: 'INVESTOR',
name: 'Investor',
priceMonthlyVND: '999000',
priceYearlyVND: '9990000',
maxListings: 20,
maxSavedSearches: 100,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 15,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: false,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
},
isActive: true,
},
{
id: 'fallback-enterprise',
tier: 'ENTERPRISE',
name: 'Enterprise',
priceMonthlyVND: '4990000',
priceYearlyVND: '49900000',
maxListings: -1,
maxSavedSearches: -1,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 100,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
apiAccess: true,
whiteLabel: true,
dedicatedSupport: true,
},
isActive: true,
},
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function formatVND(amount: string | number): string {
const num = typeof amount === 'string' ? Number(amount) : amount;
if (num === 0) return 'Miễn phí';
if (num >= 1_000_000) {
const millions = num / 1_000_000;
return `${millions % 1 === 0 ? millions.toFixed(0) : millions.toFixed(1)} triệu đ`;
}
return num.toLocaleString('vi-VN') + ' đ';
}
// Feature labels mapped for the comparison table
const FEATURE_LABELS: Record<string, string> = {
maxListings: 'Tin đăng',
maxSavedSearches: 'Tìm kiếm đã lưu',
maxPhotos: 'Ảnh/tin đăng',
analytics: 'Phân tích thị trường',
prioritySupport: 'Hỗ trợ ưu tiên',
aiValuation: 'Định giá AI',
featuredListing: 'Tin đăng nổi bật',
leadManagement: 'Quản lý khách hàng',
agentProfile: 'Hồ sơ môi giới',
marketReports: 'Báo cáo thị trường',
priceAlerts: 'Cảnh báo giá',
portfolioTracking: 'Theo dõi danh mục',
apiAccess: 'Truy cập API',
whiteLabel: 'Giao diện riêng',
dedicatedSupport: 'Hỗ trợ chuyên biệt',
};
const COMPARISON_FEATURES = [
'maxListings',
'maxSavedSearches',
'maxPhotos',
'analytics',
'aiValuation',
'featuredListing',
'prioritySupport',
'leadManagement',
'agentProfile',
'marketReports',
'priceAlerts',
'portfolioTracking',
'apiAccess',
'whiteLabel',
'dedicatedSupport',
];
function getFeatureValue(
plan: PlanDto,
key: string,
): boolean | number | string {
if (key === 'maxListings') {
return plan.maxListings === -1 ? 'Không giới hạn' : plan.maxListings;
}
if (key === 'maxSavedSearches') {
return plan.maxSavedSearches === -1
? 'Không giới hạn'
: plan.maxSavedSearches;
}
if (key === 'maxPhotos') {
return plan.features['maxPhotos'] ?? false;
}
return plan.features[key] ?? false;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export default function PricingPage() {
const t = useTranslations('pricing');
const { data: plansData, isLoading, error } = usePlans();
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(
'monthly',
);
const plans = (plansData ?? (error ? FALLBACK_PLANS : []))
.slice()
.sort(
(a, b) =>
PLAN_TIER_ORDER.indexOf(a.tier) - PLAN_TIER_ORDER.indexOf(b.tier),
);
return (
<div className="bg-background">
{/* Hero section */}
<section className="relative overflow-hidden border-b bg-gradient-to-b from-primary/5 to-background pb-16 pt-16 sm:pt-24">
<div className="mx-auto max-w-7xl px-4 text-center">
<Badge variant="secondary" className="mb-4">
{t('badge')}
</Badge>
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
{t('title')}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
{t('subtitle')}
</p>
{/* Billing cycle toggle */}
<div className="mt-8 flex items-center justify-center gap-3">
<Button
variant={billingCycle === 'monthly' ? 'default' : 'outline'}
size="sm"
onClick={() => setBillingCycle('monthly')}
>
{t('monthly')}
</Button>
<Button
variant={billingCycle === 'yearly' ? 'default' : 'outline'}
size="sm"
onClick={() => setBillingCycle('yearly')}
>
{t('yearly')}
<Badge variant="secondary" className="ml-2">
{t('yearlyDiscount')}
</Badge>
</Button>
</div>
</div>
</section>
{/* Pricing cards */}
<section className="mx-auto -mt-10 max-w-7xl px-4 pb-16">
{isLoading ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
{t('loading')}
</div>
) : (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((plan) => {
const isPopular = plan.tier === 'AGENT_PRO';
const price =
billingCycle === 'monthly'
? plan.priceMonthlyVND
: plan.priceYearlyVND;
return (
<Card
key={plan.id}
className={cn(
'relative flex flex-col transition-shadow hover:shadow-lg',
isPopular && 'border-primary shadow-md ring-1 ring-primary',
)}
>
{isPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge className="bg-primary text-primary-foreground">
{t('popular')}
</Badge>
</div>
)}
<CardHeader className="pb-2">
<div
className={cn(
'mb-2 flex items-center gap-2',
TIER_COLORS[plan.tier],
)}
>
{TIER_ICONS[plan.tier]}
<CardTitle className="text-lg">
{t(`tiers.${plan.tier}`)}
</CardTitle>
</div>
<CardDescription className="text-sm">
{t(`tierDescriptions.${plan.tier}`)}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-1 flex-col">
{/* Price */}
<div className="mb-6">
<span className="text-3xl font-bold text-foreground">
{formatVND(price)}
</span>
{Number(price) > 0 && (
<span className="text-sm text-muted-foreground">
/{billingCycle === 'monthly' ? t('perMonth') : t('perYear')}
</span>
)}
</div>
{/* Key features list */}
<ul className="mb-6 flex-1 space-y-2.5 text-sm">
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>
{plan.maxListings === -1
? t('unlimited')
: `${plan.maxListings} ${t('listingsCount')}`}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>
{plan.maxSavedSearches === -1
? t('unlimited')
: `${plan.maxSavedSearches} ${t('savedSearchesCount')}`}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{plan.features['maxPhotos']} {t('photosPerListing')}</span>
</li>
{plan.features['analytics'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.analytics')}</span>
</li>
)}
{plan.features['aiValuation'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.aiValuation')}</span>
</li>
)}
{plan.features['prioritySupport'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.prioritySupport')}</span>
</li>
)}
{plan.features['featuredListing'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.featuredListing')}</span>
</li>
)}
{plan.features['leadManagement'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.leadManagement')}</span>
</li>
)}
{plan.features['marketReports'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.marketReports')}</span>
</li>
)}
{plan.features['portfolioTracking'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.portfolioTracking')}</span>
</li>
)}
{plan.features['apiAccess'] && (
<li className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-green-600" />
<span>{t('features.apiAccess')}</span>
</li>
)}
</ul>
{/* CTA */}
<Link href={'/register' as const} className="w-full">
<Button
className="w-full"
variant={isPopular ? 'default' : 'outline'}
size="lg"
>
{plan.tier === 'FREE'
? t('ctaFree')
: plan.tier === 'ENTERPRISE'
? t('ctaEnterprise')
: t('ctaUpgrade')}
</Button>
</Link>
</CardContent>
</Card>
);
})}
</div>
)}
</section>
{/* Feature comparison table */}
<section className="border-t bg-muted/30 py-16">
<div className="mx-auto max-w-7xl px-4">
<h2 className="mb-2 text-center text-2xl font-bold">
{t('comparisonTitle')}
</h2>
<p className="mb-8 text-center text-muted-foreground">
{t('comparisonSubtitle')}
</p>
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] border-collapse">
<thead>
<tr className="border-b">
<th className="px-4 py-3 text-left text-sm font-semibold">
{t('feature')}
</th>
{plans.map((plan) => (
<th
key={plan.id}
className={cn(
'px-4 py-3 text-center text-sm font-semibold',
plan.tier === 'AGENT_PRO' && 'bg-primary/5',
)}
>
{t(`tiers.${plan.tier}`)}
</th>
))}
</tr>
</thead>
<tbody>
{COMPARISON_FEATURES.map((featureKey) => (
<tr key={featureKey} className="border-b last:border-0">
<td className="px-4 py-3 text-sm text-muted-foreground">
{FEATURE_LABELS[featureKey]}
</td>
{plans.map((plan) => {
const val = getFeatureValue(plan, featureKey);
return (
<td
key={plan.id}
className={cn(
'px-4 py-3 text-center text-sm',
plan.tier === 'AGENT_PRO' && 'bg-primary/5',
)}
>
{typeof val === 'boolean' ? (
val ? (
<Check className="mx-auto h-4 w-4 text-green-600" />
) : (
<X className="mx-auto h-4 w-4 text-muted-foreground/40" />
)
) : (
<span className="font-medium">{String(val)}</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
{/* FAQ / CTA section */}
<section className="py-16">
<div className="mx-auto max-w-3xl px-4 text-center">
<h2 className="text-2xl font-bold">{t('ctaTitle')}</h2>
<p className="mt-3 text-muted-foreground">
{t('ctaDescription')}
</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<Link href={'/register' as const}>
<Button size="lg">{t('ctaRegister')}</Button>
</Link>
<Link href={'/' as const}>
<Button variant="outline" size="lg">
{t('ctaLearnMore')}
</Button>
</Link>
</div>
</div>
</section>
</div>
);
}

View File

@@ -25,15 +25,19 @@
"nav": {
"home": "Home",
"search": "Search",
"pricing": "Pricing",
"mainNav": "Main navigation",
"dashboardNav": "Dashboard",
"adminNav": "Administration"
"adminNav": "Administration",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"dashboard": {
"title": "Dashboard",
"listings": "Listings",
"createListing": "Create listing",
"analytics": "Analytics",
"savedSearches": "Saved searches",
"aiValuation": "AI Valuation",
"profile": "Profile",
"subscription": "Subscription",
@@ -142,6 +146,54 @@
"default": "An error occurred during login. Please try again."
}
},
"pricing": {
"badge": "Pricing Plans",
"title": "Choose the right plan for you",
"subtitle": "From individuals to enterprises — GoodGo has a plan for every real estate need",
"monthly": "Monthly",
"yearly": "Yearly",
"yearlyDiscount": "-17%",
"perMonth": "month",
"perYear": "year",
"loading": "Loading plans...",
"popular": "Most popular",
"unlimited": "Unlimited",
"listingsCount": "listings",
"savedSearchesCount": "saved searches",
"photosPerListing": "photos/listing",
"tiers": {
"FREE": "Free",
"AGENT_PRO": "Agent Pro",
"INVESTOR": "Investor",
"ENTERPRISE": "Enterprise"
},
"tierDescriptions": {
"FREE": "Get started for free, explore the platform",
"AGENT_PRO": "For professional real estate agents",
"INVESTOR": "Analytics tools for investors",
"ENTERPRISE": "Comprehensive solution for businesses"
},
"features": {
"analytics": "Market analytics",
"aiValuation": "AI valuation",
"prioritySupport": "Priority support",
"featuredListing": "Featured listings",
"leadManagement": "Lead management",
"marketReports": "Market reports",
"portfolioTracking": "Portfolio tracking",
"apiAccess": "API access"
},
"ctaFree": "Register for free",
"ctaUpgrade": "Get started",
"ctaEnterprise": "Contact sales",
"comparisonTitle": "Compare plans in detail",
"comparisonSubtitle": "See all features for each plan",
"feature": "Feature",
"ctaTitle": "Ready to get started?",
"ctaDescription": "Sign up today and start your real estate journey with GoodGo",
"ctaRegister": "Register now",
"ctaLearnMore": "Learn more"
},
"search": {
"filters": "Filters",
"allTransactions": "All transactions",

View File

@@ -25,15 +25,19 @@
"nav": {
"home": "Trang chủ",
"search": "Tìm kiếm",
"pricing": "Bảng giá",
"mainNav": "Điều hướng chính",
"dashboardNav": "Bảng điều khiển",
"adminNav": "Quản trị"
"adminNav": "Quản trị",
"openMenu": "Mở menu",
"closeMenu": "Đóng menu"
},
"dashboard": {
"title": "Bảng điều khiển",
"listings": "Tin đăng",
"createListing": "Đăng tin",
"analytics": "Phân tích",
"savedSearches": "Tìm kiếm đã lưu",
"aiValuation": "Định giá AI",
"profile": "Hồ sơ",
"subscription": "Gói dịch vụ",
@@ -142,6 +146,54 @@
"default": "Đã xảy ra lỗi khi đăng nhập. Vui lòng thử lại."
}
},
"pricing": {
"badge": "Bảng giá dịch vụ",
"title": "Chọn gói dịch vụ phù hợp",
"subtitle": "Từ cá nhân đến doanh nghiệp — GoodGo có gói dịch vụ phù hợp cho mọi nhu cầu bất động sản của bạn",
"monthly": "Theo tháng",
"yearly": "Theo năm",
"yearlyDiscount": "-17%",
"perMonth": "tháng",
"perYear": "năm",
"loading": "Đang tải gói dịch vụ...",
"popular": "Phổ biến nhất",
"unlimited": "Không giới hạn",
"listingsCount": "tin đăng",
"savedSearchesCount": "tìm kiếm đã lưu",
"photosPerListing": "ảnh/tin đăng",
"tiers": {
"FREE": "Miễn phí",
"AGENT_PRO": "Môi giới Pro",
"INVESTOR": "Nhà đầu tư",
"ENTERPRISE": "Doanh nghiệp"
},
"tierDescriptions": {
"FREE": "Bắt đầu miễn phí, khám phá nền tảng",
"AGENT_PRO": "Dành cho môi giới chuyên nghiệp",
"INVESTOR": "Công cụ phân tích cho nhà đầu tư",
"ENTERPRISE": "Giải pháp toàn diện cho doanh nghiệp"
},
"features": {
"analytics": "Phân tích thị trường",
"aiValuation": "Định giá AI",
"prioritySupport": "Hỗ trợ ưu tiên",
"featuredListing": "Tin đăng nổi bật",
"leadManagement": "Quản lý khách hàng",
"marketReports": "Báo cáo thị trường",
"portfolioTracking": "Theo dõi danh mục",
"apiAccess": "Truy cập API"
},
"ctaFree": "Đăng ký miễn phí",
"ctaUpgrade": "Bắt đầu ngay",
"ctaEnterprise": "Liên hệ tư vấn",
"comparisonTitle": "So sánh chi tiết các gói",
"comparisonSubtitle": "Xem đầy đủ tính năng của từng gói dịch vụ",
"feature": "Tính năng",
"ctaTitle": "Bạn đã sẵn sàng?",
"ctaDescription": "Đăng ký ngay hôm nay để bắt đầu hành trình bất động sản cùng GoodGo",
"ctaRegister": "Đăng ký ngay",
"ctaLearnMore": "Tìm hiểu thêm"
},
"search": {
"filters": "Bộ lọc",
"allTransactions": "Tất cả giao dịch",