Files
goodgo-platform/apps/web/app/[locale]/(public)/agents/[id]/page.tsx
Ho Ngoc Hai 9cefd439db feat(fe): trader-style agent profile — TEC-3061
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>
2026-04-21 03:46:19 +07:00

126 lines
3.8 KiB
TypeScript

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { AgentProfileClient } from '@/components/agents/agent-profile-client';
import {
JsonLd,
generateAgentJsonLd,
generateBreadcrumbJsonLd,
} from '@/components/seo/json-ld';
import {
fetchAgentProfile,
fetchAgentReviews,
fetchAgentListings,
} from '@/lib/agents-server';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
// ---------------------------------------------------------------------------
// Metadata (runs server-side, provides <title>, <meta>, OG, canonical)
// ---------------------------------------------------------------------------
interface PageProps {
params: Promise<{ locale: string; id: string }>;
}
export async function generateMetadata({ params: paramsPromise }: PageProps): Promise<Metadata> {
const params = await paramsPromise;
const agent = await fetchAgentProfile(params.id);
if (!agent) {
return { title: 'Không tìm thấy môi giới' };
}
const title = agent.isVerified
? `${agent.fullName} — Môi giới xác minh | GoodGo`
: `${agent.fullName} — Môi giới BĐS | GoodGo`;
const description = [
agent.agency ? `Công ty: ${agent.agency}` : null,
agent.serviceAreas.length > 0
? `Khu vực: ${agent.serviceAreas.join(', ')}`
: null,
agent.totalReviews > 0
? `Đánh giá: ${agent.avgReviewRating}/5 (${agent.totalReviews} lượt)`
: null,
`${agent.activeListings.length} tin đăng đang hoạt động`,
`${agent.totalDeals} giao dịch thành công`,
]
.filter(Boolean)
.join(' | ');
const canonicalUrl = `${siteUrl}/${params.locale}/agents/${params.id}`;
return {
title,
description,
alternates: {
canonical: canonicalUrl,
languages: {
vi: `${siteUrl}/vi/agents/${params.id}`,
en: `${siteUrl}/en/agents/${params.id}`,
},
},
openGraph: {
type: 'profile',
locale: params.locale === 'vi' ? 'vi_VN' : 'en_US',
url: canonicalUrl,
title,
description,
siteName: 'GoodGo',
images: agent.avatarUrl
? [{ url: agent.avatarUrl, width: 400, height: 400, alt: agent.fullName }]
: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'GoodGo' }],
},
twitter: {
card: 'summary',
title,
description,
images: agent.avatarUrl ? [agent.avatarUrl] : ['/og-image.png'],
},
};
}
// ---------------------------------------------------------------------------
// Page (Server Component)
// ---------------------------------------------------------------------------
export default async function AgentProfilePage({ params: paramsPromise }: PageProps) {
const params = await paramsPromise;
const [agent, reviewsResult, listingsResult] = await Promise.all([
fetchAgentProfile(params.id),
fetchAgentReviews(params.id, 1, 10),
fetchAgentListings(params.id, 1, 50),
]);
if (!agent) {
notFound();
}
// Build JSON-LD structured data
const agentJsonLd = generateAgentJsonLd(agent, siteUrl);
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
{ name: 'Trang chủ', url: siteUrl },
{ name: 'Môi giới', url: `${siteUrl}/${params.locale}/agents` },
{ name: agent.fullName, url: `${siteUrl}/${params.locale}/agents/${params.id}` },
]);
return (
<>
{/* Structured data for search engines */}
<JsonLd data={agentJsonLd} />
<JsonLd data={breadcrumbJsonLd} />
{/* Interactive client component */}
<AgentProfileClient
agent={agent}
reviews={reviewsResult.data}
listings={listingsResult.data}
listingsTotal={listingsResult.total}
/>
</>
);
}