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,6 +1,6 @@
# Property Detail Page - Component Map & Architecture Diagram
# Trang Chi Tiết Bất Động Sản - Bản Đồ Component & Sơ Đồ Kiến Trúc
## 🎯 Page Component Hierarchy
## 🎯 Phân Cấp Component Của Trang
```
PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/page.tsx]
@@ -21,7 +21,7 @@ PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/p
│ ├─ Price Display
│ └─ AddToCompareButton
├─ ImageGallery [MAIN FEATURE]
├─ ImageGallery [TÍNH NĂNG CHÍNH]
│ [apps/web/components/listings/image-gallery.tsx]
│ ├─ Main Image Display
│ │ ├─ Previous Button
@@ -59,9 +59,9 @@ PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/p
│ │ ├─ InfoItem (Legal Status)
│ │ └─ InfoItem (Project)
│ │
│ ├─ Amenities Card (conditional)
│ ├─ Amenities Card (có điều kiện)
│ │ ├─ CardHeader
│ │ └─ Badge(s) for each amenity
│ │ └─ Badge(s) cho mỗi tiện ích
│ │
│ └─ Map Card
│ ├─ CardHeader
@@ -74,7 +74,7 @@ PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/p
│ ├─ Seller Phone
│ ├─ Call Button
│ ├─ Message Button
│ └─ Agent Info (conditional)
│ └─ Agent Info (có điều kiện)
├─ AiEstimateButton
@@ -87,9 +87,9 @@ PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/p
---
## 🖼️ Image Gallery Component Details
## 🖼️ Chi Tiết Component Thư Viện Ảnh
### File: `apps/web/components/listings/image-gallery.tsx`
### Tệp: `apps/web/components/listings/image-gallery.tsx`
```
ImageGallery (Client Component)
@@ -110,12 +110,12 @@ ImageGallery (Client Component)
│ │ ├─ Overlay: Next Button
│ │ └─ Overlay: Counter Badge
│ │
│ └─ Thumbnail Container (if images.length > 1)
│ └─ Thumbnail Container (nếu images.length > 1)
│ ├─ className: "flex gap-2 overflow-x-auto"
│ └─ ThumbnailButton (for each image)
│ └─ ThumbnailButton (cho mỗi ảnh)
│ ├─ Image (Next.js)
│ ├─ Border: selected ? primary : transparent
│ ├─ Opacity: 70% (unselected)
│ ├─ Opacity: 70% (chưa được chọn)
│ └─ Hover: opacity 100%
└─ Handlers:
@@ -124,26 +124,26 @@ ImageGallery (Client Component)
└─ handleSelectIndex(index)
```
### Data Flow
### Luồng Dữ Liệu
```
Property.media (from API)
Property.media (từ API)
Filter by type === 'image'
Lọc theo type === 'image'
Sort by order property
Sắp xếp theo thuộc tính order
[selectedIndex, setSelectedIndex] → selectedIndex
Image.url at index → Main Display
All images → Thumbnails
selectedIndex → Highlighted Thumbnail
Image.url tại index → Hiển Thị Chính
Tất cả ảnh → Thumbnails
selectedIndex → Thumbnail Được Làm Nổi Bật
```
---
## 📱 Image Upload Component
## 📱 Component Tải Lên Ảnh
### File: `apps/web/components/listings/image-upload.tsx`
### Tệp: `apps/web/components/listings/image-upload.tsx`
```
ImageUpload (Client Component)
@@ -151,13 +151,13 @@ ImageUpload (Client Component)
├─ Props:
│ ├─ images: ImageFile[]
│ ├─ onChange: (images: ImageFile[]) => void
│ ├─ maxFiles?: number (default: 20)
│ ├─ maxFiles?: number (mặc định: 20)
│ └─ className?: string
├─ State:
│ └─ isDragging: boolean
├─ Drag & Drop Zone
├─ Vùng Kéo & Thả
│ ├─ onDragOver → setIsDragging(true)
│ ├─ onDragLeave → setIsDragging(false)
│ ├─ onDrop → addFiles()
@@ -168,33 +168,33 @@ ImageUpload (Client Component)
│ ├─ multiple: true
│ └─ hidden: true
└─ Preview Grid
└─ For each image:
├─ Image preview (URL.createObjectURL)
├─ Cover badge (first image)
├─ Delete button on hover
└─ Cleanup on unmount (URL.revokeObjectURL)
└─ Lưới Xem Trước
└─ Cho mỗi ảnh:
├─ Xem trước ảnh (URL.createObjectURL)
├─ Huy hiệu ảnh bìa (ảnh đầu tiên)
├─ Nút xóa khi di chuột qua
└─ Dọn dẹp khi unmount (URL.revokeObjectURL)
Validation:
├─ Allowed types: JPEG, PNG, WebP
├─ Max size: 10MB per file
└─ Max count: 20 files
Kiểm Tra Hợp Lệ:
├─ Các loại được phép: JPEG, PNG, WebP
├─ Kích thước tối đa: 10MB mỗi tệp
└─ Số lượng tối đa: 20 tệp
```
---
## 🧩 Related Components
## 🧩 Các Component Liên Quan
### SearchResults & PropertyCard
```
SearchResults
└─ Grid of PropertyCards
└─ Lưới PropertyCards
└─ PropertyCard (for each listing)
└─ PropertyCard (cho mỗi listing)
├─ Link to /listings/{id}
├─ Link đến /listings/{id}
└─ Card
├─ Image Container
@@ -208,17 +208,17 @@ SearchResults
├─ Price
├─ Title
├─ Location
└─ Badges (Area, Bedrooms, etc.)
└─ Badges (Area, Bedrooms, v.v.)
```
---
## 🌐 Data Flow & API Mapping
## 🌐 Luồng Dữ Liệu & Ánh Xạ API
### Server to Client Flow
### Luồng Từ Server Đến Client
```
1. Browser Request
1. Yêu Cầu Trình Duyệt
URL: /vi/listings/abc123
@@ -231,39 +231,39 @@ SearchResults
│ ListingDetail {
│ id: string
│ property: {
│ media: PropertyMedia[] ← Images
│ media: PropertyMedia[] ← Ảnh
│ }
│ seller: {...}
│ agent: {...}
│ }
├─ generateMetadata()
│ └─ Uses property.media[0] for OG image
│ └─ Sử dụng property.media[0] cho ảnh OG
├─ generateJsonLd()
│ └─ Structured data for SEO
│ └─ Dữ liệu có cấu trúc cho SEO
└─ <ListingDetailClient listing={data} />
3. Client Component (Hydrated)
3. Client Component (Đã Hydrate)
├─ <ImageGallery media={property.media} />
│ └─ Local state: selectedIndex
│ └─ State cục bộ: selectedIndex
└─ Other interactive components
└─ Các component tương tác khác
```
---
## 🎨 Styling Architecture
## 🎨 Kiến Trúc Giao Diện
### Tailwind CSS Structure
### Cấu Trúc Tailwind CSS
```
Root CSS Variables (globals.css)
Biến CSS Gốc (globals.css)
├─ Colors (HSL format)
├─ Màu sắc (định dạng HSL)
│ ├─ --primary
│ ├─ --secondary
│ ├─ --background
@@ -272,59 +272,59 @@ Root CSS Variables (globals.css)
│ ├─ --accent
│ └─ --card
├─ Spacing
│ └─ Uses standard Tailwind scale
├─ Khoảng cách
│ └─ Sử dụng thang chuẩn Tailwind
└─ Radius
└─ Bo góc
└─ --radius
Tailwind Config (tailwind.config.ts)
├─ Extends theme
│ ├─ Colors mapped from CSS variables
│ └─ Border radius configuration
├─ Mở rộng theme
│ ├─ Màu sắc ánh xạ từ biến CSS
│ └─ Cấu hình bo góc viền
└─ Plugins
└─ tailwindcss-animate
```
### Component-Level Patterns
### Các Mẫu Ở Cấp Component
```
ui/button.tsx
├─ CVA (Class Variance Authority)
│ ├─ Base classes
│ ├─ Variants
│ │ ├─ variant: default, outline, ghost, etc.
│ ├─ Các class cơ sở
│ ├─ Biến thể
│ │ ├─ variant: default, outline, ghost, v.v.
│ │ └─ size: sm, default, lg, icon
│ └─ defaultVariants
└─ Usage: <Button variant="default" size="lg" />
└─ Sử dụng: <Button variant="default" size="lg" />
ui/badge.tsx
├─ CVA variants
├─ Biến thể CVA
│ ├─ default (primary)
│ ├─ secondary
│ ├─ outline
│ └─ ...colors (success, warning, info)
│ └─ ...màu sắc (success, warning, info)
└─ Usage: <Badge variant="secondary">Text</Badge>
└─ Sử dụng: <Badge variant="secondary">Text</Badge>
```
---
## 📊 State Management Patterns
## 📊 Các Mẫu Quản Lý State
### Gallery Local State
### State Cục Bộ Của Gallery
```
Component: ImageGallery
State: selectedIndex (number)
├─ Initialize: 0
├─ Update on: prev, next, thumbnail click
└─ Use: Display main image, highlight thumbnail
├─ Khởi tạo: 0
├─ Cập nhật khi: prev, next, nhấn vào thumbnail
└─ Sử dụng: Hiển thị ảnh chính, làm nổi bật thumbnail
```
### Global State (Zustand)
### State Toàn Cục (Zustand)
```
Auth Store
├─ user: UserProfile | null
@@ -334,30 +334,30 @@ Auth Store
└─ Actions: login, logout, fetchProfile
Comparison Store
├─ selectedIds: string[] (persisted)
├─ selectedIds: string[] (được lưu trữ)
├─ listings: ListingDetail[]
├─ isLoading: boolean
├─ error: string | null
└─ Actions: addToCompare, removeFromCompare, etc.
└─ Actions: addToCompare, removeFromCompare, v.v.
```
---
## 🔗 Import Map
## 🔗 Bản Đồ Import
### File Structure References
### Tham Chiếu Cấu Trúc Tệp
```
apps/web/
├─ app/[locale]/(public)/listings/[id]/
│ └─ page.tsx ──────────────────────────── ENTRY POINT
│ └─ page.tsx ──────────────────────────── ĐIỂM VÀO
│ │
│ └─ imports:
│ ├─ ListingDetailClient
│ ├─ JsonLd
│ ├─ fetchListingById
│ └─ formatting utilities
│ └─ các tiện ích định dạng
├─ components/
│ │
@@ -368,10 +368,10 @@ apps/web/
│ │ │ ├─ imports: AiEstimateButton
│ │ │ └─ dynamic: ListingMap
│ │ │
│ │ ├─ image-gallery.tsx ───────────────── MAIN IMAGE DISPLAY
│ │ ├─ image-gallery.tsx ───────────────── HIỂN THỊ ẢNH CHÍNH
│ │ │ └─ imports: Next.js Image
│ │ │
│ │ └─ image-upload.tsx ──────────────────── FILE UPLOAD
│ │ └─ image-upload.tsx ──────────────────── TẢI LÊN TỆP
│ │ └─ imports: Button component
│ │
│ ├─ ui/
@@ -379,14 +379,14 @@ apps/web/
│ │ ├─ badge.tsx
│ │ ├─ card.tsx
│ │ ├─ dialog.tsx
│ │ └─ ...other UI components
│ │ └─ ...các component UI khác
│ │
│ └─ search/
│ └─ property-card.tsx ──────────────── THUMBNAIL VIEW
│ └─ property-card.tsx ──────────────── DẠNG XEM THUMBNAIL
│ └─ imports: ImageGallery pattern
└─ lib/
├─ listings-api.ts ────────────────────── API TYPES & FUNCTIONS
├─ listings-api.ts ────────────────────── KIỂU DỮ LIỆU & HÀM API
│ └─ PropertyMedia interface
│ └─ ListingDetail interface
@@ -397,205 +397,205 @@ apps/web/
---
## 📈 Component Complexity Levels
## 📈 Các Mức Độ Phức Tạp Của Component
### Level 1: Simple UI Components
### Cấp 1: Các Component UI Đơn Giản
```
Badge, Button, Input, Label
├─ Props: basic props + variants
├─ State: none
└─ Interactions: click, focus, hover
├─ Props: props cơ bản + biến thể
├─ State: không có
└─ Tương tác: click, focus, hover
```
### Level 2: Composite Components
### Cấp 2: Các Component Tổng Hợp
```
Card (Header + Content + Footer)
ImageUpload (Drag-drop + Preview grid)
├─ Props: content children
├─ State: local state
└─ Interactions: click, drag, input
ImageUpload (Kéo-thả + Lưới xem trước)
├─ Props: nội dung children
├─ State: state cục bộ
└─ Tương tác: click, kéo, nhập liệu
```
### Level 3: Feature Components
### Cấp 3: Các Component Tính Năng
```
ImageGallery (Main + Thumbnails)
ImageGallery (Chính + Thumbnails)
PropertyCard (Link + Image + Info)
├─ Props: data + callbacks
├─ State: selected index, UI state
└─ Interactions: navigation, filtering
├─ Props: dữ liệu + callbacks
├─ State: chỉ mục được chọn, trạng thái UI
└─ Tương tác: điều hướng, lọc
```
### Level 4: Page Components
### Cấp 4: Các Component Trang
```
ListingDetailClient (Full page layout)
├─ Props: listing data
├─ State: multiple features
├─ Interactions: all user interactions
└─ Children: multiple feature components
ListingDetailClient (Bố cục trang đầy đủ)
├─ Props: dữ liệu listing
├─ State: nhiều tính năng
├─ Tương tác: tất cả tương tác người dùng
└─ Children: nhiều component tính năng
```
---
## 🚀 Performance Considerations
## 🚀 Cân Nhắc Về Hiệu Suất
### Image Optimization
### Tối Ưu Hóa Ảnh
```
Image Component Strategy:
Chiến Lược Component Ảnh:
├─ Next.js Image component
│ ├─ Automatic format selection (WebP, AVIF)
│ ├─ Responsive serving via srcset
│ └─ On-demand resizing
│ ├─ Tự động chọn định dạng (WebP, AVIF)
│ ├─ Phục vụ đáp ứng qua srcset
│ └─ Thay đổi kích thước theo yêu cầu
├─ Lazy Loading
│ ├─ Main image: priority={selectedIndex === 0}
│ └─ Thumbnails: no priority (lazy)
│ ├─ Ảnh chính: priority={selectedIndex === 0}
│ └─ Thumbnails: không có priority (lazy)
└─ Responsive Sizing
├─ sizes prop: tells browser image dimensions
└─ Prevents layout shift
└─ Kích Thước Đáp Ứng
├─ Thuộc tính sizes: cho trình duyệt biết kích thước ảnh
└─ Ngăn chặn thay đổi bố cục
```
### Code Splitting
### Phân Tách Code
```
Dynamic Imports:
├─ ListingMap (heavy, maps library)
│ ├─ ssr: false (client-only)
│ └─ loading: placeholder component
├─ ListingMap (nặng, thư viện bản đồ)
│ ├─ ssr: false (chỉ dành cho client)
│ └─ loading: component giữ chỗ
└─ Other components: bundled with page
└─ Các component khác: đóng gói cùng trang
```
### State Optimization
### Tối Ưu Hóa State
```
Zustand:
├─ Selector pattern: useStore(state => state.field)
├─ Only re-render on selected state change
└─ Persist middleware: localStorage only on needed data
├─ Mẫu Selector: useStore(state => state.field)
├─ Chỉ render lại khi state được chọn thay đổi
└─ Middleware Persist: localStorage chỉ cho dữ liệu cần thiết
```
---
## 🔄 Navigation Flow
## 🔄 Luồng Điều Hướng
### User Journey - Image Gallery
### Hành Trình Người Dùng - Thư Viện Ảnh
```
1. User visits /vi/listings/123
└─ Page loads with first image (priority=true)
1. Người dùng truy cập /vi/listings/123
└─ Trang tải với ảnh đầu tiên (priority=true)
2. User interacts with gallery:
├─ Click thumbnail
│ └─ setSelectedIndex(index) → main image updates
2. Người dùng tương tác với gallery:
├─ Nhấn vào thumbnail
│ └─ setSelectedIndex(index) → ảnh chính cập nhật
├─ Click next button
│ └─ setSelectedIndex(i + 1) → wraps to 0
├─ Nhấn nút tiếp theo
│ └─ setSelectedIndex(i + 1) → quay về 0
└─ Click prev button
└─ setSelectedIndex(i - 1) → wraps to last
└─ Nhấn nút trước đó
└─ setSelectedIndex(i - 1) → quay về cuối
```
### User Journey - File Upload (Create/Edit Listing)
### Hành Trình Người Dùng - Tải Lên Tệp (Tạo/Chỉnh Sửa Listing)
```
1. User opens listing form
└─ ImageUpload component mounts
1. Người dùng mở biểu mẫu listing
└─ Component ImageUpload được gắn kết
2. User adds images:
├─ Drag & drop files
│ └─ addFiles() → filter + validate → onChange()
2. Người dùng thêm ảnh:
├─ Kéo & thả tệp
│ └─ addFiles() → lọc + kiểm tra → onChange()
└─ Click to browse
└─ Select files → same as drag & drop
└─ Nhấn để duyệt
└─ Chọn tệp → giống như kéo & thả
3. User removes image:
└─ removeImage(index) → cleanup URL → onChange()
3. Người dùng xóa ảnh:
└─ removeImage(index) → dọn dẹp URL → onChange()
4. User submits form:
└─ Images uploaded via listingsApi.uploadMedia()
4. Người dùng gửi biểu mẫu:
└─ Ảnh được tải lên qua listingsApi.uploadMedia()
```
---
## 📋 Component Checklist
## 📋 Danh Sách Kiểm Tra Component
### Image Gallery Features
- [x] Main image display (responsive)
- [x] Previous/Next navigation
- [x] Image counter badge
- [x] Thumbnail navigation (scrollable)
- [x] Selected thumbnail highlighting
- [x] Empty state fallback
- [ ] Lightbox/modal zoom (NOT implemented)
- [ ] Keyboard navigation (NOT implemented)
- [ ] Touch gestures (NOT implemented)
### Tính Năng Thư Viện Ảnh
- [x] Hiển thị ảnh chính (đáp ứng)
- [x] Điều hướng Trước/Tiếp theo
- [x] Huy hiệu đếm ảnh
- [x] Điều hướng thumbnail (có thể cuộn)
- [x] Làm nổi bật thumbnail được chọn
- [x] Trạng thái dự phòng khi trống
- [ ] Phóng to lightbox/modal (CHƯA triển khai)
- [ ] Điều hướng bàn phím (CHƯA triển khai)
- [ ] Cử chỉ chạm (CHƯA triển khai)
### Image Upload Features
- [x] Drag & drop
- [x] Click to browse
- [x] File type validation
- [x] File size validation
- [x] Preview grid
- [x] Delete button
- [x] Cover photo indicator
- [x] URL cleanup on unmount
- [ ] Progress bar (NOT implemented)
- [ ] Multiple upload progress (NOT implemented)
### Tính Năng Tải Lên Ảnh
- [x] Kéo & thả
- [x] Nhấn để duyệt
- [x] Kiểm tra loại tệp
- [x] Kiểm tra kích thước tệp
- [x] Lưới xem trước
- [x] Nút xóa
- [x] Chỉ báo ảnh bìa
- [x] Dọn dẹp URL khi unmount
- [ ] Thanh tiến trình (CHƯA triển khai)
- [ ] Tiến trình tải lên nhiều tệp (CHƯA triển khai)
### SEO & Metadata
- [x] Open Graph image
- [x] Twitter Card image
- [x] JSON-LD schema
- [x] Canonical URL
- [x] Alternate language links
- [x] Descriptive alt text
- [x] Ảnh Open Graph
- [x] Ảnh Twitter Card
- [x] Schema JSON-LD
- [x] URL Canonical
- [x] Liên kết ngôn ngữ thay thế
- [x] Văn bản alt mô tả
---
## 🛠️ Maintenance Guide
## 🛠️ Hướng Dẫn Bảo Trì
### Adding New Image Features
### Thêm Tính Năng Ảnh Mới
**Add to ImageGallery:**
1. Define new prop in interface
2. Update state if needed
3. Update render logic
4. Test responsive behavior
5. Update TypeScript types
**Thêm vào ImageGallery:**
1. Định nghĩa prop mới trong interface
2. Cập nhật state nếu cần
3. Cập nhật logic render
4. Kiểm tra hành vi đáp ứng
5. Cập nhật các kiểu TypeScript
**Example - Add zoom feature:**
**Ví dụ - Thêm tính năng zoom:**
```typescript
interface ImageGalleryProps {
media: PropertyMedia[];
className?: string;
onImageClick?: (index: number) => void; // NEW
onImageClick?: (index: number) => void; // MỚI
}
// In component:
const [isZoomed, setIsZoomed] = useState(false); // NEW
// Trong component:
const [isZoomed, setIsZoomed] = useState(false); // MỚI
<Image
onClick={() => setIsZoomed(true)} // NEW
cursor={isZoomed ? 'zoom-out' : 'zoom-in'} // NEW
onClick={() => setIsZoomed(true)} // MỚI
cursor={isZoomed ? 'zoom-out' : 'zoom-in'} // MỚI
/>
```
### Updating Image Data Structure
### Cập Nhật Cấu Trúc Dữ Liệu Ảnh
**If PropertyMedia changes:**
1. Update interface in `lib/listings-api.ts`
2. Update API response mapping
3. Update gallery component to use new fields
4. Update tests
5. Update API documentation
**Nếu PropertyMedia thay đổi:**
1. Cập nhật interface trong `lib/listings-api.ts`
2. Cập nhật ánh xạ phản hồi API
3. Cập nhật component gallery để sử dụng các trường mới
4. Cập nhật các bài kiểm thử
5. Cập nhật tài liệu API
---
## 📚 Related Documentation
## 📚 Tài Liệu Liên Quan
See also:
- `PROPERTY_DETAIL_PAGE_ANALYSIS.md` - Comprehensive analysis
- `PROPERTY_DETAIL_QUICK_REFERENCE.md` - Code snippets & patterns
Xem thêm:
- `PROPERTY_DETAIL_PAGE_ANALYSIS.md` - Phân tích toàn diện
- `PROPERTY_DETAIL_QUICK_REFERENCE.md` - Đoạn code & các mẫu