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

443 lines
20 KiB
Markdown

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