# 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 15 + NestJS 10 + PostgreSQL + Prisma)

---

## 1. WEB APP STRUCTURE & ROUTING

### File Structure
```
apps/web/
├── app/                          # Next.js 15 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<string, number>;
  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
<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
```typescript
<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:
```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
<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`

```typescript
// 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`

```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<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`

```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:
<JsonLd data={listingJsonLd} />
<JsonLd data={breadcrumbJsonLd} />
```

### 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<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:**
```typescript
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

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)

