diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 0e4ef3e..a0b87ae 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -13,71 +13,8 @@ import type { AboutFeaturesContent, AboutTeamContent, AboutCtaContent, - CmsContentRecord, } from '@/types/cms'; -// --------------------------------------------------------------------------- -// Hardcoded defaults (used when no CMS content is saved) -// --------------------------------------------------------------------------- - -const defaultHero: AboutHeroContent = { - badge: 'Our Mission', - headline: 'Connecting You with Trusted Agents.', - description: - 'We believe finding the right home shouldn\u2019t be complicated. Our platform bridges the gap between buyers, sellers, and verified real estate agents for a smooth and transparent experience.', - ctaButtonText: 'Find Your Agent', - bannerImageUrl: '/assets/images/user_home_top.png', - bannerOverlayText: 'Building the future of property.', -}; - -const defaultStats: AboutStatsContent = { - stats: [ - { value: '15k+', label: 'Verified Agents' }, - { value: '98%', label: 'Customer Satisfaction' }, - ], -}; - -const defaultFeatures: AboutFeaturesContent = { - badge: 'Why Choose Us', - features: [ - { - iconPath: '/assets/icons/verified-badge-orange.svg', - title: 'Verified Agents', - description: - 'All agents on our platform are carefully verified to ensure trust, proven expertise, and consistent service, helping users connect with reliable professionals and enjoy a smooth, confident, and transparent property journey', - }, - { - iconPath: '/assets/icons/shield-verified-icon.svg', - title: 'Total Transparency', - description: - 'We maintain complete transparency by clearly displaying agent details, reviews, and activity, allowing users to make informed decisions with confidence, clarity, and trust throughout their entire property journey.', - }, - { - iconPath: '/assets/icons/home-icon.svg', - title: 'Simple Journey', - description: - 'Our platform simplifies every step of the property process, from discovering the right agent to closing with confidence, making the experience smooth, fast, and stress-free for users at every stage.', - }, - ], -}; - -const defaultTeam: AboutTeamContent = { - title: 'Meet the minds behind the platform', - subtitle: - 'We are a team of passionate innovators, real estate experts, and designers working together to redefine how people connect with trusted agents.', - members: [ - { name: 'Andrew', role: 'Founder & CEO', imageUrl: '/assets/images/professional-1.jpg' }, - { name: 'Thomas', role: 'Co-Founder', imageUrl: '/assets/images/professional-2.jpg' }, - { name: 'Darren', role: 'Advisor', imageUrl: '/assets/images/professional-3.jpg' }, - ], -}; - -const defaultCta: AboutCtaContent = { - title: 'Ready to find a agent', - description: 'Search and connect with verified real estate agents who match your needs, location, and goals.', - buttonText: 'Find Agents Now', -}; - // --------------------------------------------------------------------------- // Page component // --------------------------------------------------------------------------- @@ -86,11 +23,12 @@ export default function AboutPage() { const { data: session } = useSession(); const isAgent = (session?.user as any)?.role === 'AGENT'; - const [hero, setHero] = useState(defaultHero); - const [stats, setStats] = useState(defaultStats); - const [features, setFeatures] = useState(defaultFeatures); - const [team, setTeam] = useState(defaultTeam); - const [cta, setCta] = useState(defaultCta); + const [loading, setLoading] = useState(true); + const [hero, setHero] = useState(null); + const [stats, setStats] = useState(null); + const [features, setFeatures] = useState(null); + const [team, setTeam] = useState(null); + const [cta, setCta] = useState(null); useEffect(() => { cmsService.getPageContent('about').then(async (sections) => { @@ -130,7 +68,8 @@ export default function AboutPage() { break; } } - }); + setLoading(false); + }).catch(() => setLoading(false)); }, []); return ( @@ -142,204 +81,169 @@ export default function AboutPage() { {/* Main Content */}
- {/* Hero Section */} -
-
- {/* Mission Badge */} -
- - - {hero.badge} - -
- - {/* Headline */} -

- {hero.headline} -

- - {/* Description */} -

- {hero.description} -

- - {/* CTA Button */} - - {isAgent ? 'Go to Dashboard' : hero.ctaButtonText} - -
-
- - {/* Hero Image with Overlay Text */} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - City buildings - {/* Overlay Text */} -
-

- {hero.bannerOverlayText} -

