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>
389 lines
10 KiB
Markdown
389 lines
10 KiB
Markdown
# 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
|
|
```http
|
|
GET /agents/:agentId/profile
|
|
```
|
|
|
|
### Response DTO
|
|
```typescript
|
|
{
|
|
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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
'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)
|
|
```http
|
|
# 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```json
|
|
{
|
|
"@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
|
|
|
|
```json
|
|
{
|
|
"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` |
|
|
|