diff --git a/apps/web/app/[locale]/(public)/layout.tsx b/apps/web/app/[locale]/(public)/layout.tsx
index 0e80301..54a7825 100644
--- a/apps/web/app/[locale]/(public)/layout.tsx
+++ b/apps/web/app/[locale]/(public)/layout.tsx
@@ -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 (
@@ -24,29 +45,22 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
{t('common.goodgo')}
-
+
+ {/* Mobile menu */}
+ {mobileMenuOpen && (
+
@@ -110,6 +179,7 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
{t('footer.support')}
+ - {t('nav.pricing')}
- {t('common.login')}
- {t('common.register')}
diff --git a/apps/web/app/[locale]/(public)/pricing/page.tsx b/apps/web/app/[locale]/(public)/pricing/page.tsx
new file mode 100644
index 0000000..c8c35fd
--- /dev/null
+++ b/apps/web/app/[locale]/(public)/pricing/page.tsx
@@ -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
= {
+ FREE: ,
+ AGENT_PRO: ,
+ INVESTOR: ,
+ ENTERPRISE: ,
+};
+
+const TIER_COLORS: Record = {
+ 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 = {
+ 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 (
+
+ {/* Hero section */}
+
+
+
+ {t('badge')}
+
+
+ {t('title')}
+
+
+ {t('subtitle')}
+
+
+ {/* Billing cycle toggle */}
+
+
+
+
+
+
+
+ {/* Pricing cards */}
+
+ {isLoading ? (
+
+ {t('loading')}
+
+ ) : (
+
+ {plans.map((plan) => {
+ const isPopular = plan.tier === 'AGENT_PRO';
+ const price =
+ billingCycle === 'monthly'
+ ? plan.priceMonthlyVND
+ : plan.priceYearlyVND;
+
+ return (
+
+ {isPopular && (
+
+
+ {t('popular')}
+
+
+ )}
+
+
+ {TIER_ICONS[plan.tier]}
+
+ {t(`tiers.${plan.tier}`)}
+
+
+
+ {t(`tierDescriptions.${plan.tier}`)}
+
+
+
+ {/* Price */}
+
+
+ {formatVND(price)}
+
+ {Number(price) > 0 && (
+
+ /{billingCycle === 'monthly' ? t('perMonth') : t('perYear')}
+
+ )}
+
+
+ {/* Key features list */}
+
+ -
+
+
+ {plan.maxListings === -1
+ ? t('unlimited')
+ : `${plan.maxListings} ${t('listingsCount')}`}
+
+
+ -
+
+
+ {plan.maxSavedSearches === -1
+ ? t('unlimited')
+ : `${plan.maxSavedSearches} ${t('savedSearchesCount')}`}
+
+
+ -
+
+ {plan.features['maxPhotos']} {t('photosPerListing')}
+
+ {plan.features['analytics'] && (
+ -
+
+ {t('features.analytics')}
+
+ )}
+ {plan.features['aiValuation'] && (
+ -
+
+ {t('features.aiValuation')}
+
+ )}
+ {plan.features['prioritySupport'] && (
+ -
+
+ {t('features.prioritySupport')}
+
+ )}
+ {plan.features['featuredListing'] && (
+ -
+
+ {t('features.featuredListing')}
+
+ )}
+ {plan.features['leadManagement'] && (
+ -
+
+ {t('features.leadManagement')}
+
+ )}
+ {plan.features['marketReports'] && (
+ -
+
+ {t('features.marketReports')}
+
+ )}
+ {plan.features['portfolioTracking'] && (
+ -
+
+ {t('features.portfolioTracking')}
+
+ )}
+ {plan.features['apiAccess'] && (
+ -
+
+ {t('features.apiAccess')}
+
+ )}
+
+
+ {/* CTA */}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Feature comparison table */}
+
+
+
+ {t('comparisonTitle')}
+
+
+ {t('comparisonSubtitle')}
+
+
+
+
+
+
+ |
+ {t('feature')}
+ |
+ {plans.map((plan) => (
+
+ {t(`tiers.${plan.tier}`)}
+ |
+ ))}
+
+
+
+ {COMPARISON_FEATURES.map((featureKey) => (
+
+ |
+ {FEATURE_LABELS[featureKey]}
+ |
+ {plans.map((plan) => {
+ const val = getFeatureValue(plan, featureKey);
+ return (
+
+ {typeof val === 'boolean' ? (
+ val ? (
+
+ ) : (
+
+ )
+ ) : (
+ {String(val)}
+ )}
+ |
+ );
+ })}
+
+ ))}
+
+
+
+
+
+
+ {/* FAQ / CTA section */}
+
+
+
{t('ctaTitle')}
+
+ {t('ctaDescription')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index c5676ae..097790b 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -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",
diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json
index 322f0db..a54c2e9 100644
--- a/apps/web/messages/vi.json
+++ b/apps/web/messages/vi.json
@@ -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",