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>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 16:29:24 +07:00
parent 912121cf09
commit 08b96f9c2d
39 changed files with 15129 additions and 562 deletions

View File

@@ -0,0 +1,312 @@
# 🎯 Analytics Module Architecture Exploration — Complete
## Documents Created
I've prepared **3 comprehensive guides** for you:
### 1⃣ **analytics_architecture_guide.md** (36 KB)
**The Comprehensive Reference** — Read this first for deep understanding
- ✅ DDD layer breakdown with code examples
- ✅ All 24 endpoints documented
- ✅ Query handler patterns (2 styles: @Cacheable vs manual)
- ✅ Complete caching system (Redis patterns, TTLs, invalidation)
- ✅ Real code examples from `GetMarketSnapshotHandler`, `GetDistrictStatsHandler`
- ✅ Full Prisma schema for Property, Listing, MarketIndex, Valuation
- ✅ Shared module utilities (CacheService, Cacheable decorator, interceptor)
-**7-step guide: How to add GET /analytics/trending-areas endpoint**
- ✅ Testing patterns
- ✅ Error handling conventions
### 2⃣ **quick_reference.md** (8 KB)
**The Visual Quick Start** — Use this to navigate fast
- ✅ Layer stack diagram (Presentation → Application → Domain → Infrastructure)
- ✅ Request flow example (HTTP → Controller → QueryHandler → Cache → DB)
- ✅ Caching strategy matrix (when to cache, TTLs, prefixes)
- ✅ Decorators & guards cheat sheet
- ✅ Prisma schema snapshot
- ✅ Response structure with/without cache metadata
- ✅ 7-step endpoint addition checklist
### 3⃣ **file_paths_reference.md** (8 KB)
**The Navigation Map** — Find files & understand structure
- ✅ Core module files (analytics.module.ts, index.ts)
- ✅ All 24 endpoints mapped to file paths
- ✅ DTO files organized by type (request vs response)
- ✅ All 15+ query types with descriptions
- ✅ Domain, Infrastructure, and Shared layer breakdowns
- ✅ Database schema models with fields & indexes
- ✅ Directory tree with line counts
- ✅ Import patterns reference
- ✅ Key metrics & numbers
---
## ✨ Key Findings
### Architecture Pattern
```
Domain-Driven Design (DDD) + CQRS (Command Query Responsibility Segregation)
4-layer structure: Presentation → Application → Domain → Infrastructure
```
### Controllers (Entry Points)
- **AnalyticsController**: 19 endpoints (`/analytics/...`)
- **AvmController**: 5 endpoints (`/avm/...`)
- **Total**: 24 endpoints, all with guards (JWT, Quota, Rate Limit)
### Query Handlers: 2 Caching Patterns
**Pattern 1: @Cacheable Decorator** (Simpler)
```ts
@QueryHandler(GetDistrictStatsQuery)
export class GetDistrictStatsHandler {
@Cacheable({
prefix: CachePrefix.MARKET_DISTRICT,
ttl: CacheTTL.DISTRICT_STATS,
resource: 'district_stats',
keyFrom: (query) => [query.city, query.period],
})
async execute(query): Promise<DistrictStatsDto> {
return this.marketIndexRepo.getDistrictStats(query.city, query.period);
}
}
```
**Pattern 2: cache.getOrSet()** (For complex computation)
```ts
async execute(query): Promise<MarketSnapshotDto> {
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_SNAPSHOT, query.city);
return await this.cache.getOrSet(
cacheKey,
() => this.computeSnapshot(query.city), // Heavy computation
CacheTTL.MARKET_SNAPSHOT, // TTL in seconds
'market_snapshot', // Prometheus label
);
}
```
### Redis Caching Strategy
- **Cache-aside pattern**: Try Redis → if miss, call loader, store result
- **Envelope format**: `{ __v: data, cachedAt: ISO, ttlSeconds: 300 }`
- **Graceful degradation**: If Redis down, calls loader directly (no error)
- **Metrics**: `cache_hit_total`, `cache_miss_total`, `cache_degradation_total`
- **TTLs**: Dashboard=300s, Reports=900s, Trends=1800s, Predictions=NO_CACHE
### Prisma Schema
- **Property**: id, type, address, district, city, location (PostGIS Point), area, rooms, etc.
- **Listing**: id, propertyId, sellerId, status, priceVND (BigInt), aiPriceEstimate, publishedAt
- **MarketIndex**: district, city, propertyType, period; medianPrice (BigInt), avgPriceM2, stats
- **Valuation**: id, propertyId, estimatedPrice, confidence, method, features (Json)
### DDD Layers
1. **Presentation**: Controllers, DTOs, Interceptors
2. **Application**: Query/Command Handlers (@QueryHandler, @CommandHandler)
3. **Domain**: Entities, Repository Interfaces, Service Interfaces
4. **Infrastructure**: Prisma Repositories, HTTP Services, External clients
### Shared Module Utilities
- **CacheService**: Core cache-aside with Redis
- **@Cacheable**: Method decorator for handlers
- **CacheMetaInterceptor**: Wraps responses with `{ data, cacheMeta }`
- **LoggerService**: Winston-based logging
- **PrismaService**: ORM wrapper
- **RedisService**: Redis client wrapper
- **Guards**: JWT, Quota, Rate Limit
---
## 🚀 Quick Start: Adding New Endpoint
Example: **GET /analytics/trending-areas**
### 7-Step Process
1. **Request DTO** (`presentation/dto/get-trending-areas.dto.ts`)
```ts
export class GetTrendingAreasDto {
@IsOptional() city?: string = 'Hồ Chí Minh';
@IsOptional() propertyType?: PropertyType;
@IsOptional() @Min(1) limit?: number = 10;
}
```
2. **Query Class** (`application/queries/get-trending-areas/get-trending-areas.query.ts`)
```ts
export class GetTrendingAreasQuery {
constructor(
public readonly city: string,
public readonly propertyType: PropertyType | undefined,
public readonly limit: number,
) {}
}
```
3. **Handler** (`application/queries/get-trending-areas/get-trending-areas.handler.ts`)
```ts
@QueryHandler(GetTrendingAreasQuery)
export class GetTrendingAreasHandler implements IQueryHandler {
@Cacheable({
prefix: CachePrefix.TRENDING_AREAS,
ttl: CacheTTL.TRENDING_AREAS,
resource: 'trending_areas',
keyFrom: (query) => [query.city, query.propertyType, query.limit],
})
async execute(query: GetTrendingAreasQuery): Promise<GetTrendingAreasDto> {
// Your logic here
return { city: query.city, areas: [...], cachedAt: null, nextRefreshAt: null };
}
}
export interface GetTrendingAreasDto {
city: string;
areas: TrendingAreaDto[];
cachedAt: string | null;
nextRefreshAt: string | null;
}
```
4. **Register Handler** (in `analytics.module.ts`)
```ts
const QueryHandlers = [
// ... existing
GetTrendingAreasHandler, // Add here
];
```
5. **Controller Method** (in `analytics.controller.ts`)
```ts
@ApiBearerAuth('JWT')
@UseGuards(JwtAuthGuard, QuotaGuard)
@RequireQuota('analytics_queries')
@Get('trending-areas')
@ApiOperation({ summary: 'Get trending districts' })
@ApiResponse({ status: 200, description: 'Trending areas retrieved' })
async getTrendingAreas(@Query() dto: GetTrendingAreasDto): Promise<GetTrendingAreasDto> {
return this.queryBus.execute(
new GetTrendingAreasQuery(dto.city || 'Hồ Chí Minh', dto.propertyType, dto.limit || 10),
);
}
```
6. **Export DTOs** (in `presentation/dto/index.ts`)
```ts
export * from './get-trending-areas.dto';
```
7. **Test** (in `__tests__/get-trending-areas.handler.spec.ts`)
```ts
it('should return trending areas', async () => {
const query = new GetTrendingAreasQuery('Hồ Chí Minh', undefined, 10);
const result = await handler.execute(query);
expect(result.areas).toBeDefined();
});
```
---
## 📊 Architecture Decision Points
| Decision | Current Approach | Why |
|----------|------------------|-----|
| **Caching** | Redis + cache-aside | TTL-based expiry is simple & performant |
| **Cache Invalidation** | Prefix-based SCAN | Non-blocking, doesn't require key enumeration |
| **Cache Metadata** | AsyncLocalStorage + Interceptor | Per-request context without global state |
| **Query Patterns** | CQRS with QueryBus | Separates reads from writes, enables caching layer |
| **Rate Limiting** | EndpointRateLimitGuard | Per-endpoint control, different rates for different ops |
| **Quota Metering** | @RequireQuota decorator | Subscription-aware, tracks usage |
| **Response Format** | DTO with cache metadata | Frontend knows freshness of data |
| **Error Handling** | DomainException + InternalServerError | Differentiates logic errors from system errors |
| **Graceful Degradation** | Cache bypass if Redis down | Service stays up during Redis maintenance |
---
## 🎯 Core Conventions to Remember
✅ **Always cache with TTL**
- Dashboard tiles: 300s
- Aggregations: 300s
- Reports: 900s
- Trends: 1800s
- Predictions: NO CACHE (always fresh)
✅ **Always use CacheService.buildKey()**
- Ensures deterministic, lowercase keys
- Replaces spaces with underscores
- Format: `prefix:param1:param2:param3`
✅ **Always wrap handlers in try-catch**
```ts
try {
// logic
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(...);
throw new InternalServerErrorException('...');
}
```
✅ **Always return DTO with null metadata**
```ts
return {
// ...data fields
cachedAt: null, // Filled by CacheMetaInterceptor
nextRefreshAt: null, // Filled by CacheMetaInterceptor
};
```
✅ **Always use @UseInterceptors(CacheMetaInterceptor)**
- On controllers to wrap response
- Adds: `{ data: T, cacheMeta: { cachedAt, nextRefreshAt, source } }`
✅ **Always add guards to endpoints**
```ts
@UseGuards(JwtAuthGuard, QuotaGuard) // Auth + quota check
@RequireQuota('analytics_queries') // Meter usage
@EndpointRateLimit({ limit: 10, windowSeconds: 60 }) // Rate limit if needed
```
---
## 📁 File Summary
| File Path | Purpose | Lines |
|-----------|---------|-------|
| `presentation/controllers/analytics.controller.ts` | Main endpoints | 331 |
| `presentation/controllers/avm.controller.ts` | Valuation endpoints | 171 |
| `application/queries/*/get-*.handler.ts` | Query execution + caching | 50-100 ea |
| `domain/repositories/market-index.repository.ts` | Repository interface | 58 |
| `infrastructure/repositories/prisma-market-index.repository.ts` | Prisma implementation | 150+ |
| `presentation/interceptors/cache-meta.interceptor.ts` | Response wrapper | 61 |
| `../shared/infrastructure/cache.service.ts` | Redis cache layer | 191 |
| `../shared/infrastructure/decorators/cacheable.decorator.ts` | @Cacheable | 57 |
| `analytics.module.ts` | NestJS module definition | 102 |
---
## 🎓 Next Steps
1. **Read** → Start with `quick_reference.md` for visual understanding
2. **Reference** → Use `file_paths_reference.md` to find specific files
3. **Deep Dive** → Study `analytics_architecture_guide.md` for patterns & code
4. **Build** → Follow the 7-step checklist to add your first endpoint
5. **Test** → Create query handler spec following existing patterns
---
## Questions to Validate Understanding
After reading the guides, you should be able to answer:
1. What are the 4 DDD layers and what goes in each?
2. How does the cache-aside pattern work when Redis is down?
3. What's the difference between @Cacheable and cache.getOrSet()?
4. Why do response DTOs have `cachedAt: null` and `nextRefreshAt: null`?
5. How is the cache key built deterministically?
6. What TTLs are used for different endpoint types?
7. What guards are required on all analytics endpoints?
8. How do you add a new GET endpoint in 7 steps?
9. What's the purpose of CacheMetaInterceptor?
10. How does graceful degradation work if Redis is unavailable?
---

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
# Quick Reference: Analytics Module Architecture
## 🏗️ Layer Stack (DDD + CQRS)
```
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ @Controller('analytics') / @Controller('avm') ││
│ │ ├─ @Get endpoints (call QueryBus) ││
│ │ ├─ @Post endpoints (call QueryBus or CommandBus) ││
│ │ └─ Guards: JwtAuthGuard, QuotaGuard, EndpointRateLimitGuard ││
│ ├─ DTOs: RequestDto, ResponseDto (validation) ││
│ └─ Interceptors: CacheMetaInterceptor (wraps response) ││
└─────────────────────────────────────────────────────────────────┘
↓ QueryBus.execute()
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER (CQRS) │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ @QueryHandler(SomeQuery) ││
│ │ ├─ Receives Query class instance ││
│ │ ├─ Injects dependencies (Prisma, Cache, Logger) ││
│ │ ├─ Caching: @Cacheable decorator OR cache.getOrSet() ││
│ │ └─ Returns ResponseDto ││
│ └─ Handlers indexed in analytics.module.ts ││
└─────────────────────────────────────────────────────────────────┘
↓ Injected repos/services
┌─────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER (Business Logic) │
│ ├─ Entities: MarketIndexEntity, ValuationEntity │
│ ├─ Repository Interfaces: IMarketIndexRepository, etc. │
│ ├─ Result DTOs: MarketReportResult, DistrictStatsResult │
│ └─ Services: IAVMService, INeighborhoodScoreService │
└─────────────────────────────────────────────────────────────────┘
↓ Injected implementation
┌─────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ ├─ Repositories: PrismaMarketIndexRepository (implements iface) │
│ ├─ Services: HttpAVMService, PrismaAVMService │
│ └─ External: Prisma ORM, PostgreSQL, Redis, HTTP clients │
└─────────────────────────────────────────────────────────────────┘
```
## 📂 File Structure Quick Map
```
apps/api/src/modules/analytics/
├── presentation/
│ ├── controllers/
│ │ ├── analytics.controller.ts (14 GET/POST endpoints)
│ │ └── avm.controller.ts (5 GET/POST endpoints)
│ ├── dto/ (15+ DTO files)
│ │ ├── get-market-snapshot.dto.ts
│ │ ├── predict-valuation.dto.ts
│ │ └── ...
│ └── interceptors/
│ └── cache-meta.interceptor.ts (wraps response)
├── application/
│ ├── queries/ (15+ query types)
│ │ ├── get-market-snapshot/
│ │ │ ├── .query.ts (Q: GetMarketSnapshotQuery)
│ │ │ └── .handler.ts (@QueryHandler + cache logic)
│ │ ├── get-district-stats/
│ │ ├── get-price-trend/
│ │ ├── predict-valuation/
│ │ └── ...
│ ├── commands/ (3 command types)
│ ├── event-handlers/ (1 event handler)
│ └── queries/_shared/ (shared utilities)
├── domain/
│ ├── entities/
│ │ ├── market-index.entity.ts
│ │ └── valuation.entity.ts
│ ├── repositories/ (interfaces only)
│ │ ├── market-index.repository.ts
│ │ │ └── IMarketIndexRepository interface
│ │ └── valuation.repository.ts
│ ├── services/ (interfaces only)
│ │ ├── avm-service.ts
│ │ └── neighborhood-score.service.ts
│ └── events/
│ └── market-index-updated.event.ts
├── infrastructure/
│ ├── repositories/
│ │ ├── prisma-market-index.repository.ts (implements)
│ │ └── prisma-valuation.repository.ts (implements)
│ └── services/
│ ├── http-avm.service.ts (calls Python AI)
│ ├── prisma-avm.service.ts (fallback)
│ ├── http-neighborhood-score.service.ts
│ ├── prisma-neighborhood-score.service.ts
│ ├── ai-service.client.ts (Claude API)
│ └── market-index-cron.service.ts
├── analytics.module.ts (NestJS module, registers all)
├── index.ts (exports)
└── README.md
```
## 🔄 Request Flow Example
```
HTTP GET /analytics/market-snapshot?city=Ho Chi Minh
1. AnalyticsController.getMarketSnapshot(@Query dto)
└─ Validates DTO (class-validator)
└─ Calls queryBus.execute(new GetMarketSnapshotQuery(...))
2. QueryBus routes to GetMarketSnapshotHandler
└─ Handler caches key: "cache:analytics:market_snapshot:ho_chi_minh"
└─ Calls cache.getOrSet(key, computeSnapshot, 300s, 'market_snapshot')
3. CacheService.getOrSet():
├─ IF Redis HIT: return cached value, increment cache_hit_total
└─ IF MISS: call computeSnapshot(), store in Redis, increment cache_miss_total
4. GetMarketSnapshotHandler.computeSnapshot():
├─ Parallel queries:
│ ├─ listing.aggregate() → count, avg price
│ ├─ $queryRaw PERCENTILE_CONT → median
│ ├─ $queryRaw AVG(EXTRACT...) → days on market
│ └─ computePriceChangePct (3x for 1d/7d/30d)
└─ Returns MarketSnapshotDto
5. CacheMetaInterceptor wraps response:
└─ Transforms: MarketSnapshotDto
└─ Into: { data: MarketSnapshotDto, cacheMeta: { cachedAt, nextRefreshAt, source } }
6. HTTP 200 with wrapped response
```
## 💾 Caching Strategy
### When to Cache
```
✅ Dashboard tiles → 300s TTL (5 min)
✅ Aggregations (district stats) → 300s TTL
✅ Market reports → 900s TTL (15 min)
✅ Historical trends → 1800s TTL (30 min)
❌ AI predictions → NO CACHE (always fresh)
❌ User-specific data → NO CACHE (personalized)
```
### Cache Prefix Patterns
```
Prefix Example Key
────────────────────────────────────────────────────────
MARKET_SNAPSHOT cache:analytics:market_snapshot:ho_chi_minh
MARKET_DISTRICT cache:market:district:ho_chi_minh:2024_q1
MARKET_TREND cache:market:trend:q1:ho_chi_minh:apartment:...
TRENDING_AREAS cache:analytics:trending_areas:ho_chi_minh:...
VALUATION cache:valuation:prop_123
```
### Cache Invalidation
```
Automatic: Redis TTL expires
Manual: cache.invalidateByPrefix(CachePrefix.MARKET_DISTRICT)
Scans "cache:market:district:*" and deletes all matches
```
## 🛡️ Decorators & Guards
```
┌─ @ApiBearerAuth('JWT') ← Swagger annotation
├─ @UseGuards(JwtAuthGuard) ← Requires JWT token
├─ @UseGuards(QuotaGuard) ← Checks subscription quota
├─ @RequireQuota('analytics_queries') ← Meters usage
├─ @EndpointRateLimit({ limit: 10, windowSeconds: 60 })
├─ @UseGuards(EndpointRateLimitGuard) ← Rate limiter
├─ @UseInterceptors(CacheMetaInterceptor) ← Wraps response
└─ @ApiOperation({ summary: '...' })
@ApiResponse({ status: 200, description: '...' })
@ApiResponse({ status: 403, description: 'Quota exceeded' })
(Swagger documentation)
```
## 📊 Prisma Schema Snapshot
```
Property
├─ id, propertyType (APARTMENT, VILLA, ...)
├─ address, ward, district, city
├─ location (PostGIS geometry Point)
├─ areaM2, bedrooms, bathrooms, floors
├─ yearBuilt, furnishing, condition
├─ createdAt, updatedAt
└─ Indexes: [propertyType], [district, city], [location (Gist)]
Listing
├─ id, propertyId (FK), sellerId (FK)
├─ transactionType (SALE, RENT)
├─ status (ACTIVE, SOLD, EXPIRED, ...)
├─ priceVND (BigInt, CHECK > 0)
├─ pricePerM2, aiPriceEstimate, aiConfidence
├─ publishedAt, expiresAt, createdAt
└─ Indexes: [status], [sellerId, status], [status, publishedAt]
MarketIndex
├─ district, city, propertyType, period (2024-Q1)
├─ medianPrice (BigInt), avgPriceM2
├─ totalListings, daysOnMarket
├─ inventoryLevel, absorptionRate, yoyChange
└─ Unique: [district, city, propertyType, period]
```
## 🎯 Response Structure
```
WITH CacheMetaInterceptor (@UseInterceptors):
{
"data": {
"city": "Hồ Chí Minh",
"activeCount": 2500,
...
},
"cacheMeta": {
"cachedAt": "2024-04-21T10:30:00Z",
"nextRefreshAt": "2024-04-21T10:35:00Z",
"source": "cache"
}
}
WITHOUT interceptor (plain DTO):
{
"city": "Hồ Chí Minh",
"activeCount": 2500,
...
}
```
## 🚀 Adding Endpoint: 7-Step Checklist
- [ ] Create Request DTO: `presentation/dto/get-*.dto.ts`
- [ ] Create Query: `application/queries/get-*/get-*.query.ts`
- [ ] Create Handler: `application/queries/get-*/get-*.handler.ts`
- [ ] @QueryHandler decorator
- [ ] @Cacheable or cache.getOrSet()
- [ ] Try-catch with logger
- [ ] Update module: Add handler to QueryHandlers array
- [ ] Add controller method: `analytics.controller.ts` or `avm.controller.ts`
- [ ] @Get or @Post
- [ ] Guards & decorators (@UseGuards, @RequireQuota, etc.)
- [ ] Swagger annotations (@ApiOperation, @ApiResponse)
- [ ] Export DTOs from `presentation/dto/index.ts`
- [ ] Test: Query handler test, integration test

