Files
goodgo-platform/docs/explorations/NEXTJS_VISUAL_FLOWCHART.md
Ho Ngoc Hai 08b96f9c2d docs: consolidate exploration & audit reports under docs/ (TEC-3094)
- Move 8 stray .md (+5 .txt) from ~/Desktop into docs/explorations/from-desktop/
- Reorganize 27 .md/.txt at workspace root:
  - audit reports -> docs/audits/
  - exploration reports -> docs/explorations/
  - design system -> docs/design-system/
- Keep only README/CHANGELOG/CONTRIBUTING/CLAUDE at repo root
- Refresh docs/README.md as canonical index with links to all groups
- Note: pre-existing docs/audits/AUDIT_INDEX.md and AUDIT_SUMMARY.md were
  overwritten by the newer root-level versions during the move

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 16:29:24 +07:00

20 KiB

Next.js Frontend Architecture - Visual Flowchart

📊 Data Flow Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     USER'S BROWSER                               │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Client Component ('use client')                         │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │ React Hooks: useState, useEffect, useContext       │  │  │
│  │  │ React Query: useQuery, useMutation                 │  │  │
│  │  │ Local State: filters, viewMode, form data          │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                          ↓                                 │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │ API Client (apiClient.get/post/patch/delete)      │  │  │
│  │  │  • Handles CSRF token                              │  │  │
│  │  │  • Auto-refresh on 401                             │  │  │
│  │  │  • Credentials included                            │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                          ↓↑                                     │
│            HTTP Requests/Responses                             │
└─────────────────────────────────────────────────────────────────┘
                          ↓↑
            ┌──────────────────────────────┐
            │    Backend API (Node/Express)│
            │   /projects /listings        │
            │   /projects/[slug]           │
            │   /projects/[id]/inquiries   │
            └──────────────────────────────┘

🏗️ Page Rendering Flow

Pattern 1: Public Browse Page (/du-an)

┌─────────────────────────────────┐
│ Server Component (SSR)          │
│ ✓ Layouts applied               │
│ ✓ Metadata generated            │
│ ✓ i18n applied                  │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ Client Component                │
│ ✓ useState: filters, viewMode   │
│ ✓ useProjectsSearch hook        │
│ ✓ Render grid/list/map view     │
│ ✓ Handle filter changes         │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ UI Components                   │
│ ├─ ProjectFilterBar             │
│ ├─ ProjectCard (grid)           │
│ ├─ ProjectListItem (list)       │
│ └─ ProjectMap (Mapbox)          │
└─────────────────────────────────┘

Pattern 2: Detail Page (/du-an/[slug])

REQUEST: /du-an/my-project-slug
         │
         ↓
┌─────────────────────────────────┐
│ Server Component (generateMetadata)
│ • Fetch project by slug (ISR 5min)
│ • Generate metadata (title, description, OG)
│ • Return to page
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ Server Component (Page)         │
│ • Fetch project again by slug   │
│ • notFound() if not exists      │
│ • Pass to client component      │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ Client Component (DuAnDetailClient)
│ • Tabs: amenities, location, price
│ • Live data fetch: POIs, scores
│ • Contact form handling         │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ Dynamic Components              │
│ ├─ PriceTrendChart              │
│ ├─ NeighborhoodRadarChart       │
│ └─ NeighborhoodPOIMap           │
└─────────────────────────────────┘

Pattern 3: Admin CRUD Page (/projects)

REQUEST: /projects (DEVELOPER only)
         │
         ↓
┌─────────────────────────────────┐
│ Client Component (full)         │
│ • Check auth: useAuthStore      │
│ • Define filters state          │
│ • Setup React Query             │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ useQuery {                      │
│  queryKey: ['projects', params] │
│  queryFn: duAnApi.searchMine()  │
│ }                               │
└──────────┬──────────────────────┘
           ↓
┌─────────────────────────────────┐
│ Render List + Actions:          │
│ ├─ New project button           │
│ ├─ Filter inputs                │
│ ├─ Project rows with actions    │
│ ├─ Edit link → /projects/[id]/  │
│ └─ Delete → useMutation         │
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│ useMutation {                   │
│  mutationFn: duAnApi.delete     │
│  onSuccess: invalidateQueries   │
│ }                               │
└─────────────────────────────────┘

📚 Component Hierarchy

