- Update Docker Compose configs for Redis, Typesense, and MinIO services - Update GitHub Actions deploy workflow with improved caching and steps - Extend .env.example with Stringee, Zalo OA, and FCM config keys - Update audit documentation with latest findings and recommendations - Update CHANGELOG and README with recent feature additions Co-Authored-By: Paperclip <noreply@paperclip.ing>
744 lines
21 KiB
Markdown
744 lines
21 KiB
Markdown
# 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)
|
|
|