View File

@@ -0,0 +1,384 @@
# Analytics Module — File Paths & Quick Reference
## 🔗 Core Module Files
```
/apps/api/src/modules/analytics/
├── analytics.module.ts
│ └─ Registers all handlers, repositories, services
│ └─ Module metadata: imports, controllers, providers, exports
│ └─ KEY: CommandHandlers, QueryHandlers, EventHandlers arrays
├── index.ts
│ └─ Public exports of analytics module
└── README.md
└─ Module documentation
```
## 🎯 Controllers (Entry Points)
```
/apps/api/src/modules/analytics/presentation/controllers/
analytics.controller.ts (19 endpoints)
├─ GET /analytics/market-report
├─ GET /analytics/market-snapshot
├─ GET /analytics/price-trend
├─ GET /analytics/heatmap
├─ GET /analytics/district-stats
├─ GET /analytics/valuation (query param)
├─ POST /analytics/valuation (body)
├─ POST /analytics/valuation/batch
├─ GET /analytics/valuation/history/:propertyId
├─ POST /analytics/valuation/compare
├─ GET /analytics/neighborhoods/:district/score
├─ GET /analytics/pois/nearby
├─ POST /analytics/listings/:id/ai-advice
└─ POST /analytics/projects/:id/ai-advice
avm.controller.ts (5 endpoints)
├─ POST /avm/batch
├─ GET /avm/history/:propertyId
├─ GET /avm/compare?ids=...
├─ GET /avm/explain?valuationId=...
└─ POST /avm/industrial
```
## 📋 DTOs (Requests & Responses)
```
/apps/api/src/modules/analytics/presentation/dto/
REQUEST DTOs (from @Query/@Body):
├─ get-district-stats.dto.ts (city, period)
├─ get-heatmap.dto.ts (city, period)
├─ get-market-report.dto.ts (city, period, propertyType)
├─ get-market-snapshot.dto.ts (city, propertyType)
├─ get-price-trend.dto.ts (district, city, propertyType, periods)
├─ get-valuation.dto.ts (propertyId | lat/lng/areaM2)
├─ get-nearby-pois.dto.ts (lat, lng, radius, limit)
├─ predict-valuation.dto.ts (20+ fields, v1 & v2)
├─ batch-valuation.dto.ts (propertyIds: string[])
├─ valuation-history.dto.ts (limit)
├─ valuation-comparison.dto.ts (propertyIds)
├─ avm-compare-query.dto.ts (ids)
├─ avm-explain-query.dto.ts (valuationId)
├─ industrial-valuation.dto.ts (30+ industrial fields)
└─ get-trending-areas.dto.ts (city, propertyType, limit, period)
RESPONSE DTOs (exported from handlers):
(See handler files below)
```
## 🔄 Queries (CQRS Pattern)
```
/apps/api/src/modules/analytics/application/queries/
STRUCTURE OF EACH QUERY TYPE:
get-market-snapshot/
├─ get-market-snapshot.query.ts
│ └─ export class GetMarketSnapshotQuery { ... }
└─ get-market-snapshot.handler.ts
├─ @QueryHandler(GetMarketSnapshotQuery)
├─ execute(query): Promise<MarketSnapshotDto>
└─ export interface MarketSnapshotDto { ... }
ALL QUERY TYPES (15+):
├─ get-market-snapshot/ ..................... Dashboard overview
├─ get-district-stats/ ..................... Stats aggregation
├─ get-price-trend/ ........................ Time-series data
├─ get-heatmap/ ........................... Geographic visualization
├─ get-valuation/ ......................... Single valuation
├─ predict-valuation/ ..................... AI prediction
├─ batch-valuation/ ....................... Multiple valuations
├─ valuation-history/ ..................... Time-series valuations
├─ valuation-comparison/ .................. Side-by-side comparison
├─ valuation-explanation/ ................. Model drivers
├─ get-neighborhood-score/ ................ Neighborhood quality
├─ get-nearby-pois/ ....................... Point of interests
├─ get-listing-ai-advice/ ................. Claude analysis
├─ get-project-ai-advice/ ................. Project analysis
├─ industrial-valuation/ .................. Industrial rent
├─ get-market-report/ ..................... Detailed report
└─ get-trending-areas/ .................... Trending districts
```
## 🏛️ Domain Layer
```
/apps/api/src/modules/analytics/domain/
repositories/ (Interfaces only — NO IMPLEMENTATION)
├─ market-index.repository.ts
│ ├─ export const MARKET_INDEX_REPOSITORY = Symbol(...)
│ ├─ export interface IMarketIndexRepository {
│ │ findById(id)
│ │ findByKey(district, city, propertyType, period)
│ │ save(entity)
│ │ update(entity)
│ │ getMarketReport(city, period, propertyType?)
│ │ getHeatmap(city, period)
│ │ getPriceTrend(district, city, propertyType, periods)
│ │ getDistrictStats(city, period)
│ │ }
│ └─ Result interfaces: MarketReportResult, HeatmapDataPoint, etc.
└─ valuation.repository.ts
├─ export const VALUATION_REPOSITORY = Symbol(...)
└─ export interface IValuationRepository { ... }
entities/
├─ market-index.entity.ts
│ └─ Domain logic for market data aggregation
└─ valuation.entity.ts
└─ Domain logic for property valuation
services/
├─ avm-service.ts
│ └─ export const AVM_SERVICE = Symbol(...)
│ └─ IAVMService interface: predict(), getComparables(), etc.
└─ neighborhood-score.service.ts
└─ INeighborhoodScoreService interface
events/
└─ market-index-updated.event.ts
└─ Domain event when market data updates
```
## 🔧 Infrastructure Layer
```
/apps/api/src/modules/analytics/infrastructure/
repositories/ (IMPLEMENTATIONS)
├─ prisma-market-index.repository.ts
│ ├─ @Injectable()
│ └─ class PrismaMarketIndexRepository implements IMarketIndexRepository {
│ └─ Uses PrismaService for data access
└─ prisma-valuation.repository.ts
└─ class PrismaValuationRepository implements IValuationRepository
services/ (IMPLEMENTATIONS)
├─ http-avm.service.ts
│ ├─ @Injectable()
│ ├─ Calls Python AI service (HTTP client)
│ └─ Falls back to PrismaAVMService if Python is down
├─ prisma-avm.service.ts
│ ├─ @Injectable()
│ └─ Fallback ML model using Prisma data
├─ http-neighborhood-score.service.ts
│ ├─ HTTP proxy to external scoring service
│ └─ Falls back to PrismaNeighborhoodScoreService
├─ prisma-neighborhood-score.service.ts
│ └─ In-DB scoring logic
├─ ai-service.client.ts
│ ├─ Wrapper around Anthropic SDK
│ └─ Calls Claude API for AI analysis
└─ market-index-cron.service.ts
└─ Scheduled job to update MarketIndex table
```
## 🎨 Interceptors
```
/apps/api/src/modules/analytics/presentation/interceptors/
cache-meta.interceptor.ts
├─ @Injectable() CacheMetaInterceptor
├─ Wraps response: T => { data: T; cacheMeta: {...} }
├─ cacheMeta includes: cachedAt, nextRefreshAt, source
└─ Applied via @UseInterceptors(CacheMetaInterceptor)
```
## 📦 Shared Module (Reusable Utilities)
```
/apps/api/src/modules/shared/
infrastructure/
cache.service.ts
├─ @Injectable() CacheService
├─ async getOrSet<T>(key, loader, ttl, resource)
│ └─ Cache-aside pattern
│ └─ Metrics: cache_hit_total, cache_miss_total, cache_degradation_total
├─ async invalidate(key)
├─ async invalidateByPrefix(prefix)
├─ static buildKey(prefix, ...parts)
└─ Graceful degradation when Redis is down
decorators/
├─ cacheable.decorator.ts
│ ├─ @Cacheable(options) method decorator
│ └─ Declarative caching for query handlers
└─ other decorators...
cache-meta.store.ts
├─ export const cacheMetaStorage = new AsyncLocalStorage()
└─ Per-request storage of cache metadata
logger.service.ts
├─ @Injectable() LoggerService
├─ log(), warn(), error() with context
└─ Winston integration
prisma.service.ts
├─ @Injectable() PrismaService
├─ Wrapper around Prisma Client
└─ Handles connection lifecycle
redis.service.ts
├─ @Injectable() RedisService
├─ get(), set(), del(), scan()
└─ Health check & graceful degradation
guards/
├─ endpoint-rate-limit.guard.ts
├─ ... other guards
└─ auth module exports JwtAuthGuard
shared.module.ts
└─ Registers all shared services
```
## 📊 Database Schema
```
prisma/schema.prisma
Models relevant to analytics:
├─ Property
│ ├─ id, propertyType, address, district, city
│ ├─ location (PostGIS Point)
│ ├─ areaM2, bedrooms, bathrooms, floors
│ └─ Indexes: [propertyType], [district, city], [location]
├─ Listing
│ ├─ id, propertyId (FK), sellerId (FK)
│ ├─ status (ACTIVE, SOLD, EXPIRED, ...)
│ ├─ priceVND (BigInt), pricePerM2, publishedAt
│ ├─ aiPriceEstimate, aiConfidence (for AVM)
│ └─ Indexes: [status], [sellerId, status], [publishedAt]
├─ MarketIndex
│ ├─ district, city, propertyType, period
│ ├─ medianPrice (BigInt), avgPriceM2
│ ├─ totalListings, daysOnMarket, inventoryLevel
│ └─ Unique: [district, city, propertyType, period]
├─ Valuation
│ ├─ id, propertyId (FK)
│ ├─ estimatedPrice (BigInt), confidence
│ ├─ method (AVM_v1, AVM_v2, MANUAL)
│ ├─ features (Json), comparables (Json), explainers (Json)
│ └─ Index: [propertyId, valuationDate DESC]
└─ ProjectDevelopment
├─ id, slug, developer
├─ location (PostGIS Point)
├─ minPrice, maxPrice, pricePerM2Range
└─ Index: [district, city], [location]
```
## 🌳 Directory Tree Summary
```
goodgo-platform-ai/
└─ apps/api/src/modules/
├─ analytics/ (this module) .................. ~2000 LOC
│ ├─ presentation/
│ │ ├─ controllers/
│ │ │ ├─ analytics.controller.ts ......... 331 lines
│ │ │ └─ avm.controller.ts .............. 171 lines
│ │ ├─ dto/ (15+ files)
│ │ └─ interceptors/
│ │ └─ cache-meta.interceptor.ts ....... 61 lines
│ ├─ application/
│ │ ├─ queries/ (15+ handlers)
│ │ ├─ commands/ (3 handlers)
│ │ └─ event-handlers/
│ ├─ domain/
│ │ ├─ repositories/ (2 interfaces)
│ │ ├─ entities/ (2 entities)
│ │ ├─ services/ (2 interfaces)
│ │ └─ events/
│ ├─ infrastructure/
│ │ ├─ repositories/ (2 implementations)
│ │ └─ services/ (6 implementations)
│ └─ analytics.module.ts
├─ shared/ ............................. Reusable utilities
│ ├─ infrastructure/
│ │ ├─ cache.service.ts ............... 191 lines (core pattern)
│ │ ├─ decorators/
│ │ │ └─ cacheable.decorator.ts ....... 57 lines
│ │ ├─ cache-meta.store.ts
│ │ ├─ logger.service.ts
│ │ ├─ prisma.service.ts
│ │ ├─ redis.service.ts
│ │ └─ guards/ (rate limit, auth, etc)
│ └─ shared.module.ts
├─ auth/ ............................. Authentication
├─ subscriptions/ ..................... Quota & billing
├─ listings/ ......................... Listing management
└─ [other modules]
```
## 🔑 Key Imports Pattern
```ts
// In analytics.controller.ts
import { QueryBus } from '@nestjs/cqrs';
import { JwtAuthGuard } from '@modules/auth';
import { EndpointRateLimit, EndpointRateLimitGuard } from '@modules/shared';
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
// In query handlers
import {
DomainException,
CacheService,
CachePrefix,
CacheTTL,
Cacheable,
LoggerService,
PrismaService,
} from '@modules/shared';
import { MARKET_INDEX_REPOSITORY, type IMarketIndexRepository } from '../../domain/...';
// In infrastructure repositories
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared';
import { type IMarketIndexRepository } from '../../domain/...';
```
## 🚀 Key Numbers
| Metric | Value |
|--------|-------|
| Controllers | 2 |
| Endpoints | 24 |
| Query Handlers | 15+ |
| DTOs | 15+ |
| Repository Interfaces | 2 |
| Repository Implementations | 2 |
| Services (interfaces) | 2 |
| Services (implementations) | 6+ |
| Cache Prefixes | 10+ |
| Cache TTLs | 20+ |
| Total LOC (analytics module) | ~2000 |
| Total LOC (shared/cache) | ~250 |

