# 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 │ └─ │ ↓ 3. Client Component (Hydrated) │ ├─ │ └─ 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: