Files
goodgo-platform/apps/web/app/(public)/page.tsx
Ho Ngoc Hai 2502aa69b7 fix: production readiness — resolve build, lint, and code quality issues
- Fix Next.js build failure: remove duplicate route at (dashboard)/listings/[id]
  that conflicted with (public)/listings/[id] (same URL path in two route groups)
- Fix 772 ESLint errors: auto-fix import ordering (import-x/order), remove unused
  imports/variables, convert empty interfaces to type aliases, replace require()
  with ESM imports, fix consistent-type-imports violations
- Add CLAUDE.md for developer onboarding documentation
- All checks pass: 0 lint errors, typecheck clean, 230 tests passing, build success

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 07:15:06 +07:00

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
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 { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, TRANSACTION_TYPES } from '@/lib/validations/listings';
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 },
];
const STATS = [
{ label: 'Tin đăng', value: '10,000+', icon: '🏠' },
{ label: 'Người dùng', value: '50,000+', icon: '👥' },
{ label: 'Giao dịch thành công', value: '2,000+', icon: '✅' },
{ label: 'Tỉnh thành', value: '63', icon: '📍' },
];
export default function LandingPage() {
const router = useRouter();
const [searchQuery, setSearchQuery] = React.useState('');
const [transactionType, setTransactionType] = React.useState('');
const [propertyType, _setPropertyType] = React.useState('');
const [featuredListings, setFeaturedListings] = React.useState<ListingDetail[]>([]);
const [loadingFeatured, setLoadingFeatured] = React.useState(true);
const [featuredError, setFeaturedError] = React.useState(false);
const fetchFeatured = React.useCallback(() => {
setLoadingFeatured(true);
setFeaturedError(false);
listingsApi
.search({ status: 'ACTIVE', limit: 6 })
.then((res) => setFeaturedListings(res.data))
.catch(() => setFeaturedError(true))
.finally(() => setLoadingFeatured(false));
}, []);
React.useEffect(() => {
fetchFeatured();
}, [fetchFeatured]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const params = new URLSearchParams();
if (searchQuery) params.set('q', searchQuery);
if (transactionType) params.set('transactionType', transactionType);
if (propertyType) params.set('propertyType', propertyType);
router.push(`/search?${params.toString()}`);
};
return (
<div>
{/* Hero Section */}
<section className="relative bg-gradient-to-br from-primary/5 via-background to-primary/10 py-16 md:py-24">
<div className="mx-auto max-w-7xl px-4">
<div className="mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl">
Tìm kiếm bất đng sản
<span className="text-primary"> hoàn hảo</span>
</h1>
<p className="mt-4 text-lg text-muted-foreground md:text-xl">
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
</p>
{/* Search Bar */}
<form onSubmit={handleSearch} className="mt-8">
<div className="mx-auto flex max-w-2xl flex-col gap-3 rounded-xl border bg-white p-3 shadow-lg sm:flex-row">
<Input
placeholder="Nhập khu vực, dự án, hoặc từ khóa..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border-0 shadow-none focus-visible:ring-0"
/>
<div className="flex gap-2">
<Select
value={transactionType}
onChange={(e) => setTransactionType(e.target.value)}
className="w-32 shrink-0"
>
<option value="">Loại GD</option>
{TRANSACTION_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</Select>
<Button type="submit" className="shrink-0 px-6">
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
Tìm kiếm
</Button>
</div>
</div>
</form>
{/* Quick property type links */}
<div className="mt-6 flex flex-wrap justify-center gap-2">
{PROPERTY_TYPES.map((pt) => (
<Link
key={pt.value}
href={`/search?propertyType=${pt.value}`}
>
<Badge variant="outline" className="cursor-pointer px-3 py-1.5 text-sm hover:bg-accent">
{pt.label}
</Badge>
</Link>
))}
</div>
</div>
</div>
</section>
{/* Featured Listings */}
<section className="py-12 md:py-16">
<div className="mx-auto max-w-7xl px-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold md:text-3xl">Tin đăng nổi bật</h2>
<p className="mt-1 text-muted-foreground">
Khám phá các bất đng sản đưc quan tâm nhất
</p>
</div>
<Link href="/search">
<Button variant="outline">Xem tất cả</Button>
</Link>
</div>
{loadingFeatured ? (
<div className="mt-8 flex min-h-[300px] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : featuredError ? (
<div className="mt-8 flex min-h-[200px] flex-col items-center justify-center gap-3 text-muted-foreground">
<p>Không thể tải tin đăng. Vui lòng thử lại.</p>
<Button variant="outline" size="sm" onClick={fetchFeatured}>
Thử lại
</Button>
</div>
) : featuredListings.length > 0 ? (
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{featuredListings.map((listing) => (
<PropertyCard key={listing.id} listing={listing} />
))}
</div>
) : (
<div className="mt-8 flex min-h-[200px] items-center justify-center text-muted-foreground">
<p>Chưa tin đăng nổi bật</p>
</div>
)}
</div>
</section>
{/* Districts / Quick Links */}
<section className="bg-muted/40 py-12 md:py-16">
<div className="mx-auto max-w-7xl px-4">
<h2 className="text-2xl font-bold md:text-3xl">Khu vực nổi bật</h2>
<p className="mt-1 text-muted-foreground">
Tìm kiếm theo quận huyện phổ biến
</p>
<div className="mt-8 grid gap-3 sm:grid-cols-2 md:grid-cols-4">
{DISTRICTS.map((district) => (
<Link
key={`${district.name}-${district.city}`}
href={`/search?district=${encodeURIComponent(district.name)}&city=${encodeURIComponent(district.city)}`}
>
<Card className="group cursor-pointer overflow-hidden transition-shadow hover:shadow-md">
<div className="aspect-[16/9] bg-gradient-to-br from-primary/10 to-primary/5">
<div className="flex h-full items-center justify-center">
<span className="text-3xl">🏙</span>
</div>
</div>
<CardContent className="p-3">
<p className="font-medium group-hover:text-primary">{district.name}</p>
<p className="text-xs text-muted-foreground">{district.city}</p>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
</section>
{/* Market Stats */}
<section className="py-12 md:py-16">
<div className="mx-auto max-w-7xl px-4">
<div className="text-center">
<h2 className="text-2xl font-bold md:text-3xl">GoodGo trong số liệu</h2>
<p className="mt-1 text-muted-foreground">
Nền tảng bất đng sản đáng tin cậy tại Việt Nam
</p>
</div>
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{STATS.map((stat) => (
<div
key={stat.label}
className="rounded-lg border bg-card p-6 text-center shadow-sm"
>
<span className="text-3xl">{stat.icon}</span>
<p className="mt-2 text-3xl font-bold text-primary">{stat.value}</p>
<p className="mt-1 text-sm text-muted-foreground">{stat.label}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-primary py-12 md:py-16">
<div className="mx-auto max-w-7xl px-4 text-center">
<h2 className="text-2xl font-bold text-primary-foreground md:text-3xl">
Bạn bất đng sản muốn đăng?
</h2>
<p className="mt-2 text-primary-foreground/80">
Đăng tin miễn phí ngay hôm nay, tiếp cận hàng ngàn người mua tiềm năng
</p>
<div className="mt-6 flex justify-center gap-3">
<Link href="/register">
<Button
variant="secondary"
size="lg"
className="font-semibold"
>
Đăng miễn phí
</Button>
</Link>
<Link href="/search">
<Button
variant="outline"
size="lg"
className="border-primary-foreground/30 text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground"
>
Tìm kiếm ngay
</Button>
</Link>
</div>
</div>
</section>
</div>
);
}