# GoodGo Platform - Property Detail Page Analysis

## Project Overview
- **Framework**: Next.js 15.5.14 (App Router)
- **Styling**: Tailwind CSS 3.4.0 with CSS variables
- **State Management**: Zustand 5.0.12 (with persist middleware)
- **UI Components**: Custom built with CVA (class-variance-authority) and Radix patterns
- **Internationalization**: next-intl 4.9.0 (Vietnamese/English)
- **Image Handling**: Next.js Image component with remote patterns
- **Package Manager**: pnpm 10.27.0

---

## 1. Property Detail Page Structure

### File Location
```
apps/web/app/[locale]/(public)/listings/[id]/
├── page.tsx          # Server component - fetches data, generates metadata, JSON-LD
└── (referenced) listing-detail-client.tsx  # Client component - handles interactivity
```

### Page Architecture
**Server Component** (`page.tsx`):
- Fetches listing data via `fetchListingById(params.id)`
- Generates SEO metadata (Open Graph, Twitter Cards, canonical URLs)
- Generates JSON-LD structured data (breadcrumbs, property schema)
- Renders structured data and passes data to client component

**Client Component** (`listing-detail-client.tsx`):
- All interactivity (image gallery state, forms, etc.)
- Uses dynamic imports for heavy components (ListingMap)
- Main sections:
  - Breadcrumb navigation
  - Header with title, price, badges
  - **Image Gallery** (main content area)
  - Quick stats bar (area, bedrooms, bathrooms, floors, direction)
  - Two-column layout:
    - Left (2/3): Description, Details, Amenities, Map, Contact Card
    - Right (1/3): Sticky sidebar with contact info, AI Estimate, Stats

### Data Flow
```
page.tsx (Server)
  └─> fetchListingById() ─> ListingDetail object
  └─> generateMetadata() ─> SEO metadata
  └─> ListingDetailClient (Client)
      └─> ImageGallery component
      └─> AddToCompareButton component
      └─> AiEstimateButton component
      └─> dynamic ListingMap component
```

---

## 2. Property Images - Current Implementation

### Image Gallery Component
**File**: `apps/web/components/listings/image-gallery.tsx`

#### Features:
- **Main Display**:
  - Aspect ratio: 16:9 (aspect-video)
  - Uses Next.js `Image` component with `fill` layout
  - Object fit: cover
  - Rounded corners
  - Previous/Next navigation buttons (semi-transparent overlay, hover effects)
  - Current image counter (bottom-right: "X / Total")

- **Thumbnail Navigation**:
  - Horizontal scrollable row (flex with overflow-x-auto)
  - Each thumbnail: 64px × 64px (h-16 w-16)
  - Border indicates selected state (2px border-primary vs border-transparent with opacity)
  - Smooth transitions

- **Empty State**:
  - Falls back to gray placeholder if no images: "Chưa có hình ảnh"

#### State Management:
- Local React state (`selectedIndex`): Tracks which image is displayed
- One-way: thumbnail click → main image update

#### Image Handling:
- Filters media by `type === 'image'`
- Sorts by `order` property
- Supports captions (from `PropertyMedia.caption`)
- Uses `next/image` with optimized sizes

#### Technical Details:
```typescript
interface PropertyMedia {
  id: string;
  url: string;
  type: 'image' | 'video';
  order: number;
  caption: string | null;
}
```

### Image Upload Component
**File**: `apps/web/components/listings/image-upload.tsx`

#### Features:
- Drag & drop zone
- Click to browse
- File validation:
  - Allowed types: JPEG, PNG, WebP
  - Max size: 10MB per file
  - Max files: 20
- Preview grid (2 cols mobile, 3 cols tablet, 4 cols desktop)
- Delete button on hover
- First image labeled "Ảnh bìa" (Cover photo)
- URL.createObjectURL for previews (properly cleaned up on unmount)

#### State Management:
- Local state: `ImageFile[]` (file + preview URL)
- onChange callback pattern

---

## 3. Image-Related Components

