feat: Integrate CMS to dynamically populate home page sections with dedicated content types and a new service.
This commit is contained in:
@@ -1,17 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { HeroSection } from '@/components/home/HeroSection';
|
||||
import { FeaturesSection } from '@/components/home/FeaturesSection';
|
||||
import { TopProfessionals } from '@/components/home/TopProfessionals';
|
||||
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() {
|
||||
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 (
|
||||
<div>
|
||||
<HeroSection />
|
||||
<FeaturesSection />
|
||||
<TopProfessionals />
|
||||
<TestimonialsSection />
|
||||
<HeroSection content={cmsData.hero as HeroContent | undefined} />
|
||||
<FeaturesSection content={cmsData.features as FeaturesContent | undefined} />
|
||||
<TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
|
||||
<TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import type { FeaturesContent } from '@/types/cms';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: '/assets/icons/hourglass-icon.svg',
|
||||
title: 'Hire Quickly',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
icon: '/assets/icons/verified-badge-icon.svg',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
icon: '/assets/icons/star-orange-icon.svg',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
icon: '/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 defaultContent: FeaturesContent = {
|
||||
title: 'Find Trusted Real Estate Professionals On Demand',
|
||||
subtitle: 'Quickly connect with the right agents exactly when you need them.',
|
||||
features: [
|
||||
{
|
||||
iconPath: '/assets/icons/hourglass-icon.svg',
|
||||
title: 'Hire Quickly',
|
||||
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/verified-badge-icon.svg',
|
||||
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/star-orange-icon.svg',
|
||||
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/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 = [
|
||||
{
|
||||
@@ -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 (
|
||||
<section className="py-16 md:py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -140,23 +147,23 @@ export function FeaturesSection() {
|
||||
{/* Section Header */}
|
||||
<div className="mb-8">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Features Grid - 2x2 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{features.map((feature, index) => (
|
||||
{data.features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-[#00293d]/10 rounded-[15px] p-5"
|
||||
>
|
||||
<div className="mb-3">
|
||||
<Image
|
||||
src={feature.icon}
|
||||
src={feature.iconPath}
|
||||
alt={feature.title}
|
||||
width={35}
|
||||
height={35}
|
||||
|
||||
@@ -4,9 +4,17 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
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 [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
@@ -106,10 +114,10 @@ export function HeroSection() {
|
||||
{/* Headline */}
|
||||
<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">
|
||||
“Discover verified, top-rated real estate professionals to guide your buying, selling, or investing journey.”
|
||||
{data.headline}
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -245,13 +253,13 @@ export function HeroSection() {
|
||||
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"
|
||||
>
|
||||
See All Agents
|
||||
{data.ctaButtonText}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Helper Text */}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import type { TestimonialsContent } from '@/types/cms';
|
||||
|
||||
const testimonials = [
|
||||
const defaultTestimonials = [
|
||||
{
|
||||
id: '1',
|
||||
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 (
|
||||
<section className="py-12 md:py-16 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d] mb-4">
|
||||
Our Clients' Trust Is Our Foundation
|
||||
{data.title}
|
||||
</h2>
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
<div className="flex justify-center items-center gap-8 md:gap-12 mb-10">
|
||||
{/* Cities Stat */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src="/assets/icons/cities-icon.svg"
|
||||
alt="Cities"
|
||||
width={27}
|
||||
height={27}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-serif font-bold text-[14px] leading-normal text-[#00293d]">
|
||||
25+ Cities &
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-serif font-normal text-[14px] leading-normal text-[#00293d]">
|
||||
neighborhoods served
|
||||
</span>
|
||||
{data.stats.map((stat, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<Image
|
||||
src={stat.iconPath}
|
||||
alt=""
|
||||
width={27}
|
||||
height={27}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-serif font-bold text-[14px] leading-normal text-[#00293d]">
|
||||
{stat.boldText}
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-serif font-normal text-[14px] leading-normal text-[#00293d]">
|
||||
{stat.normalText}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
{/* Testimonials Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{testimonials.map((testimonial) => (
|
||||
{data.testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard
|
||||
key={testimonial.id}
|
||||
key={index}
|
||||
title={testimonial.title}
|
||||
content={testimonial.content}
|
||||
author={testimonial.author}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { TopProfessionalsContent } from '@/types/cms';
|
||||
|
||||
// Sample agents data for tabs
|
||||
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 displayData = activeTab === 'agents' ? agentsData : lendersData;
|
||||
const displayData = activeTab === 'agents' ? data.agents : data.lenders;
|
||||
|
||||
return (
|
||||
<section className="py-10 md:py-12 bg-white">
|
||||
@@ -187,7 +197,7 @@ export function TopProfessionals() {
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="font-fractul font-bold text-[32px] md:text-[40px] leading-[1.2] text-[#00293d]">
|
||||
“Meet top real estate professionals”
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -238,9 +248,9 @@ export function TopProfessionals() {
|
||||
{/* Cards Grid - 3 cards + CTA sidebar */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Professional Cards */}
|
||||
{displayData.map((professional) => (
|
||||
{displayData.map((professional, index) => (
|
||||
<ProfessionalCard
|
||||
key={professional.id}
|
||||
key={index}
|
||||
name={professional.name}
|
||||
subtitle={professional.subtitle}
|
||||
location={professional.location}
|
||||
@@ -269,13 +279,13 @@ export function TopProfessionals() {
|
||||
|
||||
{/* Text */}
|
||||
<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>
|
||||
|
||||
{/* Browse Experts Button */}
|
||||
<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">
|
||||
Browse Experts
|
||||
{data.ctaButtonText}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -94,6 +94,7 @@ const publicEndpoints = [
|
||||
'/auth/2fa/verify',
|
||||
'/auth/2fa/verify-backup',
|
||||
'/agent-types',
|
||||
'/cms',
|
||||
];
|
||||
|
||||
// Check if URL is a public endpoint
|
||||
|
||||
24
src/services/cms.service.ts
Normal file
24
src/services/cms.service.ts
Normal 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();
|
||||
@@ -51,3 +51,6 @@ export type {
|
||||
// Messaging Services
|
||||
export { socketService } from './socket.service';
|
||||
export { messagesService } from './messages.service';
|
||||
|
||||
// CMS Service
|
||||
export { cmsService } from './cms.service';
|
||||
|
||||
67
src/types/cms.ts
Normal file
67
src/types/cms.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user