feat: Implement S3 presigned URL resolution for CMS images across pages and apply minor styling adjustments to the header and hero sections.
This commit is contained in:
@@ -5,7 +5,7 @@ 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 { cmsService, resolveImageUrl } from '@/services/cms.service';
|
||||||
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent, CmsContentRecord } from '@/types/cms';
|
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent, CmsContentRecord } from '@/types/cms';
|
||||||
|
|
||||||
export default function UserDashboard() {
|
export default function UserDashboard() {
|
||||||
@@ -15,9 +15,21 @@ export default function UserDashboard() {
|
|||||||
const fetchCms = async () => {
|
const fetchCms = async () => {
|
||||||
const sections = await cmsService.getPageContent('landing');
|
const sections = await cmsService.getPageContent('landing');
|
||||||
const data: Record<string, unknown> = {};
|
const data: Record<string, unknown> = {};
|
||||||
sections.forEach((s: CmsContentRecord) => {
|
for (const s of sections) {
|
||||||
data[s.sectionKey] = s.content;
|
const content = s.content as Record<string, unknown>;
|
||||||
});
|
// Resolve S3 keys in image fields
|
||||||
|
if (s.sectionKey === 'features' && Array.isArray(content.features)) {
|
||||||
|
for (const feat of content.features as { iconPath?: string }[]) {
|
||||||
|
if (feat.iconPath) feat.iconPath = await resolveImageUrl(feat.iconPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.sectionKey === 'testimonials' && Array.isArray(content.stats)) {
|
||||||
|
for (const stat of content.stats as { iconPath?: string }[]) {
|
||||||
|
if (stat.iconPath) stat.iconPath = await resolveImageUrl(stat.iconPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data[s.sectionKey] = content;
|
||||||
|
}
|
||||||
setCmsData(data);
|
setCmsData(data);
|
||||||
};
|
};
|
||||||
fetchCms();
|
fetchCms();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Link from 'next/link';
|
|||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { CommonHeader } from '@/components/layout/CommonHeader';
|
import { CommonHeader } from '@/components/layout/CommonHeader';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { cmsService } from '@/services/cms.service';
|
import { cmsService, resolveImageUrl } from '@/services/cms.service';
|
||||||
import type {
|
import type {
|
||||||
AboutHeroContent,
|
AboutHeroContent,
|
||||||
AboutStatsContent,
|
AboutStatsContent,
|
||||||
@@ -93,28 +93,43 @@ export default function AboutPage() {
|
|||||||
const [cta, setCta] = useState<AboutCtaContent>(defaultCta);
|
const [cta, setCta] = useState<AboutCtaContent>(defaultCta);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cmsService.getPageContent('about').then((sections) => {
|
cmsService.getPageContent('about').then(async (sections) => {
|
||||||
sections.forEach((s: CmsContentRecord) => {
|
for (const s of sections) {
|
||||||
const c = s.content as Record<string, unknown>;
|
const c = s.content as Record<string, unknown>;
|
||||||
// Only apply CMS content if it has meaningful data; otherwise keep defaults
|
|
||||||
switch (s.sectionKey) {
|
switch (s.sectionKey) {
|
||||||
case 'hero':
|
case 'hero':
|
||||||
if (c.headline || c.description) setHero(c as unknown as AboutHeroContent);
|
if (c.headline || c.description) {
|
||||||
|
const heroData = c as unknown as AboutHeroContent;
|
||||||
|
heroData.bannerImageUrl = await resolveImageUrl(heroData.bannerImageUrl);
|
||||||
|
setHero(heroData);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'stats':
|
case 'stats':
|
||||||
if (Array.isArray(c.stats) && c.stats.length > 0) setStats(c as unknown as AboutStatsContent);
|
if (Array.isArray(c.stats) && c.stats.length > 0) setStats(c as unknown as AboutStatsContent);
|
||||||
break;
|
break;
|
||||||
case 'features':
|
case 'features':
|
||||||
if (c.badge || (Array.isArray(c.features) && c.features.length > 0)) setFeatures(c as unknown as AboutFeaturesContent);
|
if (c.badge || (Array.isArray(c.features) && c.features.length > 0)) {
|
||||||
|
const featData = c as unknown as AboutFeaturesContent;
|
||||||
|
for (const feat of featData.features) {
|
||||||
|
feat.iconPath = await resolveImageUrl(feat.iconPath);
|
||||||
|
}
|
||||||
|
setFeatures(featData);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'team':
|
case 'team':
|
||||||
if (Array.isArray(c.members) && c.members.length > 0) setTeam(c as unknown as AboutTeamContent);
|
if (Array.isArray(c.members) && c.members.length > 0) {
|
||||||
|
const teamData = c as unknown as AboutTeamContent;
|
||||||
|
for (const member of teamData.members) {
|
||||||
|
member.imageUrl = await resolveImageUrl(member.imageUrl);
|
||||||
|
}
|
||||||
|
setTeam(teamData);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'cta':
|
case 'cta':
|
||||||
if (c.title || c.description) setCta(c as unknown as AboutCtaContent);
|
if (c.title || c.description) setCta(c as unknown as AboutCtaContent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Image from 'next/image';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { CommonHeader } from '@/components/layout/CommonHeader';
|
import { CommonHeader } from '@/components/layout/CommonHeader';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { cmsService } from '@/services/cms.service';
|
import { cmsService, resolveImageUrl } from '@/services/cms.service';
|
||||||
import { FaqContent, FaqCategoryItem, FaqItem, CmsContentRecord } from '@/types/cms';
|
import { FaqContent, FaqCategoryItem, FaqItem, CmsContentRecord } from '@/types/cms';
|
||||||
|
|
||||||
const defaultCategories: FaqCategoryItem[] = [
|
const defaultCategories: FaqCategoryItem[] = [
|
||||||
@@ -91,6 +91,12 @@ export default function FAQPage() {
|
|||||||
setCategories(cmsCategories);
|
setCategories(cmsCategories);
|
||||||
}
|
}
|
||||||
if (content.faqs && content.faqs.length > 0) {
|
if (content.faqs && content.faqs.length > 0) {
|
||||||
|
// Resolve S3 keys for FAQ icons
|
||||||
|
for (const faq of content.faqs) {
|
||||||
|
if (faq.icon) {
|
||||||
|
faq.icon = await resolveImageUrl(faq.icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
setFaqs(content.faqs);
|
setFaqs(content.faqs);
|
||||||
setExpandedFaq(content.faqs[0]?.id ?? null);
|
setExpandedFaq(content.faqs[0]?.id ?? null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-[600px] overflow-hidden">
|
<section className="relative min-h-[480px] overflow-hidden">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<Image
|
<Image
|
||||||
@@ -110,7 +110,7 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pt-28 pb-8 min-h-[600px] flex flex-col">
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pt-16 pb-8 min-h-[480px] flex flex-col">
|
||||||
{/* 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">
|
||||||
@@ -245,8 +245,8 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Section - pushed to bottom */}
|
{/* Bottom Section */}
|
||||||
<div className="mt-auto">
|
<div className="mt-8">
|
||||||
{/* See All Agents Button */}
|
{/* See All Agents Button */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function CommonHeader() {
|
|||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="font-fractul font-bold text-[20px] leading-[24px] text-[#00293D] hover:text-[#e58625] transition-colors"
|
className="font-fractul font-bold text-[14px] leading-[18px] text-[#00293D] hover:text-[#e58625] transition-colors"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
import api, { ApiResponse } from './api';
|
import api, { ApiResponse } from './api';
|
||||||
|
import { uploadService } from './upload.service';
|
||||||
import { CmsContentRecord } from '@/types/cms';
|
import { CmsContentRecord } from '@/types/cms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string looks like an S3 key (not a URL or local path).
|
||||||
|
*/
|
||||||
|
function isS3Key(value: string): boolean {
|
||||||
|
if (!value) return false;
|
||||||
|
return !value.startsWith('http') && !value.startsWith('/') && !value.startsWith('data:');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an image value: if it's an S3 key, fetch a presigned download URL.
|
||||||
|
* Otherwise return as-is (already a full URL or local path).
|
||||||
|
*/
|
||||||
|
export async function resolveImageUrl(value: string | undefined | null): Promise<string> {
|
||||||
|
if (!value) return '';
|
||||||
|
if (!isS3Key(value)) return value;
|
||||||
|
try {
|
||||||
|
return await uploadService.getPresignedDownloadUrl(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CmsService {
|
class CmsService {
|
||||||
async getPageContent(pageSlug: string): Promise<CmsContentRecord[]> {
|
async getPageContent(pageSlug: string): Promise<CmsContentRecord[]> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user