feat: Integrate CMS to dynamically populate home page sections with dedicated content types and a new service.

This commit is contained in:
pradeepkumar
2026-02-22 21:52:54 +05:30
parent c5dc139633
commit b235378815
9 changed files with 227 additions and 86 deletions

View File

@@ -1,17 +1,34 @@
'use client'; 'use client';
import { useState, useEffect } from 'react';
import { HeroSection } from '@/components/home/HeroSection'; import { HeroSection } from '@/components/home/HeroSection';
import { FeaturesSection } from '@/components/home/FeaturesSection'; import { FeaturesSection } from '@/components/home/FeaturesSection';
import { TopProfessionals } from '@/components/home/TopProfessionals'; import { TopProfessionals } from '@/components/home/TopProfessionals';
import { TestimonialsSection } from '@/components/home/TestimonialsSection'; import { TestimonialsSection } from '@/components/home/TestimonialsSection';
import { cmsService } from '@/services/cms.service';
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent, CmsContentRecord } from '@/types/cms';
export default function UserDashboard() { export default function UserDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({});
useEffect(() => {
const fetchCms = async () => {
const sections = await cmsService.getPageContent('landing');
const data: Record<string, unknown> = {};
sections.forEach((s: CmsContentRecord) => {
data[s.sectionKey] = s.content;
});
setCmsData(data);
};
fetchCms();
}, []);
return ( return (
<div> <div>
<HeroSection /> <HeroSection content={cmsData.hero as HeroContent | undefined} />
<FeaturesSection /> <FeaturesSection content={cmsData.features as FeaturesContent | undefined} />
<TopProfessionals /> <TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
<TestimonialsSection /> <TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
</div> </div>
); );
} }

View File

