```
---
## 🔗 Các Import Thông Dụng
### Import Thiết Yếu
```typescript
// Components
import Image from 'next/image';
import Link from 'next/link';
import dynamic from 'next/dynamic';
// UI Components
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
// Tiện ích
import { cn } from '@/lib/utils';
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
// State & API
import { useAuthStore } from '@/lib/auth-store';
import { useComparisonStore } from '@/lib/comparison-store';
import { listingsApi } from '@/lib/listings-api';
// Hooks
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
```
---
## 📊 Lấy Dữ Liệu
### Lấy Dữ Liệu Phía Server
```typescript
// apps/web/lib/listings-server.ts
import { fetchListingById } from '@/lib/listings-server';
// Trong page.tsx (Server Component)
const listing = await fetchListingById(params.id);
if (!listing) notFound();
```
### API Phía Client
```typescript
// apps/web/lib/listings-api.ts
import { listingsApi } from '@/lib/listings-api';
// Cách dùng:
const listing = await listingsApi.getById(id);
const results = await listingsApi.search({ city: 'Ho Chi Minh' });
```
### Sử Dụng React Query (có thể)
```typescript
// Mẫu thông thường để lấy dữ liệu
import { useQuery } from '@tanstack/react-query';
const { data, isLoading, error } = useQuery({
queryKey: ['listing', id],
queryFn: () => listingsApi.getById(id),
});
```
---
## 🌐 Đa Ngôn Ngữ
### Hỗ Trợ Ngôn Ngữ
- Tiếng Việt (vi)
- Tiếng Anh (en)
### Sử Dụng i18n
```typescript
// Trong các component, dùng nhãn tiếng Việt trực tiếp hoặc từ hằng số
const PROPERTY_TYPES: Record
= {
APARTMENT: 'Căn hộ',
HOUSE: 'Nhà riêng',
VILLA: 'Biệt thự',
// ...
};
// Từ @/lib/validations/listings
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';
```
### Route Nhận Biết Ngôn Ngữ
```
/vi/listings/123 # Tiếng Việt
/en/listings/123 # Tiếng Anh
```
---
## 🔐 Tính Năng Bảo Mật
### Tiêu Đề CSP (next.config.js)
```javascript
img-src 'self' data: blob: https://*.mapbox.com https://
font-src 'self' data:
```
### Danh Sách Cho Phép Domain Hình Ảnh
```javascript
// Cho phép hình ảnh HTTPS từ bất kỳ domain nào
remotePatterns: [
{ protocol: 'https', hostname: '**' }
]
```
---
## 🧪 Cân Nhắc Khi Kiểm Thử
### Các Tệp Component Cần Kiểm Thử
- `image-gallery.tsx` - Điều hướng thư viện, thay đổi trạng thái
- `image-upload.tsx` - Xác thực tệp, kéo và thả
- `property-card.tsx` - Hiển thị hình ảnh, responsive
- `listing-detail-client.tsx` - Chức năng tổng thể của trang
### Các Mẫu Kiểm Thử
```typescript
// Mock component Next.js Image
jest.mock('next/image', () => ({
__esModule: true,
default: (props) =>
,
}));
// Mock Zustand stores
jest.mock('@/lib/auth-store', () => ({
useAuthStore: jest.fn(),
}));
```
---
## 🚀 Mẹo Tối Ưu Hiệu Suất
1. **Ưu Tiên Hình Ảnh**
```typescript
priority={selectedIndex === 0} // Ảnh đầu tiên tải cùng trang
```
2. **Kích Thước Responsive**
```typescript
sizes="(max-width: 768px) 100vw, 60vw" // Cho trình duyệt biết chiều rộng ảnh
```
3. **Tải Lười (Lazy Loading)**
- Hình thu nhỏ tải theo yêu cầu (không đặt priority)
- Giảm tải ban đầu của trang
4. **Dynamic Import**
```typescript
const ListingMap = dynamic(() => import('@/components/map/listing-map'), {
ssr: false,
loading: () => Loading...
,
});
```
5. **Dọn Dẹp Object URL**
```typescript
React.useEffect(() => {
return () => {
images.forEach((img) => URL.revokeObjectURL(img.preview));
};
}, []);
```
---
## 📋 Các Tác Vụ Thông Dụng
### Thêm Một Phần Tử UI Mới
1. Tạo trong `components/ui/ComponentName.tsx`
2. Dùng CVA cho các biến thể
3. Export từ cùng tệp
4. Import và sử dụng trong các component tính năng
### Thêm Một Component Tính Năng Mới
1. Tạo trong `components/feature-name/ComponentName.tsx`
2. Thêm 'use client' nếu có tương tác
3. Import các component UI
4. Dùng Zustand stores nếu cần trạng thái toàn cục
5. Dùng trạng thái cục bộ cho trạng thái UI
### Chỉnh Sửa Thư Viện Ảnh
1. Chỉnh sửa `components/listings/image-gallery.tsx`
2. Cập nhật interface PropertyMedia nếu cần (trong `lib/listings-api.ts`)
3. Điều chỉnh tỉ lệ khung hình / kích thước theo nhu cầu
4. Kiểm tra hành vi responsive
### Thêm Lightbox Hình Ảnh
1. Chọn thư viện (embla-carousel, yet-another-react-lightbox, v.v.)
2. Cài đặt: `pnpm add package-name -F @goodgo/web`
3. Tạo component wrapper trong `components/listings/image-lightbox.tsx`
4. Tích hợp với `image-gallery.tsx`
5. Kiểm thử với nhiều hình ảnh
---
## 🐛 Các Vấn Đề Thường Gặp & Giải Pháp
### Hình Ảnh Không Tải Được
- Kiểm tra URL hợp lệ và là HTTPS
- Xác minh domain trong `remotePatterns`
- Kiểm tra tiêu đề CSP cho phép domain đó
### Điều Hướng Thư Viện Bị Đóng Băng
- Kiểm tra cập nhật trạng thái `selectedIndex`
- Xác minh các handler onClick được bind đúng cách
- Kiểm tra lỗi JavaScript trong console
### Vấn Đề Cuộn Hình Thu Nhỏ
- Đảm bảo container cha có `overflow-x-auto`
- Kiểm tra thuộc tính flex trên hình thu nhỏ
- Xác minh ràng buộc chiều rộng (flex-shrink-0)
### Dịch Chuyển Bố Cục Khi Tải Ảnh
- Dùng container tỉ lệ khung hình
- Đặt chiều rộng/chiều cao rõ ràng
- Dùng bố cục `fill` với container
---
## 📚 Tài Nguyên Bổ Sung
- **Next.js Image**: https://nextjs.org/docs/app/api-reference/components/image
- **Tailwind CSS**: https://tailwindcss.com/docs
- **Zustand**: https://github.com/pmndrs/zustand
- **CVA**: https://cva.style/docs
- **React Query**: https://tanstack.com/query/latest