Move 36 root-level audit/analysis documents and 7 web app audit documents into docs/audits/ directory to declutter the project root. Remove stale EXPLORATION_SUMMARY.txt. Co-Authored-By: Paperclip <noreply@paperclip.ing>
21 KiB
GoodGo Agent Public Profile Page — Comprehensive Exploration Report
Date: April 11, 2026
Scope: Full stack exploration for implementing /agents/[id] public profile page
Codebase: GoodGo Platform (Next.js 14 + NestJS 10 + PostgreSQL + Prisma)
1. WEB APP STRUCTURE & ROUTING
File Structure
apps/web/
├── app/ # Next.js 14 App Router (Server Components)
│ ├── [locale]/ # Internationalization (i18n) at root level
│ │ ├── (admin)/ # Admin routes (protected)
│ │ ├── (auth)/ # Auth routes (sign-in, etc.)
│ │ ├── (dashboard)/ # Authenticated user dashboard
│ │ └── (public)/ # Public-facing routes
│ │ ├── listings/[id]/ # Existing listing detail page pattern
│ │ ├── search/
│ │ ├── compare/
│ │ ├── pricing/
│ │ └── page.tsx # Landing page
│ ├── layout.tsx # Root layout
│ ├── robots.ts # SEO: robots.txt
│ └── sitemap.ts # SEO: sitemap.xml
├── components/
│ ├── ui/ # UI library (button, card, badge, input, etc.)
│ ├── listings/ # Listing-specific components
│ ├── search/ # Search & property card components
│ ├── seo/ # JSON-LD, structured data
│ └── providers/ # Context providers
├── lib/
│ ├── api-client.ts # Fetch wrapper with CSRF protection
│ ├── listings-api.ts # API client for listings
│ ├── profile-api.ts # Auth/agent profile API client
│ ├── listings-server.ts # Server-side data fetching
│ ├── currency.ts # Currency formatting utilities
│ └── validations/ # Zod schemas (listings, auth, etc.)
└── public/ # Static assets
Routing Patterns
Public Routes (under (public)):
/→ Home/landing page/listings/[id]→ Listing detail (EXISTING PATTERN)/search→ Search results page/compare→ Property comparison page/pricing→ Pricing page
Key Insight: The (public) route group is for unauthenticated users. Agent profiles should follow the same pattern: /agents/[id] under the (public) group.
2. EXISTING AGENT-RELATED CODE
Agent Profile Type (Frontend)
File: apps/web/lib/profile-api.ts
export interface AgentProfile {
id: string;
email: string | null;
phone: string;
fullName: string;
avatarUrl: string | null;
role: string;
kycStatus: string;
isActive: boolean;
createdAt: string;
licenseNumber: string | null;
agency: string | null;
qualityScore: number | null;
serviceAreas: string[];
isVerified: boolean;
}
Existing Agent API Endpoints (Backend)
File: apps/api/src/modules/agents/presentation/controllers/agents.controller.ts
// Endpoints:
GET /agents/me/dashboard # Agent dashboard (authenticated)
POST /agents/:agentId/recalculate-score # Recalculate quality score (admin)
// Returns:
interface AgentDashboardData {
agentId: string;
qualityScore: number;
totalDeals: number;
responseTimeAvg: number | null;
isVerified: boolean;
totalLeads: number;
leadsByStatus: Record<string, number>;
conversionRate: number;
totalInquiries: number;
unreadInquiries: number;
totalListings: number;
activeListings: number;
avgReviewRating: number;
totalReviews: number;
}
Prisma Schema — Agent Model
File: prisma/schema.prisma
model Agent {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
licenseNumber String?
agency String?
qualityScore Float @default(0)
totalDeals Int @default(0)
responseTimeAvg Int?
bio String?
serviceAreas Json // JSON array: ["quan-1", "quan-7", "thu-duc"]
isVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
listings Listing[]
leads Lead[]
@@index([qualityScore])
@@index([isVerified])
}
Related models:
User— Agent's user account (fullName, avatarUrl, phone, email, role, kycStatus)Listing— Properties agent represents (hasagentIdforeign key)Lead— Leads tracked by agent
3. AGENT API ENDPOINTS NEEDED FOR PUBLIC PROFILE
Based on the existing architecture, we need to create a public endpoint to fetch agent profile data:
Proposed Endpoint
GET /agents/:agentId/profile
Response Structure (Public Profile DTO):
interface AgentPublicProfile {
id: string;
fullName: string;
avatarUrl: string | null;
// Agent-specific fields
licenseNumber: string | null;
agency: string | null;
qualityScore: number;
bio: string | null;
serviceAreas: string[];
isVerified: boolean;
// Stats
totalListings: number;
activeListings: number;
avgReviewRating: number;
totalReviews: number;
// Contact (optional, may require user preferences)
phone?: string;
// Timestamps
createdAt: string;
updatedAt: string;
}
Related Endpoints Needed:
GET /listings?agentId=:agentId&status=ACTIVE # Agent's active listings
GET /reviews/stats?targetType=AGENT&targetId=:agentId # Agent reviews stats
GET /reviews?targetType=AGENT&targetId=:agentId&limit=10 # Recent agent reviews
These endpoints already exist and are public (no authentication required).
4. SHARED UI COMPONENTS & DESIGN PATTERNS
Tailwind/Design System
File: apps/web/tailwind.config.ts
// CSS Variables used (dark mode support)
colors: {
primary, primary-foreground
secondary, secondary-foreground
destructive, destructive-foreground
muted, muted-foreground
accent, accent-foreground
card, card-foreground
}
// Radius variable: var(--radius)
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
}
Available UI Components
File: apps/web/components/ui/
button.tsx— Styled button with variantscard.tsx— Card (CardContent, etc.)badge.tsx— Badge with variantsinput.tsx,label.tsx— Form controlsdialog.tsx— Modal dialogtabs.tsx— Tab navigationtable.tsx— Data table
Key Component Patterns
Badge Component
<Badge variant="default">Đã xác minh</Badge>
<Badge variant="secondary">Info badge</Badge>
<Badge variant="outline">Outline badge</Badge>
<Badge variant="destructive">Danger</Badge>
Card Pattern
<Card>
<CardContent className="p-4">
{/* Content */}
</CardContent>
</Card>
Property Card (Listing Display) - REUSE THIS
File: apps/web/components/search/property-card.tsx
Shows a property with:
- Image gallery
- Price (formatted)
- Title & address
- Type, area, bedrooms badges
- Transaction type badge
Reusable for: Agent's listings display
5. STYLING & DESIGN PATTERNS
Global CSS
File: apps/web/app/globals.css
Uses CSS variables for theming:
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l))
--background: hsl(...)
--card: hsl(...)
/* Dark mode support via [data-theme="dark"] */
Typography
- Font: Inter (configured in tailwind.config.ts via CSS variable
--font-inter) - Heading levels: h1, h2, h3, h4
- Use classes:
text-lg font-bold,text-sm text-muted-foreground
Spacing
- Tailwind standard:
p-4,mt-8,gap-3, etc. - Card padding:
p-4 - Section padding:
py-16,py-24for hero sections
Example Layout Pattern
<section className="py-16 md:py-24">
<div className="mx-auto max-w-7xl px-4">
{/* Content */}
</div>
</section>
6. STATE MANAGEMENT & DATA FETCHING
API Client Pattern
File: apps/web/lib/api-client.ts
// Usage:
const apiClient = {
get: <T>(endpoint: string, headers?: HeadersInit) => request<T>(endpoint, { method: 'GET', headers }),
post: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) => request<T>(endpoint, { method: 'POST', body, headers }),
patch: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) => request<T>(endpoint, { method: 'PATCH', body, headers }),
delete: <T>(endpoint: string, headers?: HeadersInit) => request<T>(endpoint, { method: 'DELETE', headers }),
};
// CSRF protection included automatically
Server-Side Data Fetching Pattern
File: apps/web/lib/listings-server.ts
// Example: fetch on server at build time or request time
export async function fetchListingById(id: string) {
try {
const res = await fetch(`${API_BASE_URL}/listings/${id}`, {
next: { revalidate: 3600 } // ISR: revalidate every 1 hour
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
Client-Side Data Fetching Pattern
// In React component (using 'use client')
const [data, setData] = useState(null);
React.useEffect(() => {
apiClient
.get('/endpoint')
.then((res) => setData(res))
.catch(() => setError(true))
.finally(() => setLoading(false));
}, []);
No Global State (Zustand)
- Currently no Zustand stores for agent data in codebase
- Pattern: Fetch data in page component → pass to child components
- Dashboard uses local useState for profile fetch
7. SEO PATTERNS & STRUCTURED DATA
Metadata Generation Pattern
File: apps/web/app/[locale]/(public)/listings/[id]/page.tsx
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const listing = await fetchListingById(params.id);
if (!listing) {
return { title: 'Không tìm thấy' };
}
return {
title: `${property.title} - ${formatPrice(listing.priceVND)} VND`,
description: '...',
alternates: {
canonical: `${siteUrl}/${params.locale}/listings/${params.id}`,
languages: { vi: '...', en: '...' }
},
openGraph: {
type: 'article',
locale: params.locale === 'vi' ? 'vi_VN' : 'en_US',
url: canonicalUrl,
title,
images: [{ url: firstImage.url, width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title,
images: [firstImage.url],
},
};
}
JSON-LD Structured Data
File: apps/web/components/seo/json-ld.tsx
// Example: RealEstateListing schema
export function generateListingJsonLd(listing, siteUrl) {
return {
'@context': 'https://schema.org',
'@type': 'RealEstateListing',
name: property.title,
url: `${siteUrl}/listings/${listing.id}`,
offers: { '@type': 'Offer', price: priceNum, priceCurrency: 'VND' },
// ... more properties
};
}
// Usage in page:
<JsonLd data={listingJsonLd} />
<JsonLd data={breadcrumbJsonLd} />
For Agent Profile (Schema.org)
Appropriate schema: LocalBusiness or ProfessionalService
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Agent Full Name",
"description": "Bio",
"url": "https://goodgo.vn/en/agents/[id]",
"image": "avatarUrl",
"address": {
"@type": "PostalAddress",
"addressLocality": "Ho Chi Minh"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": avgReviewRating,
"reviewCount": totalReviews
}
}
8. EXISTING LISTING CARD COMPONENTS
Property Card Component
File: apps/web/components/search/property-card.tsx
interface PropertyCardProps {
listing: ListingDetail;
compact?: boolean;
}
// Displays:
// - Image gallery (with count badge)
// - Price (formatted)
// - Title & address
// - Badges: Transaction type, property type, area, bedrooms, bathrooms, direction
// - Compare button
Used in:
- Home page featured listings
- Search results
- Comparison page
For Agent Profile: Can reuse this component to display agent's listings!
9. REVIEW & RATING COMPONENTS
Review API Endpoints
File: apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts
// Endpoints:
GET /reviews # List reviews by target (pagination)
GET /reviews/stats # Get aggregate rating stats
GET /reviews/me # Get authenticated user's reviews
POST /reviews # Create review (authenticated)
DELETE /reviews/:id # Delete own review
// Query params:
GET /reviews?targetType=AGENT&targetId=:id&page=1&limit=20
GET /reviews/stats?targetType=AGENT&targetId=:id
Review DTO
interface ReviewItemData {
id: string;
userId: string;
targetType: string;
targetId: string;
rating: number; // 1-5
comment: string | null;
createdAt: string;
// User info:
user: {
id: string;
fullName: string;
avatarUrl: string | null;
};
}
interface ReviewStatsData {
targetType: string;
targetId: string;
totalReviews: number;
averageRating: number;
ratingDistribution: {
"1": number;
"2": number;
"3": number;
"4": number;
"5": number;
};
}
No Review Display Component Yet
- Dashboard profile shows
agentProfile.totalReviewsandavgReviewRating - Opportunity: Create a reusable
ReviewCardandRatingStarscomponent for agent profile
10. TYPE DEFINITIONS & INTERFACES
Core Types Used Frontend
// From listings-api.ts
export type TransactionType = 'SALE' | 'RENT';
export type PropertyType = 'APARTMENT' | 'HOUSE' | 'VILLA' | 'LAND' | 'OFFICE' | 'SHOPHOUSE';
export type ListingStatus = 'DRAFT' | 'PENDING_REVIEW' | 'ACTIVE' | 'RESERVED' | 'SOLD' | 'RENTED' | 'EXPIRED' | 'REJECTED';
export type Direction = 'NORTH' | 'SOUTH' | 'EAST' | 'WEST' | 'NORTHEAST' | 'NORTHWEST' | 'SOUTHEAST' | 'SOUTHWEST';
// From profile-api.ts
export interface AgentProfile {
id: string;
email: string | null;
phone: string;
fullName: string;
avatarUrl: string | null;
role: string;
kycStatus: string;
isActive: boolean;
createdAt: string;
licenseNumber: string | null;
agency: string | null;
qualityScore: number | null;
serviceAreas: string[];
isVerified: boolean;
}
// From listings-api.ts
export interface ListingDetail {
id: string;
status: ListingStatus;
transactionType: TransactionType;
priceVND: string;
publishedAt: string | null;
property: {
id: string;
propertyType: PropertyType;
title: string;
areaM2: number;
address: string;
ward: string;
district: string;
city: string;
media: PropertyMedia[];
// ... 15+ other properties
};
}
Validation Schemas (Zod)
File: apps/web/lib/validations/listings.ts
export const TRANSACTION_TYPES = [
{ value: 'SALE', label: 'Bán' },
{ value: 'RENT', label: 'Cho thuê' },
] as const;
export const PROPERTY_TYPES = [
{ value: 'APARTMENT', label: 'Căn hộ' },
{ value: 'HOUSE', label: 'Nhà riêng' },
// ... more
] as const;
export const LISTING_STATUSES = {
DRAFT: { label: 'Nháp', variant: 'secondary' },
ACTIVE: { label: 'Đang bán', variant: 'success' },
// ...
};
11. IMPLEMENTATION CHECKLIST FOR AGENT PROFILE PAGE
Backend (NestJS API)
New DTO & Interfaces:
✓ CreateGetAgentPublicProfileQuery
✓ GetAgentPublicProfileHandler
✓ AgentPublicProfileDto (response)
✓ Update agent.repository.ts with method: getPublicProfile(agentId: string)
✓ Update prisma-agent.repository.ts to fetch via Prisma
New Endpoint:
✓ GET /agents/:agentId/profile (public, no auth)
✓ Returns: AgentPublicProfileDto
✓ Validates agentId exists
✓ Returns 404 if not found
Leverage Existing:
- Review queries: Already public
- Listings queries: Already public
- User profile: Linking to agent.userId
Frontend (Next.js)
New Files:
✓ apps/web/app/[locale]/(public)/agents/ (directory)
✓ apps/web/app/[locale]/(public)/agents/[id]/ (directory)
✓ apps/web/app/[locale]/(public)/agents/[id]/page.tsx (server component)
✓ apps/web/app/[locale]/(public)/agents/[id]/layout.tsx (optional)
✓ apps/web/lib/agents-api.ts (API client)
✓ apps/web/lib/agents-server.ts (server-side fetch for ISR)
✓ apps/web/components/agents/ (new directory)
✓ apps/web/components/agents/agent-detail-client.tsx (client component)
✓ apps/web/components/agents/agent-listings-section.tsx
✓ apps/web/components/agents/agent-reviews-section.tsx
Page Structure (follows listing pattern):
// [id]/page.tsx (Server Component)
export async function generateMetadata({ params }): Promise<Metadata> {
// Fetch agent
// Return title, description, OG image, canonical URL
}
export default async function AgentProfilePage({ params }) {
// Fetch agent profile (server-side, ISR)
// Render JsonLd breadcrumb
// Render JsonLd LocalBusiness or ProfessionalService
// Pass to client component
return <>
<JsonLd data={agentJsonLd} />
<AgentDetailClient agent={agent} />
</>
}
Client Component:
- Display agent info (name, avatar, bio, license, agency)
- Show quality score & badges
- Render reviews section
- Render listings section
- Contact/inquiry button (optional)
API Client:
export const agentsApi = {
getById: (id: string) => apiClient.get<AgentPublicProfile>(`/agents/${id}/profile`),
};
12. KEY FILES TO REFERENCE/ADAPT
Backend
| File | Purpose |
|---|---|
apps/api/src/modules/agents/presentation/controllers/agents.controller.ts |
Add new public endpoint |
apps/api/src/modules/agents/domain/repositories/agent.repository.ts |
Add interface for public profile method |
apps/api/src/modules/agents/infrastructure/repositories/prisma-agent.repository.ts |
Implement public profile fetch |
apps/api/src/modules/agents/application/queries/ |
Create new query handler for public profile |
prisma/schema.prisma |
Reference for Agent model |
Frontend — Reference Examples
| File | Purpose | Reuse Pattern |
|---|---|---|
apps/web/app/[locale]/(public)/listings/[id]/page.tsx |
Listing detail page | Use as template for agent page |
apps/web/components/search/property-card.tsx |
Property card | Reuse for agent's listings |
apps/web/lib/listings-api.ts |
Listings API client | Create similar agents-api.ts |
apps/web/lib/listings-server.ts |
Server-side fetch | Create similar agents-server.ts |
apps/web/components/seo/json-ld.tsx |
Structured data | Adapt for LocalBusiness schema |
apps/web/lib/currency.ts |
Price formatting | Reuse for listing prices |
apps/web/tailwind.config.ts |
Design system | Reference for styling |
13. SUMMARY OF KEY FINDINGS
What Exists (Reusable)
✅ Agent model in Prisma with all needed fields
✅ API endpoints for listings, reviews (public)
✅ UI components: Card, Badge, Button, etc.
✅ Tailwind design system with dark mode
✅ SEO pattern with metadata generation & JSON-LD
✅ Image gallery component for listings
✅ PropertyCard component for listings display
✅ API client with CSRF protection
✅ Server-side data fetching with ISR pattern
What Needs Building (Agent Profile)
🔨 /agents/[id] page (server component with metadata)
🔨 AgentDetailClient component (client-side rendering)
🔨 Public endpoint: GET /agents/:agentId/profile
🔨 Agent listings section (reuse PropertyCard)
🔨 Agent reviews section (fetch & display reviews)
🔨 Rating stars/aggregate display component
🔨 agents-api.ts (fetch agent profile)
🔨 agents-server.ts (server-side fetch for ISR)
🔨 JSON-LD LocalBusiness schema for agent
Architecture Decisions
- Routing: Place at
apps/web/app/[locale]/(public)/agents/[id]/page.tsx - Pattern: Follow listing detail page pattern exactly
- Metadata: Use generateMetadata() server function
- Components: Split into Server Component (page) + Client Component (interactive)
- SEO: Include breadcrumb + LocalBusiness JSON-LD
- Styling: Use existing Tailwind tokens + components
- Data Fetching: Server-side fetch with ISR revalidation (3600s)
14. NEXT STEPS
- Design API DTO for public agent profile
- Create backend query handler for fetching public agent profile
- Create frontend API client (agents-api.ts)
- Build page structure following listing detail pattern
- Create agent detail client component with sections
- Add reviews display section with star ratings
- Add listings display section reusing PropertyCard
- Generate JSON-LD structured data for SEO
- Test ISR & metadata generation
- Add international routes (e.g., /en/agents/[id] via locale)