# Property Detail Page - Quick Reference & Code Snippets ## 🎯 Quick Navigation ### Page Routes - **Detail Page**: `apps/web/app/[locale]/(public)/listings/[id]/page.tsx` - **Client Component**: `apps/web/components/listings/listing-detail-client.tsx` - **Gallery Component**: `apps/web/components/listings/image-gallery.tsx` - **Upload Component**: `apps/web/components/listings/image-upload.tsx` - **Property Card**: `apps/web/components/search/property-card.tsx` ### Data Flow ``` URL: /vi/listings/abc123 ↓ [id]/page.tsx (Server) β”œβ”€ fetchListingById('abc123') β”œβ”€ generateMetadata() └─ └─ ``` --- ## πŸ–ΌοΈ Working with Images ### Current Gallery Features ```typescript // apps/web/components/listings/image-gallery.tsx interface ImageGalleryProps { media: PropertyMedia[]; className?: string; } // Features: // βœ“ Main image (16:9 aspect ratio) // βœ“ Previous/Next buttons // βœ“ Image counter badge // βœ“ Horizontal scrollable thumbnails (64x64px) // βœ“ Selected state highlighting // βœ“ Empty state fallback ``` ### Data Structure ```typescript interface PropertyMedia { id: string; url: string; // Full URL to image type: 'image' | 'video'; // Media type filter order: number; // Sort order (0, 1, 2...) caption: string | null; // Optional caption } interface Property { // ... other fields media: PropertyMedia[]; // Array of images/videos } ``` ### Using Images in Components ```typescript // βœ“ Import Next.js Image import Image from 'next/image'; // βœ“ Main image with fill layout (responsive) {`Image // βœ“ Fixed-size thumbnail {`Thumbnail // βœ“ Responsive property card Property ``` ### Image Upload API ```typescript // From: apps/web/lib/listings-api.ts const listingsApi = { uploadMedia: async ( listingId: string, file: File, caption?: string ) => { const formData = new FormData(); formData.append('file', file); if (caption) formData.append('caption', caption); // POST /api/v1/listings/{listingId}/media // Returns: { mediaId: string; url: string } }, }; ``` ### File Upload Component (for reference) ```typescript // apps/web/components/listings/image-upload.tsx interface ImageFile { file: File; preview: string; } // Usage: setImages(newImages)} maxFiles={20} /> // Validation: // βœ“ Types: JPEG, PNG, WebP // βœ“ Max size: 10MB per file // βœ“ Max count: 20 files // βœ“ Drag & drop support // βœ“ Preview grid with delete button ``` --- ## 🎨 Styling Patterns ### Aspect Ratios (Tailwind) ```html
``` ### Image Container Pattern ```jsx
description
``` ### Common Tailwind Classes ``` // Layout aspect-video # 16:9 ratio aspect-square # 1:1 ratio relative / absolute # Positioning fill # Object-fit with aspect ratio // Styling object-cover # Image fit (crop to fill) object-contain # Image fit (preserve ratio) rounded-lg # Border radius bg-muted # Placeholder background // Interactive transition-colors # Smooth color changes group-hover:scale-105 # Hover effect opacity-70 # Partial transparency // Overlay absolute inset-0 # Full coverage overlay bg-black/50 # Semi-transparent black hover:bg-black/70 # Darker on hover ``` --- ## πŸ”„ State Management ### Zustand Store Pattern ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; // Simple store const useMyStore = create((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), asyncAction: async () => { set({ isLoading: true }); // ... async work set({ data: result, isLoading: false }); }, })); // Store with persistence const useComparisonStore = create()( persist( (set, get) => ({ // store logic }), { name: 'storage-key', partialize: (state) => ({ selectedIds: state.selectedIds }), } ) ); // Usage in component const count = useMyStore((state) => state.count); const increment = useMyStore((state) => state.increment); ``` ### Image Gallery Local State ```typescript const [selectedIndex, setSelectedIndex] = React.useState(0); const handleNext = () => { setSelectedIndex((i) => (i < images.length - 1 ? i + 1 : 0)); }; const handlePrev = () => { setSelectedIndex((i) => (i > 0 ? i - 1 : images.length - 1)); }; ``` --- ## 🧩 UI Component Patterns ### Button Component (with variants) ```typescript // From: apps/web/components/ui/button.tsx import { Button } from '@/components/ui/button'; // Variants: // Primary // Border // Secondary color // Red // No background // Text link // Sizes: // 40px height // 36px height // 44px height // Square button ``` ### Badge Component (with variants) ```typescript import { Badge } from '@/components/ui/badge'; Primary Secondary Outline Destructive Success Warning Info ``` ### Card Component ```typescript import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; Title {/* content */} ``` ### Dialog/Modal Pattern ```typescript import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; const [isOpen, setIsOpen] = React.useState(false); Dialog Title {/* content */} ``` --- ## πŸ“± Responsive Design ### Breakpoints ```css xs: 0px /* Default */ sm: 640px /* Mobile landscape */ md: 768px /* Tablet */ lg: 1024px /* Desktop */ xl: 1280px /* Wide desktop */ 2xl: 1536px /* Ultra-wide */ ``` ### Common Patterns ```jsx // Mobile-first
// Conditional display
Show on tablet+
Show on mobile
// Responsive grid
// Responsive text size

// Responsive padding