### Current Locations:
```
apps/web/components/
├── listings/
│   ├── image-gallery.tsx       ✓ Main image display with thumbnails
│   ├── image-upload.tsx        ✓ Upload with drag-drop
│   ├── listing-detail-client.tsx ✓ Uses image gallery
│   └── ...other listing components
├── ui/
│   ├── button.tsx              ✓ Navigation buttons
│   ├── badge.tsx               ✓ Image counter badge
│   ├── dialog.tsx              ✓ Custom modal implementation
│   ├── card.tsx
│   └── ...other UI components
├── search/
│   └── property-card.tsx       ✓ Thumbnail display with images
└── comparison/
    └── ...comparison components
```

### Property Card (Search/Listing View)
**File**: `apps/web/components/search/property-card.tsx`
- Uses first media item (`media[0]?.url`)
- Shows badge indicating total media count if > 1
- Has hover scale effect (group-hover:scale-105)
- Aspect ratio options: 16/10 (compact) or 4/3 (default)

---

## 4. Project Component Structure & Patterns

### Design System Approach
- **UI Components**: Located in `components/ui/`
- **Pattern**: CVA (class-variance-authority) for variants
- **Example** (button.tsx):
  ```typescript
  const buttonVariants = cva(
    'inline-flex items-center justify-center ...',
    {
      variants: {
        variant: { default: '...', outline: '...', ghost: '...', ... },
        size: { default: '...', sm: '...', lg: '...', icon: '...' },
      },
      defaultVariants: { variant: 'default', size: 'default' },
    }
  );
  ```

### Composition Pattern
- Small, focused components
- Props-based configuration
- Utility function composition (`cn()` from `@/lib/utils` - likely clsx + tailwind-merge)
- Forward refs for form components

### Dialog Component
**File**: `apps/web/components/ui/dialog.tsx`
- Custom implementation (not Radix)
- Features:
  - Backdrop overlay (fixed, z-50, black/80)
  - Center content positioning
  - Close on backdrop click
  - Body overflow hidden when open
  - Composable parts: Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter

---

## 5. Next.js Image Usage Patterns

### Configuration
**File**: `apps/web/next.config.js`
```javascript
images: {
  remotePatterns: [
    { protocol: 'https', hostname: '**' },  // All HTTPS domains allowed
  ],
}
```

### Usage Pattern in Components:
```typescript
import Image from 'next/image';

// Main image (fill layout)
<Image
  src={images[selectedIndex]?.url ?? ''}
  alt={`Ảnh ${selectedIndex + 1}`}
  fill
  sizes="(max-width: 768px) 100vw, 60vw"
  className="object-cover"
  priority={selectedIndex === 0}
/>

// Thumbnail (fixed size)
<Image
  src={img.url}
  alt={`Thumbnail ${index + 1}`}
  fill
  sizes="64px"
  className="object-cover"
/>

// Property card (fill layout)
<Image
  src={listing.property.media[0]?.url ?? ''}
  alt={`Ảnh bất động sản: ${listing.property.title}`}
  fill
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  className="object-cover transition-transform group-hover:scale-105"
/>
```

### Best Practices Observed:
✓ Always provide `alt` text  
✓ Use responsive `sizes` prop  
✓ Use `fill` layout with `object-cover`  
✓ Set `priority={true}` for above-fold images  
✓ Use aspect ratio containers (aspect-video, aspect-square, etc.)

---

## 6. State Management Patterns

### Using Zustand

#### Auth Store
**File**: `apps/web/lib/auth-store.ts`
```typescript
const useAuthStore = create<AuthState>((set, get) => ({
  user: null,
  isAuthenticated: false,
  isLoading: false,
  error: null,

  login: async (data) => { /* ... */ },
  logout: async () => { /* ... */ },
  fetchProfile: async () => { /* ... */ },
  initialize: async () => { /* ... */ },
}));
```

#### Comparison Store (with persistence)
**File**: `apps/web/lib/comparison-store.ts`
```typescript
export const useComparisonStore = create<ComparisonState>()(
  persist(
    (set, get) => ({
      selectedIds: [],
      listings: [],
      isLoading: false,
      error: null,

      addToCompare: (id: string) => { /* ... */ },
      removeFromCompare: (id: string) => { /* ... */ },
      isSelected: (id: string) => { /* ... */ },
      setListings: (listings: ListingDetail[]) => { /* ... */ },
      setLoading: (loading: boolean) => { /* ... */ },
      setError: (error: string | null) => { /* ... */ },
    }),
    {
      name: 'goodgo-compare',
      partialize: (state) => ({ selectedIds: state.selectedIds }),
    }
  )
);
```