(Root Layout)
├── Locale Selector [locale]
│   ├── Locale Provider
│   ├── Auth Provider
│   ├── Theme Provider
│   │
│   ├── Route Group: (public)
│   │   ├── Header/Footer
│   │   ├── Route: du-an/
│   │   │   ├── Server: DuAnPage (Server)
│   │   │   │   └── Client: DuAnPage (Client) ← Main browsing
│   │   │   │       ├── ProjectFilterBar
│   │   │   │       ├── ProjectCard[] (grid)
│   │   │   │       ├── ProjectListItem[] (list)
│   │   │   │       └── ProjectMap (Mapbox) [dynamic]
│   │   │   │
│   │   │   └── Route: [slug]/
│   │   │       ├── Server: generateMetadata() → fetch()
│   │   │       ├── Server: Page() → fetch()
│   │   │       └── Client: DuAnDetailClient
│   │   │           ├── Tabs
│   │   │           ├── PriceTrendChart [dynamic]
│   │   │           ├── NeighborhoodRadarChart [dynamic]
│   │   │           └── NeighborhoodPOIMap [dynamic]
│   │   │
│   │   └── Other routes: listings/, search/, etc.
│   │
│   ├── Route Group: (auth)
│   │   ├── Login
│   │   └── Register
│   │
│   └── Route Group: (dashboard)
│       ├── ProtectedLayout (auth check)
│       ├── Sidebar/Navbar
│       │
│       ├── Route: dashboard/ (main dashboard)
│       │
│       ├── Route: projects/ ← Project Management
│       │   ├── Page (CRUD list)
│       │   ├── new/Page (create form)
│       │   └── [id]/edit/Page (update form)
│       │
│       ├── Route: listings/ (list management)
│       ├── Route: leads/ (lead management)
│       ├── Route: analytics/ (analytics dashboard)
│       └── ...other dashboard routes
│
└── Route Group: (admin)
    └── Route: admin/ (admin panel)

🔄 Data Fetching Timeline

User navigates to /du-an/my-project-slug
│
├─ [1] generateMetadata() in Server Component
│   └─ fetchProjectBySlug(slug)  ← fetch() + normalization
│       └─ Returns Metadata { title, description, og }
│
├─ [2] Page() Server Component renders
│   └─ fetchProjectBySlug(slug)  ← fetch() again (cached)
│       └─ Pass project to Client Component
│
├─ [3] DuAnDetailClient renders
│   ├─ useEffect: Fetch neighborhood scores
│   │  └─ analyticsApi.getNeighborhoodScore()
│   │
│   └─ useEffect: Fetch POIs
│      └─ analyticsApi.getNearbyPOIs()
│
└─ Browser receives complete HTML + JS
   └─ Hydration → interactive

🧠 API Layer Organization

┌────────────────────────────────────────────────────────┐
│              API Client Layer                          │
│                                                        │
│  apiClient = {                                         │
│    get<T>(endpoint, headers)                           │
│    post<T>(endpoint, body, headers)                    │
│    patch<T>(endpoint, body, headers)                   │
│    delete<T>(endpoint, headers)                        │
│  }                                                     │
│                                                        │
│  Features:                                             │
│  • CSRF token extraction from cookies                 │
│  • Auto-refresh on 401                                │
│  • Credentials included                               │
│  • Coalesced refresh (only once)                       │
└────────────────────────────────────────────────────────┘
                      ↓
┌────────────────────────────────────────────────────────┐
│              Domain APIs                               │
│                                                        │
│  duAnApi = {                                           │
│    search(params) → /projects?...                      │
│    searchMine(params) → /projects/mine/list            │
│    getBySlug(slug) → /projects/[slug]                  │
│    getStats(id) → /projects/[id]/stats                │
│    create(payload) → POST /projects                    │
│    update(id, payload) → PATCH /projects/[id]         │
│    delete(id) → DELETE /projects/[id]                 │
│  }                                                     │
│                                                        │
│  listingsApi = { ... }                                │
│  khuCongNghiepApi = { ... }                            │
│  inquiriesApi = { ... }                                │
│  analyticsApi = { ... }                                │
│  ... (other domains)                                   │
└────────────────────────────────────────────────────────┘
                      ↓
┌────────────────────────────────────────────────────────┐
│              React Query Hooks                         │
│                                                        │
│  useProjectsSearch(params) {                           │
│    return useQuery({                                   │
│      queryKey: ['projects', 'search', params],         │
│      queryFn: () => duAnApi.search(params),            │
│    })                                                  │
│  }                                                     │
│                                                        │
│  useProjectDetail(slug) {                              │
│    return useQuery({                                   │
│      queryKey: ['projects', 'detail', slug],           │
│      queryFn: () => duAnApi.getBySlug(slug),           │
│      enabled: !!slug,                                  │
│    })                                                  │
│  }                                                     │
│                                                        │
│  ... (other hooks for each API)                        │
└────────────────────────────────────────────────────────┘
                      ↓
