From b4ef4fc81c4067f4938a09da4cd543ce46156793 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 18 Apr 2026 21:52:36 +0700 Subject: [PATCH] feat(web): redesign homepage with solutions showcase + tabbed featured section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Giải pháp GoodGo" section after hero with 4 feature cards linking to the platform's core products: Dự án, Khu công nghiệp, Chuyển nhượng, Định giá BĐS. - Convert "Tin đăng nổi bật" from residential-only 3-column grid into a tabbed section with one tab per core feature. Items render as a vertical list of horizontal cards (image left, title/location/meta right, price + arrow). Valuation tab shows a highlight CTA since it's a tool, not a listing type. - Remove "Khu vực nổi bật" district quick-links block (didn't fit the platform's multi-product positioning). - Fix invisible "Tìm kiếm ngay" button on CTA section — outline variant defaulted to bg-background (white) masking text-primary-foreground (white) on the primary background. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../(public)/__tests__/landing.spec.tsx | 9 - apps/web/app/[locale]/(public)/page.tsx | 338 ++++++++++++++---- apps/web/messages/en.json | 23 +- apps/web/messages/vi.json | 23 +- 4 files changed, 306 insertions(+), 87 deletions(-) diff --git a/apps/web/app/[locale]/(public)/__tests__/landing.spec.tsx b/apps/web/app/[locale]/(public)/__tests__/landing.spec.tsx index f45c96d..aa0455e 100644 --- a/apps/web/app/[locale]/(public)/__tests__/landing.spec.tsx +++ b/apps/web/app/[locale]/(public)/__tests__/landing.spec.tsx @@ -80,15 +80,6 @@ describe('LandingPage', () => { }); }); - it('renders districts section', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Quận 1')).toBeInTheDocument(); - expect(screen.getByText('Quận 7')).toBeInTheDocument(); - }); - }); - it('renders stats section', async () => { render(); diff --git a/apps/web/app/[locale]/(public)/page.tsx b/apps/web/app/[locale]/(public)/page.tsx index 5f2f47f..3db7e4f 100644 --- a/apps/web/app/[locale]/(public)/page.tsx +++ b/apps/web/app/[locale]/(public)/page.tsx @@ -1,26 +1,35 @@ 'use client'; -import { Building2, CheckCircle2, Home, MapPin, Users, type LucideIcon } from 'lucide-react'; +import { + ArrowRight, + ArrowRightLeft, + Building2, + Calculator, + CheckCircle2, + Factory, + Home, + MapPin, + Users, + type LucideIcon, +} from 'lucide-react'; import { useTranslations } from 'next-intl'; import * as React from 'react'; -import { PropertyCard } from '@/components/search/property-card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Select } from '@/components/ui/select'; import { Link, useRouter } from '@/i18n/navigation'; -import { listingsApi, type ListingDetail } from '@/lib/listings-api'; +import { transferApi, type TransferListingListItem } from '@/lib/chuyen-nhuong-api'; +import { duAnApi, type ProjectSummary } from '@/lib/du-an-api'; +import { industrialApi, type IndustrialParkListItem } from '@/lib/khu-cong-nghiep-api'; -const DISTRICTS = [ - { name: 'Quận 1', city: 'Hồ Chí Minh', img: null }, - { name: 'Quận 2', city: 'Hồ Chí Minh', img: null }, - { name: 'Quận 7', city: 'Hồ Chí Minh', img: null }, - { name: 'Bình Thạnh', city: 'Hồ Chí Minh', img: null }, - { name: 'Thủ Đức', city: 'Hồ Chí Minh', img: null }, - { name: 'Ba Đình', city: 'Hà Nội', img: null }, - { name: 'Hoàn Kiếm', city: 'Hà Nội', img: null }, - { name: 'Hải Châu', city: 'Đà Nẵng', img: null }, +type FeatureKey = 'projects' | 'industrial' | 'transfer' | 'valuation'; + +const FEATURES: { key: FeatureKey; href: string; icon: LucideIcon }[] = [ + { key: 'projects', href: '/du-an', icon: Building2 }, + { key: 'industrial', href: '/khu-cong-nghiep', icon: Factory }, + { key: 'transfer', href: '/chuyen-nhuong', icon: ArrowRightLeft }, + { key: 'valuation', href: '/dashboard/valuation', icon: Calculator }, ]; type StatKey = 'listings' | 'users' | 'transactions' | 'provinces'; @@ -35,29 +44,108 @@ const STATS: { key: StatKey; value: string; icon: LucideIcon }[] = [ const PROPERTY_TYPE_KEYS = ['APARTMENT', 'HOUSE', 'VILLA', 'LAND', 'OFFICE', 'SHOPHOUSE'] as const; const TRANSACTION_TYPE_KEYS = ['SALE', 'RENT'] as const; +type FeaturedItem = { + id: string; + href: string; + imageUrl: string | null; + fallbackIcon: LucideIcon; + title: string; + location: string; + priceLabel: string; + meta: string[]; +}; + +const VIEW_ALL_HREFS: Record = { + projects: '/du-an', + industrial: '/khu-cong-nghiep', + transfer: '/chuyen-nhuong', + valuation: '/dashboard/valuation', +}; + +function formatVND(value: string | number | null | undefined): string { + if (value == null) return '—'; + const num = typeof value === 'string' ? Number(value) : value; + if (!Number.isFinite(num) || num <= 0) return '—'; + if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; + if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`; + return num.toLocaleString('vi-VN'); +} + export default function LandingPage() { const router = useRouter(); const t = useTranslations(); const [searchQuery, setSearchQuery] = React.useState(''); const [transactionType, setTransactionType] = React.useState(''); const [propertyType, _setPropertyType] = React.useState(''); - const [featuredListings, setFeaturedListings] = React.useState([]); + const [activeFeature, setActiveFeature] = React.useState('projects'); + const [projects, setProjects] = React.useState([]); + const [parks, setParks] = React.useState([]); + const [transfers, setTransfers] = React.useState([]); const [loadingFeatured, setLoadingFeatured] = React.useState(true); const [featuredError, setFeaturedError] = React.useState(false); - const fetchFeatured = React.useCallback(() => { + const fetchFeatured = React.useCallback((feature: FeatureKey) => { + if (feature === 'valuation') { + setLoadingFeatured(false); + setFeaturedError(false); + return; + } setLoadingFeatured(true); setFeaturedError(false); - listingsApi - .search({ status: 'ACTIVE', limit: 6 }) - .then((res) => setFeaturedListings(res.data)) + const request = + feature === 'projects' + ? duAnApi.search({ limit: 4 }).then((res) => setProjects(res.data)) + : feature === 'industrial' + ? industrialApi.search({ limit: 4 }).then((res) => setParks(res.data)) + : transferApi.search({ limit: 4 }).then((res) => setTransfers(res.data)); + request .catch(() => setFeaturedError(true)) .finally(() => setLoadingFeatured(false)); }, []); React.useEffect(() => { - fetchFeatured(); - }, [fetchFeatured]); + fetchFeatured(activeFeature); + }, [activeFeature, fetchFeatured]); + + const featuredItems: FeaturedItem[] = React.useMemo(() => { + if (activeFeature === 'projects') { + return projects.map((p) => ({ + id: p.id, + href: `/du-an/${p.slug}`, + imageUrl: p.thumbnailUrl, + fallbackIcon: Building2, + title: p.name, + location: `${p.district}, ${p.city}`, + priceLabel: p.minPrice ? `Từ ${formatVND(p.minPrice)} VNĐ` : '—', + meta: [p.developer.name, `${p.totalUnits} căn`].filter(Boolean) as string[], + })); + } + if (activeFeature === 'industrial') { + return parks.map((k) => ({ + id: k.id, + href: `/khu-cong-nghiep/${k.slug}`, + imageUrl: null, + fallbackIcon: Factory, + title: k.name, + location: k.province, + priceLabel: k.landRentUsdM2Year ? `${k.landRentUsdM2Year} USD/m²/năm` : '—', + meta: [`${k.totalAreaHa} ha`, `Lấp đầy ${Math.round(k.occupancyRate)}%`], + })); + } + if (activeFeature === 'transfer') { + return transfers.map((tr) => ({ + id: tr.id, + href: `/chuyen-nhuong/${tr.id}`, + imageUrl: tr.media?.[0]?.url ?? null, + fallbackIcon: ArrowRightLeft, + title: tr.title, + location: `${tr.district}, ${tr.city}`, + priceLabel: `${formatVND(tr.askingPriceVND)} VNĐ`, + meta: [tr.areaM2 ? `${tr.areaM2} m²` : null, `${tr.itemCount} món`].filter(Boolean) as string[], + })); + } + return []; + }, [activeFeature, projects, parks, transfers]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); @@ -144,75 +232,145 @@ export default function LandingPage() { + {/* Core Features */} +
+
+
+

