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
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:
@@ -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 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)
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user