# 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` ```typescript 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` ```typescript // 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; conversionRate: number; totalInquiries: number; unreadInquiries: number; totalListings: number; activeListings: number; avgReviewRating: number; totalReviews: number; } ``` ### Prisma Schema — Agent Model **File:** `prisma/schema.prisma` ```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 (has `agentId` foreign 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 ```http GET /agents/:agentId/profile ``` **Response Structure (Public Profile DTO):** ```typescript 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:** ```http 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` ```typescript // 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 variants - `card.tsx` — Card (CardContent, etc.) - `badge.tsx` — Badge with variants - `input.tsx`, `label.tsx` — Form controls - `dialog.tsx` — Modal dialog - `tabs.tsx` — Tab navigation - `table.tsx` — Data table ### Key Component Patterns #### Badge Component ```typescript Đã xác minh Info badge Outline badge Danger ``` #### Card Pattern ```typescript {/* Content */} ``` #### 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: ```css --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-24` for hero sections ### Example Layout Pattern ```typescript
{/* Content */}
``` --- ## 6. STATE MANAGEMENT & DATA FETCHING ### API Client Pattern **File:** `apps/web/lib/api-client.ts` ```typescript // Usage: const apiClient = { get: (endpoint: string, headers?: HeadersInit) => request(endpoint, { method: 'GET', headers }), post: (endpoint: string, body?: unknown, headers?: HeadersInit) => request(endpoint, { method: 'POST', body, headers }), patch: (endpoint: string, body?: unknown, headers?: HeadersInit) => request(endpoint, { method: 'PATCH', body, headers }), delete: (endpoint: string, headers?: HeadersInit) => request(endpoint, { method: 'DELETE', headers }), }; // CSRF protection included automatically ``` ### Server-Side Data Fetching Pattern **File:** `apps/web/lib/listings-server.ts` ```typescript // 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 ```typescript // 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` ```typescript export async function generateMetadata({ params }: PageProps): Promise { 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` ```typescript // 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: ``` ### For Agent Profile (Schema.org) **Appropriate schema:** `LocalBusiness` or `ProfessionalService` ```json { "@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` ```typescript 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` ```typescript // 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 ```typescript 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.totalReviews` and `avgReviewRating` - **Opportunity:** Create a reusable `ReviewCard` and `RatingStars` component for agent profile --- ## 10. TYPE DEFINITIONS & INTERFACES ### Core Types Used Frontend ```typescript // 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` ```typescript 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):** ```typescript // [id]/page.tsx (Server Component) export async function generateMetadata({ params }): Promise { // 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 <> } ``` **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:** ```typescript export const agentsApi = { getById: (id: string) => apiClient.get(`/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 1. **Design API DTO** for public agent profile 2. **Create backend query handler** for fetching public agent profile 3. **Create frontend API client** (agents-api.ts) 4. **Build page structure** following listing detail pattern 5. **Create agent detail client component** with sections 6. **Add reviews display section** with star ratings 7. **Add listings display section** reusing PropertyCard 8. **Generate JSON-LD** structured data for SEO 9. **Test ISR & metadata** generation 10. **Add international routes** (e.g., /en/agents/[id] via locale)