+ {t('landing.featuresTitle')} +

+

+ {t('landing.featuresSubtitle')} +

+
+ +
+ {FEATURES.map((feature) => ( + +
+
+
+

+ {t(`landing.features.${feature.key}.title`)} +

+

+ {t(`landing.features.${feature.key}.description`)} +

+ + {t('landing.features.explore')} + +
+ + ))} +
+
+
+ {/* Featured Listings */}
-
+

{t('landing.featuredSubtitle')}

- +
- {loadingFeatured ? ( -
- - ) : featuredError ? ( -
-

{t('landing.loadError')}

- -
- ) : featuredListings.length > 0 ? ( -
- {featuredListings.map((listing) => ( - - ))} -
- ) : ( -
-

{t('landing.noFeatured')}

-
- )} -
-
- - {/* Districts / Quick Links */} -
-
-

{t('landing.districtsTitle')}

-

- {t('landing.districtsSubtitle')} -

- -
- {DISTRICTS.map((district) => ( - + {FEATURES.map((feature) => ( + ))}
+ + {/* List */} +
+ {activeFeature === 'valuation' ? ( + + ) : loadingFeatured ? ( +
+ + ) : featuredError ? ( +
+

{t('landing.loadError')}

+ +
+ ) : featuredItems.length > 0 ? ( +
    + {featuredItems.map((item) => ( +
  • + +
    + {item.imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {item.title} + ) : ( +
    +
    + )} +
    +
    +

    + {item.title} +

    +

    +

    + {item.meta.length > 0 ? ( +

    + {item.meta.join(' • ')} +

    + ) : null} +

    {item.priceLabel}

    +
    +
  • + ))} +
+ ) : ( +
+

{t('landing.noFeatured')}

+
+ )} +
@@ -264,7 +422,7 @@ export default function LandingPage() { @@ -275,3 +433,35 @@ export default function LandingPage() { ); } + +function ValuationHighlight({ + tReady, + tDesc, + tExplore, +}: { + tReady: string; + tDesc: string; + tExplore: string; +}) { + return ( +
+
+
+
+
+
+

{tReady}

+

{tDesc}

+
+
+ + + +
+
+ ); +} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b135671..7cb17a9 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -65,13 +65,32 @@ "heroSubtitle": "Smart real estate platform in Vietnam — buy, sell, and rent properties with ease", "searchPlaceholder": "Enter area, project, or keyword...", "transactionTypeLabel": "Type", + "featuresTitle": "GoodGo solutions", + "featuresSubtitle": "Four core services for Vietnam's real estate market", + "features": { + "explore": "Explore", + "projects": { + "title": "Projects", + "description": "Discover the latest apartment, villa, and residential projects" + }, + "industrial": { + "title": "Industrial parks", + "description": "Find industrial land, factories, and warehouses for rent or sale" + }, + "transfer": { + "title": "Transfers", + "description": "Transfer business premises, offices, and retail spaces" + }, + "valuation": { + "title": "Property valuation", + "description": "AI-powered valuation based on real market data" + } + }, "featuredTitle": "Featured listings", "featuredSubtitle": "Explore the most popular properties", "viewAll": "View all", "loadError": "Unable to load listings. Please try again.", "noFeatured": "No featured listings yet", - "districtsTitle": "Popular areas", - "districtsSubtitle": "Search by popular districts", "statsTitle": "GoodGo in numbers", "statsSubtitle": "Vietnam's trusted real estate platform", "ctaTitle": "Have a property to list?", diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json index 04a3542..371990f 100644 --- a/apps/web/messages/vi.json +++ b/apps/web/messages/vi.json @@ -65,13 +65,32 @@ "heroSubtitle": "Nền tảng bất động sản thông minh tại Việt Nam — mua bán, cho thuê nhà đất dễ dàng", "searchPlaceholder": "Nhập khu vực, dự án, hoặc từ khóa...", "transactionTypeLabel": "Loại GD", + "featuresTitle": "Giải pháp GoodGo", + "featuresSubtitle": "Bốn dịch vụ cốt lõi cho thị trường bất động sản Việt Nam", + "features": { + "explore": "Khám phá", + "projects": { + "title": "Dự án", + "description": "Khám phá các dự án căn hộ, biệt thự, khu đô thị mới nhất" + }, + "industrial": { + "title": "Khu công nghiệp", + "description": "Tìm kiếm đất KCN, nhà xưởng, kho bãi cho thuê & mua bán" + }, + "transfer": { + "title": "Chuyển nhượng", + "description": "Sang nhượng mặt bằng kinh doanh, văn phòng, cửa hàng" + }, + "valuation": { + "title": "Định giá BĐS", + "description": "Định giá tài sản bằng AI dựa trên dữ liệu thị trường thực" + } + }, "featuredTitle": "Tin đăng nổi bật", "featuredSubtitle": "Khám phá các bất động sản được quan tâm nhất", "viewAll": "Xem tất cả", "loadError": "Không thể tải tin đăng. Vui lòng thử lại.", "noFeatured": "Chưa có tin đăng nổi bật", - "districtsTitle": "Khu vực nổi bật", - "districtsSubtitle": "Tìm kiếm theo quận huyện phổ biến", "statsTitle": "GoodGo trong số liệu", "statsSubtitle": "Nền tảng bất động sản đáng tin cậy tại Việt Nam", "ctaTitle": "Bạn có bất động sản muốn đăng?",