### Store Patterns:
- **Actions as methods** in store object
- **Async support** with `set()` and `get()`
- **Persistence middleware** for localStorage (comparison store)
- **Error handling** with dedicated error fields
- **Loading states** for async operations

### Hooks Pattern
**File**: `apps/web/lib/hooks/`
```
use-analytics.ts
use-listings.ts      # Likely wraps API calls
use-payments.ts
use-saved-searches.ts
use-subscription.ts
use-valuation.ts
```

These likely use React Query + custom Zustand stores

---

## 7. Existing third-party Libraries

### No Lightbox/Gallery Libraries Installed
The project does NOT currently use:
- ❌ react-lightbox
- ❌ yet-another-react-lightbox
- ❌ photoswipe
- ❌ swiper (gallery carousel)
- ❌ react-image-gallery
- ❌ embla-carousel (for carousels)

### Available Dependencies:
```json
{
  "@tanstack/react-query": "^5.96.2",    // Data fetching
  "zustand": "^5.0.12",                  // State management
  "lucide-react": "^1.7.0",              // Icons
  "mapbox-gl": "^3.21.0",                // Maps
  "recharts": "^3.8.1",                  // Charts
  "next-intl": "^4.9.0",                 // i18n
  "class-variance-authority": "^0.7.1",  // CVA for components
  "clsx": "^2.1.1",                      // Conditional classNames
  "tailwind-merge": "^3.5.0",            // Merge Tailwind classes
}
```

---

## 8. Tailwind & Design Tokens

### CSS Variable System
**File**: `apps/web/app/globals.css`
Color tokens available:
- `--border`
- `--input`
- `--ring`
- `--background`
- `--foreground`
- `--primary` / `--primary-foreground`
- `--secondary` / `--secondary-foreground`
- `--destructive` / `--destructive-foreground`
- `--muted` / `--muted-foreground`
- `--accent` / `--accent-foreground`
- `--card` / `--card-foreground`
- `--radius` (border radius)

### Responsive Breakpoints (standard Tailwind):
- `sm`: 640px
- `md`: 768px
- `lg`: 1024px
- `xl`: 1280px
- `2xl`: 1536px

### Animations Available:
From `tailwindcss-animate` plugin

---

## 9. API & Data Types

### Listing Detail Type
```typescript
interface ListingDetail {
  id: string;
  status: ListingStatus;
  transactionType: 'SALE' | 'RENT';
  priceVND: string;
  pricePerM2: number | null;
  rentPriceMonthly: string | null;
  commissionPct: number | null;
  viewCount: number;
  saveCount: number;
  inquiryCount: number;
  publishedAt: string | null;
  createdAt: string;
  property: {
    id: string;
    propertyType: 'APARTMENT' | 'HOUSE' | 'VILLA' | 'LAND' | 'OFFICE' | 'SHOPHOUSE';
    title: string;
    description: string;
    address: string;
    ward: string;
    district: string;
    city: string;
    areaM2: number;
    bedrooms: number | null;
    bathrooms: number | null;
    floors: number | null;
    direction: Direction | null;
    yearBuilt: number | null;
    legalStatus: string | null;
    amenities: string[] | null;
    projectName: string | null;
    latitude: number | null;
    longitude: number | null;
    media: PropertyMedia[];  // ← Array of images/videos
  };
  seller: { id: string; fullName: string; phone: string };
  agent: { id: string; userId: string; agency: string | null } | null;
}

interface PropertyMedia {
  id: string;
  url: string;
  type: 'image' | 'video';
  order: number;
  caption: string | null;
}
```

### API Functions
**File**: `apps/web/lib/listings-api.ts`
```typescript
const listingsApi = {
  create: (data: CreateListingPayload) => { /* POST /listings */ },
  getById: (id: string) => { /* GET /listings/{id} */ },
  search: (params: SearchListingsParams) => { /* GET /listings?... */ },
  updateStatus: (id, status, notes?) => { /* POST /listings/{id}/status */ },
  uploadMedia: async (listingId, file, caption?) => { /* POST /listings/{id}/media */ },
};
```

---

## 10. File Structure Summary

