Files
goodgo-platform/docs/audits/AGENT_PROFILE_QUICK_REFERENCE.md
Ho Ngoc Hai b8512ebff4 docs: consolidate audit and analysis reports into docs/audits/
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>
2026-04-11 01:37:50 +07:00

10 KiB

Agent Public Profile Page — Quick Reference Guide

🎯 Implementation Overview

URL Pattern

/agents/[id]                    # Desktop
/agents/[id]?locale=vi          # With locale (i18n)
/en/agents/[id]                 # Explicit locale

Page Location

apps/web/app/[locale]/(public)/agents/[id]/page.tsx

📦 Backend Setup (API Changes Required)

New Public Endpoint

GET /agents/:agentId/profile

Response DTO

{
  id: string;
  fullName: string;
  avatarUrl: string | null;
  licenseNumber: string | null;
  agency: string | null;
  qualityScore: number;
  bio: string | null;
  serviceAreas: string[];
  isVerified: boolean;
  totalListings: number;
  activeListings: number;
  avgReviewRating: number;
  totalReviews: number;
  phone?: string;
  createdAt: string;
  updatedAt: string;
}

Query Handler Files to Create

apps/api/src/modules/agents/application/queries/get-agent-profile/
├── get-agent-profile.query.ts
└── get-agent-profile.handler.ts

apps/api/src/modules/agents/presentation/dto/
└── agent-public-profile.dto.ts

🎨 Frontend Architecture

Directory Structure to Create

apps/web/
├── app/[locale]/(public)/agents/
│   └── [id]/
│       ├── page.tsx              ← Server component (metadata, ISR)
│       └── layout.tsx            ← Optional shared layout
│
├── components/agents/
│   ├── agent-detail-client.tsx   ← Main interactive component
│   ├── agent-header.tsx          ← Profile info section
│   ├── agent-listings-section.tsx ← Grid of listings
│   └── agent-reviews-section.tsx  ← Reviews with stars
│
└── lib/
    ├── agents-api.ts            ← API client
    └── agents-server.ts         ← Server-side ISR fetch

🔄 Data Flow

1. Browser requests: /agents/[id]
                ↓
2. Next.js generates metadata (SEO)
   - Title: "Agent Name — Real Estate Agent at GoodGo"
   - Description: Bio + stats
   - Image: Avatar
   - Canonical URL
                ↓
3. Server-side fetch agent profile data
   - GET /api/v1/agents/:id/profile (ISR: revalidate 3600s)
                ↓
4. Render Server Component with metadata + JSON-LD
                ↓
5. Pass data to Client Component for interactivity
   - Fetch reviews in parallel
   - Fetch listings in parallel
                ↓
6. Client renders:
   - Agent header (avatar, name, stats, badges)
   - Reviews section (star ratings, comment cards)
   - Listings section (reuse PropertyCard component)

🎭 Component Composition

Server Component (page.tsx)

export async function generateMetadata({ params }): Promise<Metadata> {
  // 1. Fetch agent using agents-server.ts
  // 2. Build SEO metadata
  // 3. Return title, description, OG, canonical
}

export default async function AgentProfilePage({ params }) {
  // 1. Fetch agent profile (with ISR)
  // 2. Generate JSON-LD (LocalBusiness schema)
  // 3. Render <JsonLd> for structured data
  // 4. Pass agent to client component
  return <>
    <JsonLd data={agentJsonLd} />
    <AgentDetailClient agent={agent} />
  </>
}

Client Component (agent-detail-client.tsx)

'use client';

export function AgentDetailClient({ agent }: Props) {
  const [listings, setListings] = useState([]);
  const [reviews, setReviews] = useState([]);
  const [reviewStats, setReviewStats] = useState(null);

  useEffect(() => {
    // Fetch agent's active listings
    // Fetch reviews & stats
  }, [agent.id]);

  return <>
    <AgentHeader agent={agent} />
    <AgentReviewsSection reviews={reviews} stats={reviewStats} />
    <AgentListingsSection listings={listings} />
  </>
}

🔗 API Calls Used

Existing Public Endpoints (No Auth Required)

# Get agent profile (NEW - to be created)
GET /api/v1/agents/:agentId/profile

# Get agent's listings (EXISTING)
GET /api/v1/listings?agentId=:agentId&status=ACTIVE

# Get agent reviews (EXISTING)
GET /api/v1/reviews?targetType=AGENT&targetId=:agentId&limit=20

# Get agent review stats (EXISTING)
GET /api/v1/reviews/stats?targetType=AGENT&targetId=:agentId

🎨 UI Sections & Reusable Components

1. Agent Header Section

┌─────────────────────────────────┐
│ [Avatar] Name                   │
│          License: ABC123        │
│          Agency: XYZ Agency     │
│          ✓ Verified             │
│                                 │
│ ⭐ 4.5 (120 reviews)            │
│ 📍 Serves: Quan 1, Quan 7, ...  │
│ 🏠 45 active listings           │
└─────────────────────────────────┘

Components: Card, Badge, Image, Text
Styling: Tailwind (p-6, flex, gap-4)

2. Reviews Section

