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>
This commit is contained in:
743
docs/audits/AGENT_PROFILE_EXPLORATION.md
Normal file
743
docs/audits/AGENT_PROFILE_EXPLORATION.md
Normal file
@@ -0,0 +1,743 @@
|
||||
# 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<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)
|
||||
|
||||
Reference in New Issue
Block a user