- 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>
443 lines
20 KiB
Markdown
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
|
|
```
|
|
|