diff --git a/apps/web/app/[locale]/(public)/du-an/[slug]/page.tsx b/apps/web/app/[locale]/(public)/du-an/[slug]/page.tsx new file mode 100644 index 0000000..1a8c892 --- /dev/null +++ b/apps/web/app/[locale]/(public)/du-an/[slug]/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { DuAnDetailClient } from '@/components/du-an/du-an-detail-client'; +import { fetchProjectBySlug } from '@/lib/du-an-server'; + +interface PageProps { + params: Promise<{ slug: string; locale: string }>; +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const project = await fetchProjectBySlug(slug); + if (!project) return { title: 'Không tìm thấy dự án' }; + + return { + title: `${project.name} — ${project.developer.name}`, + description: project.description?.slice(0, 160), + openGraph: { + title: project.name, + description: project.description?.slice(0, 160), + images: project.media + .filter((m) => m.type === 'image') + .slice(0, 1) + .map((m) => ({ url: m.url })), + }, + }; +} + +export default async function DuAnDetailPage({ params }: PageProps) { + const { slug } = await params; + const project = await fetchProjectBySlug(slug); + + if (!project) { + notFound(); + } + + return ; +} diff --git a/apps/web/app/[locale]/(public)/du-an/page.tsx b/apps/web/app/[locale]/(public)/du-an/page.tsx new file mode 100644 index 0000000..3eceb35 --- /dev/null +++ b/apps/web/app/[locale]/(public)/du-an/page.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { Building2 } from 'lucide-react'; +import * as React from 'react'; +import { ProjectCard } from '@/components/du-an/project-card'; +import { ProjectFilterBar } from '@/components/du-an/project-filter-bar'; +import { Button } from '@/components/ui/button'; +import type { SearchProjectsParams } from '@/lib/du-an-api'; +import { useProjectsSearch } from '@/lib/hooks/use-du-an'; + +const PAGE_SIZE = 12; + +export default function DuAnPage() { + const [filters, setFilters] = React.useState({ + page: 1, + limit: PAGE_SIZE, + }); + + const { data, isLoading, isError } = useProjectsSearch(filters); + + const handleFilterChange = (newFilters: SearchProjectsParams) => { + setFilters({ ...newFilters, limit: PAGE_SIZE }); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handlePageChange = (page: number) => { + setFilters((prev) => ({ ...prev, page })); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+ {/* Page header */} +
+

Dự án bất động sản

+

+ Khám phá các dự án mới nhất từ các chủ đầu tư uy tín +

+
+ + {/* Filters */} + + + {/* Results */} +
+ {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ) : isError ? ( +
+

+ Không thể tải danh sách dự án. Vui lòng thử lại. +

+ +
+ ) : data && data.data.length > 0 ? ( + <> +

+ {data.total} dự án được tìm thấy +

+
+ {data.data.map((project) => ( + + ))} +
+ + {/* Pagination */} + {data.totalPages > 1 && ( +
+ + + Trang {data.page} / {data.totalPages} + + +
+ )} + + ) : ( +
+ +

Không tìm thấy dự án

+

+ Thử thay đổi bộ lọc để tìm kiếm nhiều hơn +

+
+ )} +
+
+ ); +} diff --git a/apps/web/components/du-an/du-an-detail-client.tsx b/apps/web/components/du-an/du-an-detail-client.tsx new file mode 100644 index 0000000..33a1d46 --- /dev/null +++ b/apps/web/components/du-an/du-an-detail-client.tsx @@ -0,0 +1,541 @@ +'use client'; + +import { + Building2, + Calendar, + Download, + FileText, + Grid3X3, + Home, + MapPin, + Phone, +} from 'lucide-react'; +import Image from 'next/image'; +import * as React from 'react'; +import { ImageGallery } from '@/components/listings/image-gallery'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { formatPrice } from '@/lib/currency'; +import { + PROJECT_PROPERTY_TYPE_LABELS, + PROJECT_STATUS_COLORS, + PROJECT_STATUS_LABELS, + duAnApi, + type ProjectDetail, +} from '@/lib/du-an-api'; +import { cn } from '@/lib/utils'; + +type Tab = 'amenities' | 'location' | 'price' | 'listings' | 'documents'; + +const TABS: { key: Tab; label: string }[] = [ + { key: 'amenities', label: 'Tiện ích' }, + { key: 'location', label: 'Vị trí' }, + { key: 'price', label: 'Giá' }, + { key: 'listings', label: 'Tin đăng' }, + { key: 'documents', label: 'Tài liệu' }, +]; + +interface DuAnDetailClientProps { + project: ProjectDetail; +} + +export function DuAnDetailClient({ project }: DuAnDetailClientProps) { + const [activeTab, setActiveTab] = React.useState('amenities'); + const [inquiryForm, setInquiryForm] = React.useState({ + name: '', + phone: '', + message: '', + }); + const [inquiryState, setInquiryState] = React.useState< + 'idle' | 'loading' | 'success' | 'error' + >('idle'); + + const statusLabel = PROJECT_STATUS_LABELS[project.status]; + const statusColor = PROJECT_STATUS_COLORS[project.status]; + + const handleInquirySubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!inquiryForm.name.trim() || !inquiryForm.phone.trim()) return; + + setInquiryState('loading'); + try { + await duAnApi.submitInquiry(project.id, { + name: inquiryForm.name.trim(), + phone: inquiryForm.phone.trim(), + message: inquiryForm.message.trim(), + }); + setInquiryState('success'); + } catch { + setInquiryState('error'); + } + }; + + return ( +
+ {/* Header */} +
+
+ + {statusLabel} + + {project.propertyTypes.map((t) => ( + + {PROJECT_PROPERTY_TYPE_LABELS[t]} + + ))} +
+

{project.name}

+
+ + + {project.address}, {project.district}, {project.city} + + + + {project.developer.name} + + {project.completionDate && ( + + + Bàn giao: {new Date(project.completionDate).toLocaleDateString('vi-VN')} + + )} +
+
+ + {/* Gallery */} + m.type === 'image') + .map((m) => ({ id: m.id, type: 'image' as const, url: m.url, order: m.order, caption: m.caption }))} + /> + + {/* Quick stats */} +
+ } + label="Tổng diện tích" + value={`${project.totalArea.toLocaleString('vi-VN')} m²`} + /> + } + label="Số căn" + value={`${project.totalUnits}`} + /> + } + label="Số block" + value={`${project.blocks.length}`} + /> + +
+ +
+ {/* Main content */} +
+ {/* Description */} + + + Tổng quan dự án + + +

