Files
goodgo-platform/apps/web/components/agents/agent-profile-client.tsx
Ho Ngoc Hai ea5d4af30c fix(web): wire up Nhắn tin button on agent profile page
The "Nhắn tin" (Message) button on the agent profile ContactCard had no
onClick handler. Now opens the InquiryModal using the agent's first
active listing, or falls back to SMS for agents with no listings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 03:18:14 +07:00

434 lines
15 KiB
TypeScript

'use client';
import {
BadgeCheck,
Building2,
Calendar,
MapPin,
Phone,
Mail,
Star,
Home,
MessageSquare,
} from 'lucide-react';
import Image from 'next/image';
import * as React from 'react';
import { InquiryModal } from '@/components/listings/inquiry-modal';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Link } from '@/i18n/navigation';
import type { AgentPublicProfile, AgentReviewItem } from '@/lib/agents-api';
import { formatPrice } from '@/lib/currency';
import { shimmerBlurDataURL } from '@/lib/image-blur';
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface AgentProfileClientProps {
agent: AgentPublicProfile;
reviews: AgentReviewItem[];
}
// ---------------------------------------------------------------------------
// Main Component
// ---------------------------------------------------------------------------
export function AgentProfileClient({ agent, reviews }: AgentProfileClientProps) {
const [inquiryOpen, setInquiryOpen] = React.useState(false);
const firstListing = agent.activeListings[0] ?? null;
const handleMessageClick = React.useCallback(() => {
if (firstListing) {
setInquiryOpen(true);
} else {
window.location.href = `sms:${agent.phone}`;
}
}, [firstListing, agent.phone]);
return (
<div className="mx-auto max-w-6xl px-4 py-6">
{/* Breadcrumb */}
<nav className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground">
<Link href="/" className="hover:text-foreground">Trang chủ</Link>
<span>/</span>
<span className="truncate text-foreground">{agent.fullName}</span>
</nav>
{/* Profile Header */}
<div className="mb-8 flex flex-col gap-6 sm:flex-row sm:items-start">
{/* Avatar */}
<div className="shrink-0">
{agent.avatarUrl ? (
<Image
src={agent.avatarUrl}
alt={agent.fullName}
width={120}
height={120}
className="h-28 w-28 rounded-full border-4 border-primary/10 object-cover sm:h-32 sm:w-32"
/>
) : (
<div className="flex h-28 w-28 items-center justify-center rounded-full border-4 border-primary/10 bg-primary/5 sm:h-32 sm:w-32">
<span className="text-4xl font-bold text-primary">
{agent.fullName.charAt(0).toUpperCase()}
</span>
</div>
)}
</div>
{/* Agent Info */}
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
<h1 className="text-2xl font-bold md:text-3xl">{agent.fullName}</h1>
{agent.isVerified && (
<Badge variant="success" className="gap-1">
<BadgeCheck className="h-3.5 w-3.5" />
Đã xác minh
</Badge>
)}
</div>
{agent.agency && (
<p className="mb-1 flex items-center gap-1.5 text-muted-foreground">
<Building2 className="h-4 w-4 shrink-0" />
{agent.agency}
</p>
)}
{agent.licenseNumber && (
<p className="mb-1 text-sm text-muted-foreground">
giấy phép: {agent.licenseNumber}
</p>
)}
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Calendar className="h-4 w-4 shrink-0" />
Thành viên từ {new Date(agent.memberSince).toLocaleDateString('vi-VN', {
month: 'long',
year: 'numeric',
})}
</p>
{/* Quick stats */}
<div className="mt-4 flex flex-wrap gap-4">
<StatPill
icon={<Star className="h-4 w-4 text-yellow-500" />}
label="Đánh giá"
value={agent.totalReviews > 0
? `${agent.avgReviewRating}/5 (${agent.totalReviews})`
: 'Chưa có'}
/>
<StatPill
icon={<Home className="h-4 w-4 text-primary" />}
label="Tin đăng"
value={`${agent.activeListings.length}`}
/>
<StatPill
icon={<BadgeCheck className="h-4 w-4 text-green-500" />}
label="Giao dịch"
value={`${agent.totalDeals}`}
/>
</div>
</div>
{/* CTA Sidebar (desktop) */}
<div className="hidden shrink-0 sm:block">
<ContactCard agent={agent} onMessageClick={handleMessageClick} />
</div>
</div>
<div className="grid gap-6 lg:grid-cols-3">
{/* Main Content */}
<div className="space-y-6 lg:col-span-2">
{/* Bio */}
{agent.bio && (
<Card>
<CardHeader>
<CardTitle>Giới thiệu</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-sm leading-relaxed">{agent.bio}</p>
</CardContent>
</Card>
)}
{/* Service Areas */}
{agent.serviceAreas.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Khu vực hoạt đng</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{agent.serviceAreas.map((area) => (
<Badge key={area} variant="secondary" className="gap-1">
<MapPin className="h-3 w-3" />
{area}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{/* Quality Score */}
<Card>
<CardHeader>
<CardTitle>Chỉ số chất lượng</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="relative h-20 w-20">
<svg className="h-20 w-20 -rotate-90 transform" viewBox="0 0 36 36">
<path
className="text-muted"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
<path
className="text-primary"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeDasharray={`${agent.qualityScore}, 100`}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-lg font-bold">{agent.qualityScore}</span>
</div>
</div>
<div>
<p className="font-medium">
{agent.qualityScore >= 80
? 'Xuất sắc'
: agent.qualityScore >= 60
? 'Tốt'
: agent.qualityScore >= 40
? 'Trung bình'
: 'Cần cải thiện'}
</p>
<p className="text-sm text-muted-foreground">
Dựa trên phản hồi, thời gian phản hồi giao dịch thành công
</p>
</div>
</div>
</CardContent>
</Card>
{/* Active Listings */}
{agent.activeListings.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Tin đăng đang hoạt đng ({agent.activeListings.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
{agent.activeListings.map((listing) => (
<ListingCard key={listing.id} listing={listing} />
))}
</div>
</CardContent>
</Card>
)}
{/* Reviews */}
<Card>
<CardHeader>
<CardTitle>Đánh giá ({agent.totalReviews})</CardTitle>
</CardHeader>
<CardContent>
{reviews.length > 0 ? (
<div className="space-y-4">
{reviews.map((review) => (
<ReviewCard key={review.id} review={review} />
))}
</div>
) : (
<p className="text-center text-sm text-muted-foreground py-4">
Chưa đánh giá nào
</p>
)}
</CardContent>
</Card>
</div>
{/* Sidebar (mobile + desktop fallback) */}
<div className="space-y-6">
<div className="sm:hidden">
<ContactCard agent={agent} onMessageClick={handleMessageClick} />
</div>
<div className="hidden sm:block lg:block">
<div className="lg:sticky lg:top-20">
<div className="hidden lg:block">
<ContactCard agent={agent} onMessageClick={handleMessageClick} />
</div>
</div>
</div>
</div>
</div>
{firstListing && (
<InquiryModal
open={inquiryOpen}
onOpenChange={setInquiryOpen}
listingId={firstListing.id}
listingTitle={firstListing.property.title}
sellerName={agent.fullName}
/>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Sub-Components
// ---------------------------------------------------------------------------
function StatPill({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: string;
}) {
return (
<div className="flex items-center gap-2 rounded-lg border bg-card px-3 py-2">
{icon}
<div>
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-sm font-semibold">{value}</p>
</div>
</div>
);
}
function ContactCard({ agent, onMessageClick }: { agent: AgentPublicProfile; onMessageClick: () => void }) {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Liên hệ môi giới</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<a href={`tel:${agent.phone}`} className="block">
<Button className="w-full gap-2">
<Phone className="h-4 w-4" />
Gọi ngay
</Button>
</a>
<Button variant="outline" className="w-full gap-2" onClick={onMessageClick}>
<MessageSquare className="h-4 w-4" />
Nhắn tin
</Button>
{agent.email && (
<a href={`mailto:${agent.email}`} className="block">
<Button variant="outline" className="w-full gap-2">
<Mail className="h-4 w-4" />
Email
</Button>
</a>
)}
<div className="border-t pt-3">
<p className="text-xs text-muted-foreground">Số điện thoại</p>
<p className="text-sm font-medium">{agent.phone}</p>
{agent.email && (
<>
<p className="mt-2 text-xs text-muted-foreground">Email</p>
<p className="text-sm font-medium">{agent.email}</p>
</>
)}
</div>
</CardContent>
</Card>
);
}
function ListingCard({ listing }: { listing: AgentPublicProfile['activeListings'][number] }) {
const { property } = listing;
return (
<Link href={`/listings/${listing.id}` as never} className="block">
<div className="group overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-md">
{/* Image */}
<div className="relative aspect-[16/10] overflow-hidden bg-muted">
{property.imageUrl ? (
<Image
src={property.imageUrl}
alt={property.title}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 640px) 100vw, 50vw"
placeholder="blur"
blurDataURL={shimmerBlurDataURL()}
/>
) : (
<div className="flex h-full items-center justify-center">
<Home className="h-8 w-8 text-muted-foreground" />
</div>
)}
<Badge className="absolute left-2 top-2" variant={listing.transactionType === 'SALE' ? 'default' : 'secondary'}>
{listing.transactionType === 'SALE' ? 'Bán' : 'Cho thuê'}
</Badge>
</div>
{/* Content */}
<div className="p-3">
<h3 className="line-clamp-1 text-sm font-semibold">{property.title}</h3>
<p className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
<MapPin className="h-3 w-3 shrink-0" />
{property.district}, {property.city}
</p>
<div className="mt-2 flex items-center justify-between">
<p className="text-sm font-bold text-primary">{formatPrice(listing.priceVND)} VND</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{property.areaM2} m²</span>
{property.bedrooms != null && <span>{property.bedrooms} PN</span>}
</div>
</div>
</div>
</div>
</Link>
);
}
function ReviewCard({ review }: { review: AgentReviewItem }) {
return (
<div className="rounded-lg border p-4">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
{(review.userName ?? 'Ẩn danh').charAt(0).toUpperCase()}
</div>
<div>
<p className="text-sm font-medium">{review.userName ?? 'Ẩn danh'}</p>
<p className="text-xs text-muted-foreground">
{new Date(review.createdAt).toLocaleDateString('vi-VN')}
</p>
</div>
</div>
<div className="flex items-center gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${
i < review.rating
? 'fill-yellow-400 text-yellow-400'
: 'text-muted-foreground/30'
}`}
/>
))}
</div>
</div>
{review.comment && (
<p className="text-sm text-muted-foreground">{review.comment}</p>
)}
</div>
);
}