feat: implement dynamic content loading for home sections with Riverpod and default fallbacks.

This commit is contained in:
pradeepkumar
2026-02-25 07:48:02 +05:30
parent c9366dedd0
commit 379fbfe0a8
3 changed files with 238 additions and 84 deletions

View File

@@ -1,20 +1,76 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/home/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
class FeaturesSection extends StatelessWidget { /// Default features content — matches web's hardcoded defaultContent fallback.
const _defaultFeatures = FeaturesContent(
title: 'Find Trusted Real Estate Professionals On Demand',
subtitle:
'Quickly connect with the right agents exactly when you need them.',
features: [
FeatureItem(
iconPath: 'hire_quickly',
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.',
),
FeatureItem(
iconPath: 'verified',
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.',
),
FeatureItem(
iconPath: 'top_rated',
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.',
),
FeatureItem(
iconPath: 'trusted',
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.',
),
],
);
/// Map of feature icon keys to local SVG asset paths and Material fallback icons.
const _iconMap = {
'hire_quickly': ('assets/icons/hire_quickly_icon.svg', Icons.bolt),
'verified': ('assets/icons/verified_icon.svg', Icons.verified_user_outlined),
'top_rated': ('assets/icons/top_rated_icon.svg', Icons.star_outline),
'trusted': ('assets/icons/trusted_icon.svg', Icons.people_outline),
};
class FeaturesSection extends ConsumerWidget {
const FeaturesSection({super.key}); const FeaturesSection({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final homeState = ref.watch(homeProvider);
final cmsFeatures = homeState.content?.features;
// Use CMS data if available with non-empty features, otherwise fallback
final data = (cmsFeatures != null && cmsFeatures.features.isNotEmpty)
? cmsFeatures
: _defaultFeatures;
final title = data.title.isNotEmpty ? data.title : _defaultFeatures.title;
final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column( child: Column(
children: [ children: [
const Text( Text(
'Find Trusted Real Estate Professionals On Demand', title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 25, fontSize: 25,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -22,10 +78,10 @@ class FeaturesSection extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'Quickly connect with the right agents exactly when you need them.', subtitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -33,48 +89,21 @@ class FeaturesSection extends StatelessWidget {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildFeatureCard( ...data.features.map((feature) => Padding(
icon: 'assets/icons/hire_quickly_icon.svg', padding: const EdgeInsets.only(bottom: 12),
fallbackIcon: Icons.bolt, child: _buildFeatureCard(feature),
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.',
),
const SizedBox(height: 12),
_buildFeatureCard(
icon: 'assets/icons/verified_icon.svg',
fallbackIcon: Icons.verified_user_outlined,
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.',
),
const SizedBox(height: 12),
_buildFeatureCard(
icon: 'assets/icons/top_rated_icon.svg',
fallbackIcon: Icons.star_outline,
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.',
),
const SizedBox(height: 12),
_buildFeatureCard(
icon: 'assets/icons/trusted_icon.svg',
fallbackIcon: Icons.people_outline,
title: 'Trusted by Thousands',
description:
'Thousands of buyers, sellers, and renters trust our platform to connect with reliable, verified agents. Our experienced professionals support every step of the property journey, helping users make confident decisions with clarity, trust, and ease.',
),
], ],
), ),
); );
} }
Widget _buildFeatureCard({ Widget _buildFeatureCard(FeatureItem feature) {
required String icon, // Resolve icon: try icon map first, then default
required IconData fallbackIcon, final iconEntry = _iconMap[feature.iconPath];
required String title, final svgPath = iconEntry?.$1 ?? 'assets/icons/hire_quickly_icon.svg';
required String description, final fallbackIcon = iconEntry?.$2 ?? Icons.bolt;
}) {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@@ -91,7 +120,7 @@ class FeaturesSection extends StatelessWidget {
Row( Row(
children: [ children: [
SvgPicture.asset( SvgPicture.asset(
icon, svgPath,
width: 35, width: 35,
height: 35, height: 35,
placeholderBuilder: (_) => Icon( placeholderBuilder: (_) => Icon(
@@ -101,20 +130,22 @@ class FeaturesSection extends StatelessWidget {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Expanded(
title, child: Text(
style: const TextStyle( feature.title,
fontFamily: 'Fractul', style: const TextStyle(
fontSize: 20, fontFamily: 'Fractul',
fontWeight: FontWeight.w600, fontSize: 20,
color: AppColors.primaryDark, fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
), ),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
description, feature.description,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,

View File

@@ -1,11 +1,35 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/home/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
class HeroSection extends StatelessWidget { /// Default hero content — matches web's hardcoded defaultHeroContent fallback.
const _defaultHero = HeroContent(
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.',
);
class HeroSection extends ConsumerWidget {
const HeroSection({super.key}); const HeroSection({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final homeState = ref.watch(homeProvider);
final hero = homeState.content?.hero ?? _defaultHero;
// Use CMS headline if available, otherwise fallback
final headline = hero.headline.isNotEmpty
? hero.headline
: _defaultHero.headline;
final ctaButtonText = hero.ctaButtonText.isNotEmpty
? hero.ctaButtonText
: _defaultHero.ctaButtonText;
return Stack( return Stack(
children: [ children: [
// Background image // Background image
@@ -24,10 +48,10 @@ class HeroSection extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 40), const SizedBox(height: 40),
const Text( Text(
'Discover verified, top-rated real estate professionals.', headline,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -37,7 +61,7 @@ class HeroSection extends StatelessWidget {
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// Search Agents Card // Search Agents Card
_buildSearchCard(), _buildSearchCard(ctaButtonText),
], ],
), ),
), ),
@@ -45,7 +69,7 @@ class HeroSection extends StatelessWidget {
); );
} }
Widget _buildSearchCard() { Widget _buildSearchCard(String ctaButtonText) {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -75,7 +99,7 @@ class HeroSection extends StatelessWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildSearchField('Categories'), _buildSearchField('Categories'),
const SizedBox(height: 12), const SizedBox(height: 12),
// See All Agents button // CTA button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@@ -88,9 +112,9 @@ class HeroSection extends StatelessWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
), ),
child: const Text( child: Text(
'See All Agents', ctaButtonText,
style: TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,

View File

@@ -1,23 +1,122 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/home/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
class TestimonialsSection extends StatelessWidget { /// Default testimonials — matches web's hardcoded defaultTestimonials fallback.
const _defaultTestimonials = [
TestimonialItem(
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',
),
TestimonialItem(
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',
),
TestimonialItem(
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',
),
];
/// Default section content — matches web's hardcoded defaultContent fallback.
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: [
StatItem(
iconPath: 'cities',
boldText: '25+ Cities &',
normalText: 'neighborhoods served',
),
StatItem(
iconPath: 'properties',
boldText: '1,000+ Properties',
normalText: 'bought and sold for our clients.',
),
],
testimonials: _defaultTestimonials,
);
class TestimonialsSection extends ConsumerWidget {
const TestimonialsSection({super.key}); const TestimonialsSection({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final homeState = ref.watch(homeProvider);
final cmsTestimonials = homeState.content?.testimonials;
// Use CMS data if available with non-empty testimonials, otherwise fallback
final data =
(cmsTestimonials != null && cmsTestimonials.testimonials.isNotEmpty)
? cmsTestimonials
: _defaultContent;
final title = data.title.isNotEmpty ? data.title : _defaultContent.title;
final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
final testimonials =
data.testimonials.isNotEmpty ? data.testimonials : _defaultTestimonials;
return Column( return Column(
children: [ children: [
// Testimonial card // Section header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
const SizedBox(height: 24),
// Testimonial cards carousel
SizedBox( SizedBox(
height: 380, height: 380,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
itemCount: 3, itemCount: testimonials.length,
itemBuilder: (context, index) => Padding( itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
child: _buildTestimonialCard(), child: _buildTestimonialCard(testimonials[index]),
), ),
), ),
), ),
@@ -25,7 +124,7 @@ class TestimonialsSection extends StatelessWidget {
); );
} }
Widget _buildTestimonialCard() { Widget _buildTestimonialCard(TestimonialItem testimonial) {
return Container( return Container(
width: 315, width: 315,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@@ -48,9 +147,9 @@ class TestimonialsSection extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'I have been working with Lorem ..', testimonial.title,
style: TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -58,10 +157,10 @@ class TestimonialsSection extends StatelessWidget {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Expanded( Expanded(
child: Text( child: Text(
'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.', testimonial.content,
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -73,7 +172,7 @@ class TestimonialsSection extends StatelessWidget {
// Stars // Stars
Row( Row(
children: List.generate( children: List.generate(
5, testimonial.rating,
(index) => const Padding( (index) => const Padding(
padding: EdgeInsets.only(right: 4), padding: EdgeInsets.only(right: 4),
child: Icon( child: Icon(
@@ -86,18 +185,18 @@ class TestimonialsSection extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Author info // Author info
const Text( Text(
'Conor Kenney', testimonial.author,
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.white, color: Colors.white,
), ),
), ),
const Text( Text(
'Chief Operations Officer', testimonial.role,
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,