feat: Add user profile page and new modular profile components, integrating them into the agent dashboard.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
|
||||
// Import components from component folder
|
||||
// Import shared components
|
||||
import {
|
||||
InfoCard,
|
||||
ExperienceSection,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TestimonialsSection,
|
||||
StatusButtons,
|
||||
ContactInfo,
|
||||
} from './component';
|
||||
} from '@/components/profile';
|
||||
|
||||
// Mock agent data - in production, this would come from API
|
||||
const agentData = {
|
||||
@@ -132,6 +132,10 @@ export default function AgentDashboard() {
|
||||
memberSince={agentData.memberSince}
|
||||
bio={agentData.bio}
|
||||
expertise={agentData.expertise}
|
||||
showEditButton={true}
|
||||
editHref="/agent/edit"
|
||||
messageHref="/agent/message"
|
||||
requestsHref="/agent/network"
|
||||
/>
|
||||
|
||||
{/* Experience Section */}
|
||||
|
||||
195
src/app/(user)/user/profile/[id]/page.tsx
Normal file
195
src/app/(user)/user/profile/[id]/page.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
// Import shared components
|
||||
import {
|
||||
InfoCard,
|
||||
ExperienceSection,
|
||||
ProfileCard,
|
||||
SpecializationSection,
|
||||
TestimonialsSection,
|
||||
StatusButtons,
|
||||
ContactInfo,
|
||||
} from '@/components/profile';
|
||||
|
||||
// Mock agent data - in production, this would come from API based on id
|
||||
const getAgentData = (id: string) => ({
|
||||
id,
|
||||
name: 'Brian Neeland',
|
||||
isVerified: true,
|
||||
title: 'Licensed Real Estate Agent',
|
||||
location: 'Colorado',
|
||||
memberSince: 'March 2020',
|
||||
bio: 'Brian brings eight years of hands-on experience helping clients confidently buy, sell, and invest in real estate. He combines strong analytical skills with deep market knowledge to identify the right opportunities and guide clients through every step of the process. With a data-driven approach and clear communication, Brian ensures smooth transactions and informed decisions tailored to each client\'s goals.',
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural', 'FHA', 'Conventional', 'USDA', 'Retirement'],
|
||||
email: 'brian@gmail.com',
|
||||
phone: '+91*******7493',
|
||||
profileImage: '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg',
|
||||
experience: {
|
||||
years: '10+ years',
|
||||
contracts: '10+ Contracts',
|
||||
licensingAreas: ['Colorado Springs', 'Monument', 'Falcon', 'Castle Rock', 'Fountain', 'Momentum'],
|
||||
expertiseYears: [
|
||||
{ area: 'Colorado', years: '10 yrs' },
|
||||
{ area: 'Falcon', years: '10 yrs' },
|
||||
{ area: 'Colorado', years: '10 yrs' },
|
||||
{ area: 'Colorado', years: '10 yrs' },
|
||||
],
|
||||
certifications: [
|
||||
{ name: 'Certified Residential Specialist (CRS)', org: 'RESIDENTIAL REAL ESTATE COUNCIL' },
|
||||
{ name: 'Certified Residential Specialist (CRS)', org: 'RESIDENTIAL REAL ESTATE COUNCIL' },
|
||||
],
|
||||
},
|
||||
availability: {
|
||||
type: 'Full-time',
|
||||
days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||||
},
|
||||
preferredWorkEnvironment: 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes',
|
||||
testimonialHighlight: "The most amazing experience I've had as a real estate professional is helping a family secure their dream home and seeing their happiness when they received the keys.",
|
||||
specialization: {
|
||||
types: ['Buyers', 'Sellers', 'First time Buyers', 'First time Sellers', 'Divorce'],
|
||||
loanTypes: ['Conventional', 'FHA', 'VA', 'USDA'],
|
||||
propertyTypes: ['SFR', 'Town home', 'Rural', 'Luxury', 'Condo'],
|
||||
hobbies: ['Boating', 'Horses', 'RV', 'Automotive', 'Aviation'],
|
||||
pricePoints: ['Under $100K', '$100K – $300K', '$300K – $500K', '$500K – $1M', '$1M and above'],
|
||||
},
|
||||
testimonials: [
|
||||
{
|
||||
id: 1,
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
|
||||
author: 'Kenedy Kenney',
|
||||
role: 'Chief Operations Officer',
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
|
||||
author: 'Kenedy Kenney',
|
||||
role: 'Chief Operations Officer',
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
|
||||
author: 'Kenedy Kenney',
|
||||
role: 'Chief Operations Officer',
|
||||
rating: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default function AgentProfileView() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// In production, fetch agent data based on id
|
||||
const agentData = getAgentData(id);
|
||||
const [firstName, lastName] = agentData.name.split(' ');
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6 space-y-6">
|
||||
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
|
||||
{/* Left Sidebar - Status & Contact */}
|
||||
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
|
||||
{/* Profile Image */}
|
||||
<div className="relative w-[200px] lg:w-[260px]">
|
||||
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden">
|
||||
<Image
|
||||
src={agentData.profileImage}
|
||||
alt="Profile"
|
||||
width={260}
|
||||
height={260}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Buttons */}
|
||||
<StatusButtons />
|
||||
|
||||
{/* Contact Info */}
|
||||
<ContactInfo email={agentData.email} phone={agentData.phone} />
|
||||
</div>
|
||||
|
||||
{/* Right Content - Profile Info + Experience + All Sections */}
|
||||
<div className="flex-1 space-y-4">
|
||||
{/* Profile Card - No edit button for user view */}
|
||||
<ProfileCard
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isVerified={agentData.isVerified}
|
||||
title={agentData.title}
|
||||
location={agentData.location}
|
||||
memberSince={agentData.memberSince}
|
||||
bio={agentData.bio}
|
||||
expertise={agentData.expertise}
|
||||
showEditButton={false}
|
||||
/>
|
||||
|
||||
{/* Experience Section */}
|
||||
<ExperienceSection experience={agentData.experience} />
|
||||
|
||||
{/* Info Cards Section */}
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
|
||||
<InfoCard
|
||||
title="Availability"
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/availability-clock-icon.svg"
|
||||
alt="Availability"
|
||||
width={28}
|
||||
height={31}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<div>
|
||||
<p className="font-semibold font-serif text-[14px] leading-[19px] text-[#00293D] mb-2">{agentData.availability.type}</p>
|
||||
<div className="space-y-1">
|
||||
{agentData.availability.days.map((day) => (
|
||||
<p key={day} className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]">{day}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title=""
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/work-environment-icon.svg"
|
||||
alt="Work environment"
|
||||
width={28}
|
||||
height={28}
|
||||
/>
|
||||
}
|
||||
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{agentData.preferredWorkEnvironment}</p>}
|
||||
/>
|
||||
<InfoCard
|
||||
title=""
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/testimonial-star-icon.svg"
|
||||
alt="Testimonial"
|
||||
width={28}
|
||||
height={28}
|
||||
/>
|
||||
}
|
||||
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">“{agentData.testimonialHighlight}”</p>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Specialization Section */}
|
||||
<SpecializationSection specialization={agentData.specialization} />
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<TestimonialsSection testimonials={agentData.testimonials} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/components/profile/ContactInfo.tsx
Normal file
67
src/components/profile/ContactInfo.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface ContactInfoProps {
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
// Helper function to mask email
|
||||
function maskEmail(email: string): string {
|
||||
const [localPart, domain] = email.split('@');
|
||||
if (!domain) return '********';
|
||||
const maskedLocal = localPart.slice(0, 2) + '********';
|
||||
return `${maskedLocal}@${domain}`;
|
||||
}
|
||||
|
||||
// Helper function to mask phone
|
||||
function maskPhone(phone: string): string {
|
||||
if (phone.length <= 4) return '********';
|
||||
return phone.slice(0, -4).replace(/./g, '*') + phone.slice(-4);
|
||||
}
|
||||
|
||||
export function ContactInfo({ email, phone }: ContactInfoProps) {
|
||||
const [showEmail, setShowEmail] = useState(false);
|
||||
const [showPhone, setShowPhone] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-bold text-[#00293d] text-sm">Email:</span>
|
||||
<span className="text-[#00293d] text-sm font-serif">
|
||||
{showEmail ? email : maskEmail(email)}
|
||||
</span>
|
||||
<button
|
||||
className="p-1 hover:bg-gray-100 rounded ml-auto"
|
||||
onClick={() => setShowEmail(!showEmail)}
|
||||
>
|
||||
<Image
|
||||
src={showEmail ? "/assets/icons/eye-off-icon.svg" : "/assets/icons/eye-icon.svg"}
|
||||
alt={showEmail ? "Hide email" : "Show email"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-[#00293d] text-sm">Ph.No:</span>
|
||||
<span className="text-[#00293d] text-sm font-serif">
|
||||
{showPhone ? phone : maskPhone(phone)}
|
||||
</span>
|
||||
<button
|
||||
className="p-1 hover:bg-gray-100 rounded ml-auto"
|
||||
onClick={() => setShowPhone(!showPhone)}
|
||||
>
|
||||
<Image
|
||||
src={showPhone ? "/assets/icons/eye-off-icon.svg" : "/assets/icons/eye-icon.svg"}
|
||||
alt={showPhone ? "Hide phone" : "Show phone"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/components/profile/ExperienceSection.tsx
Normal file
83
src/components/profile/ExperienceSection.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { Tag } from './Tag';
|
||||
|
||||
interface ExperienceData {
|
||||
years: string;
|
||||
contracts: string;
|
||||
licensingAreas: string[];
|
||||
expertiseYears: { area: string; years: string }[];
|
||||
certifications: { name: string; org: string }[];
|
||||
}
|
||||
|
||||
interface ExperienceSectionProps {
|
||||
experience: ExperienceData;
|
||||
}
|
||||
|
||||
export function ExperienceSection({ experience }: ExperienceSectionProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
||||
<h2 className="text-[20px] font-bold text-[#00293D] font-fractul leading-[24px] mb-4 text-center lg:text-left">Experience</h2>
|
||||
<div className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||
{/* Left Column */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-2">Years in Experience</p>
|
||||
<ul className="list-disc list-inside">
|
||||
<li className="font-serif font-medium text-[15px] leading-[21px] text-[#00293D]">{experience.years}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Horizontal divider */}
|
||||
<div className="w-full h-0 border-t-[0.5px] border-solid border-[#00293D]/20" />
|
||||
<div>
|
||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-2">Number of contract closed</p>
|
||||
<ul className="list-disc list-inside">
|
||||
<li className="font-serif font-medium text-[15px] leading-[21px] text-[#00293D]">{experience.contracts}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-3">Licensing & Areas</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{experience.licensingAreas.map((area, idx) => (
|
||||
<Tag key={idx} variant="light">
|
||||
{area}
|
||||
</Tag>
|
||||
))}
|
||||
<span className="inline-flex items-center justify-center h-[28px] px-3 rounded-[15px] border border-[#00293d]/10 text-[15px] font-light font-serif text-[#00293d] cursor-pointer">+3 More</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider - Hidden on mobile, visible on desktop */}
|
||||
<div className="hidden lg:block w-px bg-gray-200" />
|
||||
<div className="lg:hidden border-t border-gray-200" />
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-3">Areas in expertise & Years</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{experience.expertiseYears.slice(0, 4).map((item, idx) => (
|
||||
<Tag key={idx} variant="light">
|
||||
{item.area} – {item.years}
|
||||
</Tag>
|
||||
))}
|
||||
<span className="inline-flex items-center justify-center h-[28px] px-3 rounded-[15px] border border-[#00293d]/10 text-[15px] font-light font-serif text-[#00293d] cursor-pointer">+3 More</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-3">Certifications</p>
|
||||
<ul className="space-y-3">
|
||||
{experience.certifications.map((cert, idx) => (
|
||||
<li key={idx}>
|
||||
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D]">{cert.name}</span>
|
||||
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] opacity-50 ml-3">{cert.org}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/components/profile/InfoCard.tsx
Normal file
17
src/components/profile/InfoCard.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export function InfoCard({ title, content, icon }: InfoCardProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-[10px] shadow-[0px_10px_20px_0px_rgba(217,217,217,0.5)] p-6 text-center">
|
||||
<div className="flex justify-center mb-3">{icon}</div>
|
||||
{title && <h4 className="font-semibold text-[#00293D] text-[14px] leading-[19px] mb-3 font-serif">{title}</h4>}
|
||||
<div className="text-[#00293d] text-sm font-serif">{content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
172
src/components/profile/ProfileCard.tsx
Normal file
172
src/components/profile/ProfileCard.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
interface ProfileCardProps {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isVerified: boolean;
|
||||
title: string;
|
||||
location: string;
|
||||
memberSince: string;
|
||||
bio: string;
|
||||
expertise: string[];
|
||||
showEditButton?: boolean;
|
||||
editHref?: string;
|
||||
messageHref?: string;
|
||||
requestsHref?: string;
|
||||
}
|
||||
|
||||
export function ProfileCard({
|
||||
firstName,
|
||||
lastName,
|
||||
isVerified,
|
||||
title,
|
||||
location,
|
||||
memberSince,
|
||||
bio,
|
||||
expertise,
|
||||
showEditButton = false,
|
||||
editHref = '/agent/edit',
|
||||
messageHref,
|
||||
requestsHref,
|
||||
}: ProfileCardProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
||||
{/* Name and Actions Row */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 mb-4">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="flex items-center justify-center lg:justify-start gap-2 mb-1">
|
||||
<h1 className="text-[18px] text-[#00293d] font-serif">
|
||||
<span className="font-semibold">{firstName}</span>{' '}
|
||||
<span className="font-normal">{lastName}</span>
|
||||
</h1>
|
||||
{isVerified && (
|
||||
<>
|
||||
<Image
|
||||
src="/assets/icons/verified-icon.svg"
|
||||
alt="Verified"
|
||||
width={15}
|
||||
height={14}
|
||||
/>
|
||||
<span className="text-[14px] text-[#638559] font-medium font-serif">
|
||||
(Verified Expert)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{showEditButton && (
|
||||
<Link href={editHref} className="p-1 hover:bg-gray-100 rounded transition-colors">
|
||||
<Image
|
||||
src="/assets/icons/edit-pencil-orange-icon.svg"
|
||||
alt="Edit profile"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[#00293d] text-sm mb-2 font-fractul">{title}</p>
|
||||
<div className="flex items-center justify-center lg:justify-start gap-4 text-sm text-[#00293d] font-fractul">
|
||||
<span className="flex items-center gap-1">
|
||||
<Image
|
||||
src="/assets/icons/location-pin-icon.svg"
|
||||
alt="Location"
|
||||
width={21}
|
||||
height={19}
|
||||
/>
|
||||
<span className="text-[14px] font-bold text-[#00293D] font-serif leading-[19px]">
|
||||
{location}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Image
|
||||
src="/assets/icons/calendar-icon.svg"
|
||||
alt="Calendar"
|
||||
width={23}
|
||||
height={21}
|
||||
/>
|
||||
<span className="text-[13px] font-medium text-[#00293D] font-serif leading-[18px]">
|
||||
Member Since {memberSince}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-center lg:justify-start gap-3">
|
||||
{messageHref ? (
|
||||
<Link
|
||||
href={messageHref}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src="/assets/icons/message-dark-icon.svg"
|
||||
alt="Message"
|
||||
width={14}
|
||||
height={13}
|
||||
/>
|
||||
Message
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src="/assets/icons/message-dark-icon.svg"
|
||||
alt="Message"
|
||||
width={14}
|
||||
height={13}
|
||||
/>
|
||||
Message
|
||||
</button>
|
||||
)}
|
||||
{requestsHref ? (
|
||||
<Link
|
||||
href={requestsHref}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src="/assets/icons/requests-dark-icon.svg"
|
||||
alt="Connect"
|
||||
width={14}
|
||||
height={13}
|
||||
/>
|
||||
{showEditButton ? 'Requests' : 'Connect'}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src="/assets/icons/requests-dark-icon.svg"
|
||||
alt="Connect"
|
||||
width={14}
|
||||
height={13}
|
||||
/>
|
||||
{showEditButton ? 'Requests' : 'Connect'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<p className="text-[14px] font-normal text-[#00293D] font-serif leading-[20px] mb-4 text-center lg:text-left">
|
||||
{bio}
|
||||
</p>
|
||||
|
||||
{/* Expertise Tags */}
|
||||
<div>
|
||||
<p className="text-[14px] font-bold text-[#00293D] font-fractul leading-[17px] mb-2 text-center lg:text-left">
|
||||
Expertise:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center lg:justify-start">
|
||||
{expertise.map((tag, idx) => (
|
||||
<Tag key={idx}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/components/profile/SpecializationCard.tsx
Normal file
36
src/components/profile/SpecializationCard.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
interface SpecializationCardProps {
|
||||
title: string;
|
||||
items: string[];
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SpecializationCard({ title, items, icon }: SpecializationCardProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-[20px] p-6 text-center flex flex-col h-full border-[0.7px] border-[#E58625]">
|
||||
<div className="flex justify-center mb-3">{icon}</div>
|
||||
<h4 className="font-bold text-[#00293D] text-[14px] leading-[17px] mb-4 font-fractul">{title}</h4>
|
||||
<div className="flex-1">
|
||||
{items.map((item, idx) => (
|
||||
<p key={idx} className="text-[#00293D] text-[14px] leading-[25px] font-normal font-serif text-center">
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button className="inline-flex items-center justify-center gap-1 h-[20px] w-[87px] border border-[#e58625] rounded-[20px] text-[#e58625] text-[10px] leading-[22px] font-bold font-serif hover:bg-[#e58625]/5 transition-colors">
|
||||
Show More
|
||||
<Image
|
||||
src="/assets/icons/chevron-down-icon.svg"
|
||||
alt="Show more"
|
||||
width={10}
|
||||
height={10}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/profile/SpecializationSection.tsx
Normal file
91
src/components/profile/SpecializationSection.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { SpecializationCard } from './SpecializationCard';
|
||||
|
||||
interface SpecializationData {
|
||||
types: string[];
|
||||
loanTypes: string[];
|
||||
propertyTypes: string[];
|
||||
hobbies: string[];
|
||||
pricePoints: string[];
|
||||
}
|
||||
|
||||
interface SpecializationSectionProps {
|
||||
specialization: SpecializationData;
|
||||
}
|
||||
|
||||
export function SpecializationSection({ specialization }: SpecializationSectionProps) {
|
||||
return (
|
||||
<div className="bg-[#e8e8e8] rounded-[20px] p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-[20px] font-bold text-[#00293D] font-fractul leading-[24px]">Specialization</h2>
|
||||
<p className="text-[14px] font-medium text-[#00293D] font-fractul leading-[17px]">Area Of Expertise and Focus</p>
|
||||
</div>
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6 mb-6">
|
||||
<SpecializationCard
|
||||
title="Specialization"
|
||||
items={specialization.types}
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/home-icon.svg"
|
||||
alt="Specialization"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SpecializationCard
|
||||
title="Loan Type"
|
||||
items={specialization.loanTypes}
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/wallet-icon.svg"
|
||||
alt="Loan Type"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SpecializationCard
|
||||
title="Property Type"
|
||||
items={specialization.propertyTypes}
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/chart-icon.svg"
|
||||
alt="Property Type"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-6 lg:max-w-2xl lg:mx-auto">
|
||||
<SpecializationCard
|
||||
title="Hobbies & Interests"
|
||||
items={specialization.hobbies}
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/heart-icon.svg"
|
||||
alt="Hobbies"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SpecializationCard
|
||||
title="Price Point"
|
||||
items={specialization.pricePoints}
|
||||
icon={
|
||||
<Image
|
||||
src="/assets/icons/wallet-icon.svg"
|
||||
alt="Price Point"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/profile/StarRating.tsx
Normal file
23
src/components/profile/StarRating.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
interface StarRatingProps {
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export function StarRating({ rating }: StarRatingProps) {
|
||||
return (
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Image
|
||||
key={star}
|
||||
src={star <= rating ? '/assets/icons/star-filled-icon.svg' : '/assets/icons/star-empty-icon.svg'}
|
||||
alt={star <= rating ? 'Filled star' : 'Empty star'}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/components/profile/StatusButtons.tsx
Normal file
30
src/components/profile/StatusButtons.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
interface StatusButtonsProps {
|
||||
isAvailable?: boolean;
|
||||
}
|
||||
|
||||
export function StatusButtons({ isAvailable = true }: StatusButtonsProps) {
|
||||
return (
|
||||
<div className="w-full max-w-[354px] lg:max-w-none space-y-2">
|
||||
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="text-sm font-bold text-[#00293d] font-fractul">Available.</span>
|
||||
</div>
|
||||
<button className="px-5 py-1.5 bg-[#e58625] text-white text-sm rounded-[15px] hover:bg-[#d47720] transition-colors font-fractul">
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="w-3 h-3 bg-white border border-gray-400 rounded-full" />
|
||||
<span className="text-sm font-bold text-[#00293d] font-fractul">Unavailable.</span>
|
||||
</div>
|
||||
<button className="px-5 py-1.5 bg-[#00293d] text-white text-sm rounded-[15px] hover:bg-[#001d2b] transition-colors font-fractul">
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/profile/Tag.tsx
Normal file
22
src/components/profile/Tag.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
interface TagProps {
|
||||
children: React.ReactNode;
|
||||
variant?: 'default' | 'light' | 'orange';
|
||||
}
|
||||
|
||||
export function Tag({ children, variant = 'default' }: TagProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center justify-center px-3 h-[28px] rounded-[15px] text-[14px] font-medium font-serif ${
|
||||
variant === 'light'
|
||||
? 'border-[0.7px] border-[#00293d] text-[#00293d] bg-transparent'
|
||||
: variant === 'orange'
|
||||
? 'border-[0.7px] border-[#e58625] text-[#e58625] bg-transparent'
|
||||
: 'border-[0.7px] border-[#00293d] text-[#00293d] bg-transparent'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
34
src/components/profile/TestimonialCard.tsx
Normal file
34
src/components/profile/TestimonialCard.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { StarRating } from './StarRating';
|
||||
|
||||
interface TestimonialCardProps {
|
||||
text: string;
|
||||
author: string;
|
||||
role: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export function TestimonialCard({ text, author, role, rating }: TestimonialCardProps) {
|
||||
return (
|
||||
<div className="p-4 border border-[#00293D]/10 rounded-[15px]">
|
||||
<div className="mb-4">
|
||||
<Image
|
||||
src="/assets/icons/quote-icon.svg"
|
||||
alt="Quote"
|
||||
width={23}
|
||||
height={13}
|
||||
className="mb-3"
|
||||
/>
|
||||
<p className="font-medium text-[#00293D] text-[14px] leading-[17px] font-fractul">I have been working with Lorem .</p>
|
||||
</div>
|
||||
<p className="text-[#00293D] text-[14px] leading-[19px] font-normal font-serif mb-4">{text}</p>
|
||||
<div className="pt-4 border-t border-gray-100 space-y-2">
|
||||
<StarRating rating={rating} />
|
||||
<p className="font-normal text-[#00293D] text-[14px] leading-[17px] font-fractul">{author}</p>
|
||||
<p className="font-fractul font-medium text-[14px] leading-[17px] text-[#00293D]">{role}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/profile/TestimonialsSection.tsx
Normal file
38
src/components/profile/TestimonialsSection.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { TestimonialCard } from './TestimonialCard';
|
||||
|
||||
interface Testimonial {
|
||||
id: number;
|
||||
text: string;
|
||||
author: string;
|
||||
role: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface TestimonialsSectionProps {
|
||||
testimonials: Testimonial[];
|
||||
}
|
||||
|
||||
export function TestimonialsSection({ testimonials }: TestimonialsSectionProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-[20px] p-8">
|
||||
<h2 className="text-[20px] font-bold text-[#00293D] font-fractul leading-[24px] text-center mb-4">Testimonials</h2>
|
||||
<div className="w-full h-px bg-[#00293D]/20 mb-4" />
|
||||
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] text-center mb-8">
|
||||
Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.
|
||||
</p>
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
|
||||
{testimonials.map((testimonial) => (
|
||||
<TestimonialCard
|
||||
key={testimonial.id}
|
||||
text={testimonial.text}
|
||||
author={testimonial.author}
|
||||
role={testimonial.role}
|
||||
rating={testimonial.rating}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/components/profile/index.ts
Normal file
12
src/components/profile/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Shared Profile Components
|
||||
export { StarRating } from './StarRating';
|
||||
export { Tag } from './Tag';
|
||||
export { InfoCard } from './InfoCard';
|
||||
export { SpecializationCard } from './SpecializationCard';
|
||||
export { ProfileCard } from './ProfileCard';
|
||||
export { ExperienceSection } from './ExperienceSection';
|
||||
export { SpecializationSection } from './SpecializationSection';
|
||||
export { TestimonialCard } from './TestimonialCard';
|
||||
export { TestimonialsSection } from './TestimonialsSection';
|
||||
export { StatusButtons } from './StatusButtons';
|
||||
export { ContactInfo } from './ContactInfo';
|
||||
Reference in New Issue
Block a user