- 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>
380 lines
24 KiB
Plaintext
380 lines
24 KiB
Plaintext
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
║ GOODGO FRONTEND ARCHITECTURE & DATA FLOW DIAGRAM ║
|
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ ROUTING & LAYOUT HIERARCHY │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
app/
|
|
└── layout.tsx (global)
|
|
└── [locale]/
|
|
├── layout.tsx (i18n wrapper)
|
|
│
|
|
├── (public)/ ─────────────────── PUBLIC ROUTES
|
|
│ ├── listings/
|
|
│ │ ├── page.tsx ────────────────────────────────┐
|
|
│ │ └── [id]/page.tsx (Server RSC) ────────────┐ │
|
|
│ │ └── <ListingDetailClient/> (Client) ─┐ │ │
|
|
│ │ ├── <NeighborhoodRadarChart/> ──┐├─┼─┤ Neighborhood
|
|
│ │ ├── <NeighborhoodPOIMap/> │ │ │
|
|
│ │ └── <PriceHistoryChart/> ├─┘ │
|
|
│ ├── search/
|
|
│ ├── khu-cong-nghiep/
|
|
│ └── du-an/
|
|
│
|
|
├── (auth)/ ──────────────────── AUTH ROUTES
|
|
│ ├── login/
|
|
│ └── register/
|
|
│
|
|
├── (dashboard)/ ──────────────── PROTECTED ROUTES
|
|
│ ├── dashboard/
|
|
│ ├── analytics/
|
|
│ ├── valuation/
|
|
│ ├── industrial-parks/
|
|
│ └── reports/
|
|
│
|
|
└── (admin)/ ──────────────────── ADMIN ROUTES
|
|
└── admin/
|
|
├── users/
|
|
├── kyc/
|
|
└── moderation/
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ LISTING DETAIL PAGE FLOW │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ /[locale]/(public)/listings/[id]/page.tsx (Server RSC) │
|
|
│ ────────────────────────────────────────────────────────── │
|
|
│ • generateMetadata() → SEO, OG, schema.json │
|
|
│ • await fetchListingById(id) → Backend API │
|
|
│ • Returns: <ListingDetailClient data={...} /> │
|
|
└────┬───────────────────────────────────────────────────────┬┘
|
|
│ SSR data injection (props) │
|
|
▼ ▼
|
|
┌───────────────────────────────────────────────────────────────┐
|
|
│ <ListingDetailClient/> (Client Component, 39 KB) │
|
|
│ ───────────────────────────────────────────────────────── │
|
|
│ Layout Sections (top to bottom): │
|
|
│ │
|
|
│ 1. IMAGE GALLERY ─────────── <ImageGallery /> │
|
|
│ │
|
|
│ 2. KPI STRIP ─────────────── Price | m² | DOM | Views │
|
|
│ │
|
|
│ 3. CORE DETAILS ──────────── Address, Bedrooms, etc. │
|
|
│ │
|
|
│ 4. PRICE HISTORY ─────────── <PriceHistoryChart /> │
|
|
│ (Recharts AreaChart) │
|
|
│ │
|
|
│ 5. NEIGHBORHOOD SECTION ──── │
|
|
│ ├─ <NeighborhoodRadarChart/> │
|
|
│ │ (6 categories, 0-10 scores) │
|
|
│ │ Dynamically loaded, SSR: false │
|
|
│ │ │
|
|
│ └─ <NeighborhoodPOIMap/> │
|
|
│ (Mapbox GL with 6 POI types) │
|
|
│ Dynamically loaded, SSR: false │
|
|
│ • Schools, Hospitals, Transit, Shopping, etc. │
|
|
│ • Category filters │
|
|
│ • Distance display │
|
|
│ │
|
|
│ 6. AI ADVICE CARDS ────────── <AiAdviceCards /> │
|
|
│ (Personas) │
|
|
│ │
|
|
│ 7. SIMILAR LISTINGS ───────── Recommendations │
|
|
│ │
|
|
│ 8. AGENT CARD ────────────── Agent quality score │
|
|
│ │
|
|
│ 9. CTA SECTION ────────────── Inquiry | Compare | Estimate │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ API & STATE MANAGEMENT DATA FLOW │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────────────┐
|
|
│ Backend API │
|
|
│ (api/v1/...) │
|
|
└────────┬────────────┘
|
|
│ HTTPS + CSRF Token
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────┐
|
|
│ lib/api-client.ts │
|
|
│ ───────────────────────────────────── │
|
|
│ • Base URL: NEXT_PUBLIC_API_URL │
|
|
│ • CSRF handling (from cookies) │
|
|
│ • 401 auto-refresh + retry │
|
|
│ • Type-safe generics: <T> │
|
|
└────┬──────────────────────────────────┬──┘
|
|
│ │
|
|
├─ apiClient.get<T>() │ HTTP Methods
|
|
├─ apiClient.post<T>() │
|
|
├─ apiClient.patch<T>() │
|
|
└─ apiClient.delete<T>() │
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────┐
|
|
│ Domain-Specific API Clients │
|
|
│ ───────────────────────────────────────── │
|
|
│ • listings-api.ts → ListingDetail │
|
|
│ • analytics-api.ts → MarketReport │
|
|
│ • valuation-api.ts → AVM estimates │
|
|
│ • agents-api.ts → AgentQualityScore │
|
|
│ • khu-cong-nghiep-api.ts │
|
|
│ • du-an-api.ts │
|
|
│ • inquiries-api.ts │
|
|
└────┬─────────────────────────────────────────┘
|
|
│ Returns typed data (interfaces)
|
|
│
|
|
▼
|
|
┌────────────────────────────────────────────────┐
|
|
│ React Query (lib/hooks/) │
|
|
│ ───────────────────────────────────────── │
|
|
│ useQuery() with query keys: │
|
|
│ │
|
|
│ • useMarketReport(city, period) │
|
|
│ • usePriceTrend(district, city, type) │
|
|
│ • useListing(id) │
|
|
│ • useIndustrialParks() │
|
|
│ • useValuation() │
|
|
│ │
|
|
│ Features: │
|
|
│ ✓ Automatic caching + deduplication │
|
|
│ ✓ Background refetch intervals │
|
|
│ ✓ Error boundaries │
|
|
│ ✓ Loading states │
|
|
└────┬──────────────────────────────────────────┬┘
|
|
│ Provides data │
|
|
│ & loading/error states │
|
|
│ │
|
|
├──────────────────┬───────────────────────┤
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
Components Zustand Stores React Context
|
|
(render data) (client state) (global features)
|
|
|
|
• Listings • auth-store • QueryProvider
|
|
• Charts • comparison-store • ThemeProvider
|
|
• Maps • preferences-store • AuthProvider
|
|
• Neighborhood • NotificationsProvider
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ NEIGHBORHOOD COMPONENTS DETAIL │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
API Response (from backend)
|
|
│
|
|
├─ NeighborhoodScoreData
|
|
│ ├─ overallScore: number (0-100)
|
|
│ ├─ categories: NeighborhoodCategory[]
|
|
│ │ └─ [
|
|
│ │ { category: "education", label: "Giáo dục", score: 7.5 },
|
|
│ │ { category: "healthcare", label: "Y tế", score: 8.2 },
|
|
│ │ { category: "transport", label: "Giao thông", score: 6.8 },
|
|
│ │ { category: "shopping", label: "Mua sắm", score: 7.9 },
|
|
│ │ { category: "dining", label: "Ẩm thực", score: 8.1 },
|
|
│ │ { category: "environment", label: "Môi trường", score: 7.3 },
|
|
│ │ ]
|
|
│ ├─ pois: POIItem[]
|
|
│ │ └─ [
|
|
│ │ { id: "1", name: "School X", category: "school", lat: 10.123, lng: 105.456, distance: 500 },
|
|
│ │ { id: "2", name: "Hospital Y", category: "hospital", lat: 10.124, lng: 105.457, distance: 800 },
|
|
│ │ ...
|
|
│ │ ]
|
|
│ └─ center: { lat: 10.123, lng: 105.456 }
|
|
│
|
|
└─ Rendered as:
|
|
│
|
|
├─────────────────────────────────────────────┐
|
|
│ <NeighborhoodRadarChart/> │
|
|
│ ───────────────────────────────────────── │
|
|
│ Recharts RadarChart Component │
|
|
│ │
|
|
│ Input: categories (0-10 scores) │
|
|
│ Output: │
|
|
│ • Radar polygon visualization │
|
|
│ • Badge strips below (Tốt/TB/Yếu) │
|
|
│ • Dark/light theme CSS variables │
|
|
└─────────────────────────────────────────────┘
|
|
│
|
|
├─────────────────────────────────────────────┐
|
|
│ <NeighborhoodPOIMap/> │
|
|
│ ───────────────────────────────────────── │
|
|
│ Mapbox GL Component (Client) │
|
|
│ │
|
|
│ Features: │
|
|
│ • Displays 6 POI category types │
|
|
│ • SVG icon markers (school, hospital, etc.)│
|
|
│ • Category filter toggles │
|
|
│ • Distance display on hover │
|
|
│ • Responsive map container │
|
|
│ • Theme-aware styling │
|
|
│ • Click handlers for POI selection │
|
|
└─────────────────────────────────────────────┘
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ CHART COMPONENTS STACK │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
Recharts Library
|
|
│
|
|
├─ PriceAreaChart (price-area-chart.tsx)
|
|
│ ├─ Input: PriceAreaChartPoint[] (period, avgPriceM2)
|
|
│ ├─ Renders: AreaChart with:
|
|
│ │ • Gradient fill (green/red based on trend)
|
|
│ │ • Responsive container
|
|
│ │ • XAxis: period labels
|
|
│ │ • YAxis: formatted Vietnamese currency (tr/k)
|
|
│ │ • Tooltip: formatted prices
|
|
│ └─ Used in: Listing detail page
|
|
│
|
|
├─ DistrictHeatmap (district-heatmap.tsx - 9 KB)
|
|
│ ├─ Input: HeatmapDataPoint[] (district, avgPrice, totalListings)
|
|
│ ├─ Possibly uses Mapbox layers for geo-visualization
|
|
│ └─ Used in: Analytics page
|
|
│
|
|
├─ PriceTrendChart (price-trend-chart.tsx)
|
|
│ ├─ Multiple period comparison
|
|
│ └─ Used in: Analytics, comparative analysis
|
|
│
|
|
├─ DistrictBarChart (district-bar-chart.tsx)
|
|
│ ├─ Bar chart for district-level metrics
|
|
│ └─ Used in: Market comparisons
|
|
│
|
|
├─ AgentPerformance (agent-performance.tsx - 6 KB)
|
|
│ ├─ Agent metrics visualization
|
|
│ └─ Used in: Agent detail pages
|
|
│
|
|
└─ NeighborhoodRadarChart (neighborhood-radar-chart.tsx)
|
|
├─ Radar polygon for 6 categories (0-10 scores)
|
|
└─ Used in: Listing detail, neighborhood info
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ COMPONENT IMPORT PATTERNS │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
STATIC IMPORT (always loaded):
|
|
──────────────────────────────
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
Used for: Base UI components, utilities, small components
|
|
Bundle impact: Included in main bundle
|
|
|
|
|
|
DYNAMIC IMPORT (lazy-loaded):
|
|
──────────────────────────────
|
|
const NeighborhoodRadarChart = dynamic(
|
|
() => import('@/components/neighborhood')
|
|
.then(m => m.NeighborhoodRadarChart),
|
|
{
|
|
ssr: false, // Don't server-render (client-only due to Recharts)
|
|
loading: () => <ChartSkeleton /> // Loading UI
|
|
}
|
|
);
|
|
|
|
Used for: Heavy components (maps, charts, 3D)
|
|
Bundle impact: Code-split into separate chunk, loaded on-demand
|
|
|
|
|
|
BARREL EXPORT (for organization):
|
|
─────────────────────────────────
|
|
// components/neighborhood/index.ts
|
|
export { NeighborhoodRadarChart } from './neighborhood-radar-chart';
|
|
export { NeighborhoodPOIMap } from './neighborhood-poi-map';
|
|
|
|
// Usage:
|
|
import { NeighborhoodRadarChart, NeighborhoodPOIMap } from '@/components/neighborhood';
|
|
|
|
Benefits: Cleaner imports, single export point
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ MAPBOX GL INITIALIZATION FLOW │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
'use client' ← Must be client component (Mapbox GL requires DOM)
|
|
│
|
|
├─ import mapboxgl from 'mapbox-gl'
|
|
├─ import 'mapbox-gl/dist/mapbox-gl.css' ← CSS styles
|
|
│
|
|
├─ const mapStyle = useMapboxStyle() ← Get current theme
|
|
│ └─ Returns: MAPBOX_STYLE_LIGHT or MAPBOX_STYLE_DARK
|
|
│
|
|
├─ new mapboxgl.Map({
|
|
│ container: mapContainerRef.current,
|
|
│ style: mapStyle,
|
|
│ center: [lng, lat],
|
|
│ zoom: 14
|
|
│ })
|
|
│
|
|
├─ On theme change:
|
|
│ └─ map.setStyle(newStyle) ← Update map style reactively
|
|
│
|
|
└─ Cleanup:
|
|
└─ map?.remove() ← On unmount
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ INTERNATIONALIZATION (i18n) FLOW │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
Request: /vi/listings/123
|
|
│
|
|
├─ [locale] route parameter = "vi"
|
|
├─ Loaded from: messages/vi.json
|
|
│
|
|
└─ Component:
|
|
const t = useTranslations();
|
|
<span>{t('listings.detail.title')}</span>
|
|
│
|
|
└─ Output: Vietnamese text from translation file
|
|
|
|
|
|
Request: /en/listings/123
|
|
│
|
|
├─ [locale] route parameter = "en"
|
|
├─ Loaded from: messages/en.json
|
|
│
|
|
└─ Output: English text from translation file
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ SEO & METADATA GENERATION FLOW │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
Server Component:
|
|
generateMetadata({ params }) {
|
|
│
|
|
├─ await fetchListingById(params.id) ← Get listing data
|
|
├─ Construct title, description
|
|
├─ Get first image for OG
|
|
│
|
|
└─ Return Metadata object:
|
|
{
|
|
title: "Property Title - Price",
|
|
description: "Type | Area | Bedrooms | Address | Price",
|
|
openGraph: {
|
|
url: "https://goodgo.vn/vi/listings/123",
|
|
type: "article",
|
|
images: [{ url: "...", width: 1200, height: 630 }],
|
|
},
|
|
alternates: {
|
|
canonical: "...",
|
|
languages: { vi: "...", en: "..." }
|
|
}
|
|
}
|
|
|
|
Output: <meta> tags in <head>
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|
END OF ARCHITECTURE
|
|
═══════════════════════════════════════════════════════════════════════════════
|