```
---
## π 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