# GoodGo Platform - Next.js Frontend Structure Guide

## 📁 1. App Router Structure (`apps/web/app/`)

### Directory Organization
The app follows Next.js 13+ App Router with i18n using `[locale]` dynamic segment and route groups:

```
app/
├── [locale]/                              # i18n dynamic segment
│   ├── (public)/                          # Public pages
│   │   ├── du-an/                         # Projects listing (residential projects)
│   │   │   ├── page.tsx                   # Search/grid view
│   │   │   └── [slug]/page.tsx            # Project detail page
│   │   ├── listings/                      # Property listings
│   │   ├── khu-cong-nghiep/               # Industrial parks
│   │   ├── agents/                        # Agent profiles
│   │   ├── search/                        # Search interface
│   │   ├── bao-cao/                       # Reports
│   │   └── ...others
│   ├── (auth)/                            # Auth pages (login, register)
│   ├── (dashboard)/                       # Protected dashboard routes
│   │   ├── projects/                      # Project management (DEVELOPER role)
│   │   │   ├── page.tsx                   # Projects list
│   │   │   ├── new/page.tsx               # Create new project
│   │   │   └── [id]/edit/page.tsx         # Edit project
│   │   ├── listings/                      # Listing management
│   │   ├── dashboard/                     # Main dashboard
│   │   ├── analytics/                     # Analytics dashboard
│   │   ├── leads/                         # Leads management
│   │   └── ...others
│   ├── (admin)/                           # Admin-only routes
│   │   └── admin/                         # Admin panel
│   ├── auth/callback/                     # OAuth callbacks
│   └── layout.tsx                         # Root layout with locale
├── api/                                   # API routes (edge endpoints)
└── robots.ts                              # robots.txt generation
```

### Key Patterns
- **Route Groups** (parentheses): Don't affect URL, used for organizing layouts
  - `(public)` - Public pages with public header/footer
  - `(auth)` - Auth pages (login/register layout)
  - `(dashboard)` - Protected pages behind authentication
  - `(admin)` - Admin-only pages

- **Dynamic Segments**: `[locale]`, `[id]`, `[slug]`
- **Server Components by default**, `'use client'` for interactivity

---

## 🎨 2. Existing Page Patterns

### Pattern A: Public Browsing Page (e.g., du-an/page.tsx)

**Server Component** wraps Client Component:
```tsx
// Page: Server Component
export async function generateMetadata() { ... }  // SEO metadata
export default async function DuAnDetailPage({ params }) { ... }

// Detail fetching
const project = await fetchProjectBySlug(slug);
return <DuAnDetailClient project={project} />;  // Pass to client
```

**Client Component** handles state & interactivity:
```tsx
'use client';
// Use hooks, state, client APIs
const { data, isLoading } = useProjectsSearch(filters);
// Multiple view modes: grid, list, map
// Filters, pagination
```

### Pattern B: Admin/Dashboard Page (projects/page.tsx)

**Full Client Component** with:
- Authentication check via `useAuthStore`
- React Query for data fetching & mutations
- Filters & search
- CRUD operations

```tsx
'use client';
const { data: result } = useQuery({
  queryKey: ['admin-projects', ...],
  queryFn: () => duAnApi.searchMine(queryParams),
});
const deleteMutation = useMutation({
  mutationFn: duAnApi.delete,
  onSuccess: () => queryClient.invalidateQueries(...),
});
```

### Pattern C: Detail Pages with Metadata

**Server-side data fetching** for SEO:
```tsx
export async function generateMetadata({ params }: PageProps) {
  const project = await fetchProjectBySlug(slug);
  return {
    title: `${project.name} — ${project.developer.name}`,
    description: project.description?.slice(0, 160),
  };
}

export default async function Page({ params }: PageProps) {
  const project = await fetchProjectBySlug(slug);
  if (!project) notFound();
  return <DetailClient project={project} />;
}
```

### Pattern D: Dynamic Routes with API Integration

```tsx
export async function generateStaticParams() {
  // Pre-render static pages
  const projects = await fetchProjects({ limit: 100 });
  return projects.map(p => ({ slug: p.slug }));
}

export const revalidate = 3600; // ISR: revalidate every hour
```

---

## 🧩 3. UI Components (`apps/web/components/`)

### Component Structure