View File

@@ -0,0 +1,379 @@
╔══════════════════════════════════════════════════════════════════════════════╗
║ 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
═══════════════════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,468 @@
# GoodGo Platform AI - Next.js Frontend Exploration Report
## 1. Overall Directory Structure
### Root Layout
```
/Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web/
├── app/ # Next.js App Router directory
├── components/ # Reusable React components
├── lib/ # API clients, hooks, utilities, stores
├── i18n/ # Internationalization config
├── messages/ # Translation files
├── public/ # Static assets
├── node_modules/ # Dependencies
└── package.json # Project metadata & dependencies
```
### Key Dependencies (from package.json)
- **Framework**: Next.js 15.5.14, React 18.3.0
- **State Management**: Zustand 5.0.12 (lightweight state), @tanstack/react-query 5.96.2 (server state)
- **Forms**: react-hook-form 7.72.1, @hookform/resolvers 5.2.2, zod 4.3.6
- **Mapping**: mapbox-gl 3.21.0, @types/mapbox-gl 3.5.0
- **Charts**: recharts 3.8.1
- **UI**: Tailwind CSS 3.4.0, class-variance-authority 0.7.1
- **Icons**: lucide-react 1.7.0
- **Other**: next-intl 4.9.0, next-themes 0.4.6, socket.io-client 4.8.3, html2canvas + jspdf
---
## 2. App Router Structure (Next.js 15 App Router)
### Directory: `/app/[locale]/`
Routes are organized by locale (vi/en) with multiple layout groups:
```
app/[locale]/
├── (public)/ # Public routes (no auth required)
│ ├── listings/
│ │ ├── page.tsx # Listings search/browse page
│ │ └── [id]/page.tsx # Individual listing detail page
│ ├── search/ # Advanced search interface
│ ├── khu-cong-nghiep/ # Industrial parks (Vietnamese)
│ ├── du-an/ # Projects
│ ├── bao-cao/ # Reports
│ ├── agents/[id]/ # Agent profiles
│ └── payment/return/ # Payment callbacks
├── (auth)/ # Authentication routes
│ ├── login/page.tsx
│ ├── register/page.tsx
│ └── auth/callback/ # OAuth callbacks (Google, Zalo)
├── (dashboard)/ # Protected routes (auth required)
│ ├── dashboard/ # Main dashboard
│ ├── analytics/ # Market analytics
│ ├── industrial-parks/ # Industrial park management
│ ├── valuation/ # Valuation service
│ ├── reports/ # User's reports
│ └── ...
├── (admin)/ # Admin routes
│ └── admin/
│ ├── users/
│ ├── kyc/
│ ├── moderation/
│ └── ...
├── auth/callback/ # Special auth callbacks
│ ├── zalo/page.tsx
│ └── google/page.tsx
└── [locale]/layout.tsx # Root layout wrapping all routes
```
---
## 3. Components Directory Structure
### Organized by Domain:
```
components/
├── listings/ # Listing-related components
│ ├── listing-detail-client.tsx (39KB - main detail component)
│ ├── listing-form-steps.tsx (14KB - form wizard)
│ ├── ai-advice-cards.tsx (9KB - AI recommendations)
│ ├── image-gallery.tsx
│ ├── image-lightbox.tsx
│ ├── image-upload.tsx
│ ├── inquiry-modal.tsx
│ ├── price-history-chart.tsx (uses recharts)
│ ├── social-share.tsx
│ └── sparkline.tsx
├── neighborhood/ # Neighborhood analysis components
│ ├── neighborhood-poi-map.tsx (11KB - POI map with Mapbox)
│ ├── neighborhood-radar-chart.tsx (2KB - radar chart)
│ ├── neighborhood-score.tsx (2KB - score display)
│ ├── types.ts # Shared types & config
│ └── index.ts # Barrel export
├── map/ # Map components
│ ├── listing-map.tsx (10KB - listings on Mapbox)
│ └── location-picker.tsx (9KB - location selection)
├── charts/ # Recharts-based charts
│ ├── price-area-chart.tsx (2KB - trend visualization)
│ ├── price-trend-chart.tsx
│ ├── district-bar-chart.tsx
│ ├── district-heatmap.tsx (9KB - with Mapbox)
│ └── agent-performance.tsx
├── design-system/ # Reusable UI primitives & patterns
│ ├── badge.tsx
│ ├── kpi-card.tsx # Key performance indicator card
│ ├── stat-card.tsx # Statistics card
│ ├── ticker-strip.tsx # Horizontal ticker
│ ├── price-delta.tsx # Price change indicator
│ ├── signal.tsx # Up/down signal display
│ ├── numeric.tsx # Formatted number display
│ ├── status-chip.tsx
│ ├── empty-state.tsx
│ ├── skeleton.tsx
│ ├── data-table.tsx # @tanstack/react-table wrapper
│ ├── dashboard-layout.tsx
│ ├── density-provider.tsx # Density/compact mode
│ └── index.ts
├── khu-cong-nghiep/ # Industrial park domain
├── du-an/ # Project domain
├── agents/ # Real estate agents
├── valuation/ # Valuation/AVM features
├── search/ # Search interface
├── comparison/ # Property comparison
├── leads/ # Lead management
├── inquiries/ # Inquiry management
├── reports/ # Report generation
├── chuyen-nhuong/ # Transfer/ownership change
├── notifications/
├── auth/
├── providers/ # React context providers
│ ├── auth-provider.tsx
│ ├── query-provider.tsx # React Query with error boundary
│ ├── theme-provider.tsx # next-themes integration
│ ├── notifications-provider.tsx
│ └── web-vitals.tsx
├── seo/ # SEO components
│ └── json-ld.tsx # JSON-LD schema generation
├── subscription/
├── ui/ # Base UI components (from shadcn/ui)
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ └── ... (standard shadcn components)
```
---
## 4. Existing Neighborhood Components
### Key Files:
- **`components/neighborhood/types.ts`**
- `NeighborhoodCategory`: score category with label, score (0-10), icon
- `POIItem`: Point of Interest (school, hospital, etc.) with lat/lng
- `NeighborhoodScoreData`: aggregated neighborhood data
- `POI_CATEGORY_CONFIG`: config for 6 categories (school, hospital, transit, shopping, restaurant, park)
- `DEFAULT_CATEGORIES`: default scoring categories
- **`components/neighborhood/neighborhood-poi-map.tsx`**
- Uses Mapbox GL with hardcoded SVG icons for 6 POI categories
- Category filter toggles
- Responsive map with custom markers
- Integrated POI display
- **`components/neighborhood/neighborhood-radar-chart.tsx`**
- Recharts `RadarChart` component
- 0-10 score scale
- Badge display below chart
- Responsive, themed
- **`components/neighborhood/neighborhood-score.tsx`**
- Small score display component
- Used as quick reference
### Integration in Listing Detail:
- Dynamically imported in `listing-detail-client.tsx`
- Used when neighborhood data is available from API
- Part of the enrichment data pipeline
---
## 5. Mapbox GL Integration
### Setup & Styling:
- **`lib/mapbox-style.ts`**
```typescript
export const MAPBOX_STYLE_LIGHT = 'mapbox://styles/mapbox/streets-v12';
export const MAPBOX_STYLE_DARK = 'mapbox://styles/mapbox/dark-v11';
export function useMapboxStyle(): string {
const { theme } = useTheme();
return theme === 'dark' ? MAPBOX_STYLE_DARK : MAPBOX_STYLE_LIGHT;
}
```
### Map Components:
1. **`components/map/listing-map.tsx`** (10KB)
- Displays multiple listings as markers
- Custom markers with price popup
- Click handler for marker selection
- Responsive sizing
- City coordinate presets for auto-centering
2. **`components/map/location-picker.tsx`** (9KB)
- Interactive location selection
- User-clickable map for address input
- Reverse geocoding integration
3. **`components/neighborhood/neighborhood-poi-map.tsx`** (11KB)
- POI visualization with category filters
- SVG marker icons (6 types)
- Distance display
- Category toggle buttons
### Pattern:
- Maps created as client components (`'use client'`)
- Mapbox GL CSS imported per file
- Theme-aware style selection via `useMapboxStyle()`
- Responsive containers
---
## 6. API Client Setup
### Main Client: `lib/api-client.ts` (127 lines)
```typescript
// Core features:
- Base URL from environment: process.env.NEXT_PUBLIC_API_URL
- CSRF token handling from cookies
- Automatic 401 refresh-and-retry (except auth endpoints)
- Concurrent refresh coalescing
- Type-safe request/response with generics
- Credentials: include for cookies
// API methods:
apiClient.get<T>(endpoint, headers?)
apiClient.post<T>(endpoint, body?, headers?)
apiClient.patch<T>(endpoint, body?, headers?)
apiClient.delete<T>(endpoint, headers?)
```
### Domain-Specific API Clients (in `lib/`):
- `listings-api.ts` - Listing CRUD, search, recommendations
- `analytics-api.ts` - Market reports, heatmaps, price trends
- `agents-api.ts` - Agent profiles & stats
- `valuation-api.ts` - AVM (Automated Valuation Model) estimates
- `khu-cong-nghiep-api.ts` - Industrial park data
- `du-an-api.ts` - Project data
- `inquiries-api.ts` - Inquiry management
- `leads-api.ts` - Lead data
- `reports-api.ts` - Report generation
- `admin-api.ts` - Admin operations
- `auth-api.ts` - Auth endpoints
- `payment-api.ts` - Payment processing
Each API module:
- Defines request/response interfaces
- Uses `apiClient` helper for HTTP calls
- May have both client and server implementations
---
## 7. Hooks & React Query Integration
### React Query Setup: `lib/query-client.ts`
```typescript
// Creates QueryClient singleton
// Provider in components/providers/query-provider.tsx
// Includes QueryErrorResetBoundary error handling
```
### Custom Hooks: `lib/hooks/`
```
use-analytics.ts - Market reports, heatmaps, trends
use-khu-cong-nghiep.ts - Industrial parks
use-du-an.ts - Projects
use-listings.ts - Listings data
use-valuation.ts - AVM valuations
use-inquiries.ts - Inquiries
use-leads.ts - Leads
use-reports.ts - Reports
use-saved-searches.ts - Saved search queries
use-socket-notifications.ts - Real-time notifications
```
### Hook Pattern (example from `use-analytics.ts`):
```typescript
export const analyticsKeys = {
all: ['analytics'] as const,
marketReport: (city, period) => [...],
// ...
};
export function useMarketReport(city: string, period: string) {
return useQuery({
queryKey: analyticsKeys.marketReport(city, period),
queryFn: () => analyticsApi.getMarketReport(city, period),
});
}
```
---
## 8. Chart Components
### Using Recharts:
1. **`components/charts/price-area-chart.tsx`**
- AreaChart with gradient fills
- Up/down signal coloring
- Formatted Y-axis (tr/k notation for Vietnamese)
- Responsive container
2. **`components/charts/district-heatmap.tsx`** (9KB)
- District-level visualization
- Possibly Mapbox layer integration
3. **`components/charts/price-trend-chart.tsx`**
- Historical price trends
- Multi-period comparison
4. **`components/charts/district-bar-chart.tsx`**
- Bar chart for district comparisons
5. **`components/charts/agent-performance.tsx`** (6KB)
- Agent metrics visualization
6. **`components/listings/price-history-chart.tsx`** (2KB)
- Listing-specific price history
- Small inline chart
### Recharts Pattern:
- `ResponsiveContainer` for responsive sizing
- Custom CSS variable colors (--color-signal-up, --signal-down, etc.)
- Tooltip with custom styling
- Dark/light theme support via CSS variables
---
## 9. Listing Detail Page Structure
### Route: `app/[locale]/(public)/listings/[id]/page.tsx`
- Server component (RSC) for metadata generation
- Uses `generateMetadata()` for SEO (Open Graph, canonical URLs)
- Fetches listing via `fetchListingById()`
- Returns `<ListingDetailClient>` for interactivity
### Client Component: `components/listings/listing-detail-client.tsx` (39KB)
**Core Sections:**
1. **Image Gallery** - `<ImageGallery>`
2. **KPI Strip** - Trader-style metrics (price, m², DOM, etc.)
3. **Core Details** - Address, bedrooms, furnishing, etc.
4. **Price History Chart** - `<PriceHistoryChart>`
5. **Neighborhood Section** - Dynamically loaded:
- `<NeighborhoodRadarChart>` (6 categories, 0-10 scores)
- `<NeighborhoodPOIMap>` (POI layer)
6. **AI Advice Cards** - `<AiAdviceCards>` with personas
7. **Similar Listings** - Recommendations
8. **Agent Card** - Agent quality score & info
9. **Call-to-Action** - Inquiry modal, comparison, AI estimate
### Key Data Types:
```typescript
export interface ListingDetail {
id: string;
status: ListingStatus;
transactionType: 'SALE' | 'RENT';
priceVND: string;
property: { title, address, bedrooms, areaM2, media[], ... };
valuationEstimate?: { value, confidence, ... };
neighborhoodScore?: NeighborhoodScoreData;
agentQualityScore?: AgentQualityScore;
similarListings?: ListingSimilarItem[];
// ...
}
```
### Dynamic Imports (performance):
- `NeighborhoodRadarChart` - SSR disabled
- `NeighborhoodPOIMap` - SSR disabled + loading fallback
---
## 10. State Management Patterns
### React Query (Server State):
- Used for API data fetching & caching
- Query keys follow convention pattern
- Error boundaries handle failures
- Automatic refetch intervals for live data
### Zustand Stores (Client State):
- Examples: `auth-store.ts`, `comparison-store.ts`, `preferences-store.ts`
- Lightweight, minimal boilerplate
- Integrated with localStorage where needed
### Context (Theme, Notifications):
- `ThemeProvider` (via next-themes)
- `QueryProvider` (React Query)
- `NotificationsProvider`
- `AuthProvider`
---
## 11. Key File Patterns & Conventions
### Naming:
- Pages: `page.tsx` in route directories
- Layouts: `layout.tsx`
- Components: PascalCase with domain prefix (e.g., `NeighborhoodPOIMap`)
- API modules: `*-api.ts` for client-side, `*-server.ts` for server-side
- Hooks: `use-*` pattern
- Types: inline in files or `types.ts` per domain
### File Organization:
- Domain-based (listings/, neighborhood/, khu-cong-nghiep/)
- Shared utilities in `/lib` and `/components/design-system`
- Barrel exports with `index.ts` for easy importing
### Code Structure:
- 'use client' at top of client components
- TypeScript strict mode
- Props interfaces above component functions
- Sub-components extracted for readability
- Helpers defined near top-level components
---
## 12. Performance & SEO
### SEO:
- Server-side metadata generation with `generateMetadata()`
- JSON-LD schema generation (`components/seo/json-ld.tsx`)
- Open Graph + canonical URLs
- Multi-language support (vi/en)
### Performance:
- Dynamic imports with code splitting (e.g., maps, heavy charts)
- Lazy-loading fallbacks
- Image optimization (Next.js Image component)
- CSS-in-JS via Tailwind + CVA
- Responsive containers for charts/maps
### Internationalization:
- `next-intl` for translations
- Route segments by locale: `[locale]/`
- Translations in `/messages` directory
---
## Summary Table
| Aspect | Technology | Key Files |
|--------|-----------|-----------|
| **Mapping** | Mapbox GL 3.21.0 | `components/map/`, `lib/mapbox-style.ts` |
| **Charts** | Recharts 3.8.1 | `components/charts/`, `components/neighborhood/neighborhood-radar-chart.tsx` |
| **API Client** | Fetch + CSRF | `lib/api-client.ts`, `lib/*-api.ts` |
| **State (Server)** | React Query 5.96.2 | `lib/query-client.ts`, `lib/hooks/` |
| **State (Client)** | Zustand 5.0.12 | `lib/*-store.ts` |
| **Forms** | react-hook-form + Zod | `lib/validations/` |
| **UI** | Tailwind + CVA | `components/design-system/`, `components/ui/` |
| **Routing** | Next.js App Router | `app/[locale]/` |
| **i18n** | next-intl | `/messages/`, route `[locale]/` |

