chore: update project documentation, audit reports, and initialize IDE configuration files
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled

This commit is contained in:
Ho Ngoc Hai
2026-04-19 03:12:54 +07:00
parent 3be106074d
commit 11f2bf26e6
101 changed files with 21312 additions and 20672 deletions

View File

@@ -1,15 +1,15 @@
# Property Detail Page - Quick Reference & Code Snippets
# Trang Chi Tiết Bất Động Sản - Tài Liệu Tham Khảo Nhanh & Đoạn Mã
## 🎯 Quick Navigation
## 🎯 Điều Hướng Nhanh
### Page Routes
- **Detail Page**: `apps/web/app/[locale]/(public)/listings/[id]/page.tsx`
- **Client Component**: `apps/web/components/listings/listing-detail-client.tsx`
- **Gallery Component**: `apps/web/components/listings/image-gallery.tsx`
- **Upload Component**: `apps/web/components/listings/image-upload.tsx`
- **Property Card**: `apps/web/components/search/property-card.tsx`
### Các Route Trang
- **Trang Chi Tiết**: `apps/web/app/[locale]/(public)/listings/[id]/page.tsx`
- **Component Phía Client**: `apps/web/components/listings/listing-detail-client.tsx`
- **Component Thư Viện Ảnh**: `apps/web/components/listings/image-gallery.tsx`
- **Component Tải Ảnh Lên**: `apps/web/components/listings/image-upload.tsx`
- **Thẻ Bất Động Sản**: `apps/web/components/search/property-card.tsx`
### Data Flow
### Luồng Dữ Liệu
```
URL: /vi/listings/abc123
@@ -22,9 +22,9 @@ URL: /vi/listings/abc123
---
## 🖼️ Working with Images
## 🖼️ Làm Việc Với Hình Ảnh
### Current Gallery Features
### Tính Năng Thư Viện Ảnh Hiện Tại
```typescript
// apps/web/components/listings/image-gallery.tsx
@@ -33,37 +33,37 @@ interface ImageGalleryProps {
className?: string;
}
// Features:
// ✓ Main image (16:9 aspect ratio)
// ✓ Previous/Next buttons
// ✓ Image counter badge
// ✓ Horizontal scrollable thumbnails (64x64px)
// ✓ Selected state highlighting
// ✓ Empty state fallback
// Tính năng:
// ✓ Ảnh chính (tỉ lệ khung hình 16:9)
// ✓ Nút Trước/Tiếp theo
// ✓ Huy hiệu đếm ảnh
// ✓ Hình thu nhỏ cuộn ngang (64x64px)
// ✓ Nổi bật trạng thái đã chọn
// ✓ Trạng thái dự phòng khi trống
```
### Data Structure
### Cấu Trúc Dữ Liệu
```typescript
interface PropertyMedia {
id: string;
url: string; // Full URL to image
type: 'image' | 'video'; // Media type filter
order: number; // Sort order (0, 1, 2...)
caption: string | null; // Optional caption
url: string; // URL đầy đủ đến hình ảnh
type: 'image' | 'video'; // Bộ lọc loại phương tiện
order: number; // Thứ tự sắp xếp (0, 1, 2...)
caption: string | null; // Chú thích tùy chọn
}
interface Property {
// ... other fields
media: PropertyMedia[]; // Array of images/videos
// ... các trường khác
media: PropertyMedia[]; // Mảng hình ảnh/video
}
```
### Using Images in Components
### Sử Dụng Hình Ảnh Trong Các Component
```typescript
// ✓ Import Next.js Image
import Image from 'next/image';
// ✓ Main image with fill layout (responsive)
// ✓ Ảnh chính với bố cục fill (responsive)
<Image
src={images[index]?.url}
alt={`Image ${index + 1}`}
@@ -73,7 +73,7 @@ import Image from 'next/image';
priority={index === 0}
/>
// ✓ Fixed-size thumbnail
// ✓ Hình thu nhỏ kích thước cố định
<Image
src={img.url}
alt={`Thumbnail ${i}`}
@@ -82,7 +82,7 @@ import Image from 'next/image';
className="object-cover"
/>
// ✓ Responsive property card
// ✓ Thẻ bất động sản responsive
<Image
src={media[0]?.url}
alt="Property"
@@ -92,9 +92,9 @@ import Image from 'next/image';
/>
```
### Image Upload API
### API Tải Ảnh Lên
```typescript
// From: apps/web/lib/listings-api.ts
// Từ: apps/web/lib/listings-api.ts
const listingsApi = {
uploadMedia: async (
@@ -107,12 +107,12 @@ const listingsApi = {
if (caption) formData.append('caption', caption);
// POST /api/v1/listings/{listingId}/media
// Returns: { mediaId: string; url: string }
// Trả về: { mediaId: string; url: string }
},
};
```
### File Upload Component (for reference)
### Component Tải Tệp Lên (để tham khảo)
```typescript
// apps/web/components/listings/image-upload.tsx
@@ -121,41 +121,41 @@ interface ImageFile {
preview: string;
}
// Usage:
// Cách dùng:
<ImageUpload
images={images}
onChange={(newImages) => setImages(newImages)}
maxFiles={20}
/>
// Validation:
// ✓ Types: JPEG, PNG, WebP
// ✓ Max size: 10MB per file
// ✓ Max count: 20 files
// ✓ Drag & drop support
// ✓ Preview grid with delete button
// Xác thực:
// ✓ Định dạng: JPEG, PNG, WebP
// ✓ Kích thước tối đa: 10MB mỗi tệp
// ✓ Số lượng tối đa: 20 tệp
// ✓ Hỗ trợ kéo và thả
// ✓ Lưới xem trước với nút xóa
```
---
## 🎨 Styling Patterns
## 🎨 Các Mẫu Tạo Kiểu
### Aspect Ratios (Tailwind)
### Tỉ Lệ Khung Hình (Tailwind)
```html
<!-- 16:9 (videos, main images) -->
<!-- 16:9 (video, ảnh chính) -->
<div class="aspect-video"><!-- 1.777:1 --></div>
<!-- 4:3 (property cards) -->
<!-- 4:3 (thẻ bất động sản) -->
<div class="aspect-[4/3]"><!-- 1.333:1 --></div>
<!-- 16:10 (compact property cards) -->
<!-- 16:10 (thẻ bất động sản thu gọn) -->
<div class="aspect-[16/10]"><!-- 1.6:1 --></div>
<!-- Square (thumbnails) -->
<!-- Vuông (hình thu nhỏ) -->
<div class="aspect-square"><!-- 1:1 --></div>
```
### Image Container Pattern
### Mẫu Container Hình Ảnh
```jsx
<div className="relative aspect-video overflow-hidden rounded-lg bg-muted">
<Image
@@ -169,56 +169,56 @@ interface ImageFile {
</div>
```
### Common Tailwind Classes
### Các Class Tailwind Thông Dụng
```
// Layout
aspect-video # 16:9 ratio
aspect-square # 1:1 ratio
relative / absolute # Positioning
fill # Object-fit with aspect ratio
// Bố cục
aspect-video # Tỉ lệ 16:9
aspect-square # Tỉ lệ 1:1
relative / absolute # Định vị
fill # Object-fit với tỉ lệ khung hình
// Styling
object-cover # Image fit (crop to fill)
object-contain # Image fit (preserve ratio)
rounded-lg # Border radius
bg-muted # Placeholder background
// Tạo kiểu
object-cover # Vừa khít ảnh (cắt để lấp đầy)
object-contain # Vừa khít ảnh (giữ nguyên tỉ lệ)
rounded-lg # Bo góc
bg-muted # Nền giữ chỗ
// Interactive
transition-colors # Smooth color changes
group-hover:scale-105 # Hover effect
opacity-70 # Partial transparency
// Tương tác
transition-colors # Chuyển đổi màu mượt mà
group-hover:scale-105 # Hiệu ứng di chuột
opacity-70 # Độ trong suốt một phần
// Overlay
absolute inset-0 # Full coverage overlay
bg-black/50 # Semi-transparent black
hover:bg-black/70 # Darker on hover
// Lớp phủ
absolute inset-0 # Lớp phủ toàn bộ
bg-black/50 # Đen bán trong suốt
hover:bg-black/70 # Tối hơn khi di chuột
```
---
## 🔄 State Management
## 🔄 Quản Lý Trạng Thái
### Zustand Store Pattern
### Mẫu Zustand Store
```typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Simple store
// Store đơn giản
const useMyStore = create<MyState>((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
asyncAction: async () => {
set({ isLoading: true });
// ... async work
// ... công việc bất đồng bộ
set({ data: result, isLoading: false });
},
}));
// Store with persistence
// Store với lưu trữ bền vững
const useComparisonStore = create<State>()(
persist(
(set, get) => ({
// store logic
// logic store
}),
{
name: 'storage-key',
@@ -227,12 +227,12 @@ const useComparisonStore = create<State>()(
)
);
// Usage in component
// Sử dụng trong component
const count = useMyStore((state) => state.count);
const increment = useMyStore((state) => state.increment);
```
### Image Gallery Local State
### Trạng Thái Cục Bộ Thư Viện Ảnh
```typescript
const [selectedIndex, setSelectedIndex] = React.useState(0);
@@ -247,30 +247,30 @@ const handlePrev = () => {
---
## 🧩 UI Component Patterns
## 🧩 Các Mẫu Component UI
### Button Component (with variants)
### Component Button (với các biến thể)
```typescript
// From: apps/web/components/ui/button.tsx
// Từ: apps/web/components/ui/button.tsx
import { Button } from '@/components/ui/button';
// Variants:
<Button variant="default">Default</Button> // Primary
<Button variant="outline">Outline</Button> // Border
<Button variant="secondary">Secondary</Button> // Secondary color
<Button variant="destructive">Delete</Button> // Red
<Button variant="ghost">Ghost</Button> // No background
<Button variant="link">Link</Button> // Text link
// Biến thể:
<Button variant="default">Default</Button> // Chính
<Button variant="outline">Outline</Button> // Viền
<Button variant="secondary">Secondary</Button> // Màu phụ
<Button variant="destructive">Delete</Button> // Đỏ
<Button variant="ghost">Ghost</Button> // Không có nền
<Button variant="link">Link</Button> // Liên kết văn bản
// Sizes:
<Button size="default">Default</Button> // 40px height
<Button size="sm">Small</Button> // 36px height
<Button size="lg">Large</Button> // 44px height
<Button size="icon">Icon</Button> // Square button
// Kích thước:
<Button size="default">Default</Button> // Cao 40px
<Button size="sm">Small</Button> // Cao 36px
<Button size="lg">Large</Button> // Cao 44px
<Button size="icon">Icon</Button> // Nút vuông
```
### Badge Component (with variants)
### Component Badge (với các biến thể)
```typescript
import { Badge } from '@/components/ui/badge';
@@ -283,7 +283,7 @@ import { Badge } from '@/components/ui/badge';
<Badge variant="info">Info</Badge>
```
### Card Component
### Component Card
```typescript
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -292,12 +292,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
{/* content */}
{/* nội dung */}
</CardContent>
</Card>
```
### Dialog/Modal Pattern
### Mẫu Dialog/Modal
```typescript
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
@@ -308,49 +308,49 @@ const [isOpen, setIsOpen] = React.useState(false);
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
</DialogHeader>
{/* content */}
{/* nội dung */}
</DialogContent>
</Dialog>
```
---
## 📱 Responsive Design
## 📱 Thiết Kế Responsive
### Breakpoints
### Các Điểm Ngắt
```css
xs: 0px /* Default */
sm: 640px /* Mobile landscape */
md: 768px /* Tablet */
lg: 1024px /* Desktop */
xl: 1280px /* Wide desktop */
2xl: 1536px /* Ultra-wide */
xs: 0px /* Mặc định */
sm: 640px /* Ngang màn hình điện thoại */
md: 768px /* Máy tính bảng */
lg: 1024px /* Máy tính để bàn */
xl: 1280px /* Màn hình rộng */
2xl: 1536px /* Siêu rộng */
```
### Common Patterns
### Các Mẫu Thông Dụng
```jsx
// Mobile-first
// Ưu tiên mobile
<div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4">
// Conditional display
<div className="hidden md:block">Show on tablet+</div>
<div className="block md:hidden">Show on mobile</div>
// Hiển thị có điều kiện
<div className="hidden md:block">Hiện trên máy tính bảng trở lên</div>
<div className="block md:hidden">Hiện trên điện thoại</div>
// Responsive grid
// Lưới responsive
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
// Responsive text size
// Cỡ chữ responsive
<p className="text-sm md:text-base lg:text-lg">
// Responsive padding
// Padding responsive
<div className="p-4 sm:p-6 md:p-8">
```
---
## 🔗 Common Imports
## 🔗 Các Import Thông Dụng
### Essential Imports
### Import Thiết Yếu
```typescript
// Components
import Image from 'next/image';
@@ -363,7 +363,7 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
// Utilities
// Tiện ích
import { cn } from '@/lib/utils';
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
@@ -379,31 +379,31 @@ import { useCallback, useEffect, useState } from 'react';
---
## 📊 Data Fetching
## 📊 Lấy Dữ Liệu
### Server-side Fetching
### Lấy Dữ Liệu Phía Server
```typescript
// apps/web/lib/listings-server.ts
import { fetchListingById } from '@/lib/listings-server';
// In page.tsx (Server Component)
// Trong page.tsx (Server Component)
const listing = await fetchListingById(params.id);
if (!listing) notFound();
```
### Client-side API
### API Phía Client
```typescript
// apps/web/lib/listings-api.ts
import { listingsApi } from '@/lib/listings-api';
// Usage:
// Cách dùng:
const listing = await listingsApi.getById(id);
const results = await listingsApi.search({ city: 'Ho Chi Minh' });
```
### React Query Usage (likely)
### Sử Dụng React Query (có thể)
```typescript
// Typical pattern for fetching
// Mẫu thông thường để lấy dữ liệu
import { useQuery } from '@tanstack/react-query';
const { data, isLoading, error } = useQuery({
@@ -414,15 +414,15 @@ const { data, isLoading, error } = useQuery({
---
## 🌐 Internationalization
## 🌐 Đa Ngôn Ngữ
### Language Support
- Vietnamese (vi)
- English (en)
### Hỗ Trợ Ngôn Ngữ
- Tiếng Việt (vi)
- Tiếng Anh (en)
### Using i18n
### Sử Dụng i18n
```typescript
// In components, use Vietnamese labels directly or from constants
// 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<string, string> = {
APARTMENT: 'Căn hộ',
HOUSE: 'Nhà riêng',
@@ -430,29 +430,29 @@ const PROPERTY_TYPES: Record<string, string> = {
// ...
};
// From @/lib/validations/listings
// Từ @/lib/validations/listings
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';
```
### Language-aware Routes
### Route Nhận Biết Ngôn Ngữ
```
/vi/listings/123 # Vietnamese
/en/listings/123 # English
/vi/listings/123 # Tiếng Việt
/en/listings/123 # Tiếng Anh
```
---
## 🔐 Security Features
## 🔐 Tính Năng Bảo Mật
### CSP Headers (next.config.js)
### Tiêu Đề CSP (next.config.js)
```javascript
img-src 'self' data: blob: https://*.mapbox.com https://
font-src 'self' data:
```
### Image Domain Whitelist
### Danh Sách Cho Phép Domain Hình Ảnh
```javascript
// Allows HTTPS images from any domain
// Cho phép hình ảnh HTTPS từ bất kỳ domain nào
remotePatterns: [
{ protocol: 'https', hostname: '**' }
]
@@ -460,17 +460,17 @@ remotePatterns: [
---
## 🧪 Testing Considerations
## 🧪 Cân Nhắc Khi Kiểm Thử
### Component Files to Test
- `image-gallery.tsx` - Gallery navigation, state changes
- `image-upload.tsx` - File validation, drag-drop
- `property-card.tsx` - Image display, responsive
- `listing-detail-client.tsx` - Overall page functionality
### 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
### Test Patterns
### Các Mẫu Kiểm Thử
```typescript
// Mock Next.js Image component
// Mock component Next.js Image
jest.mock('next/image', () => ({
__esModule: true,
default: (props) => <img {...props} />,
@@ -484,23 +484,23 @@ jest.mock('@/lib/auth-store', () => ({
---
## 🚀 Performance Optimization Tips
## 🚀 Mẹo Tối Ưu Hiệu Suất
1. **Image Priority**
1. **Ưu Tiên Hình Ảnh**
```typescript
priority={selectedIndex === 0} // First image loads with page
priority={selectedIndex === 0} // Ảnh đầu tiên tải cùng trang
```
2. **Responsive Sizes**
2. **Kích Thước Responsive**
```typescript
sizes="(max-width: 768px) 100vw, 60vw" // Tells browser image width
sizes="(max-width: 768px) 100vw, 60vw" // Cho trình duyệt biết chiều rộng ảnh
```
3. **Lazy Loading**
- Thumbnails load on demand (no priority set)
- Reduces initial page weight
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 Imports**
4. **Dynamic Import**
```typescript
const ListingMap = dynamic(() => import('@/components/map/listing-map'), {
ssr: false,
@@ -508,7 +508,7 @@ jest.mock('@/lib/auth-store', () => ({
});
```
5. **Object URLs Cleanup**
5. **Dọn Dẹp Object URL**
```typescript
React.useEffect(() => {
return () => {
@@ -519,65 +519,64 @@ jest.mock('@/lib/auth-store', () => ({
---
## 📋 Common Tasks
## 📋 Các Tác Vụ Thông Dụng
### Add a New UI Element
1. Create in `components/ui/ComponentName.tsx`
2. Use CVA for variants
3. Export from the same file
4. Import and use in feature components
### 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
### Add a New Feature Component
1. Create in `components/feature-name/ComponentName.tsx`
2. Make 'use client' if interactive
3. Import UI components
4. Use Zustand stores if needed global state
5. Use local state for UI state
### 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
### Modify Image Gallery
1. Edit `components/listings/image-gallery.tsx`
2. Update PropertyMedia interface if needed (in `lib/listings-api.ts`)
3. Adjust aspect ratio / sizes as needed
4. Test responsive behavior
### 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
### Add Image Lightbox
1. Choose library (embla-carousel, yet-another-react-lightbox, etc.)
2. Install: `pnpm add package-name -F @goodgo/web`
3. Create wrapper component in `components/listings/image-lightbox.tsx`
4. Integrate with `image-gallery.tsx`
5. Test with multiple images
### 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
---
## 🐛 Common Issues & Solutions
## 🐛 Các Vấn Đề Thường Gặp & Giải Pháp
### Image Not Loading
- Check URL is valid and HTTPS
- Verify domain in `remotePatterns`
- Check CSP headers allow the domain
### 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 đó
### Gallery Navigation Frozen
- Check `selectedIndex` state updates
- Verify onClick handlers are properly bound
- Check for JavaScript errors in console
### Đ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
### Thumbnail Scroll Issues
- Ensure parent container has `overflow-x-auto`
- Check flex properties on thumbnails
- Verify width constraints (flex-shrink-0)
### Vấn Đề Cuộn Hình Thu Nhỏ
- Đảm bảo container cha `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)
### Layout Shifting on Image Load
- Use aspect ratio container
- Set explicit width/height
- Use `fill` layout with container
### 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
---
## 📚 Additional Resources
## 📚 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