```
components/
├── ui/                        # shadcn/ui primitives
│   ├── button.tsx
│   ├── card.tsx
│   ├── input.tsx
│   ├── label.tsx
│   ├── dialog.tsx
│   ├── select.tsx
│   ├── tabs.tsx
│   ├── table.tsx
│   ├── textarea.tsx
│   ├── badge.tsx
│   └── language-switcher.tsx
├── du-an/                     # Project-specific components
│   ├── project-card.tsx       # Grid card display
│   ├── project-map.tsx        # Mapbox map integration
│   ├── project-filter-bar.tsx # Search filters
│   ├── du-an-detail-client.tsx # Detail page tabs
│   └── project-ai-advice-card.tsx
├── listings/
│   ├── image-gallery.tsx
│   └── ...
├── map/
│   ├── listing-map.tsx
│   └── ...
├── neighborhood/
│   ├── neighborhood-poi-map.tsx
│   ├── neighborhood-radar-chart.tsx
│   └── ...
└── ...others
```

### Component Patterns

**Functional Component** (most common):
```tsx
'use client'; // if interactive

interface ProjectCardProps {
  project: ProjectSummary;
}

export function ProjectCard({ project }: ProjectCardProps) {
  return (
    <Card className="group hover:shadow-lg transition-shadow">
      {/* Content */}
    </Card>
  );
}
```

**With Query Hooks** (data fetching):
```tsx
'use client';
import { useQuery } from '@tanstack/react-query';

export function ProjectsList() {
  const { data, isLoading } = useQuery({
    queryKey: ['projects', params],
    queryFn: () => duAnApi.search(params),
  });
  
  return ...
}
```

**Image Components** (Next.js):
```tsx
import Image from 'next/image';

<Image
  src={project.thumbnailUrl}
  alt={project.name}
  fill
  className="object-cover"
  sizes="(max-width: 768px) 100vw, 50vw"
/>
```

---

## 🗺️ 4. Mapbox Integration

### Setup
- **Token**: `NEXT_PUBLIC_MAPBOX_TOKEN` environment variable
- **Library**: `mapbox-gl` (imported with `/* eslint-disable import-x/no-named-as-default-member */`)
- **CSS**: `import 'mapbox-gl/dist/mapbox-gl.css'`

### Example: ProjectMap Component

```tsx
'use client';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

export function ProjectMap({ projects }: ProjectMapProps) {
  const mapContainerRef = React.useRef<HTMLDivElement>(null);
  const mapRef = React.useRef<mapboxgl.Map | null>(null);
  const markersRef = React.useRef<mapboxgl.Marker[]>([]);

  React.useEffect(() => {
    mapboxgl.accessToken = process.env['NEXT_PUBLIC_MAPBOX_TOKEN']!;
    
    const map = new mapboxgl.Map({
      container: mapContainerRef.current!,
      style: mapStyle,  // Dynamic style from useMapboxStyle()
      center: [106.6297, 10.8231], // HCMC default
      zoom: 12,
    });

    map.addControl(new mapboxgl.NavigationControl(), 'top-right');
    mapRef.current = map;
  }, []);

  // Add markers with click popups
  React.useEffect(() => {
    projects.forEach(project => {
      const marker = new mapboxgl.Marker(...)
        .setLngLat([project.longitude, project.latitude])
        .setPopup(popup)
        .addTo(map);
    });
  }, [projects]);

  return <div ref={mapContainerRef} className="h-full w-full" />;
}
```

### Features
- **Dynamic map styles** via `useMapboxStyle()` hook
- **Custom markers** with HTML elements
- **Popups** with project information
- **Auto-fit bounds** with `fitBounds()`
- **Navigation control** for zooming

### Other Maps in Codebase
- `listing-map.tsx` - Individual property map
- `neighborhood-poi-map.tsx` - POI overlay (restaurants, schools, etc.)
- `park-map.tsx` - Industrial park locations

---

## 🎨 5. Tailwind/Styling Setup

### Configuration
- **File**: `apps/web/tailwind.config.ts`
- **Plugins**: `tailwindcss-animate`
- **Color system**: CSS custom properties (HSL variables)

### Design Tokens

**Colors** (via CSS variables):
```tsx
// Usage in tailwind.config
colors: {
  primary: 'hsl(var(--primary))',
  secondary: 'hsl(var(--secondary))',
  destructive: 'hsl(var(--destructive))',
  muted: 'hsl(var(--muted))',
  accent: { blue, purple },
  background: { DEFAULT, elevated, surface },
  signal: { up, down, neutral },
  card: 'hsl(var(--card))',
  // ...
}
```

**Spacing**:
```tsx
spacing: {
  cell: '0.5rem',
  row: '2.25rem',
  sidebar: '15rem',
  'sidebar-collapsed': '3.5rem',
}
```

**Typography**:
```tsx
fontSize: {
  ticker: '0.8125rem',
  'heading-sm': '0.875rem',
  'heading-md': '1.125rem',
  'heading-lg': '1.5rem',
  'heading-xl': '1.875rem',
}
```