-
-
-
- - {/* Stats Section */} - {stats.stats.length > 0 && ( -
-
-
- {stats.stats.map((stat, index) => ( -
-

- {stat.value} -

-

- {stat.label} -

-
- ))} -
-
-
- )} - - {/* Why Choose Us Section */} - {(features.badge || features.features.length > 0) && ( -
- {/* Section Header */} -
- {features.badge && ( -

- {features.badge} -

- )} -

- Our core principles guide every interaction, ensuring you feel confident and supported at every step. -

-
- - {/* Feature Cards */} - {features.features.length > 0 && ( -
- {features.features.map((feature, index) => ( -
+ ) : ( + <> + {/* Hero Section */} + {hero && ( +
+
+ {hero.badge && ( +
+ + + {hero.badge} + +
+ )} +

+ {hero.headline} +

+

+ {hero.description} +

+ - {feature.iconPath && ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - -
- )} -

- {feature.title} -

-

- {feature.description} -

-
- ))} -
+ {isAgent ? 'Go to Dashboard' : (hero.ctaButtonText || 'Find Your Agent')} + +
+
)} - - )} - {/* Team Section */} - {team.members.length > 0 && ( -
- {/* Section Header */} -
-

- {team.title} -

-

- {team.subtitle} -

-
+ {/* Hero Image */} + {hero?.bannerImageUrl && ( +
+
+ + {hero.bannerOverlayText && ( +
+

+ {hero.bannerOverlayText} +

+
+ )} +
+
+ )} - {/* Team Cards */} -
- {team.members.map((member, index) => ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {member.name} + {/* Stats Section */} + {stats && stats.stats.length > 0 && ( +
+
+
+ {stats.stats.map((stat, index) => ( +
+

+ {stat.value} +

+

+ {stat.label} +

+
+ ))}
-

- {member.name} -

-

- {member.role} +

+
+ )} + + {/* Why Choose Us Section */} + {features && features.features.length > 0 && ( +
+
+ {features.badge && ( +

+ {features.badge} +

+ )} +

+ Our core principles guide every interaction, ensuring you feel confident and supported at every step.

- ))} +
+ {features.features.map((feature, index) => ( + + ))} +
+
+ )} + + {/* Team Section */} + {team && team.members.length > 0 && ( +
+
+

+ {team.title} +

+

+ {team.subtitle} +

+
+
+ {team.members.map((member, index) => ( + + ))} +
+
+ )} + + {/* Divider */} +
+
-
- )} - {/* Divider */} -
-
-
- - {/* CTA Section */} -
-
-
-

- {isAgent ? 'Manage your profile' : 'Ready to find a agent'} -

-

- {isAgent ? 'Keep your profile updated and connect with potential clients.' : 'Search and connect with verified real estate agents who match your needs, location, and goals.'} -

-
- - {isAgent ? 'Go to Dashboard' : 'Find Agents Now'} - - - Get Help - + {/* CTA Section */} +
+
+
+

+ {isAgent ? 'Manage your profile' : (cta?.title || 'Ready to find a agent')} +

+

+ {isAgent + ? 'Keep your profile updated and connect with potential clients.' + : (cta?.description || 'Search and connect with verified real estate agents who match your needs, location, and goals.')} +

+
+ + {isAgent ? 'Go to Dashboard' : (cta?.buttonText || 'Find Agents Now')} + + + Get Help + +
+
+
+ Find an agent +
-
-
- Find an agent -
-
-
+ + + )}
{/* Footer */} @@ -347,3 +251,203 @@ export default function AboutPage() { ); } + +// --------------------------------------------------------------------------- +// Full page skeleton while CMS loads +// --------------------------------------------------------------------------- + +function AboutSkeleton() { + return ( +
+ {/* Hero skeleton */} +
+
+
+
+
+
+
+
+
+ + {/* Banner skeleton */} +
+
+
+ + {/* Stats skeleton */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Features skeleton */} +
+
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+
+ + {/* Team skeleton */} +
+
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+ ))} +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Smart image — shimmer while loading, hidden on error +// --------------------------------------------------------------------------- + +function SmartImage({ + src, + alt, + className, + shimmerClassName, +}: { + src: string | undefined | null; + alt: string; + className?: string; + shimmerClassName?: string; +}) { + const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>(src ? 'loading' : 'error'); + + useEffect(() => { + setStatus(src ? 'loading' : 'error'); + }, [src]); + + if (!src || status === 'error') return null; + + return ( +
+ {status === 'loading' && ( +
+ )} + {/* eslint-disable-next-line @next/next/no-img-element */} + {alt} setStatus('loaded')} + onError={() => setStatus('error')} + /> +
+ ); +} + +// --------------------------------------------------------------------------- +// Feature card — shimmer icon while loading +// --------------------------------------------------------------------------- + +function FeatureCard({ feature }: { feature: AboutFeaturesContent['features'][number] }) { + const [iconStatus, setIconStatus] = useState<'loading' | 'loaded' | 'error'>( + feature.iconPath ? 'loading' : 'error' + ); + + useEffect(() => { + setIconStatus(feature.iconPath ? 'loading' : 'error'); + }, [feature.iconPath]); + + return ( +
+
+
+ {iconStatus === 'loading' && ( +
+ )} + {feature.iconPath && iconStatus !== 'error' && ( + /* eslint-disable-next-line @next/next/no-img-element */ + setIconStatus('loaded')} + onError={() => setIconStatus('error')} + /> + )} +
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); +} + +// --------------------------------------------------------------------------- +// Team card — shimmer image while loading +// --------------------------------------------------------------------------- + +function TeamCard({ member }: { member: AboutTeamContent['members'][number] }) { + const [imgStatus, setImgStatus] = useState<'loading' | 'loaded' | 'error'>( + member.imageUrl ? 'loading' : 'error' + ); + + useEffect(() => { + setImgStatus(member.imageUrl ? 'loading' : 'error'); + }, [member.imageUrl]); + + return ( +
+
+ {imgStatus === 'loading' && ( +
+ )} + {member.imageUrl && imgStatus !== 'error' && ( + /* eslint-disable-next-line @next/next/no-img-element */ + {member.name} setImgStatus('loaded')} + onError={() => setImgStatus('error')} + /> + )} +
+

+ {member.name} +

+

+ {member.role} +

+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 41ebe63..404eb48 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,30 @@ @import "tailwindcss"; +/* Shimmer loading animation */ +@keyframes shimmer-slide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.shimmer-loading { + position: relative; + overflow: hidden; + background-color: #e5e7eb; +} + +.shimmer-loading::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.6) 50%, + transparent 100% + ); + animation: shimmer-slide 1.5s ease-in-out infinite; +} + :root { /* RE-Quest Design Tokens */ --color-primary-dark: #00293d;