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>
This commit is contained in:
601
docs/audits/PROPERTY_DETAIL_COMPONENTS_MAP.md
Normal file
601
docs/audits/PROPERTY_DETAIL_COMPONENTS_MAP.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user