**Shadows**:
```tsx
boxShadow: {
  'elevation-1': '0 1px 2px rgba(0,0,0,.30), 0 0 0 1px hsl(var(--border))',
  'elevation-2': '0 4px 12px rgba(0,0,0,.40)',
  'elevation-3': '0 12px 32px rgba(0,0,0,.50)',
}
```

**Utilities**:
```tsx
cn('rounded-lg', 'border', 'p-4', isHovered && 'shadow-lg')
```

### Dark Mode
```tsx
darkMode: ['class']  // Toggle via class on root element
```

---

## 🔌 6. API Client & Data Fetching Patterns

### API Client (`apps/web/lib/api-client.ts`)

Core fetch wrapper with:
- **CSRF token** handling
- **Auto-refresh** on 401 (expired token)
- **Coalesced refresh** (only refresh once if multiple 401s)
- **Credentials** included (cookies)

```tsx
export const apiClient = {
  get: <T>(endpoint: string, headers?: HeadersInit) =>
    request<T>(endpoint, { method: 'GET', headers }),
  
  post: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) =>
    request<T>(endpoint, { method: 'POST', body, headers }),
  
  patch: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) =>
    request<T>(endpoint, { method: 'PATCH', body, headers }),
  
  delete: <T>(endpoint: string, headers?: HeadersInit) =>
    request<T>(endpoint, { method: 'DELETE', headers }),
};
```

### Domain APIs

**File: `apps/web/lib/du-an-api.ts`** (Projects API)
```tsx
export const duAnApi = {
  search: (params: SearchProjectsParams) =>
    apiClient.get<PaginatedResult<ProjectSummary>>(`/projects?...`),
  
  searchMine: (params) =>  // DEVELOPER/ADMIN only
    apiClient.get<PaginatedResult<ProjectSummary>>(`/projects/mine/list?...`),
  
  getBySlug: (slug: string) =>
    apiClient.get<ProjectDetail>(`/projects/${slug}`),
  
  getLinkedListings: (projectId, params) =>
    apiClient.get<PaginatedResult<ListingDetail>>(`/projects/${projectId}/listings?...`),
  
  submitInquiry: (projectId, data) =>
    apiClient.post<{ inquiryId: string }>(`/projects/${projectId}/inquiries`, data),
  
  create: (payload) =>
    apiClient.post<{ id: string; slug: string }>('/projects', payload),
  
  update: (id, payload) =>
    apiClient.patch<ProjectDetail>(`/projects/${id}`, payload),
  
  delete: (id) =>
    apiClient.delete<{ success: boolean }>(`/projects/${id}`),
};
```

### Query Hooks (`apps/web/lib/hooks/use-du-an.ts`)

React Query hook pattern:
```tsx
export const projectKeys = {
  all: ['projects'] as const,
  search: (params) => ['projects', 'search', params] as const,
  detail: (slug) => ['projects', 'detail', slug] as const,
  linkedListings: (projectId, page) =>
    ['projects', 'listings', projectId, page] as const,
};

export function useProjectsSearch(params: SearchProjectsParams = {}) {
  return useQuery({
    queryKey: projectKeys.search(params),
    queryFn: () => duAnApi.search(params),
  });
}

export function useProjectDetail(slug: string) {
  return useQuery({
    queryKey: projectKeys.detail(slug),
    queryFn: () => duAnApi.getBySlug(slug),
    enabled: !!slug,
  });
}
```

### Server-Side Fetching (`apps/web/lib/du-an-server.ts`)

For `generateMetadata`, `generateStaticParams`, etc. (server components only):
```tsx
export async function fetchProjectBySlug(slug: string): Promise<ProjectDetail | null> {
  try {
    const res = await fetch(`${API_BASE_URL}/projects/${slug}`, {
      next: { revalidate: 300 }, // ISR: 5 minutes
    });
    if (!res.ok) return null;
    return normalizeProjectDetail(await res.json());
  } catch {
    return null;
  }
}

export async function fetchProjects(params) {
  const query = new URLSearchParams({
    page: String(params.page ?? 1),
    limit: String(params.limit ?? 100),
  });
  // ...
  const res = await fetch(`${API_BASE_URL}/projects?${query}`, {
    next: { revalidate: 3600 }, // ISR: 1 hour
  });
  return res.json();
}
```

**Key: Normalization**
- Backend returns thin projections
- Normalize on frontend to ensure UI never crashes on missing fields
- Handles both string arrays and object arrays for flexible data shapes

### Other Domain APIs
- `listings-api.ts` - Property listings
- `khu-cong-nghiep-server.ts` - Industrial parks
- `agents-api.ts` - Agent profiles
- `inquiries-api.ts` - Inquiries
- `leads-api.ts` - Leads
- `auth-api.ts` - Authentication
- `admin-api.ts` - Admin endpoints

---