```
apps/web/
├── app/
│   ├── globals.css                        # Design tokens, CSS variables
│   └── [locale]/
│       ├── layout.tsx                     # Root layout with providers
│       └── (public)/
│           ├── listings/
│           │   └── [id]/
│           │       └── page.tsx           # Property detail page
│           └── page.tsx                   # Home page
├── components/
│   ├── listings/
│   │   ├── listing-detail-client.tsx      # Main detail view
│   │   ├── image-gallery.tsx              # Gallery component
│   │   ├── image-upload.tsx               # Upload component
│   │   ├── listing-form-steps.tsx
│   │   └── listing-status-badge.tsx
│   ├── ui/
│   │   ├── button.tsx                     # Button with variants
│   │   ├── badge.tsx                      # Badge with variants
│   │   ├── card.tsx                       # Card component
│   │   ├── dialog.tsx                     # Modal/Dialog
│   │   ├── input.tsx
│   │   ├── label.tsx
│   │   ├── select.tsx
│   │   ├── table.tsx
│   │   ├── tabs.tsx
│   │   └── textarea.tsx
│   ├── search/
│   │   ├── property-card.tsx              # Listing card with image
│   │   ├── filter-bar.tsx
│   │   └── search-results.tsx
│   ├── comparison/
│   │   ├── add-to-compare-button.tsx
│   │   ├── compare-floating-bar.tsx
│   │   ├── comparison-stats.tsx
│   │   └── comparison-table.tsx
│   ├── map/
│   │   └── listing-map.tsx                # Mapbox integration
│   ├── seo/
│   │   └── json-ld.tsx                    # Schema.org structured data
│   ├── auth/                              # Auth components
│   ├── agents/                            # Agent components
│   ├── valuation/                         # AI valuation
│   ├── charts/                            # Chart components
│   └── providers/                         # Context providers
├── lib/
│   ├── auth-store.ts                      # Zustand auth
│   ├── comparison-store.ts                # Zustand comparison (persisted)
│   ├── auth-api.ts                        # Auth endpoints
│   ├── listings-api.ts                    # Listing endpoints & types
│   ├── listings-server.ts                 # Server-only functions
│   ├── currency.ts                        # Currency formatting
│   ├── api-client.ts                      # Fetch wrapper
│   ├── query-client.ts                    # React Query config
│   ├── utils.ts                           # Helper functions
│   ├── hooks/
│   │   ├── use-listings.ts
│   │   ├── use-analytics.ts
│   │   ├── use-payments.ts
│   │   ├── use-saved-searches.ts
│   │   ├── use-subscription.ts
│   │   └── use-valuation.ts
│   └── validations/
│       └── listings.ts                    # Zod schemas
├── middleware.ts                          # i18n middleware
├── instrumentation.ts                     # Observability (Sentry)
├── tailwind.config.ts                     # Tailwind configuration
├── next.config.js                         # Next.js configuration
└── package.json
```

---

## 11. Key Insights & Best Practices

### Image Strategy
1. **Responsive Images**: Uses `sizes` prop for responsive serving
2. **Lazy Loading**: Non-priority images load on demand
3. **Performance**: Object-fit cover with aspect ratios
4. **SEO**: First image used for OG tags in metadata
5. **No 3rd-party**: Custom gallery implementation = lightweight

### Component Architecture
1. **Separation of Concerns**: Server fetch → Client interactivity
2. **Dynamic Imports**: Heavy components (Map) loaded on demand
3. **Composition**: Small, reusable UI components with variants
4. **Type Safety**: Full TypeScript with Zod validation

### State Management
1. **Zustand for Global State**: Auth, Comparisons
2. **React Query**: Likely for server state (data fetching)
3. **Local State**: For UI state (gallery index, form inputs)

### i18n
- Vietnamese (vi) and English (en) support
- Labels: `@/lib/validations/listings` for property types, directions, etc.

### SEO
- JSON-LD schema for listings and breadcrumbs
- Open Graph and Twitter Cards
- Canonical URLs
- Alternate language links

---

## 12. Dependencies Not Present

⚠️ **Potential Opportunities** (if needed):
- No full-featured carousel library (could use embla-carousel if complex carousel needed)
- No lightbox library (current implementation is basic - consider if modal zoom needed)
- No image optimization service (relying on Next.js Image component)
- No form builder library (using react-hook-form + manual forms)
- No animation library (using Tailwind animations)
- No virtualization (could add if listing 1000s of items)

