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>
602 lines
14 KiB
Markdown
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
|
|
|