## 📊 7. Prisma Schema: ProjectDevelopment Model

### Model Definition
```prisma
enum ProjectDevelopmentStatus {
  PLANNING
  UNDER_CONSTRUCTION
  COMPLETED
  HANDOVER
}

model ProjectDevelopment {
  id              String                      @id @default(cuid())
  name            String
  slug            String                      @unique
  developer       String                      # Developer name (company)
  developerLogo   String?
  totalUnits      Int
  completedUnits  Int                         @default(0)
  status          ProjectDevelopmentStatus    @default(PLANNING)
  startDate       DateTime?
  completionDate  DateTime?
  description     String?                     @db.Text
  amenities       Json?                       # Array of amenities
  masterPlanUrl   String?
  location        Unsupported("geometry(Point, 4326)")  # PostGIS Point
  address         String
  ward            String
  district        String
  city            String
  minPrice        BigInt?
  maxPrice        BigInt?
  pricePerM2Range Json?
  totalArea       Float?
  buildingCount   Int?
  floorCount      Int?
  unitTypes       Json?                       # Property types
  media           Json?                       # Images, videos, documents
  documents       Json?
  tags            String[]
  suitableFor     String[]                    @default([])
  whyThisLocation String?                     @db.Text
  isVerified      Boolean                     @default(false)
  ownerId         String?                     # DEVELOPER user who owns this
  createdAt       DateTime                    @default(now())
  updatedAt       DateTime                    @updatedAt

  properties  Property[]
  owner       User?       @relation("ProjectOwner", fields: [ownerId], references: [id], onDelete: SetNull)

  @@index([status])
  @@index([district, city])
  @@index([location], type: Gist)  # PostGIS spatial index
  @@index([ownerId])
  @@index([createdAt])
}
```

### Related Models
- **User** (role: DEVELOPER) → owns projects via `ownedProjects` relation
- **Property** → listings within a project
- **IndustrialPark** → separate model for khu-cong-nghiep

### Key Fields
- **location**: PostGIS `geometry(Point, 4326)` for geographic queries
- **media/documents**: Flexible JSON arrays
- **amenities**: Array of amenity objects or strings
- **tags/suitableFor**: String arrays for filtering

---

## 🛣️ 8. Existing du-an Routes

### Public Routes
- `/du-an` - Browse all projects (with filters, search, map view)
- `/du-an/[slug]` - Project detail page

### Dashboard Routes (DEVELOPER role)
- `/projects` - My projects list with CRUD
- `/projects/new` - Create new project
- `/projects/[id]/edit` - Edit project details

### API Routes
- `GET /projects` - Search all projects (public)
- `GET /projects/mine/list` - Get current developer's projects (DEVELOPER/ADMIN only)
- `GET /projects/[slug]` - Get project detail
- `GET /projects/[id]/listings` - Get listings linked to project
- `GET /projects/[id]/stats` - Project stats (admin + owner only)
- `POST /projects` - Create project (DEVELOPER/ADMIN)
- `PATCH /projects/[id]` - Update project
- `DELETE /projects/[id]` - Delete project
- `POST /projects/[id]/inquiries` - Submit project inquiry

### Feature Flag
- `use-residential-projects-flag.ts` - Feature flag for enabling/disabling du-an module
- Check: `useResidentialProjectsFlag()` in client, `isResidentialProjectsEnabledServer()` on server

---

## 📝 Summary Checklist for Building New Pages

Before building new pages, ensure you:

✅ **1. Choose the right route group** (`(public)`, `(dashboard)`, `(admin)`)
✅ **2. Use Server Components by default**, wrap with Client Component for interactivity
✅ **3. For detail pages**: Use `generateMetadata()` + `generateStaticParams()` for SEO
✅ **4. Data fetching**:
  - Server components: `fetch()` directly or use `du-an-server.ts` functions
  - Client components: Use React Query hooks (`useProjectsSearch`, etc.)
✅ **5. Use shadcn/ui components** from `components/ui/`
✅ **6. Styling**: Tailwind classes + `cn()` utility from `lib/utils`
✅ **7. Colors**: Use design token variables (primary, secondary, muted, etc.)
✅ **8. Images**: Use `next/image` with `fill` for responsive layouts
✅ **9. Links**: Use `Link` from `@/i18n/navigation` for i18n support
✅ **10. Maps**: Use Mapbox via `project-map.tsx` component (check token env var)
✅ **11. Authentication**: Check role via `useAuthStore().user?.role` in client components
✅ **12. Error handling**: Return `notFound()` in pages, error boundaries in components
✅ **13. Loading states**: Skeleton loaders, loading.tsx file, suspense boundaries
✅ **14. Tests**: Mirror structure with `__tests__/` folder, name `.spec.tsx`