+ {project.description} +

+
+
+ + {/* Blocks */} + {project.blocks.length > 0 && ( + + + Phân khu / Block + + +
+ {project.blocks.map((block) => ( +
+

{block.name}

+
+ {block.totalUnits} căn + {block.availableUnits} còn trống + {block.floors} tầng +
+
+ ))} +
+
+
+ )} + + {/* Tabs */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ +
+ {activeTab === 'amenities' && } + {activeTab === 'location' && } + {activeTab === 'price' && } + {activeTab === 'listings' && } + {activeTab === 'documents' && } +
+
+
+ + {/* Sidebar */} +
+ {/* Developer card */} + + + Chủ đầu tư + + + {project.developer.logoUrl ? ( + {project.developer.name} + ) : ( +
+ +
+ )} +
+

{project.developer.name}

+

+ {project.developer.totalProjects} dự án +

+
+
+
+ + {/* Inquiry form */} + + + Nhận tư vấn + + + {inquiryState === 'success' ? ( +
+
+ +
+

Đã gửi thành công!

+

+ Chúng tôi sẽ liên hệ bạn sớm nhất. +

+
+ ) : ( +
+ {inquiryState === 'error' && ( +

+ Gửi thất bại. Vui lòng thử lại. +

+ )} +
+ + + setInquiryForm((f) => ({ ...f, name: e.target.value })) + } + required + disabled={inquiryState === 'loading'} + /> +
+
+ + + setInquiryForm((f) => ({ ...f, phone: e.target.value })) + } + required + disabled={inquiryState === 'loading'} + /> +
+
+ +