View File

@@ -0,0 +1,357 @@
╔════════════════════════════════════════════════════════════════════════════╗
║ GOODGO PLATFORM AI - NEXT.JS FRONTEND QUICK REFERENCE ║
╚════════════════════════════════════════════════════════════════════════════╝
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. PROJECT STRUCTURE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/apps/web/
├─ app/[locale]/ ← Next.js 15 App Router (public, auth, dashboard, admin)
├─ components/ ← React components (listings, neighborhood, map, charts, design-system)
├─ lib/ ← API clients, hooks, stores, utilities
├─ i18n/ & messages/ ← Internationalization (vi/en)
└─ public/ ← Static assets
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. CORE TECHNOLOGIES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Framework: Next.js 15.5.14 (App Router)
Runtime: React 18.3.0
Styling: Tailwind CSS 3.4.0 + CVA 0.7.1
Forms: react-hook-form 7.72.1 + Zod 4.3.6
State: Zustand 5.0.12 (client) + React Query 5.96.2 (server)
Mapping: Mapbox GL 3.21.0
Charts: Recharts 3.8.1
Icons: lucide-react 1.7.0
i18n: next-intl 4.9.0
Theme: next-themes 0.4.6
Real-time: socket.io-client 4.8.3
PDF Export: html2canvas 1.4.1 + jspdf 4.2.1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. NEIGHBORHOOD FEATURES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMPONENTS:
✓ NeighborhoodPOIMap (11 KB, Mapbox GL + 6 POI categories)
✓ NeighborhoodRadarChart (2 KB, Recharts radar, 0-10 scores, 6 categories)
✓ NeighborhoodScore (2 KB, compact score display)
TYPES & CONFIG:
✓ NeighborhoodCategory (label, score 0-10, icon)
✓ POIItem (id, name, category, lat/lng, distance)
✓ POICategory (school, hospital, transit, shopping, restaurant, park)
✓ NeighborhoodScoreData (overall score, categories, POIs, center)
INTEGRATION:
✓ Dynamically loaded in listing-detail-client.tsx
✓ Part of enrichment data pipeline (API response)
✓ Maps imported with SSR disabled for performance
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. MAPBOX GL USAGE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMPONENTS:
• listing-map.tsx Multiple listings + click handlers
• location-picker.tsx Interactive location selection
• neighborhood-poi-map.tsx POI visualization with filters
STYLING:
• Light: mapbox://styles/mapbox/streets-v12
• Dark: mapbox://styles/mapbox/dark-v11
• Theme-aware via useMapboxStyle() hook
PATTERN:
'use client' ← Must be client component
import 'mapbox-gl/dist/mapbox-gl.css'
const style = useMapboxStyle() ← Get theme-aware style
new mapboxgl.Map({ style }) ← Create map instance
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5. CHART COMPONENTS (RECHARTS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ price-area-chart.tsx Trend visualization (up/down coloring)
✓ price-trend-chart.tsx Historical trends
✓ district-bar-chart.tsx District comparisons
✓ district-heatmap.tsx Heat visualization (9 KB)
✓ agent-performance.tsx Agent metrics (6 KB)
✓ neighborhood-radar-chart.tsx Radar chart (0-10 scores)
PATTERN:
ResponsiveContainer ← Responsive sizing
CSS variables for theming ← --color-signal-up, --color-signal-down
Custom Tooltip styling ← Themed backgrounds
Vietnamese number formatting ← tr/k notation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6. API CLIENT PATTERN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BASE CLIENT (lib/api-client.ts):
apiClient.get<T>(endpoint, headers?)
apiClient.post<T>(endpoint, body?, headers?)
apiClient.patch<T>(endpoint, body?, headers?)
apiClient.delete<T>(endpoint, headers?)
FEATURES:
✓ CSRF token handling (from cookies)
✓ Automatic 401 refresh-and-retry (except auth endpoints)
✓ Concurrent refresh coalescing
✓ Type-safe with generics
✓ Base URL from NEXT_PUBLIC_API_URL env var
DOMAIN-SPECIFIC CLIENTS (lib/*-api.ts):
listings-api.ts | Listing CRUD, search
analytics-api.ts | Market reports, heatmaps, trends
neighborhood-api.ts | Neighborhood scoring (if exists)
valuation-api.ts | AVM estimates
agents-api.ts | Agent profiles
khu-cong-nghiep-api | Industrial parks
du-an-api.ts | Projects
inquiries-api.ts | Inquiries
leads-api.ts | Leads
admin-api.ts | Admin operations
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7. REACT QUERY HOOKS (lib/hooks/)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PATTERN:
const analyticsKeys = {
all: ['analytics'] as const,
marketReport: (city, period) => ['analytics', 'market-report', city, period] as const,
};
export function useMarketReport(city: string, period: string) {
return useQuery({
queryKey: analyticsKeys.marketReport(city, period),
queryFn: () => analyticsApi.getMarketReport(city, period),
});
}
AVAILABLE HOOKS:
use-analytics.ts useMarketReport, useHeatmap, usePriceTrend, etc.
use-listings.ts useListings, useListing
use-khu-cong-nghiep.ts useIndustrialParks
use-du-an.ts useProjects
use-valuation.ts useValuation
use-inquiries.ts useInquiries
use-leads.ts useLeads
use-reports.ts useReports
use-saved-searches.ts useSavedSearches
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8. LISTING DETAIL PAGE FLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ROUTE: /[locale]/(public)/listings/[id]/page.tsx
1. SERVER COMPONENT (RSC)
├─ generateMetadata() ← SEO: OG, canonical, schema
└─ fetchListingById(id) ← Data fetching
2. CLIENT COMPONENT (listing-detail-client.tsx - 39 KB)
├─ ImageGallery ← Media slideshow
├─ KPI Strip ← Trader-style metrics
├─ Core Details ← Address, bedrooms, etc.
├─ PriceHistoryChart ← Recharts trend
├─ Neighborhood Section
│ ├─ NeighborhoodRadarChart ← (dynamic import, SSR: false)
│ └─ NeighborhoodPOIMap ← (dynamic import, SSR: false)
├─ AiAdviceCards ← Personas
├─ SimilarListings ← Recommendations
├─ AgentCard ← Quality score
└─ CTA Section ← Inquiry, comparison, estimate
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9. STATE MANAGEMENT LAYERS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LAYER 1: REACT QUERY (Server State)
Purpose: Fetch & cache API data
Provider: QueryProvider (components/providers/query-provider.tsx)
Error Handle: QueryErrorResetBoundary + custom error boundary
Usage: useQuery, useMutation hooks
LAYER 2: ZUSTAND (Client State)
Purpose: UI state, preferences
Examples: auth-store, comparison-store, preferences-store
Persistence: localStorage integration
Usage: Direct store access or hooks
LAYER 3: CONTEXT (Global Features)
Theme: ThemeProvider (next-themes)
Notifications: NotificationsProvider
Auth: AuthProvider
Query: QueryProvider
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10. DESIGN SYSTEM COMPONENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FINANCIAL METRICS:
✓ kpi-card.tsx Key performance indicator
✓ stat-card.tsx Statistics display
✓ ticker-strip.tsx Horizontal ticker/marquee
✓ price-delta.tsx Price change with signal
✓ signal.tsx Up/down indicator
✓ numeric.tsx Formatted number display
DATA DISPLAY:
✓ data-table.tsx @tanstack/react-table wrapper
✓ empty-state.tsx Empty data fallback
✓ skeleton.tsx Loading skeleton
✓ status-chip.tsx Status badge
LAYOUT:
✓ dashboard-layout.tsx Dashboard structure
✓ density-provider.tsx Compact/dense mode toggle
✓ divider.tsx Visual separator
UI PRIMITIVES (from shadcn/ui via design-system/):
✓ badge.tsx, button.tsx, card.tsx, dialog.tsx, etc.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11. INTERNATIONALIZATION (i18n)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SETUP:
Library: next-intl 4.9.0
Locales: vi (Vietnamese), en (English)
Route param: [locale] prefix
Translations: /messages/ directory
USAGE:
const t = useTranslations();
<span>{t('key.path')}</span>
ROUTES:
/vi/listings/[id] ← Vietnamese
/en/listings/[id] ← English
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12. PERFORMANCE OPTIMIZATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CODE SPLITTING:
dynamic(() => import(...), { ssr: false }) ← Heavy components (maps, charts)
LAZY LOADING:
loading: () => <Fallback /> ← Show UI while loading
IMAGE OPTIMIZATION:
next/image ← Built-in optimization
CACHING:
React Query query keys ← Automatic deduplication & refetch intervals
CSS-IN-JS:
Tailwind + CVA ← Static + composable variants
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13. KEY FILE PATHS REFERENCE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEIGHBORHOOD:
lib/components/neighborhood/ Components
lib/components/neighborhood/types.ts Type definitions
lib/components/neighborhood/neighborhood-poi-map.tsx Mapbox POI layer
lib/components/neighborhood/neighborhood-radar-chart.tsx Radar scores
MAPPING:
lib/mapbox-style.ts Theme-aware styles
lib/components/map/listing-map.tsx Listings on map
lib/components/map/location-picker.tsx Location selection
CHARTS:
lib/components/charts/ Chart components
lib/components/charts/price-area-chart.tsx Area trend chart
API & STATE:
lib/api-client.ts Base HTTP client
lib/listings-api.ts Listings endpoints
lib/analytics-api.ts Analytics endpoints
lib/hooks/use-analytics.ts Analytics React Query hooks
LISTING DETAIL:
app/[locale]/(public)/listings/[id]/page.tsx Server component
lib/components/listings/listing-detail-client.tsx Client component (39 KB)
PROVIDERS:
lib/components/providers/query-provider.tsx React Query setup
lib/components/providers/theme-provider.tsx Theme management
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14. CODING PATTERNS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMPONENT FILE HEADER:
'use client' ← Client component marker
import ... ← External imports
import ... ← Internal imports
interface ComponentProps { ... } ← Props interface
export function Component({ ... }: ComponentProps) {
// implementation
}
API CLIENT USAGE:
const data = await apiClient.get<Type>('/endpoint');
const result = await apiClient.post<Type>('/endpoint', body);
REACT QUERY HOOK:
export function useData(param: string) {
return useQuery({
queryKey: ['key', param],
queryFn: () => api.getData(param),
enabled: !!param, ← Conditional queries
});
}
COMPONENT IMPORTS:
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('...').then(m => m.Component), {
ssr: false,
loading: () => <LoadingState />
});
ZUSTAND STORE:
import { create } from 'zustand';
const useStore = create((set) => ({
value: null,
setValue: (v) => set({ value: v }),
}));
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15. DEBUGGING & DEVELOPMENT TIPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ENV VARS:
NEXT_PUBLIC_API_URL ← Backend API base URL
NEXT_PUBLIC_SITE_URL ← Frontend base URL
MAPBOX_ACCESS_TOKEN ← Mapbox API key
SCRIPTS:
npm run dev ← Start dev server (port 3000)
npm run build ← Production build
npm run lint ← ESLint
npm test ← Vitest
npm run typecheck ← TypeScript check
HOT TIPS:
• Dynamic imports reduce bundle size for heavy components
• React Query keys are the foundation of caching strategy
• Use CSS variables for theming (see design-system/)
• Barrel exports (index.ts) for cleaner imports
• Zustand for simple state, React Query for server state
• Always check enabled condition in useQuery() for dependent queries
╔════════════════════════════════════════════════════════════════════════════╗
║ Report Generated: April 2026 ║
║ Base Path: /Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web ║
╚════════════════════════════════════════════════════════════════════════════╝

View File

@@ -0,0 +1,241 @@
# GoodGo Frontend Exploration - Complete Documentation
## 📁 Generated Files
All files are saved to your Desktop. Read them in this order:
### 1. **README_EXPLORATION.txt** (16 KB) ⭐ START HERE
- **Purpose**: Overview & quick summary
- **Contents**:
- Key findings (10 major insights)
- Dependencies summary
- Component import patterns
- Quick start guide
- Notable implementation details
- Pro tips & file size reference
- Next steps for development
### 2. **ARCHITECTURE_OVERVIEW.txt** (24 KB)
- **Purpose**: Visual diagrams & data flows
- **Contents**:
- Routing & layout hierarchy (ASCII tree)
- Listing detail page component flow
- API & state management data flow
- Neighborhood components detail
- Chart components stack
- Component import patterns (static/dynamic)
- Mapbox GL initialization flow
- i18n flow diagram
- SEO & metadata generation flow
### 3. **FRONTEND_QUICK_REFERENCE.txt** (19 KB)
- **Purpose**: Quick lookup reference
- **Contents**: 15 sections covering:
- Project structure
- Core tech stack
- Neighborhood features detail
- Mapbox GL patterns
- Chart components
- API client architecture
- React Query hooks
- Listing detail page flow
- State management layers
- Design system components
- i18n setup
- Performance optimizations
- Key file paths
- Coding patterns
- Debugging tips
### 4. **FRONTEND_EXPLORATION_REPORT.md** (16 KB)
- **Purpose**: Deep technical dive
- **Contents**: 12 comprehensive sections:
1. Overall directory structure
2. App Router structure
3. Components directory (23 domains)
4. Existing neighborhood components
5. Mapbox GL integration
6. API client setup
7. Hooks & React Query integration
8. Chart components (Recharts)
9. Listing detail page structure
10. State management patterns
11. Key file patterns & conventions
12. Performance & SEO optimizations
---
## 🗺️ Quick Navigation
### For different needs, use:
**"I want a 5-minute overview"**
→ Read: **README_EXPLORATION.txt** (Key Findings section)
**"I need to understand the architecture"**
→ Read: **ARCHITECTURE_OVERVIEW.txt** (start with the diagrams)
**"I need to find a specific file or pattern"**
→ Search: **FRONTEND_QUICK_REFERENCE.txt** (Use Ctrl+F)
**"I need complete technical details"**
→ Read: **FRONTEND_EXPLORATION_REPORT.md** (full reference)
---
## 📊 Project Stats
- **Framework**: Next.js 15.5.14 with App Router
- **Components**: 23 domains with 15+ major feature areas
- **Mapping**: Mapbox GL 3.21.0 (3 map components)
- **Charts**: Recharts 3.8.1 (6 chart types)
- **Neighborhood Features**: Fully implemented (3 components)
- **API Modules**: 12+ domain-specific clients
- **React Query Hooks**: 10+ custom hooks
- **Routes**: Public, Auth, Dashboard, Admin (all locale-aware)
- **Listing Detail Page**: 39 KB client component with 9 sections
---
## 🎯 Key Takeaways
### Architecture
- ✅ Domain-based component organization
- ✅ Separated concerns (API, hooks, stores, components)
- ✅ Locale-aware routing with i18n
- ✅ Dynamic imports for performance
- ✅ Type-safe API client with CSRF protection
### Features
- ✅ Neighborhood scoring (6 categories, 0-10 scale)
- ✅ POI mapping (6 categories with SVG icons)
- ✅ Price history charts
- ✅ Theme switching (light/dark) with Mapbox
- ✅ Multi-language support (Vietnamese/English)
### Tech Stack
- ✅ React 18 + Next.js 15 App Router
- ✅ Tailwind CSS + CVA for styling
- ✅ React Query + Zustand for state
- ✅ Mapbox GL for mapping
- ✅ Recharts for data visualization
- ✅ next-intl for internationalization
---
## 🔍 Key Files
```
lib/
├── api-client.ts # Base HTTP client (CSRF, 401 refresh)
├── listings-api.ts # Listing endpoints
├── analytics-api.ts # Analytics/market data
├── mapbox-style.ts # Theme-aware map styles
└── hooks/
└── use-analytics.ts # React Query pattern example
components/
├── listings/
│ └── listing-detail-client.tsx # Main detail page (39 KB)
├── neighborhood/
│ ├── neighborhood-poi-map.tsx # Mapbox POI layer (11 KB)
│ ├── neighborhood-radar-chart.tsx # Recharts radar (2 KB)
│ └── types.ts # Neighborhood types
├── map/
│ ├── listing-map.tsx # Listings on map
│ └── location-picker.tsx # Location selection
├── charts/
│ ├── price-area-chart.tsx # Area chart
│ ├── district-heatmap.tsx # Heat visualization
│ └── ...
└── design-system/ # UI components & patterns
app/
└── [locale]/
├── (public)/listings/[id]/page.tsx # Detail page (Server RSC)
├── (dashboard)/ # Protected routes
└── (admin)/ # Admin routes
```
---
## 💡 Important Patterns
### API Usage
```typescript
const data = await apiClient.get<Type>('/endpoint');
```
### React Query Hook
```typescript
export function useData(param: string) {
return useQuery({
queryKey: ['key', param],
queryFn: () => api.getData(param),
enabled: !!param,
});
}
```
### Dynamic Component Import
```typescript
const Component = dynamic(() => import('...'), {
ssr: false,
loading: () => <Skeleton />
});
```
### Mapbox Integration
```typescript
'use client'
const style = useMapboxStyle(); // light/dark
const map = new mapboxgl.Map({ style });
```
---
## 🚀 Commands
```bash
npm run dev # Dev server (port 3000)
npm run build # Production build
npm run lint # ESLint check
npm test # Vitest tests
npm run typecheck # TypeScript check
```
---
## 📝 Notes
- All reports are **self-contained** - you can read any of them independently
- File paths are **absolute** from `/apps/web`
- Component sizes range from **2 KB** (radar chart) to **39 KB** (listing detail)
- Neighborhood features are **already implemented** and ready to use
- Mapbox GL requires **'use client'** directive
- All charts use **CSS variables** for theming
- i18n uses **[locale]** route parameter (vi/en)
---
## 📞 Quick Reference
**What does NeighborhoodPOIMap do?**
→ Section 3 of README, or QUICK_REFERENCE section 3
**How do I add a new chart?**
→ QUICK_REFERENCE section 5 (Chart Components)
**Where's the API client?**
→ QUICK_REFERENCE section 6 (API Client Pattern)
**How does state management work?**
→ README section "State Management Layers" or QUICK_REFERENCE section 9
**What about performance?**
→ QUICK_REFERENCE section 12 (Performance Optimizations)
---
Generated: April 21, 2026
Base Path: `/Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web`

View File

@@ -0,0 +1,287 @@
# Notifications Module - Complete Exploration
## 📊 Summary
- **Total Files**: 67 TypeScript files
- **Architecture**: DDD + CQRS
- **Channels**: EMAIL, SMS, PUSH (FCM), ZALO_OA
- **Event Listeners**: 18 domain event listeners
- **Current SMS Provider**: Stringee (fully implemented)
---
## 🎯 Main Interface: NotificationChannelPort
**Location**: `domain/ports/notification-channel.port.ts`
```typescript
export interface SendChannelMessageDto {
recipient: string; // Email/phone/token
subject: string; // For EMAIL
body: string; // HTML content
templateKey: string; // e.g., 'user.registered'
metadata?: Record<string, unknown>;
}
export interface SendChannelMessageResult {
messageId: string;
}
export interface NotificationChannelPort {
readonly channel: NotificationChannel; // 'EMAIL' | 'SMS' | 'PUSH' | 'ZALO_OA'
readonly isAvailable: boolean;
send(dto: SendChannelMessageDto): Promise<SendChannelMessageResult>;
}
export const SMS_NOTIFICATION_CHANNEL = Symbol('SMS_NOTIFICATION_CHANNEL');
```
---
## 📡 SMS Rate Limiting
**Location**: `infrastructure/services/sms-rate-limiter.service.ts`
- **Technology**: Redis + Lua script (atomic, sliding window)
- **Buckets**:
- `otp`: 5/min, 10/hour (strict)
- `transactional`: 20/min, 100/hour (lenient)
- **Graceful degradation**: Redis error → allow request
---
## 🔧 Stringee SMS Service (Reference Implementation)
**Location**: `infrastructure/services/stringee-sms.service.ts`
### Constructor Dependencies
- `LoggerService`
- `SmsRateLimiterService`
### Environment Variables
- `STRINGEE_API_KEY` (required)
- `STRINGEE_BRANDNAME` (optional, default: "GoodGo")
### Key Methods
1. `onModuleInit()` - Read env, validate, initialize
2. `get isAvailable()` - Check if initialized
3. `async send(dto)` - NotificationChannelPort implementation
4. `async sendOTP(dto)` - Specific OTP handling
5. `async dispatch(dto)` - Main workflow
6. `async enforceRateLimit()` - Check per-minute AND hourly
7. `async sendWithRetry()` - 3 attempts with exponential backoff
8. `normalizePhone()` - +84xxx format conversion
9. `stripHtml()` - Remove HTML tags
### Workflow
1. Rate limit check (per-minute + hourly)
2. HTML stripping
3. Phone normalization (+84, 84, 0 formats)
4. Retry logic (1s → 2s → 4s)
5. API call to https://api.stringee.com/v1/sms
6. Return messageId
---
## 🏗️ DI Configuration
**Location**: `notifications.module.ts`
```typescript
@Module({
providers: [
// Repositories
{ provide: NOTIFICATION_REPOSITORY, useClass: PrismaNotificationRepository },
{ provide: NOTIFICATION_PREFERENCE_REPOSITORY, useClass: PrismaNotificationPreferenceRepository },
// Services
EmailService,
FcmService,
SmsRateLimiterService,
StringeeSmsService,
{ provide: SMS_NOTIFICATION_CHANNEL, useExisting: StringeeSmsService },
ZaloOaService,
TemplateService,
// Others
NotificationsGateway,
SendNotificationHandler,
...EventListeners,
],
exports: [
EmailService,
FcmService,
SmsRateLimiterService,
StringeeSmsService,
SMS_NOTIFICATION_CHANNEL,
ZaloOaService,
TemplateService,
NotificationsGateway,
],
})
export class NotificationsModule {}
```
---
## 🔄 Message Flow
```
Domain Event (e.g., user.registered)
@OnEvent('user.registered') Listener
CommandBus.execute(SendNotificationCommand)
SendNotificationHandler
├─ Check user preference (enabled?)
├─ Render template
├─ Persist notification log
├─ Dispatch to channel:
│ ├─ EMAIL → EmailService.send()
│ ├─ SMS → StringeeSmsService.send() [with rate limit + retry]
│ ├─ PUSH → FcmService.send()
│ └─ ZALO → ZaloOaService.send()
├─ Update status (SENT/FAILED)
└─ Publish NotificationSentEvent
```
---
## 💡 Key Patterns
### Pattern 1: OnModuleInit + Lazy Initialization
- Read env vars in `onModuleInit()`
- Set `initialized = true` only if valid
- Check `isAvailable` getter before operations
### Pattern 2: DI Token Binding
```typescript
export const MY_TOKEN = Symbol('MY_TOKEN');
providers: [
MyService,
{ provide: MY_TOKEN, useExisting: MyService }
]
// Inject
@Inject(MY_TOKEN) myService: SomeInterface
```
### Pattern 3: Rate Limiting
- Always call `rateLimiter.check(phone, bucket)` first
- Support OTP vs transactional buckets
- Check BOTH per-minute and hourly limits
- Throw `DomainException` on violation
### Pattern 4: Error Handling
```typescript
throw new DomainException(
ErrorCode.TOO_MANY_REQUESTS,
`SMS limit exceeded. Retry after ${retryAfterSeconds}s.`,
HttpStatus.TOO_MANY_REQUESTS,
{ bucket, retryAfterSeconds }
);
```
### Pattern 5: Retry Logic
- MAX_RETRIES = 3
- Exponential backoff: 1s → 2s → 4s
- Log each attempt, fail silently on final retry
---
## 📁 Directory Structure
```
notifications/
├── domain/
│ ├── ports/
│ │ └── notification-channel.port.ts ⭐ Main interface
│ ├── repositories/ ← DI Tokens
│ ├── value-objects/
│ │ └── notification-channel.vo.ts ← Channel enum
│ ├── entities/
│ └── events/
│ └── notification-sent.event.ts
├── infrastructure/
│ ├── services/
│ │ ├── stringee-sms.service.ts ⭐ SMS provider example
│ │ ├── sms-rate-limiter.service.ts ← Rate limiting
│ │ ├── email.service.ts
│ │ ├── fcm.service.ts
│ │ └── zalo-oa.service.ts
│ └── repositories/
│ ├── prisma-notification.repository.ts
│ └── prisma-notification-preference.repository.ts
├── application/
│ ├── commands/
│ │ └── send-notification/
│ │ ├── send-notification.command.ts
│ │ └── send-notification.handler.ts ⭐ Command dispatcher
│ └── listeners/ ← 18 event listeners
├── presentation/
│ ├── controllers/
│ └── gateways/
│ └── notifications.gateway.ts ← WebSocket
├── notifications.module.ts ⭐ DI setup
└── index.ts
```
---
## 🛠️ Implementation Checklist (for new SMS provider)
- [ ] Create `infrastructure/services/{provider}-sms.service.ts`
- [ ] Implement `NotificationChannelPort` interface
- [ ] Implement `OnModuleInit` for env var reading
- [ ] Inject `SmsRateLimiterService`
- [ ] Implement rate limit enforcement
- [ ] Implement phone normalization
- [ ] Strip HTML from body
- [ ] Implement retry logic (3 attempts, exponential backoff)
- [ ] Register in `notifications.module.ts`
- [ ] Integrate with `SendNotificationHandler`
- [ ] Write unit tests
---
## 📊 SMS Rate Limits
| Bucket | Per-Minute | Per-Hour | Use Case |
|--------|-----------|----------|----------|
| `otp` | 5 | 10 | OTP codes, verification |
| `transactional` | 20 | 100 | Regular notifications |
**OTP Templates** (use strict limits):
- user.phone_change_otp
- auth.login_otp
- auth.kyc_otp
- auth.phone_verify_otp
---
## 🔗 Quick Reference
| File | Purpose |
|------|---------|
| `domain/ports/notification-channel.port.ts` | Interface + DI symbols |
| `infrastructure/services/stringee-sms.service.ts` | SMS provider example |
| `infrastructure/services/sms-rate-limiter.service.ts` | Rate limiting |
| `application/commands/send-notification/send-notification.handler.ts` | Command handler |
| `notifications.module.ts` | DI configuration |
| `application/listeners/user-registered.listener.ts` | Event listener example |
---
## ✅ Everything You Need
- [x] Full directory structure
- [x] Main interface (NotificationChannelPort)
- [x] Existing SMS implementation (Stringee - complete reference)
- [x] Rate limiting system (Redis-based)
- [x] DI configuration patterns
- [x] Message flow understanding
- [x] Handler integration patterns
- [x] Error handling patterns
- [x] Testing patterns

View File

@@ -0,0 +1,319 @@
╔══════════════════════════════════════════════════════════════════════════════╗
║ FRONTEND EXPLORATION SUMMARY ║
║ GoodGo Platform AI - Next.js ║
╚══════════════════════════════════════════════════════════════════════════════╝
EXPLORATION COMPLETED: April 21, 2026
BASE PATH: /Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web
📋 REPORTS GENERATED:
────────────────────────────────────────────────────────────────────────────
1. FRONTEND_EXPLORATION_REPORT.md (16 KB)
└─ Comprehensive 12-section technical deep-dive
• Project structure (directory layout)
• Core technologies & dependencies
• App Router structure & route organization
• Component directory (23 domains)
• Neighborhood components detail
• Mapbox GL integration patterns
• API client setup & domain-specific modules
• React Query hooks & caching strategy
• Chart components (Recharts)
• Listing detail page structure (39 KB client component)
• State management layers (React Query, Zustand, Context)
• Key file patterns & conventions
• Performance & SEO optimizations
2. FRONTEND_QUICK_REFERENCE.txt (19 KB)
└─ 15-section quick lookup guide
• Project structure overview
• Core tech stack
• Neighborhood features & types
• Mapbox GL usage patterns
• Chart components
• API client patterns
• React Query hooks
• Listing detail page flow
• State management layers
• Design system components
• Internationalization setup
• Performance optimizations
• Key file paths reference
• Coding patterns examples
• Debugging tips
3. ARCHITECTURE_OVERVIEW.txt (16 KB)
└─ Visual ASCII diagrams & data flows
• Routing & layout hierarchy
• Listing detail page component flow
• API & state management data flow
• Neighborhood components detail
• Chart components stack
• Component import patterns (static/dynamic)
• Mapbox GL initialization flow
• i18n flow
• SEO & metadata generation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏗️ KEY FINDINGS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. DIRECTORY STRUCTURE
✓ Domain-based organization (listings/, neighborhood/, map/, charts/)
✓ Shared utilities in /lib (API clients, hooks, stores)
✓ Design system components in /components/design-system/
✓ Next.js 15 App Router with locale prefix [locale]
✓ Multi-level layout hierarchy: root → locale → group → feature
2. NEIGHBORHOOD FEATURES (FULLY IMPLEMENTED)
✓ NeighborhoodPOIMap (11 KB) - Mapbox GL with 6 POI categories
✓ NeighborhoodRadarChart (2 KB) - Recharts radar with 0-10 scores
✓ NeighborhoodScore (2 KB) - Compact score display
✓ Rich type definitions (NeighborhoodScoreData, POIItem, NeighborhoodCategory)
✓ Integration point: Dynamically loaded in listing-detail-client.tsx
3. MAPBOX GL INTEGRATION
✓ 3 map components: listing-map, location-picker, neighborhood-poi-map
✓ Theme-aware styling (light/dark) via useMapboxStyle() hook
✓ CSS imported per component: 'mapbox-gl/dist/mapbox-gl.css'
✓ SVG marker icons for POI categories (school, hospital, transit, shopping, restaurant, park)
✓ Client-only rendering (no SSR)
4. CHART COMPONENTS (RECHARTS)
✓ 6 chart types: price area, price trend, district bar, heatmap, performance, radar
✓ Vietnamese number formatting (tr/k notation for millions)
✓ Signal colors (green/red) via CSS variables
✓ Responsive containers with theme-aware tooltips
✓ Dynamic imports for performance
5. API ARCHITECTURE
✓ Base client: lib/api-client.ts (127 lines)
• CSRF token handling
• Automatic 401 refresh-and-retry
• Concurrent refresh coalescing
• Type-safe generics
✓ 12+ domain-specific API modules (listings, analytics, valuation, agents, etc.)
✓ Both client-side and server-side implementations
6. STATE MANAGEMENT
✓ Layer 1: React Query (server state caching)
✓ Layer 2: Zustand (client UI state)
✓ Layer 3: React Context (global features: theme, notifications, auth)
✓ Query keys follow strict naming convention for cache invalidation
7. LISTING DETAIL PAGE
✓ Server component: /[locale]/(public)/listings/[id]/page.tsx
✓ generateMetadata() for SEO (OG, canonical, schema)
✓ Client component: 39 KB listing-detail-client.tsx
✓ 9 major sections with dynamic imports for heavy components
✓ Neighborhood section uses dynamic imports (SSR: false)
8. PERFORMANCE OPTIMIZATIONS
✓ Code splitting via dynamic imports (maps, charts)
✓ Lazy loading with loading fallbacks
✓ React Query automatic caching + deduplication
✓ Tailwind CSS + CVA for efficient styling
✓ Responsive containers for all visualizations
9. INTERNATIONALIZATION
✓ next-intl 4.9.0 for translations
✓ Locale prefix in routes: [locale]
✓ Two locales: vi (Vietnamese), en (English)
✓ Translations in /messages directory
10. DESIGN SYSTEM
✓ Financial components (KPI cards, stat cards, price delta, signal display)
✓ Data display (data table wrapper, empty states, skeletons)
✓ Layout components (dashboard layout, density provider)
✓ UI primitives from shadcn/ui (badge, button, card, dialog, etc.)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 DEPENDENCIES SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CORE FRAMEWORK:
• Next.js 15.5.14 (App Router)
• React 18.3.0
STATE & DATA:
• @tanstack/react-query 5.96.2 (server state)
• @tanstack/react-table 8.21.3 (table rendering)
• zustand 5.0.12 (client state)
UI & STYLING:
• Tailwind CSS 3.4.0
• class-variance-authority 0.7.1
• lucide-react 1.7.0 (icons)
FORMS & VALIDATION:
• react-hook-form 7.72.1
• @hookform/resolvers 5.2.2
• zod 4.3.6
MAPPING & CHARTS:
• mapbox-gl 3.21.0 ⭐
• recharts 3.8.1 ⭐
UTILITIES:
• next-intl 4.9.0 (i18n)
• next-themes 0.4.6 (theme)
• socket.io-client 4.8.3 (real-time)
• html2canvas 1.4.1 + jspdf 4.2.1 (PDF export)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔗 COMPONENT IMPORT PATTERNS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Static imports (always in bundle):
import { Badge } from '@/components/ui/badge';
import { useMarketReport } from '@/lib/hooks/use-analytics';
Dynamic imports (code-split):
const NeighborhoodMap = dynamic(
() => import('@/components/neighborhood').then(m => m.NeighborhoodPOIMap),
{ ssr: false, loading: () => <Skeleton /> }
);
Barrel exports (clean imports):
// components/neighborhood/index.ts
export { NeighborhoodRadarChart } from './neighborhood-radar-chart';
export { NeighborhoodPOIMap } from './neighborhood-poi-map';
// Usage:
import { NeighborhoodRadarChart, NeighborhoodPOIMap } from '@/components/neighborhood';
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 QUICK START FOR DEVELOPMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ENVIRONMENT VARIABLES:
NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1
NEXT_PUBLIC_SITE_URL=https://goodgo.vn
MAPBOX_ACCESS_TOKEN=pk_... (from Mapbox)
COMMANDS:
npm run dev ← Start dev server (port 3000)
npm run build ← Production build
npm run lint ← ESLint check
npm test ← Vitest tests
npm run typecheck ← TypeScript check
KEY FILES TO KNOW:
lib/api-client.ts ← Base HTTP client
lib/listings-api.ts ← Listings endpoints
lib/hooks/use-analytics.ts ← React Query pattern
components/listings/listing-detail-client.tsx ← Main detail page
components/neighborhood/ ← Neighborhood features
components/charts/ ← Chart components
lib/mapbox-style.ts ← Map styling
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 NOTABLE IMPLEMENTATION DETAILS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. CSRF PROTECTION
└─ Token read from cookies on every request
└─ Sent as X-CSRF-Token header for non-GET requests
2. AUTH TOKEN REFRESH
└─ Automatic 401 handling with silent refresh
└─ Concurrent requests coalesced to single refresh call
└─ Auth endpoints excluded from refresh-retry
3. POI CATEGORIES
└─ 6 types: school, hospital, transit, shopping, restaurant, park
└─ SVG icons embedded in marker creation
└─ Color-coded by category in POI_CATEGORY_CONFIG
4. NEIGHBORHOOD SCORING
└─ 6 categories on 0-10 scale: education, healthcare, transport, shopping, dining, environment
└─ RadarChart displays all at once with badges below
└─ POIMap shows actual locations with distance info
5. PRICE FORMATTING
└─ Vietnamese currency (VND) with tr/k notation
└─ 1 tỷ = 1 billion (1,000,000,000)
└─ 1 tr = 1 million (1,000,000)
└─ 1 k = 1 thousand (1,000)
6. THEME SWITCHING
└─ next-themes manages light/dark mode
└─ Mapbox styles update reactively
└─ CSS variables for component theming
7. DYNAMIC COMPONENT LOADING
└─ Heavy components (NeighborhoodMap, charts) code-split
└─ SSR disabled for client-only components
└─ Loading fallback UI shown during load
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 PRO TIPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Use barrel exports (index.ts) for cleaner component imports
✓ Always add 'enabled' condition to useQuery() for dependent queries
✓ CSS variables (--signal-up, --color-border) support light/dark themes
✓ React Query query keys are the foundation—get them right for caching
✓ Mapbox GL must be in client components ('use client')
✓ Dynamic imports reduce initial bundle size significantly
✓ Design system provides trader-style financial components
✓ API responses are type-safe via domain-specific API modules
✓ SVG icons in POI markers avoid loading external font files
✓ Responsive containers automatically adjust chart/map sizing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📚 FILE SIZE REFERENCE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LARGE COMPONENTS (candidates for dynamic import):
listing-detail-client.tsx 39 KB (main listing detail)
listing-form-steps.tsx 14 KB (form wizard)
neighborhood-poi-map.tsx 11 KB (Mapbox visualization)
ai-advice-cards.tsx 9 KB (AI recommendations)
district-heatmap.tsx 9 KB (heat visualization)
location-picker.tsx 9 KB (map picker)
MEDIUM COMPONENTS:
agent-performance.tsx 6 KB (agent metrics chart)
price-history-chart.tsx 2 KB (inline price chart)
SMALL COMPONENTS:
neighborhood-radar-chart.tsx 2 KB (radar chart)
neighborhood-score.tsx 2 KB (score display)
price-area-chart.tsx 2 KB (area chart)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🚀 NEXT STEPS FOR DEVELOPMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Study the three generated reports in this order:
a) ARCHITECTURE_OVERVIEW.txt (visual understanding)
b) FRONTEND_QUICK_REFERENCE.txt (quick lookups)
c) FRONTEND_EXPLORATION_REPORT.md (deep dive)
2. Familiarize yourself with key files:
- lib/api-client.ts (understanding HTTP patterns)
- lib/listings-api.ts (domain API example)
- components/listings/listing-detail-client.tsx (main page structure)
- components/neighborhood/ (neighborhood features)
3. Run the dev server:
npm run dev
Open http://localhost:3000/vi/listings/[any-id]
4. Explore in browser:
- Check Network tab to see API calls
- Toggle theme to see Mapbox style change
- Inspect React tree to understand component hierarchy
- Check local storage for persisted state
5. Experiment with modifications:
- Add console.log to understand data flow
- Try modifying neighborhood scores to see chart update
- Add new query hooks following existing patterns
- Create a test component using existing patterns
═══════════════════════════════════════════════════════════════════════════════
Questions? Reference the reports or check key file paths in QUICK_REFERENCE.txt
═══════════════════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,266 @@
# GoodGo Analytics Module — Complete Architecture Guide
## 📚 Overview
This package contains **4 comprehensive guides** (2,174 lines, 71 KB) to understand the GoodGo Analytics Module architecture, caching system, and endpoint patterns.
## 📖 Quick Navigation
| File | Purpose | Read Time | Best For |
|------|---------|-----------|----------|
| **00_SUMMARY.md** | Overview & key findings | 5 min | Getting oriented |
| **02_quick_reference.md** | Visual diagrams & checklists | 10 min | Visual learners |
| **03_file_paths_reference.md** | File navigation & structure | 15 min | Finding code |
| **01_analytics_architecture_guide.md** | Deep dive with code examples | 30+ min | Full understanding |
## 🚀 Recommended Reading Path
1. Start with **00_SUMMARY.md** — understand what you're learning
2. Read **02_quick_reference.md** — visualize the architecture
3. Skim **03_file_paths_reference.md** — know where to find things
4. Deep dive **01_analytics_architecture_guide.md** — master the patterns
**Total time: ~60 minutes to fully understand the module**
## 🎯 What You'll Learn
### Architecture
- ✅ Domain-Driven Design (DDD) with CQRS pattern
- ✅ 4-layer structure: Presentation → Application → Domain → Infrastructure
- ✅ 24 endpoints across 2 controllers
- ✅ 15+ query handlers with caching
### Caching
- ✅ Redis cache-aside pattern
- ✅ Two caching styles: @Cacheable decorator vs manual cache.getOrSet()
- ✅ Cache TTLs for different endpoint types
- ✅ Graceful degradation when Redis is down
- ✅ Cache metadata in responses
### Database
- ✅ Prisma schema for Property, Listing, MarketIndex, Valuation
- ✅ PostGIS spatial queries
- ✅ Indexes for query optimization
### Implementation
- ✅ How to add a new GET endpoint in 7 steps
- ✅ Query handler patterns with real examples
- ✅ DTO design and validation
- ✅ Error handling conventions
- ✅ Testing patterns
## 🏗️ Architecture Overview
```
┌─────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ • AnalyticsController (19 endpoints) │
│ • AvmController (5 endpoints) │
│ • DTOs, Guards, Interceptors │
└──────────────────┬──────────────────────┘
↓ QueryBus.execute()
┌──────────────────────────────────────────┐
│ APPLICATION LAYER (CQRS) │
│ • 15+ Query Handlers (@QueryHandler) │
│ • @Cacheable or cache.getOrSet() │
│ • Try-catch error handling │
└──────────────────┬──────────────────────┘
↓ Injected services
┌──────────────────────────────────────────┐
│ DOMAIN LAYER (Business Logic) │
│ • Entities, Repository Interfaces │
│ • Service Interfaces, Result DTOs │
└──────────────────┬──────────────────────┘
↓ Injected implementation
┌──────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ • Prisma Repositories │
│ • HTTP Services, External clients │
└──────────────────────────────────────────┘
```
## 💾 Caching Pattern
```
HTTP GET /analytics/market-snapshot?city=Ho Chi Minh
QueryHandler builds key: "cache:analytics:market_snapshot:ho_chi_minh"
cache.getOrSet(key, loader, TTL, resource):
├─ Redis HIT: return cached value
├─ Redis MISS: call loader() → compute → store
└─ Redis DOWN: call loader() directly (graceful degradation)
CacheMetaInterceptor wraps: { data: result, cacheMeta: {...} }
HTTP 200: { data: {...}, cacheMeta: { cachedAt, nextRefreshAt, source } }
```
## 🔑 Key Findings
- **24 Endpoints**: 19 in AnalyticsController + 5 in AvmController
- **15+ Query Handlers**: All with caching (except predictions)
- **2 Caching Patterns**: @Cacheable decorator or manual cache.getOrSet()
- **Redis Cache-Aside**: TTL-based expiry, graceful degradation
- **4 Prisma Models**: Property, Listing, MarketIndex, Valuation
- **DTOs + Guards**: All endpoints secured with JWT, Quota, Rate Limit
## 📚 Detailed Contents
### 00_SUMMARY.md
- Overview of all 4 guides
- Key architecture decisions
- 7-step endpoint addition walkthrough
- Architecture decision matrix
- Core conventions checklist
### 01_analytics_architecture_guide.md ⭐ COMPREHENSIVE
- DDD layer breakdown with code examples
- All 24 endpoints documented
- Query handler patterns (decorator vs manual)
- Complete Redis caching system
- Real code from GetMarketSnapshotHandler, GetDistrictStatsHandler
- Full Prisma schema documentation
- Shared module utilities (CacheService, @Cacheable, CacheMetaInterceptor)
- Complete 7-step guide: adding GET /trending-areas endpoint
- Testing patterns & error handling conventions
### 02_quick_reference.md ⭐ VISUAL QUICK START
- Architecture layer stack diagram
- Request flow visualization
- Caching strategy matrix
- Decorators & guards cheat sheet
- Prisma schema snapshot
- Response structure examples
- 7-step endpoint addition checklist
### 03_file_paths_reference.md ⭐ NAVIGATION MAP
- Core module files (analytics.module.ts, index.ts)
- All 24 endpoints mapped to file paths
- DTO files organized by type (request vs response)
- All 15+ query types with descriptions
- Domain, Infrastructure, Shared layer breakdowns
- Database schema models with fields & indexes
- Directory tree with line counts
- Import patterns reference
- Key metrics & numbers
## 🎯 Quick Start: Adding New Endpoint
### 7 Steps to Add GET /analytics/trending-areas
1. **Request DTO**`presentation/dto/get-trending-areas.dto.ts`
2. **Query Class**`application/queries/get-trending-areas/query.ts`
3. **Handler**`application/queries/get-trending-areas/handler.ts` (with @Cacheable)
4. **Register** → Add to QueryHandlers array in analytics.module.ts
5. **Controller** → Add method to analytics.controller.ts
6. **Export** → Add to presentation/dto/index.ts
7. **Test** → Create handler spec
Full walkthrough with code in guides!
## ✅ Core Conventions
```ts
// Cache with TTL
Dashboard tiles: 300s (5 min)
Aggregations: 300s (5 min)
Reports: 900s (15 min)
Trends: 1800s (30 min)
Predictions: NO_CACHE (always fresh)
// Build cache keys deterministically
CacheService.buildKey(CachePrefix.MARKET_DISTRICT, city, period)
// Result: "cache:market:district:ho_chi_minh:2024_q1"
// Always wrap handlers in try-catch
try { ... } catch(error) {
if (error instanceof DomainException) throw error;
logger.error(...);
throw new InternalServerErrorException('...');
}
// Always return DTOs with null metadata
return { city, data, cachedAt: null, nextRefreshAt: null };
// Always use guards
@UseGuards(JwtAuthGuard, QuotaGuard)
@RequireQuota('analytics_queries')
```
## 📊 Key Metrics
| Metric | Value |
|--------|-------|
| Controllers | 2 |
| Endpoints | 24 |
| Query Handlers | 15+ |
| DTOs | 15+ |
| Caching Patterns | 2 |
| Cache Prefixes | 10+ |
| Cache TTLs | 20+ |
| Prisma Models | 4 |
| Repository Interfaces | 2 |
| Services (interfaces) | 2 |
| Services (implementations) | 6+ |
| Module LOC | ~2,000 |
| Shared Cache LOC | ~250 |
## 🧠 Test Your Understanding
After reading all guides, you should be able to:
1. Explain the 4 DDD layers and what goes in each
2. Describe how cache-aside pattern works
3. Distinguish @Cacheable from cache.getOrSet()
4. Explain why response DTOs have null metadata
5. Build deterministic cache keys
6. Choose appropriate TTLs for endpoints
7. List required guards for endpoints
8. Add a new endpoint in 7 steps
9. Explain CacheMetaInterceptor purpose
10. Describe graceful degradation behavior
## 📁 Prisma Schema Models
**Property** — Real estate property details
- Type, location (PostGIS), area, rooms, condition
- Indexes: [propertyType], [district, city], [location (Gist)]
**Listing** — Property listings on platform
- Price (BigInt), status, AVM estimates
- Indexes: [status], [sellerId, status], [publishedAt]
**MarketIndex** — Aggregated market data by period
- District, city, propertyType, period
- Median price, average price/m², statistics
- Unique: [district, city, propertyType, period]
**Valuation** — AI valuation estimates
- Property, estimated price, confidence, method
- Features, comparables, explainers (Json)
- Index: [propertyId, valuationDate DESC]
## 🚀 Next Steps
1. Read all 4 guides in recommended order (60 min)
2. Review the 7-step endpoint addition guide
3. Study real handler code examples in guide
4. Explore actual files in codebase:
- `apps/api/src/modules/analytics/`
- `apps/api/src/modules/shared/`
5. Add your first new endpoint following patterns
## 📞 Questions & Understanding
If you have questions after reading:
- Refer back to specific sections in guides
- Check code examples in guide vs actual code
- Review the 10 validation questions
- Study the architecture decision matrix
---
**Total package: 2,174 lines, 71 KB of comprehensive documentation**
Start with 00_SUMMARY.md and follow the reading path. You'll understand the entire architecture in about 60 minutes!