``` --- ## πŸ”— Common Imports ### Essential Imports ```typescript // Components import Image from 'next/image'; import Link from 'next/link'; import dynamic from 'next/dynamic'; // UI Components import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; // Utilities import { cn } from '@/lib/utils'; import { formatPrice, formatPricePerM2 } from '@/lib/currency'; // State & API import { useAuthStore } from '@/lib/auth-store'; import { useComparisonStore } from '@/lib/comparison-store'; import { listingsApi } from '@/lib/listings-api'; // Hooks import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; ``` --- ## πŸ“Š Data Fetching ### Server-side Fetching ```typescript // apps/web/lib/listings-server.ts import { fetchListingById } from '@/lib/listings-server'; // In page.tsx (Server Component) const listing = await fetchListingById(params.id); if (!listing) notFound(); ``` ### Client-side API ```typescript // apps/web/lib/listings-api.ts import { listingsApi } from '@/lib/listings-api'; // Usage: const listing = await listingsApi.getById(id); const results = await listingsApi.search({ city: 'Ho Chi Minh' }); ``` ### React Query Usage (likely) ```typescript // Typical pattern for fetching import { useQuery } from '@tanstack/react-query'; const { data, isLoading, error } = useQuery({ queryKey: ['listing', id], queryFn: () => listingsApi.getById(id), }); ``` --- ## 🌐 Internationalization ### Language Support - Vietnamese (vi) - English (en) ### Using i18n ```typescript // In components, use Vietnamese labels directly or from constants const PROPERTY_TYPES: Record = { APARTMENT: 'CΔƒn hα»™', HOUSE: 'NhΓ  riΓͺng', VILLA: 'Biệt thα»±', // ... }; // From @/lib/validations/listings import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings'; ``` ### Language-aware Routes ``` /vi/listings/123 # Vietnamese /en/listings/123 # English ``` --- ## πŸ” Security Features ### CSP Headers (next.config.js) ```javascript img-src 'self' data: blob: https://*.mapbox.com https:// font-src 'self' data: ``` ### Image Domain Whitelist ```javascript // Allows HTTPS images from any domain remotePatterns: [ { protocol: 'https', hostname: '**' } ] ``` --- ## πŸ§ͺ Testing Considerations ### Component Files to Test - `image-gallery.tsx` - Gallery navigation, state changes - `image-upload.tsx` - File validation, drag-drop - `property-card.tsx` - Image display, responsive - `listing-detail-client.tsx` - Overall page functionality ### Test Patterns ```typescript // Mock Next.js Image component jest.mock('next/image', () => ({ __esModule: true, default: (props) => , })); // Mock Zustand stores jest.mock('@/lib/auth-store', () => ({ useAuthStore: jest.fn(), })); ``` --- ## πŸš€ Performance Optimization Tips 1. **Image Priority** ```typescript priority={selectedIndex === 0} // First image loads with page ``` 2. **Responsive Sizes** ```typescript sizes="(max-width: 768px) 100vw, 60vw" // Tells browser image width ``` 3. **Lazy Loading** - Thumbnails load on demand (no priority set) - Reduces initial page weight 4. **Dynamic Imports** ```typescript const ListingMap = dynamic(() => import('@/components/map/listing-map'), { ssr: false, loading: () =>
Loading...
, }); ``` 5. **Object URLs Cleanup** ```typescript React.useEffect(() => { return () => { images.forEach((img) => URL.revokeObjectURL(img.preview)); }; }, []); ``` --- ## πŸ“‹ Common Tasks ### Add a New UI Element 1. Create in `components/ui/ComponentName.tsx` 2. Use CVA for variants 3. Export from the same file 4. Import and use in feature components ### Add a New Feature Component 1. Create in `components/feature-name/ComponentName.tsx` 2. Make 'use client' if interactive 3. Import UI components 4. Use Zustand stores if needed global state 5. Use local state for UI state ### Modify Image Gallery 1. Edit `components/listings/image-gallery.tsx` 2. Update PropertyMedia interface if needed (in `lib/listings-api.ts`) 3. Adjust aspect ratio / sizes as needed 4. Test responsive behavior ### Add Image Lightbox 1. Choose library (embla-carousel, yet-another-react-lightbox, etc.) 2. Install: `pnpm add package-name -F @goodgo/web` 3. Create wrapper component in `components/listings/image-lightbox.tsx` 4. Integrate with `image-gallery.tsx` 5. Test with multiple images --- ## πŸ› Common Issues & Solutions ### Image Not Loading - Check URL is valid and HTTPS - Verify domain in `remotePatterns` - Check CSP headers allow the domain ### Gallery Navigation Frozen - Check `selectedIndex` state updates - Verify onClick handlers are properly bound - Check for JavaScript errors in console ### Thumbnail Scroll Issues - Ensure parent container has `overflow-x-auto` - Check flex properties on thumbnails - Verify width constraints (flex-shrink-0) ### Layout Shifting on Image Load - Use aspect ratio container - Set explicit width/height - Use `fill` layout with container --- ## πŸ“š Additional Resources - **Next.js Image**: https://nextjs.org/docs/app/api-reference/components/image - **Tailwind CSS**: https://tailwindcss.com/docs - **Zustand**: https://github.com/pmndrs/zustand - **CVA**: https://cva.style/docs - **React Query**: https://tanstack.com/query/latest