@@ -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')} +

+ +
+ + + + + {plans.map((plan) => ( + + ))} + + + + {COMPARISON_FEATURES.map((featureKey) => ( + + + {plans.map((plan) => { + const val = getFeatureValue(plan, featureKey); + return ( + + ); + })} + + ))} + +
+ {t('feature')} + + {t(`tiers.${plan.tier}`)} +
+ {FEATURE_LABELS[featureKey]} + + {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",