@@ -1,29 +1,34 @@
'use client'; 'use client';
import Image from 'next/image'; import Image from 'next/image';
import type { FeaturesContent } from '@/types/cms';
const features = [ const defaultContent: FeaturesContent = {
{ title: 'Find Trusted Real Estate Professionals On Demand',
icon: '/assets/icons/hourglass-icon.svg', subtitle: 'Quickly connect with the right agents exactly when you need them.',
title: 'Hire Quickly', features: [
description: 'Connect with experienced local real estate agents who match your needs and preferences. Our simple process helps you get started quickly and move forward without delays or unnecessary complexity.', {
}, iconPath: '/assets/icons/hourglass-icon.svg',
{ title: 'Hire Quickly',
icon: '/assets/icons/verified-badge-icon.svg', description: 'Connect with experienced local real estate agents who match your needs and preferences. Our simple process helps you get started quickly and move forward without delays or unnecessary complexity.',
title: 'Verified Agents', },
description: 'All agents are carefully identity-verified and background-checked to ensure trust and safety. You can confidently work with professionals who meet quality and reliability standards.', {
}, iconPath: '/assets/icons/verified-badge-icon.svg',
{ title: 'Verified Agents',
icon: '/assets/icons/star-orange-icon.svg', description: 'All agents are carefully identity-verified and background-checked to ensure trust and safety. You can confidently work with professionals who meet quality and reliability standards.',
title: 'Top Rated', },
description: 'Work with highly rated agents known for strong expertise and positive client feedback. Their consistent performance helps you make informed and confident property decisions.', {
}, iconPath: '/assets/icons/star-orange-icon.svg',
{ title: 'Top Rated',
icon: '/assets/icons/trusted-people-icon.svg', description: 'Work with highly rated agents known for strong expertise and positive client feedback. Their consistent performance helps you make informed and confident property decisions.',
title: 'Trusted by Thousands', },
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.', {
}, iconPath: '/assets/icons/trusted-people-icon.svg',
]; title: 'Trusted by Thousands',
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.',
},
],
};
const agents = [ const agents = [
{ {
@@ -130,7 +135,9 @@ function AgentCard({ agent, className = '' }: { agent: typeof agents[0]; classNa
); );
} }
export function FeaturesSection() { export function FeaturesSection({ content }: { content?: FeaturesContent }) {
const data = content ?? defaultContent;
return ( return (
<section className="py-16 md:py-20 bg-white"> <section className="py-16 md:py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -140,23 +147,23 @@ export function FeaturesSection() {
{/* Section Header */} {/* 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">
Find Trusted Real Estate Professionals On Demand {data.title}
</h2> </h2>
<p className="font-fractul font-medium text-[18px] md:text-[20px] leading-[1.4] text-[#00293d]"> <p className="font-fractul font-medium text-[18px] md:text-[20px] leading-[1.4] text-[#00293d]">
Quickly connect with the right agents exactly when you need them. {data.subtitle}
</p> </p>
</div> </div>
{/* Features Grid - 2x2 */} {/* 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">
{features.map((feature, index) => ( {data.features.map((feature, index) => (
<div <div
key={index} key={index}
className="border border-[#00293d]/10 rounded-[15px] p-5" className="border border-[#00293d]/10 rounded-[15px] p-5"
> >
<div className="mb-3"> <div className="mb-3">
<Image <Image
src={feature.icon} src={feature.iconPath}
alt={feature.title} alt={feature.title}
width={35} width={35}
height={35} height={35}

View File

@@ -4,9 +4,17 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Image from 'next/image'; import Image from 'next/image';
import { agentsService, AgentType } from '@/services/agents.service'; import { agentsService, AgentType } from '@/services/agents.service';
import type { HeroContent } from '@/types/cms';
const defaultHeroContent: HeroContent = {
headline: '\u201cDiscover verified, top-rated real estate professionals to guide your buying, selling, or investing journey.\u201d',
description: 'Discover verified, top-rated real estate professionals',
ctaButtonText: 'See All Agents',
helperText: 'Connect with trusted local agents to explore homes, compare options, and make smarter decisions.',
};
export function HeroSection() { export function HeroSection({ content }: { content?: HeroContent }) {
const data = content ?? defaultHeroContent;
const router = useRouter(); const router = useRouter();
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]); const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
@@ -106,10 +114,10 @@ export function HeroSection() {
{/* Headline */} {/* Headline */}
<div className="text-center mb-5"> <div className="text-center mb-5">
<h1 className="font-fractul font-bold text-[24px] md:text-[28px] lg:text-[30px] leading-[50px] text-[#00293d] max-w-4xl mx-auto"> <h1 className="font-fractul font-bold text-[24px] md:text-[28px] lg:text-[30px] leading-[50px] text-[#00293d] max-w-4xl mx-auto">
&ldquo;Discover verified, top-rated real estate professionals to guide your buying, selling, or investing journey.&rdquo; {data.headline}
</h1> </h1>
<p className="font-fractul font-bold text-[20px] md:text-[24px] leading-[40px] text-[#00293d] mt-2"> <p className="font-fractul font-bold text-[20px] md:text-[24px] leading-[40px] text-[#00293d] mt-2">
Discover verified, top-rated real estate professionals {data.description}
</p> </p>
</div> </div>
@@ -245,13 +253,13 @@ export function HeroSection() {
onClick={handleSeeAllAgents} onClick={handleSeeAllAgents}
className="px-8 py-3 rounded-[15px] bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-base transition-colors shadow-md hover:shadow-lg" className="px-8 py-3 rounded-[15px] bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-base transition-colors shadow-md hover:shadow-lg"
> >
See All Agents {data.ctaButtonText}
</button> </button>
</div> </div>
{/* Helper Text */} {/* Helper Text */}
<p className="text-center font-serif font-normal text-[14px] leading-[19px] text-[#00293D] mt-3 max-w-[419px] mx-auto"> <p className="text-center font-serif font-normal text-[14px] leading-[19px] text-[#00293D] mt-3 max-w-[419px] mx-auto">
Connect with trusted local agents to explore homes, compare options, and make smarter decisions. {data.helperText}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,9 @@
'use client'; 'use client';
import Image from 'next/image'; import Image from 'next/image';
import type { TestimonialsContent } from '@/types/cms';
const testimonials = [ const defaultTestimonials = [
{ {
id: '1', id: '1',
rating: 5, rating: 5,
@@ -86,69 +87,72 @@ function TestimonialCard({ title, content, author, role, rating }: TestimonialCa
); );
} }
export function TestimonialsSection() { const defaultContent: TestimonialsContent = {
title: "Our Clients' Trust Is Our Foundation",
subtitle: 'We help buyers, sellers, and investors close successful real estate deals with confidence in every transaction.',
ratingInfo: 'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.',
stats: [
{
iconPath: '/assets/icons/cities-icon.svg',
boldText: '25+ Cities &',
normalText: 'neighborhoods served',
},
{
iconPath: '/assets/icons/properties-icon.svg',
boldText: '1,000+ Properties',
normalText: 'bought and sold for our clients.',
},
],
testimonials: defaultTestimonials,
};
export function TestimonialsSection({ content }: { content?: TestimonialsContent }) {
const data = content ?? defaultContent;
return ( return (
<section className="py-12 md:py-16 bg-white"> <section className="py-12 md:py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */} {/* Section Header */}
<div className="text-center mb-6"> <div className="text-center mb-6">
<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">
Our Clients&apos; Trust Is Our Foundation {data.title}
</h2> </h2>
<p className="font-fractul font-medium text-[14px] leading-normal text-[#00293d] mb-2"> <p className="font-fractul font-medium text-[14px] leading-normal text-[#00293d] mb-2">
We help buyers, sellers, and investors close successful real estate deals with confidence in every transaction. {data.subtitle}
</p> </p>
<p className="font-fractul font-medium text-[14px] leading-normal text-[#00293d]"> <p className="font-fractul font-medium text-[14px] leading-normal text-[#00293d]">
Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews. {data.ratingInfo}
</p> </p>
</div> </div>
{/* Stats Section */} {/* Stats Section */}
<div className="flex justify-center items-center gap-8 md:gap-12 mb-10"> <div className="flex justify-center items-center gap-8 md:gap-12 mb-10">
{/* Cities Stat */} {data.stats.map((stat, index) => (
<div className="flex items-center gap-3"> <div key={index} className="flex items-center gap-3">
<Image <Image
src="/assets/icons/cities-icon.svg" src={stat.iconPath}
alt="Cities" alt=""
width={27} width={27}
height={27} height={27}
/> />
<div> <div>
<span className="font-serif font-bold text-[14px] leading-normal text-[#00293d]"> <span className="font-serif font-bold text-[14px] leading-normal text-[#00293d]">
25+ Cities & {stat.boldText}
</span> </span>
<br /> <br />
<span className="font-serif font-normal text-[14px] leading-normal text-[#00293d]"> <span className="font-serif font-normal text-[14px] leading-normal text-[#00293d]">
neighborhoods served {stat.normalText}
</span> </span>
</div>
</div> </div>
</div> ))}
{/* Properties Stat */}
<div className="flex items-center gap-3">
<Image
src="/assets/icons/properties-icon.svg"
alt="Properties"
width={25}
height={24}
/>
<div>
<span className="font-serif font-bold text-[14px] leading-normal text-[#00293d]">
1,000+ Properties
</span>
<br />
<span className="font-serif font-normal text-[14px] leading-normal text-[#00293d]">
bought and sold for our clients.
</span>
</div>
</div>
</div> </div>
{/* Testimonials Grid */} {/* Testimonials Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{testimonials.map((testimonial) => ( {data.testimonials.map((testimonial, index) => (
<TestimonialCard <TestimonialCard
key={testimonial.id} key={index}
title={testimonial.title} title={testimonial.title}
content={testimonial.content} content={testimonial.content}
author={testimonial.author} author={testimonial.author}

View File

@@ -3,6 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import type { TopProfessionalsContent } from '@/types/cms';
// Sample agents data for tabs // Sample agents data for tabs
const agentsData = [ const agentsData = [
@@ -177,9 +178,18 @@ function ProfessionalCard({
); );
} }
export function TopProfessionals() { const defaultContent: TopProfessionalsContent = {
title: '\u201cMeet top real estate professionals\u201d',
ctaText: 'Discover 5,000+ Top Real Estate Agents in Our Network.',
ctaButtonText: 'Browse Experts',
agents: agentsData,
lenders: lendersData,
};
export function TopProfessionals({ content }: { content?: TopProfessionalsContent }) {
const data = content ?? defaultContent;
const [activeTab, setActiveTab] = useState<'agents' | 'lenders'>('agents'); const [activeTab, setActiveTab] = useState<'agents' | 'lenders'>('agents');
const displayData = activeTab === 'agents' ? agentsData : lendersData; const displayData = activeTab === 'agents' ? data.agents : data.lenders;
return ( return (
<section className="py-10 md:py-12 bg-white"> <section className="py-10 md:py-12 bg-white">
@@ -187,7 +197,7 @@ export function TopProfessionals() {
{/* Section Header */} {/* Section Header */}
<div className="text-center mb-10"> <div className="text-center mb-10">
<h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d]"> <h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d]">
&ldquo;Meet top real estate professionals&rdquo; {data.title}
</h2> </h2>
</div> </div>
@@ -238,9 +248,9 @@ export function TopProfessionals() {
{/* Cards Grid - 3 cards + CTA sidebar */} {/* Cards Grid - 3 cards + CTA sidebar */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Professional Cards */} {/* Professional Cards */}
{displayData.map((professional) => ( {displayData.map((professional, index) => (
<ProfessionalCard <ProfessionalCard
key={professional.id} key={index}
name={professional.name} name={professional.name}
subtitle={professional.subtitle} subtitle={professional.subtitle}
location={professional.location} location={professional.location}
@@ -269,13 +279,13 @@ export function TopProfessionals() {
{/* Text */} {/* Text */}
<p className="font-fractul font-normal text-[14px] leading-[1.4] text-[#00293d] mb-6"> <p className="font-fractul font-normal text-[14px] leading-[1.4] text-[#00293d] mb-6">
Discover 5,000+ Top Real Estate Agents in Our Network. {data.ctaText}
</p> </p>
{/* Browse Experts Button */} {/* Browse Experts Button */}
<Link href="/user/profiles"> <Link href="/user/profiles">
<button className="bg-[#e58625] hover:bg-[#d47820] text-white font-fractul font-bold text-[16px] leading-[19px] px-8 py-3 rounded-[15px] transition-colors"> <button className="bg-[#e58625] hover:bg-[#d47820] text-white font-fractul font-bold text-[16px] leading-[19px] px-8 py-3 rounded-[15px] transition-colors">
Browse Experts {data.ctaButtonText}
</button> </button>
</Link> </Link>
</div> </div>

View File

@@ -94,6 +94,7 @@ const publicEndpoints = [
'/auth/2fa/verify', '/auth/2fa/verify',
'/auth/2fa/verify-backup', '/auth/2fa/verify-backup',
'/agent-types', '/agent-types',
'/cms',
]; ];
// Check if URL is a public endpoint // Check if URL is a public endpoint

View File

@@ -0,0 +1,24 @@
import api, { ApiResponse } from './api';
import { CmsContentRecord } from '@/types/cms';
class CmsService {
async getPageContent(pageSlug: string): Promise<CmsContentRecord[]> {
try {
const response = await api.get<ApiResponse<CmsContentRecord[]>>(`/cms/page/${pageSlug}`);
return response.data.data;
} catch {
return [];
}
}
async getSectionContent<T>(pageSlug: string, sectionKey: string): Promise<T | null> {
try {
const response = await api.get<ApiResponse<CmsContentRecord>>(`/cms/page/${pageSlug}/${sectionKey}`);
return response.data.data.content as T;
} catch {
return null;
}
}
}
export const cmsService = new CmsService();

View File

@@ -51,3 +51,6 @@ export type {
// Messaging Services // Messaging Services
export { socketService } from './socket.service'; export { socketService } from './socket.service';
export { messagesService } from './messages.service'; export { messagesService } from './messages.service';
// CMS Service
export { cmsService } from './cms.service';

67
src/types/cms.ts Normal file
View File

@@ -0,0 +1,67 @@
export interface HeroContent {
headline: string;
description: string;
ctaButtonText: string;
helperText: string;
}
export interface FeatureItem {
iconPath: string;
title: string;
description: string;
}
export interface FeaturesContent {
title: string;
subtitle: string;
features: FeatureItem[];
}
export interface ProfessionalItem {
name: string;
subtitle: string;
location: string;
experience: string;
expertise: string[];
imageUrl: string;
}
export interface TopProfessionalsContent {
title: string;
ctaText: string;
ctaButtonText: string;
agents: ProfessionalItem[];
lenders: ProfessionalItem[];
}
export interface TestimonialItem {
rating: number;
title: string;
content: string;
author: string;
role: string;
}
export interface StatItem {
iconPath: string;
boldText: string;
normalText: string;
}
export interface TestimonialsContent {
title: string;
subtitle: string;
ratingInfo: string;
stats: StatItem[];
testimonials: TestimonialItem[];
}
export interface CmsContentRecord {
id: string;
pageSlug: string;
sectionKey: string;
content: unknown;
isPublished: boolean;
createdAt: string;
updatedAt: string;
}