┌─────────────────────────────────┐
│ Customer Reviews (120 total)    │
├─────────────────────────────────┤
│ [Review Card]                   │
│ ⭐⭐⭐⭐⭐ John Doe  │ 2 days ago │
│ "Great agent, very professional" │
├─────────────────────────────────┤
│ [Review Card]                   │
│ ⭐⭐⭐⭐ Jane Smith │ 1 week ago │
│ "Good communication"             │
└─────────────────────────────────┘

Components: Card, Badge (rating), Avatar
Reuse: PropertyCard padding/spacing pattern

3. Listings Section

┌─────────────────────────────────┐
│ Active Listings (45 total)      │
├─────────────────────────────────┤
│ [PropertyCard]  [PropertyCard]   │
│ [PropertyCard]  [PropertyCard]   │
│ [PropertyCard]  [PropertyCard]   │
└─────────────────────────────────┘

Components: PropertyCard (reuse from search/property-card.tsx)
Grid: grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4

🎯 Copy-Paste Templates

agents-api.ts

import { apiClient } from './api-client';

export interface AgentPublicProfile {
  id: string;
  fullName: string;
  avatarUrl: string | null;
  licenseNumber: string | null;
  agency: string | null;
  qualityScore: number;
  bio: string | null;
  serviceAreas: string[];
  isVerified: boolean;
  totalListings: number;
  activeListings: number;
  avgReviewRating: number;
  totalReviews: number;
  createdAt: string;
  updatedAt: string;
}

export const agentsApi = {
  getById: (id: string) => 
    apiClient.get<AgentPublicProfile>(`/agents/${id}/profile`),
};

agents-server.ts

const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';

export async function fetchAgentById(id: string) {
  try {
    const res = await fetch(`${API_BASE_URL}/agents/${id}/profile`, {
      next: { revalidate: 3600 } // ISR: revalidate every 1 hour
    });
    if (!res.ok) return null;
    return res.json();
  } catch {
    return null;
  }
}

🔍 SEO & Structured Data

Metadata Hints

title: `${agent.fullName} — Real Estate Agent at GoodGo`
description: `${agent.bio}. Quality Score: ${agent.qualityScore}. ${agent.activeListings} active listings. ⭐ ${agent.avgReviewRating} (${agent.totalReviews} reviews)`

// OG Image: Use avatar or placeholder

JSON-LD Schema

{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "Agent Full Name",
  "description": "Bio",
  "url": "https://goodgo.vn/en/agents/[id]",
  "image": "avatarUrl",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.5,
    "reviewCount": 120
  },
  "areaServed": ["Quan 1", "Quan 7", "Thu Duc"],
  "knowsAbout": "Real Estate"
}

🚀 Implementation Phases

Phase 1: Backend (1-2 hours)

  • Create GetAgentProfileQuery
  • Create GetAgentProfileHandler
  • Create AgentPublicProfileDto
  • Add endpoint to AgentsController
  • Update AgentRepository interface
  • Implement in PrismaAgentRepository
  • Test with Postman/curl

Phase 2: Frontend Setup (1 hour)

  • Create agents-api.ts
  • Create agents-server.ts
  • Create agents folder structure
  • Create [id]/page.tsx (stub)
  • Import types from agents-api.ts

Phase 3: UI Components (2-3 hours)

  • Create AgentHeader component
  • Create AgentReviewsSection component
  • Create AgentListingsSection component
  • Create RatingStars/ReviewCard components
  • Wire up data fetching

Phase 4: SEO & Polish (1 hour)

  • Add generateMetadata()
  • Generate JSON-LD schemas
  • Test OG preview
  • Mobile responsive check
  • Dark mode testing

Phase 5: Testing (1 hour)

  • Manual e2e test
  • Check 404 handling
  • Verify ISR revalidation
  • Test pagination (listings/reviews)
  • SEO audit (Lighthouse)

📊 Example Response Structure

{
  "id": "clu1x2y3z4a5b6c7d8e9f0",
  "fullName": "Nguyễn Văn A",
  "avatarUrl": "https://cdn.goodgo.vn/avatars/agent-123.jpg",
  "licenseNumber": "DA123456",
  "agency": "GoodGo Agency",
  "qualityScore": 4.8,
  "bio": "Specialized in high-end real estate in District 1 and 7",
  "serviceAreas": ["quan-1", "quan-7", "thu-duc"],
  "isVerified": true,
  "totalListings": 45,
  "activeListings": 32,
  "avgReviewRating": 4.7,
  "totalReviews": 120,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-04-10T15:45:00Z"
}

🎯 Key Files Reference

Phase Files to Create/Modify
Backend agents/application/queries/get-agent-profile/*
Backend agents/presentation/controllers/agents.controller.ts
Backend agents/presentation/dto/agent-public-profile.dto.ts
Frontend lib/agents-api.ts
Frontend lib/agents-server.ts
Frontend app/[locale]/(public)/agents/[id]/page.tsx
Frontend components/agents/agent-detail-client.tsx
Frontend components/agents/agent-header.tsx
Frontend components/agents/agent-reviews-section.tsx
Frontend components/agents/agent-listings-section.tsx