Refactors /agents/[id] from card-avatar layout to a data-dense trading-floor style profile per TEC-3037 §5 mockup. - Profile header: avatar, KYC badge, quality score, years exp, service areas - KPI strip (5 cards): total listings, active, deals, avg price, rating - Performance line chart (12m): published vs sold, derived from real listings - Listings table (DataTable): sortable by price/area/views/inquiries, dense rows - Reviews panel: EmptyState when none, ReviewRow cards otherwise - Sticky right sidebar: contact card + quality donut + bio - fetchAgentListings() server fn (agents-server.ts) via GET /listings?agentId - SearchListingsParams.agentId added (listings-api.ts) - page.tsx fetches listings in parallel with agent + reviews - Test suite updated for new props (listings/listingsTotal) + new text copy - Web unit tests: 82/82 files pass, 697/697 tests pass Co-Authored-By: Paperclip <noreply@paperclip.ing>
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
/**
|
|
* Server-side agent data fetching for Next.js Server Components.
|
|
*
|
|
* Uses `fetch` directly (no browser-only helpers) so it can run
|
|
* inside `generateMetadata`, server pages, etc.
|
|
*/
|
|
|
|
import type { AgentPublicProfile, AgentReviewStats, PaginatedReviews } from './agents-api';
|
|
import type { ListingDetail, PaginatedResult } from './listings-api';
|
|
|
|
const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';
|
|
|
|
/**
|
|
* Fetch a public agent profile by ID — server-only.
|
|
* Returns `null` when the agent is not found (404) so callers can `notFound()`.
|
|
*/
|
|
export async function fetchAgentProfile(agentId: string): Promise<AgentPublicProfile | null> {
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/agents/${agentId}/profile`, {
|
|
next: { revalidate: 300 }, // ISR: re-validate every 5 min
|
|
});
|
|
|
|
if (!res.ok) return null;
|
|
return (await res.json()) as AgentPublicProfile;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch reviews for an agent — server-only.
|
|
*/
|
|
export async function fetchAgentReviews(
|
|
agentId: string,
|
|
page = 1,
|
|
limit = 10,
|
|
): Promise<PaginatedReviews> {
|
|
try {
|
|
const res = await fetch(
|
|
`${API_BASE_URL}/reviews?targetType=AGENT&targetId=${agentId}&page=${page}&limit=${limit}`,
|
|
{ next: { revalidate: 300 } },
|
|
);
|
|
|
|
if (!res.ok) {
|
|
return { data: [], total: 0, page: 1, limit: 10, totalPages: 0 };
|
|
}
|
|
return (await res.json()) as PaginatedReviews;
|
|
} catch {
|
|
return { data: [], total: 0, page: 1, limit: 10, totalPages: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch review stats for an agent — server-only.
|
|
*/
|
|
export async function fetchAgentReviewStats(agentId: string): Promise<AgentReviewStats | null> {
|
|
try {
|
|
const res = await fetch(
|
|
`${API_BASE_URL}/reviews/stats?targetType=AGENT&targetId=${agentId}`,
|
|
{ next: { revalidate: 300 } },
|
|
);
|
|
|
|
if (!res.ok) return null;
|
|
return (await res.json()) as AgentReviewStats;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch listings managed by a given agent — server-only.
|
|
* Returns `{ data: [], total: 0 }` on error so callers degrade gracefully.
|
|
*/
|
|
export async function fetchAgentListings(
|
|
agentId: string,
|
|
page = 1,
|
|
limit = 50,
|
|
): Promise<{ data: ListingDetail[]; total: number }> {
|
|
try {
|
|
const qs = new URLSearchParams({
|
|
agentId,
|
|
page: String(page),
|
|
limit: String(limit),
|
|
});
|
|
const res = await fetch(`${API_BASE_URL}/listings?${qs.toString()}`, {
|
|
next: { revalidate: 300 },
|
|
});
|
|
|
|
if (!res.ok) return { data: [], total: 0 };
|
|
const result = (await res.json()) as PaginatedResult<ListingDetail>;
|
|
return { data: result.data, total: result.total };
|
|
} catch {
|
|
return { data: [], total: 0 };
|
|
}
|
|
}
|