feat: Implement dynamic featured agent cards with image loading and CMS content integration in FeaturesSection.
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import type { FeaturesContent } from '@/types/cms';
|
import type { FeaturesContent, FeaturedAgentItem } from '@/types/cms';
|
||||||
|
import { resolveImageUrl } from '@/services/cms.service';
|
||||||
|
|
||||||
const defaultContent: FeaturesContent = {
|
const defaultContent: FeaturesContent = {
|
||||||
title: 'Find Trusted Real Estate Professionals On Demand',
|
title: 'Find Trusted Real Estate Professionals On Demand',
|
||||||
@@ -28,108 +30,70 @@ const defaultContent: FeaturesContent = {
|
|||||||
description: 'Thousands of buyers, sellers, and renters trust our platform to find reliable agents. Our professionals help people navigate property journeys with confidence and ease.',
|
description: 'Thousands of buyers, sellers, and renters trust our platform to find reliable agents. Our professionals help people navigate property journeys with confidence and ease.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
featuredAgents: [
|
||||||
|
{ name: 'Anderson', role: 'Rental & Investment Consultant', rating: '4.9', location: 'New York', experience: '7+ years in the real estate industry.', imageUrl: '/assets/images/agent-anderson.jpg' },
|
||||||
|
{ name: 'Deepak', role: 'Rental & Investment Consultant', rating: '4.9', location: 'New York', experience: '7+ years in the real estate industry.', imageUrl: '/assets/images/agent-deepak.jpg' },
|
||||||
|
{ name: 'Daniel', role: 'Rental & Investment Consultant', rating: '4.9', location: 'New York', experience: '7+ years in the real estate industry.', imageUrl: '/assets/images/agent-daniel.jpg' },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const agents = [
|
function AgentCard({ agent, className = '' }: { agent: FeaturedAgentItem; className?: string }) {
|
||||||
{
|
const [imgLoaded, setImgLoaded] = useState(false);
|
||||||
name: 'Anderson',
|
const [imgSrc, setImgSrc] = useState(agent.imageUrl);
|
||||||
role: 'Rental & Investment Consultant',
|
|
||||||
rating: '4.9',
|
useEffect(() => {
|
||||||
location: 'New York',
|
// Only resolve if it's an S3 key (not a URL or local path)
|
||||||
experience: '7+ years in the real estate industry.',
|
if (agent.imageUrl && !agent.imageUrl.startsWith('http') && !agent.imageUrl.startsWith('/')) {
|
||||||
image: '/assets/images/agent-anderson.jpg',
|
resolveImageUrl(agent.imageUrl).then((url) => {
|
||||||
position: 'top-left',
|
if (url) setImgSrc(url);
|
||||||
},
|
});
|
||||||
{
|
} else {
|
||||||
name: 'Deepak',
|
setImgSrc(agent.imageUrl);
|
||||||
role: 'Rental & Investment Consultant',
|
}
|
||||||
rating: '4.9',
|
}, [agent.imageUrl]);
|
||||||
location: 'New York',
|
|
||||||
experience: '7+ years in the real estate industry.',
|
|
||||||
image: '/assets/images/agent-deepak.jpg',
|
|
||||||
position: 'top-right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Daniel',
|
|
||||||
role: 'Rental & Investment Consultant',
|
|
||||||
rating: '4.9',
|
|
||||||
location: 'New York',
|
|
||||||
experience: '7+ years in the real estate industry.',
|
|
||||||
image: '/assets/images/agent-daniel.jpg',
|
|
||||||
position: 'bottom-center',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function AgentCard({ agent, className = '' }: { agent: typeof agents[0]; className?: string }) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`bg-white border border-[#00293d]/10 rounded-[15px] w-[254px] overflow-hidden ${className}`}>
|
<div className={`bg-white border border-[#00293d]/10 rounded-[15px] w-[254px] overflow-hidden ${className}`}>
|
||||||
{/* Agent Image */}
|
<div className="h-[161px] w-full overflow-hidden rounded-t-[15px] bg-gray-100 relative">
|
||||||
<div className="h-[161px] w-full overflow-hidden rounded-t-[15px]">
|
{imgSrc && (
|
||||||
<Image
|
<img
|
||||||
src={agent.image}
|
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setImgLoaded(true); }}
|
||||||
alt={agent.name}
|
src={imgSrc}
|
||||||
width={254}
|
alt={agent.name}
|
||||||
height={161}
|
className={`w-full h-full object-cover transition-opacity duration-300 ${imgLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
className="w-full h-full object-cover"
|
onLoad={() => setImgLoaded(true)}
|
||||||
/>
|
onError={() => setImgLoaded(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!imgLoaded && <div className="absolute inset-0 shimmer-loading" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent Info */}
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* Name */}
|
<h4 className="font-fractul font-bold text-[16px] leading-[19px] text-[#00293d]">{agent.name}</h4>
|
||||||
<h4 className="font-fractul font-bold text-[16px] leading-[19px] text-[#00293d]">
|
<p className="font-fractul font-normal text-[14px] leading-[17px] text-[#00293d] mt-1">{agent.role}</p>
|
||||||
{agent.name}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{/* Role */}
|
|
||||||
<p className="font-fractul font-normal text-[14px] leading-[17px] text-[#00293d] mt-1">
|
|
||||||
{agent.role}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Verified & Rating */}
|
|
||||||
<div className="flex items-center gap-4 mt-2">
|
<div className="flex items-center gap-4 mt-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Image
|
<Image src="/assets/icons/verified-icon.svg" alt="Verified" width={14} height={14} />
|
||||||
src="/assets/icons/verified-icon.svg"
|
<span className="font-serif font-medium text-[14px] leading-[19px] text-[#00293d]">Verified Agent</span>
|
||||||
alt="Verified"
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
<span className="font-serif font-medium text-[14px] leading-[19px] text-[#00293d]">
|
|
||||||
Verified Agent
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
{agent.rating && (
|
||||||
<Image
|
<div className="flex items-center gap-1">
|
||||||
src="/assets/icons/star-filled-icon.svg"
|
<Image src="/assets/icons/star-filled-icon.svg" alt="Rating" width={14} height={14} />
|
||||||
alt="Rating"
|
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293d]">{agent.rating} Rating</span>
|
||||||
width={14}
|
</div>
|
||||||
height={14}
|
)}
|
||||||
/>
|
</div>
|
||||||
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293d]">
|
{agent.location && (
|
||||||
{agent.rating} Rating
|
<div className="flex items-center gap-1 mt-2">
|
||||||
</span>
|
<Image src="/assets/icons/location-filled-icon.svg" alt="Location" width={15} height={15} />
|
||||||
|
<span className="font-serif font-medium text-[14px] leading-[19px] text-[#00293d]">{agent.location}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{agent.experience && (
|
||||||
{/* Location */}
|
<p className="mt-2 text-[14px] leading-[19px] text-[#00293d]">
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<span className="font-serif font-bold">Experience:</span>
|
||||||
<Image
|
<span className="font-serif font-normal"> {agent.experience}</span>
|
||||||
src="/assets/icons/location-filled-icon.svg"
|
</p>
|
||||||
alt="Location"
|
)}
|
||||||
width={15}
|
|
||||||
height={15}
|
|
||||||
/>
|
|
||||||
<span className="font-serif font-medium text-[14px] leading-[19px] text-[#00293d]">
|
|
||||||
{agent.location}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Experience */}
|
|
||||||
<p className="mt-2 text-[14px] leading-[19px] text-[#00293d]">
|
|
||||||
<span className="font-serif font-bold">Experience:</span>
|
|
||||||
<span className="font-serif font-normal"> {agent.experience}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -137,6 +101,8 @@ function AgentCard({ agent, className = '' }: { agent: typeof agents[0]; classNa
|
|||||||
|
|
||||||
export function FeaturesSection({ content }: { content?: FeaturesContent }) {
|
export function FeaturesSection({ content }: { content?: FeaturesContent }) {
|
||||||
const data = content ?? defaultContent;
|
const data = content ?? defaultContent;
|
||||||
|
const agents = data.featuredAgents?.length ? data.featuredAgents : defaultContent.featuredAgents!;
|
||||||
|
const [activeCard, setActiveCard] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-16 md:py-20 bg-white">
|
<section className="py-16 md:py-20 bg-white">
|
||||||
@@ -144,68 +110,47 @@ export function FeaturesSection({ content }: { content?: FeaturesContent }) {
|
|||||||
<div className="flex flex-col lg:flex-row gap-12 lg:gap-8">
|
<div className="flex flex-col lg:flex-row gap-12 lg:gap-8">
|
||||||
{/* Left Side - Title & Features */}
|
{/* Left Side - Title & Features */}
|
||||||
<div className="lg:w-1/2">
|
<div className="lg:w-1/2">
|
||||||
{/* Section Header */}
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d] mb-4">
|
<h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d] mb-4">{data.title}</h2>
|
||||||
{data.title}
|
<p className="font-fractul font-medium text-[18px] md:text-[20px] leading-[1.4] text-[#00293d]">{data.subtitle}</p>
|
||||||
</h2>
|
|
||||||
<p className="font-fractul font-medium text-[18px] md:text-[20px] leading-[1.4] text-[#00293d]">
|
|
||||||
{data.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Grid - 2x2 */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{data.features.map((feature, index) => (
|
{data.features.map((feature, index) => (
|
||||||
<div
|
<div key={index} className="border border-[#00293d]/10 rounded-[15px] p-5">
|
||||||
key={index}
|
|
||||||
className="border border-[#00293d]/10 rounded-[15px] p-5"
|
|
||||||
>
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Image
|
<Image src={feature.iconPath} alt={feature.title} width={35} height={35} />
|
||||||
src={feature.iconPath}
|
|
||||||
alt={feature.title}
|
|
||||||
width={35}
|
|
||||||
height={35}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-fractul font-semibold text-[20px] leading-[24px] text-[#00293d] mb-2">
|
<h3 className="font-fractul font-semibold text-[20px] leading-[24px] text-[#00293d] mb-2">{feature.title}</h3>
|
||||||
{feature.title}
|
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293d]">{feature.description}</p>
|
||||||
</h3>
|
|
||||||
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293d]">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Agent Cards */}
|
{/* Right Side - Agent Cards (Desktop) */}
|
||||||
<div className="lg:w-1/2 relative min-h-[650px] hidden lg:block">
|
<div className="lg:w-1/2 relative min-h-[650px] hidden lg:block">
|
||||||
{/* Anderson - Top Left */}
|
{agents[0] && <AgentCard agent={agents[0]} className="absolute top-0 left-0 shadow-lg z-10" />}
|
||||||
<AgentCard
|
{agents[1] && <AgentCard agent={agents[1]} className="absolute top-[170px] right-0 shadow-lg z-20" />}
|
||||||
agent={agents[0]}
|
{agents[2] && <AgentCard agent={agents[2]} className="absolute top-[320px] left-[60px] shadow-lg z-30" />}
|
||||||
className="absolute top-0 left-0 shadow-lg z-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Deepak - Top Right, offset down */}
|
|
||||||
<AgentCard
|
|
||||||
agent={agents[1]}
|
|
||||||
className="absolute top-[170px] right-0 shadow-lg z-20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Daniel - Bottom Left, overlapping */}
|
|
||||||
<AgentCard
|
|
||||||
agent={agents[2]}
|
|
||||||
className="absolute top-[320px] left-[60px] shadow-lg z-30"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Agent Cards - Horizontal Scroll */}
|
{/* Mobile Agent Cards - Tab style with dots */}
|
||||||
<div className="lg:hidden overflow-x-auto pb-4">
|
<div className="lg:hidden">
|
||||||
<div className="flex gap-4 min-w-max">
|
<div className="flex justify-center">
|
||||||
{agents.map((agent, index) => (
|
<AgentCard agent={agents[activeCard]} className="shadow-lg" />
|
||||||
<AgentCard key={index} agent={agent} className="shadow-lg flex-shrink-0" />
|
</div>
|
||||||
|
{/* Dot indicators */}
|
||||||
|
<div className="flex justify-center gap-2 mt-4">
|
||||||
|
{agents.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setActiveCard(index)}
|
||||||
|
className={`rounded-full transition-all ${
|
||||||
|
index === activeCard
|
||||||
|
? 'w-[10px] h-[10px] bg-[#00293d]'
|
||||||
|
: 'w-[8px] h-[8px] bg-[#00293d]/30'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,10 +11,20 @@ export interface FeatureItem {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FeaturedAgentItem {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
rating: string;
|
||||||
|
location: string;
|
||||||
|
experience: string;
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FeaturesContent {
|
export interface FeaturesContent {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
features: FeatureItem[];
|
features: FeatureItem[];
|
||||||
|
featuredAgents?: FeaturedAgentItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfessionalItem {
|
export interface ProfessionalItem {
|
||||||
|
|||||||
Reference in New Issue
Block a user