Files
goodgo-platform/docs/audits/PROPERTY_DETAIL_COMPONENTS_MAP.md
Ho Ngoc Hai b8512ebff4 docs: consolidate audit and analysis reports into docs/audits/
Move 36 root-level audit/analysis documents and 7 web app audit documents
into docs/audits/ directory to declutter the project root. Remove stale
EXPLORATION_SUMMARY.txt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:37:50 +07:00

602 lines
14 KiB
Markdown

# Property Detail Page - Component Map & Architecture Diagram
## 🎯 Page Component Hierarchy
```
PublicListingDetailPage (Server) [apps/web/app/[locale]/(public)/listings/[id]/page.tsx]
├─ JSON-LD Structured Data
│ ├─ JsonLd (Breadcrumb)
│ └─ JsonLd (Listing Schema)
└─ ListingDetailClient (Client) [apps/web/components/listings/listing-detail-client.tsx]
├─ Breadcrumb Navigation
│ └─ Link components
├─ Header Section
│ ├─ Title & Description
│ ├─ Badge (SALE/RENT)
│ ├─ Badge (Property Type)
│ ├─ Price Display
│ └─ AddToCompareButton
├─ ImageGallery [MAIN FEATURE]
│ [apps/web/components/listings/image-gallery.tsx]
│ ├─ Main Image Display
│ │ ├─ Previous Button
│ │ ├─ Image (Next.js Image)
│ │ ├─ Next Button
│ │ └─ Counter Badge
│ │
│ └─ Thumbnail Navigation
│ ├─ Thumbnail Item 1
│ ├─ Thumbnail Item 2
│ └─ Thumbnail Item N
├─ Quick Stats Bar
│ ├─ QuickStat (Area)
│ ├─ QuickStat (Bedrooms)
│ ├─ QuickStat (Bathrooms)
│ ├─ QuickStat (Floors)
│ └─ QuickStat (Direction)
├─ Main Content (2/3 width - lg:col-span-2)
│ │
│ ├─ Description Card
│ │ ├─ CardHeader
│ │ └─ CardContent
│ │
│ ├─ Details Card
│ │ ├─ CardHeader
│ │ ├─ InfoItem (Property Type)
│ │ ├─ InfoItem (Area)
│ │ ├─ InfoItem (Bedrooms)
│ │ ├─ InfoItem (Bathrooms)
│ │ ├─ InfoItem (Floors)
│ │ ├─ InfoItem (Direction)
│ │ ├─ InfoItem (Year Built)
│ │ ├─ InfoItem (Legal Status)
│ │ └─ InfoItem (Project)
│ │
│ ├─ Amenities Card (conditional)
│ │ ├─ CardHeader
│ │ └─ Badge(s) for each amenity
│ │
│ └─ Map Card
│ ├─ CardHeader
│ └─ ListingMap (dynamic import)
└─ Sidebar (1/3 width - sticky)
├─ Contact Card (sticky)
│ ├─ Seller Avatar & Name
│ ├─ Seller Phone
│ ├─ Call Button
│ ├─ Message Button
│ └─ Agent Info (conditional)
├─ AiEstimateButton
└─ Stats Card
├─ View Count
├─ Save Count
├─ Inquiry Count
└─ Published Date
```
---
## 🖼️ Image Gallery Component Details
### File: `apps/web/components/listings/image-gallery.tsx`
```
ImageGallery (Client Component)
├─ Props:
│ ├─ media: PropertyMedia[]
│ └─ className?: string
├─ State:
│ └─ selectedIndex: number
├─ Layout:
│ │
│ ├─ Main Image Container
│ │ ├─ className: "aspect-video"
│ │ ├─ Image (Next.js)
│ │ ├─ Overlay: Previous Button
│ │ ├─ Overlay: Next Button
│ │ └─ Overlay: Counter Badge
│ │
│ └─ Thumbnail Container (if images.length > 1)
│ ├─ className: "flex gap-2 overflow-x-auto"
│ └─ ThumbnailButton (for each image)
│ ├─ Image (Next.js)
│ ├─ Border: selected ? primary : transparent
│ ├─ Opacity: 70% (unselected)
│ └─ Hover: opacity 100%
└─ Handlers:
├─ handlePrev()
├─ handleNext()
└─ handleSelectIndex(index)
```
### Data Flow
```
Property.media (from API)
Filter by type === 'image'
Sort by order property
[selectedIndex, setSelectedIndex] → selectedIndex
Image.url at index → Main Display
All images → Thumbnails
selectedIndex → Highlighted Thumbnail
```
---
## 📱 Image Upload Component
### File: `apps/web/components/listings/image-upload.tsx`
```
ImageUpload (Client Component)
├─ Props:
│ ├─ images: ImageFile[]
│ ├─ onChange: (images: ImageFile[]) => void
│ ├─ maxFiles?: number (default: 20)
│ └─ className?: string
├─ State:
│ └─ isDragging: boolean
├─ Drag & Drop Zone
│ ├─ onDragOver → setIsDragging(true)
│ ├─ onDragLeave → setIsDragging(false)
│ ├─ onDrop → addFiles()
│ └─ onClick → inputRef.click()
├─ File Input
│ ├─ accept: "image/jpeg,image/png,image/webp"
│ ├─ 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)
Validation:
├─ Allowed types: JPEG, PNG, WebP
├─ Max size: 10MB per file
└─ Max count: 20 files
```
---
## 🧩 Related Components
### SearchResults & PropertyCard
```
SearchResults
└─ Grid of PropertyCards
└─ PropertyCard (for each listing)
├─ Link to /listings/{id}
└─ Card
├─ Image Container
│ ├─ Image (media[0])
│ ├─ Badge: Transaction Type (overlay)
│ ├─ Badge: Property Type (overlay)
│ ├─ AddToCompareButton (overlay)
│ └─ Badge: Media Count (bottom-right)
└─ Content
├─ Price
├─ Title
├─ Location
└─ Badges (Area, Bedrooms, etc.)
```
---
## 🌐 Data Flow & API Mapping
### Server to Client Flow
```
1. Browser Request
URL: /vi/listings/abc123
2. Next.js Route Handler
[locale]/[id]/page.tsx (Server Component)
├─ fetchListingById('abc123')
│ └─ API: GET /api/v1/listings/abc123
│ ↓
│ ListingDetail {
│ id: string
│ property: {
│ media: PropertyMedia[] ← Images
│ }
│ seller: {...}
│ agent: {...}
│ }
├─ generateMetadata()
│ └─ Uses property.media[0] for OG image
├─ generateJsonLd()
│ └─ Structured data for SEO
└─ <ListingDetailClient listing={data} />
3. Client Component (Hydrated)
├─ <ImageGallery media={property.media} />
│ └─ Local state: selectedIndex
└─ Other interactive components
```
---
## 🎨 Styling Architecture
### Tailwind CSS Structure
```
Root CSS Variables (globals.css)
├─ Colors (HSL format)
│ ├─ --primary
│ ├─ --secondary
│ ├─ --background
│ ├─ --foreground
│ ├─ --muted
│ ├─ --accent
│ └─ --card
├─ Spacing
│ └─ Uses standard Tailwind scale
└─ Radius
└─ --radius
Tailwind Config (tailwind.config.ts)
├─ Extends theme
│ ├─ Colors mapped from CSS variables
│ └─ Border radius configuration
└─ Plugins
└─ tailwindcss-animate
```
### Component-Level Patterns
```
ui/button.tsx
├─ CVA (Class Variance Authority)
│ ├─ Base classes
│ ├─ Variants
│ │ ├─ variant: default, outline, ghost, etc.
│ │ └─ size: sm, default, lg, icon
│ └─ defaultVariants
└─ Usage: <Button variant="default" size="lg" />
ui/badge.tsx
├─ CVA variants
│ ├─ default (primary)
│ ├─ secondary
│ ├─ outline
│ └─ ...colors (success, warning, info)
└─ Usage: <Badge variant="secondary">Text</Badge>
```
---
## 📊 State Management Patterns
### Gallery Local State
```
Component: ImageGallery
State: selectedIndex (number)
├─ Initialize: 0
├─ Update on: prev, next, thumbnail click
└─ Use: Display main image, highlight thumbnail
```
### Global State (Zustand)
```
Auth Store
├─ user: UserProfile | null
├─ isAuthenticated: boolean
├─ isLoading: boolean
├─ error: string | null
└─ Actions: login, logout, fetchProfile
Comparison Store
├─ selectedIds: string[] (persisted)
├─ listings: ListingDetail[]
├─ isLoading: boolean
├─ error: string | null
└─ Actions: addToCompare, removeFromCompare, etc.
```
---
## 🔗 Import Map
### File Structure References
```
apps/web/
├─ app/[locale]/(public)/listings/[id]/
│ └─ page.tsx ──────────────────────────── ENTRY POINT
│ │
│ └─ imports:
│ ├─ ListingDetailClient
│ ├─ JsonLd
│ ├─ fetchListingById
│ └─ formatting utilities
├─ components/
│ │
│ ├─ listings/
│ │ ├─ listing-detail-client.tsx ────────── CLIENT COMPONENT
│ │ │ ├─ imports: ImageGallery
│ │ │ ├─ imports: AddToCompareButton
│ │ │ ├─ imports: AiEstimateButton
│ │ │ └─ dynamic: ListingMap
│ │ │
│ │ ├─ image-gallery.tsx ───────────────── MAIN IMAGE DISPLAY
│ │ │ └─ imports: Next.js Image
│ │ │
│ │ └─ image-upload.tsx ──────────────────── FILE UPLOAD
│ │ └─ imports: Button component
│ │
│ ├─ ui/
│ │ ├─ button.tsx
│ │ ├─ badge.tsx
│ │ ├─ card.tsx
│ │ ├─ dialog.tsx
│ │ └─ ...other UI components
│ │
│ └─ search/
│ └─ property-card.tsx ──────────────── THUMBNAIL VIEW
│ └─ imports: ImageGallery pattern
└─ lib/
├─ listings-api.ts ────────────────────── API TYPES & FUNCTIONS
│ └─ PropertyMedia interface
│ └─ ListingDetail interface
├─ auth-store.ts
├─ comparison-store.ts
└─ utils.ts
```
---
## 📈 Component Complexity Levels
### Level 1: Simple UI Components
```
Badge, Button, Input, Label
├─ Props: basic props + variants
├─ State: none
└─ Interactions: click, focus, hover
```
### Level 2: Composite Components
```
Card (Header + Content + Footer)
ImageUpload (Drag-drop + Preview grid)
├─ Props: content children
├─ State: local state
└─ Interactions: click, drag, input
```
### Level 3: Feature Components
```
ImageGallery (Main + Thumbnails)
PropertyCard (Link + Image + Info)
├─ Props: data + callbacks
├─ State: selected index, UI state
└─ Interactions: navigation, filtering
```
### Level 4: Page Components
```
ListingDetailClient (Full page layout)
├─ Props: listing data
├─ State: multiple features
├─ Interactions: all user interactions
└─ Children: multiple feature components
```
---
## 🚀 Performance Considerations
### Image Optimization
```
Image Component Strategy:
├─ Next.js Image component
│ ├─ Automatic format selection (WebP, AVIF)
│ ├─ Responsive serving via srcset
│ └─ On-demand resizing
├─ Lazy Loading
│ ├─ Main image: priority={selectedIndex === 0}
│ └─ Thumbnails: no priority (lazy)
└─ Responsive Sizing
├─ sizes prop: tells browser image dimensions
└─ Prevents layout shift
```
### Code Splitting
```
Dynamic Imports:
├─ ListingMap (heavy, maps library)
│ ├─ ssr: false (client-only)
│ └─ loading: placeholder component
└─ Other components: bundled with page
```
### State Optimization
```
Zustand:
├─ Selector pattern: useStore(state => state.field)
├─ Only re-render on selected state change
└─ Persist middleware: localStorage only on needed data
```
---
## 🔄 Navigation Flow
### User Journey - Image Gallery
```
1. User visits /vi/listings/123
└─ Page loads with first image (priority=true)
2. User interacts with gallery:
├─ Click thumbnail
│ └─ setSelectedIndex(index) → main image updates
├─ Click next button
│ └─ setSelectedIndex(i + 1) → wraps to 0
└─ Click prev button
└─ setSelectedIndex(i - 1) → wraps to last
```
### User Journey - File Upload (Create/Edit Listing)
```
1. User opens listing form
└─ ImageUpload component mounts
2. User adds images:
├─ Drag & drop files
│ └─ addFiles() → filter + validate → onChange()
└─ Click to browse
└─ Select files → same as drag & drop
3. User removes image:
└─ removeImage(index) → cleanup URL → onChange()
4. User submits form:
└─ Images uploaded via listingsApi.uploadMedia()
```
---
## 📋 Component Checklist
### 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)
### 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)
### 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
---
## 🛠️ Maintenance Guide
### Adding New Image Features
**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
**Example - Add zoom feature:**
```typescript
interface ImageGalleryProps {
media: PropertyMedia[];
className?: string;
onImageClick?: (index: number) => void; // NEW
}
// In component:
const [isZoomed, setIsZoomed] = useState(false); // NEW
<Image
onClick={() => setIsZoomed(true)} // NEW
cursor={isZoomed ? 'zoom-out' : 'zoom-in'} // NEW
/>
```
### Updating Image Data Structure
**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
---
## 📚 Related Documentation
See also:
- `PROPERTY_DETAIL_PAGE_ANALYSIS.md` - Comprehensive analysis
- `PROPERTY_DETAIL_QUICK_REFERENCE.md` - Code snippets & patterns