┌────────────────────────────────────────────────────────┐
│              Client Components                         │
│                                                        │
│  export function MyComponent() {                       │
│    const { data, isLoading } =                         │
│      useProjectsSearch(filters);                       │
│    // Use data in UI                                   │
│  }                                                     │
└────────────────────────────────────────────────────────┘

📍 Route Group Strategy

App Routes
│
├─ (public) ← No auth required, public layout
│  ├─ /du-an (browse)
│  ├─ /listings
│  ├─ /khu-cong-nghiep
│  ├─ /search
│  ├─ /agents
│  └─ ...other public pages
│
├─ (auth) ← Auth layout (login/register form)
│  ├─ /login
│  └─ /register
│
├─ (dashboard) ← Protected, dashboard layout
│  ├─ /dashboard (main)
│  ├─ /projects (CRUD)
│  ├─ /listings (CRUD)
│  ├─ /leads
│  ├─ /analytics
│  └─ ...other user pages
│
├─ (admin) ← Admin-only, admin layout
│  ├─ /admin
│  ├─ /admin/users
│  ├─ /admin/kyc
│  └─ ...other admin pages
│
└─ auth/callback/ ← Unprotected, OAuth callbacks
   ├─ /auth/callback/google
   └─ /auth/callback/zalo

🎯 Type Flow

Backend Response
    ↓
┌──────────────────────────────┐
│ Raw JSON                     │
│ {                            │
│   "id": "...",               │
│   "developer": {             │
│     "logo": "url"  ← might be missing
│   },                          │
│   "media": [...],  ← might be []
│   "amenities": [...]          │
│ }                            │
└──────────────────────────────┘
    ↓
┌──────────────────────────────┐
│ normalizeProjectDetail()      │
│ • Fill missing fields         │
│ • Rename keys (logo→logoUrl)  │
│ • Validate arrays             │
│ • Ensure no null/undefined    │
└──────────────────────────────┘
    ↓
┌──────────────────────────────┐
│ ProjectDetail Interface      │
│ {                            │
│   id: string                 │
│   developer: {               │
│     id, name, logoUrl        │
│   }                          │
│   media: ProjectMedia[]      │
│   amenities: ProjectAmenity[]│
│ }                            │
└──────────────────────────────┘
    ↓
┌──────────────────────────────┐
│ React Component              │
│ Safe to use all fields       │
│ TypeScript checks types      │
└──────────────────────────────┘

🗺️ Mapbox Integration Flow

ProjectMap Component Mounts
    │
    ├─ [1] Effect 1: Initialize map
    │   ├─ Set accessToken
    │   ├─ Create Map instance
    │   ├─ Add controls (Navigation, Attribution)
    │   └─ Store in ref
    │
    ├─ [2] Effect 2: Change map style
    │   └─ map.setStyle(newStyle)
    │
    └─ [3] Effect 3: Update markers (when projects change)
        ├─ Clear old markers
        ├─ For each project with lat/lng:
        │   ├─ Create marker element
        │   ├─ Create popup HTML
        │   ├─ Add to map
        │   └─ Extend bounds
        ├─ Fit bounds to all markers
        │   └─ map.fitBounds(bounds, padding)
        └─ Single marker? Fly to it
            └─ map.flyTo(center, zoom)

🎨 Styling Cascade

Tailwind Config
├─ Design Tokens (CSS Variables)
│  ├─ --primary: 210 100% 50%
│  ├─ --secondary: 220 13% 91%
│  ├─ --muted: 210 10% 92%
│  └─ ... (full palette)
│
├─ Color System
│  ├─ primary: hsl(var(--primary))
│  ├─ secondary: hsl(var(--secondary))
│  ├─ background: { DEFAULT, elevated, surface }
│  ├─ foreground: { DEFAULT, muted, dim }
│  └─ ... (semantic colors)
│
├─ Spacing System
│  ├─ cell: 0.5rem
│  ├─ row: 2.25rem
│  ├─ sidebar: 15rem
│  └─ ... (spacing utilities)
│
├─ Typography
│  ├─ heading-sm: 0.875rem / 1.25rem
│  ├─ heading-md: 1.125rem / 1.5rem
│  └─ ... (font sizes)
│
└─ Components Use Classes
   └─ className="text-primary bg-muted px-4 py-2 rounded-lg"
      └─ Resolved at build time