From 49bd08c5fe9b7f243b7173145669610f9f8e7bb4 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sun, 22 Feb 2026 21:52:16 +0530 Subject: [PATCH] feat: Implement CMS module with API endpoints, data model, and initial seeding for landing page content. --- prisma/schema.prisma | 20 +++ prisma/seed.ts | 182 ++++++++++++++++++++++++++ src/app.module.ts | 2 + src/cms/cms.controller.ts | 44 +++++++ src/cms/cms.module.ts | 12 ++ src/cms/cms.service.ts | 65 +++++++++ src/cms/dto/create-cms-content.dto.ts | 20 +++ src/cms/dto/index.ts | 2 + src/cms/dto/update-cms-content.dto.ts | 12 ++ src/cms/index.ts | 4 + 10 files changed, 363 insertions(+) create mode 100644 src/cms/cms.controller.ts create mode 100644 src/cms/cms.module.ts create mode 100644 src/cms/cms.service.ts create mode 100644 src/cms/dto/create-cms-content.dto.ts create mode 100644 src/cms/dto/index.ts create mode 100644 src/cms/dto/update-cms-content.dto.ts create mode 100644 src/cms/index.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f38a38a..dfb9442 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -496,3 +496,23 @@ model Message { @@index([status]) @@map("messages") } + +// =========================================== +// CMS CONTENT - Dynamic page content management +// =========================================== + +model CmsContent { + id String @id @default(uuid()) + pageSlug String + sectionKey String + content Json + isPublished Boolean @default(true) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([pageSlug, sectionKey]) + @@index([pageSlug]) + @@map("cms_contents") +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 002c6c1..7ea3e9f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1353,6 +1353,186 @@ async function main() { console.log(' ───────────────────────────────\n'); } + // ============================================= + // Seed CMS Content (Landing Page) + // ============================================= + console.log('📄 Seeding CMS Content...'); + + const cmsDefaults = [ + { + pageSlug: 'landing', + sectionKey: 'hero', + content: { + headline: '"Discover verified, top-rated real estate professionals to guide your buying, selling, or investing journey."', + 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.', + }, + }, + { + pageSlug: 'landing', + sectionKey: 'features', + content: { + 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.', + }, + ], + }, + }, + { + pageSlug: 'landing', + sectionKey: 'topProfessionals', + content: { + title: '"Meet top real estate professionals"', + ctaText: 'Discover 5,000+ Top Real Estate Agents in Our Network.', + ctaButtonText: 'Browse Experts', + agents: [ + { + name: 'Arjun Mehta', + subtitle: '(Residential Property Expert)', + location: 'San Francisco, CA', + experience: '10+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], + imageUrl: '/assets/images/professional-1.jpg', + }, + { + name: 'Arjun Mehta', + subtitle: '(Residential Property Expert)', + location: 'San Francisco, CA', + experience: '10+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], + imageUrl: '/assets/images/professional-2.jpg', + }, + { + name: 'Arjun Mehta', + subtitle: '(Residential Property Expert)', + location: 'San Francisco, CA', + experience: '10+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], + imageUrl: '/assets/images/professional-3.jpg', + }, + ], + lenders: [ + { + name: 'Sarah Johnson', + subtitle: '(Mortgage Specialist)', + location: 'Los Angeles, CA', + experience: '10+ years in mortgage lending.', + expertise: ['FHA Loans', 'Conventional', 'VA Loans', 'Refinancing', 'Jumbo'], + imageUrl: '/assets/images/professional-1.jpg', + }, + { + name: 'Michael Chen', + subtitle: '(Senior Loan Officer)', + location: 'Seattle, WA', + experience: '7+ years in lending.', + expertise: ['Jumbo Loans', 'Refinancing', 'Investment', 'First-time', 'USDA'], + imageUrl: '/assets/images/professional-2.jpg', + }, + { + name: 'Emily Davis', + subtitle: '(Home Loan Advisor)', + location: 'Austin, TX', + experience: '5+ years in mortgage industry.', + expertise: ['First-time Buyers', 'FHA', 'USDA Loans', 'VA Loans', 'Conventional'], + imageUrl: '/assets/images/professional-3.jpg', + }, + ], + }, + }, + { + pageSlug: 'landing', + sectionKey: 'testimonials', + content: { + 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: [ + { + rating: 5, + title: 'I have been working with Lorem ..', + content: '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: 'Conor Kenney', + role: 'Chief Operations Officer', + }, + { + rating: 5, + title: 'I have been working with Lorem ..', + content: '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: 'Conor Kenney', + role: 'Chief Operations Officer', + }, + { + rating: 5, + title: 'I have been working with Lorem ..', + content: '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: 'Conor Kenney', + role: 'Chief Operations Officer', + }, + ], + }, + }, + ]; + + for (const cms of cmsDefaults) { + const existing = await prisma.cmsContent.findUnique({ + where: { + pageSlug_sectionKey: { + pageSlug: cms.pageSlug, + sectionKey: cms.sectionKey, + }, + }, + }); + + if (existing) { + console.log(` ⚠️ CMS content "${cms.pageSlug}/${cms.sectionKey}" already exists`); + } else { + await prisma.cmsContent.create({ + data: { + pageSlug: cms.pageSlug, + sectionKey: cms.sectionKey, + content: cms.content, + isPublished: true, + }, + }); + console.log(` ✅ Created CMS content: ${cms.pageSlug}/${cms.sectionKey}`); + } + } + + console.log(''); + // ============================================= // Summary // ============================================= @@ -1363,6 +1543,7 @@ async function main() { const fieldCount = await prisma.profileField.count(); const systemSectionCount = await prisma.profileSection.count({ where: { isSystem: true } }); const agentTypeSectionCount = await prisma.agentTypeSection.count(); + const cmsContentCount = await prisma.cmsContent.count(); console.log('📊 Database Summary:'); console.log(` Total Users: ${userCount}`); @@ -1371,6 +1552,7 @@ async function main() { console.log(` Profile Sections: ${sectionCount} (${systemSectionCount} system)`); console.log(` Profile Fields: ${fieldCount}`); console.log(` Section Assignments: ${agentTypeSectionCount}`); + console.log(` CMS Content: ${cmsContentCount}`); console.log('\n🌱 Seeding completed!\n'); await prisma.$disconnect(); diff --git a/src/app.module.ts b/src/app.module.ts index 9eeca47..259dd9a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { UploadModule } from './upload'; import { ProfileFieldsModule } from './profile-fields'; import { ConnectionRequestsModule } from './connection-requests'; import { MessagesModule } from './messages'; +import { CmsModule } from './cms'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -55,6 +56,7 @@ import { AppService } from './app.service'; ProfileFieldsModule, ConnectionRequestsModule, MessagesModule, + CmsModule, ], controllers: [AppController], providers: [ diff --git a/src/cms/cms.controller.ts b/src/cms/cms.controller.ts new file mode 100644 index 0000000..2f1809e --- /dev/null +++ b/src/cms/cms.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get, Put, Patch, Delete, Param, Body } from '@nestjs/common'; +import { UserRole } from '@prisma/client'; +import { CmsService } from './cms.service'; +import { CreateCmsContentDto, UpdateCmsContentDto } from './dto'; +import { Public } from '../auth/decorators/public.decorator'; +import { Roles } from '../auth/decorators/roles.decorator'; + +@Controller('cms') +export class CmsController { + constructor(private readonly cmsService: CmsService) {} + + @Public() + @Get('page/:pageSlug') + findByPage(@Param('pageSlug') pageSlug: string) { + return this.cmsService.findByPage(pageSlug); + } + + @Public() + @Get('page/:pageSlug/:sectionKey') + findByPageAndSection( + @Param('pageSlug') pageSlug: string, + @Param('sectionKey') sectionKey: string, + ) { + return this.cmsService.findByPageAndSection(pageSlug, sectionKey); + } + + @Roles(UserRole.ADMIN) + @Put('upsert') + upsert(@Body() dto: CreateCmsContentDto) { + return this.cmsService.upsert(dto); + } + + @Roles(UserRole.ADMIN) + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateCmsContentDto) { + return this.cmsService.update(id, dto); + } + + @Roles(UserRole.ADMIN) + @Delete(':id') + remove(@Param('id') id: string) { + return this.cmsService.remove(id); + } +} diff --git a/src/cms/cms.module.ts b/src/cms/cms.module.ts new file mode 100644 index 0000000..f96f3fe --- /dev/null +++ b/src/cms/cms.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CmsController } from './cms.controller'; +import { CmsService } from './cms.service'; +import { PrismaModule } from '../prisma'; + +@Module({ + imports: [PrismaModule], + controllers: [CmsController], + providers: [CmsService], + exports: [CmsService], +}) +export class CmsModule {} diff --git a/src/cms/cms.service.ts b/src/cms/cms.service.ts new file mode 100644 index 0000000..45bff7c --- /dev/null +++ b/src/cms/cms.service.ts @@ -0,0 +1,65 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateCmsContentDto, UpdateCmsContentDto } from './dto'; + +@Injectable() +export class CmsService { + constructor(private prisma: PrismaService) {} + + async findByPage(pageSlug: string) { + return this.prisma.cmsContent.findMany({ + where: { pageSlug, isPublished: true }, + orderBy: { sectionKey: 'asc' }, + }); + } + + async findByPageAndSection(pageSlug: string, sectionKey: string) { + const content = await this.prisma.cmsContent.findUnique({ + where: { pageSlug_sectionKey: { pageSlug, sectionKey } }, + }); + if (!content) { + throw new NotFoundException(`CMS content not found for ${pageSlug}/${sectionKey}`); + } + return content; + } + + async upsert(dto: CreateCmsContentDto) { + return this.prisma.cmsContent.upsert({ + where: { + pageSlug_sectionKey: { + pageSlug: dto.pageSlug, + sectionKey: dto.sectionKey, + }, + }, + update: { + content: dto.content, + isPublished: dto.isPublished ?? true, + }, + create: { + pageSlug: dto.pageSlug, + sectionKey: dto.sectionKey, + content: dto.content, + isPublished: dto.isPublished ?? true, + }, + }); + } + + async update(id: string, dto: UpdateCmsContentDto) { + const existing = await this.prisma.cmsContent.findUnique({ where: { id } }); + if (!existing) { + throw new NotFoundException(`CMS content with ID ${id} not found`); + } + return this.prisma.cmsContent.update({ + where: { id }, + data: dto, + }); + } + + async remove(id: string) { + const existing = await this.prisma.cmsContent.findUnique({ where: { id } }); + if (!existing) { + throw new NotFoundException(`CMS content with ID ${id} not found`); + } + return this.prisma.cmsContent.delete({ where: { id } }); + } +} diff --git a/src/cms/dto/create-cms-content.dto.ts b/src/cms/dto/create-cms-content.dto.ts new file mode 100644 index 0000000..2a0502c --- /dev/null +++ b/src/cms/dto/create-cms-content.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsNotEmpty, IsOptional, IsBoolean, IsObject } from 'class-validator'; +import { Prisma } from '@prisma/client'; + +export class CreateCmsContentDto { + @IsString() + @IsNotEmpty() + pageSlug: string; + + @IsString() + @IsNotEmpty() + sectionKey: string; + + @IsObject() + @IsNotEmpty() + content: Prisma.InputJsonValue; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} diff --git a/src/cms/dto/index.ts b/src/cms/dto/index.ts new file mode 100644 index 0000000..5af1ab6 --- /dev/null +++ b/src/cms/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateCmsContentDto } from './create-cms-content.dto'; +export { UpdateCmsContentDto } from './update-cms-content.dto'; diff --git a/src/cms/dto/update-cms-content.dto.ts b/src/cms/dto/update-cms-content.dto.ts new file mode 100644 index 0000000..a652624 --- /dev/null +++ b/src/cms/dto/update-cms-content.dto.ts @@ -0,0 +1,12 @@ +import { IsOptional, IsBoolean, IsObject } from 'class-validator'; +import { Prisma } from '@prisma/client'; + +export class UpdateCmsContentDto { + @IsObject() + @IsOptional() + content?: Prisma.InputJsonValue; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} diff --git a/src/cms/index.ts b/src/cms/index.ts new file mode 100644 index 0000000..9b7ac31 --- /dev/null +++ b/src/cms/index.ts @@ -0,0 +1,4 @@ +export { CmsModule } from './cms.module'; +export { CmsService } from './cms.service'; +export { CmsController } from './cms.controller'; +export * from './dto';