From 630fedc78afabda9b53d0d8a5e23a0681439c1df Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sat, 14 Mar 2026 23:26:56 +0530 Subject: [PATCH] refactor: update Top Professionals section to source agent and lender data from CMS content and adjust CTA button text. --- .../data/models/landing_page_content.dart | 4 +- .../widgets/top_professionals_section.dart | 232 ++++++------------ .../widgets/trust_stats_section.dart | 24 +- 3 files changed, 96 insertions(+), 164 deletions(-) diff --git a/lib/features/home/data/models/landing_page_content.dart b/lib/features/home/data/models/landing_page_content.dart index f03f361..4211e2b 100644 --- a/lib/features/home/data/models/landing_page_content.dart +++ b/lib/features/home/data/models/landing_page_content.dart @@ -37,7 +37,7 @@ class TopProfessionalsContent { const TopProfessionalsContent({ this.title = '"Meet top real estate professionals"', this.ctaText = 'Discover 5,000+ Top Real Estate Agents in Our Network.', - this.ctaButtonText = 'Browse Experts', + this.ctaButtonText = 'See All Agents', this.agents = const [], this.lenders = const [], }); @@ -47,7 +47,7 @@ class TopProfessionalsContent { title: json['title'] as String? ?? '"Meet top real estate professionals"', ctaText: json['ctaText'] as String? ?? 'Discover 5,000+ Top Real Estate Agents in Our Network.', - ctaButtonText: json['ctaButtonText'] as String? ?? 'Browse Experts', + ctaButtonText: json['ctaButtonText'] as String? ?? 'See All Agents', agents: (json['agents'] as List?) ?.map((e) => ProfessionalItem.fromJson(e as Map)) diff --git a/lib/features/home/presentation/widgets/top_professionals_section.dart b/lib/features/home/presentation/widgets/top_professionals_section.dart index 3c8b40c..0dd3a7e 100644 --- a/lib/features/home/presentation/widgets/top_professionals_section.dart +++ b/lib/features/home/presentation/widgets/top_professionals_section.dart @@ -5,8 +5,6 @@ import 'package:go_router/go_router.dart'; import 'package:shimmer/shimmer.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; -import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; -import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.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'; @@ -37,6 +35,7 @@ class TopProfessionalsSection extends ConsumerStatefulWidget { class _TopProfessionalsSectionState extends ConsumerState { int _currentPage = 0; + String _activeTab = 'agents'; late ScrollController _scrollController; @override @@ -65,13 +64,14 @@ class _TopProfessionalsSectionState @override Widget build(BuildContext context) { - // CMS data for title/CTA text only + // CMS data for everything (title, CTA, agents, lenders) final homeState = ref.watch(homeProvider); final cmsContent = homeState.content?.topProfessionals ?? _defaultCmsContent; - // Real agent data from GET /agents API (like web does) - final topProState = ref.watch(topProfessionalsProvider); - final activeList = topProState.activeList; + // Use CMS agents/lenders list based on active tab + final activeList = _activeTab == 'agents' + ? cmsContent.agents + : cmsContent.lenders; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), @@ -92,16 +92,14 @@ class _TopProfessionalsSectionState const SizedBox(height: 24), // Agents / Lenders Tab - _buildCategoryTab(topProState.activeTab), + _buildCategoryTab(_activeTab), const SizedBox(height: 24), // Content - if (topProState.isLoading) + if (homeState.isLoading) _buildShimmerCards() - else if (topProState.error != null) - _buildErrorState(topProState.error!) else if (activeList.isEmpty) - _buildEmptyState(topProState.activeTab) + _buildEmptyState(_activeTab) else ...[ // Professional Cards - horizontal scroll SizedBox( @@ -120,7 +118,7 @@ class _TopProfessionalsSectionState left: index == 0 ? 0 : 8, right: index == activeList.length - 1 ? 0 : 8, ), - child: _buildProfessionalCard(activeList[index], index: index), + child: _buildCmsProfessionalCard(activeList[index], index: index), ), ); }, @@ -153,8 +151,10 @@ class _TopProfessionalsSectionState Expanded( child: GestureDetector( onTap: () { - ref.read(topProfessionalsProvider.notifier).setActiveTab('agents'); - setState(() => _currentPage = 0); + setState(() { + _activeTab = 'agents'; + _currentPage = 0; + }); if (_scrollController.hasClients) { _scrollController.jumpTo(0); } @@ -209,8 +209,10 @@ class _TopProfessionalsSectionState Expanded( child: GestureDetector( onTap: () { - ref.read(topProfessionalsProvider.notifier).setActiveTab('lenders'); - setState(() => _currentPage = 0); + setState(() { + _activeTab = 'lenders'; + _currentPage = 0; + }); if (_scrollController.hasClients) { _scrollController.jumpTo(0); } @@ -277,12 +279,9 @@ class _TopProfessionalsSectionState ); } - // -- Professional Card using real AgentProfile data -- - Widget _buildProfessionalCard(AgentProfile agent, {int index = 0}) { - final rating = agent.averageRating ?? 0.0; - final expertise = agent.specializations; - final experience = agent.experienceText; - final subtitle = agent.agentType?.name ?? agent.headline ?? ''; + // -- Professional Card using CMS ProfessionalItem data -- + Widget _buildCmsProfessionalCard(ProfessionalItem item, {int index = 0}) { + final imageUrl = item.imageUrl; return Container( decoration: BoxDecoration( @@ -294,20 +293,22 @@ class _TopProfessionalsSectionState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Image – expands to fill available space above the content + // Image Expanded( child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), - child: S3Image( - imageUrl: agent.avatarUrl, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - placeholder: (_) => _buildImagePlaceholder(index), - errorWidget: (_) => _buildImagePlaceholder(index), - ), + child: imageUrl.isNotEmpty + ? S3Image( + imageUrl: imageUrl, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + placeholder: (_) => _buildImagePlaceholder(index), + errorWidget: (_) => _buildImagePlaceholder(index), + ) + : _buildImagePlaceholder(index), ), ), @@ -317,9 +318,9 @@ class _TopProfessionalsSectionState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Row 1: Name only (matches Figma 49:6310) + // Name Text( - agent.fullName, + item.name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, @@ -330,10 +331,10 @@ class _TopProfessionalsSectionState ), const SizedBox(height: 4), - // Row 2: Subtitle/headline - if (subtitle.isNotEmpty) + // Subtitle + if (item.subtitle.isNotEmpty) Text( - '($subtitle)', + item.subtitle, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, @@ -345,36 +346,35 @@ class _TopProfessionalsSectionState ), const SizedBox(height: 4), - // Row 3: Verified badge + "Verified local agent" in green (Figma 49:6319) - if (agent.isVerified) - Row( - children: [ - SvgPicture.asset( - 'assets/icons/verified_badge_icon.svg', - width: 16, - height: 16, - placeholderBuilder: (_) => const Icon( - Icons.verified, - color: Color(0xFF638559), - size: 16, - ), + // Verified badge + Row( + children: [ + SvgPicture.asset( + 'assets/icons/verified_badge_icon.svg', + width: 16, + height: 16, + placeholderBuilder: (_) => const Icon( + Icons.verified, + color: Color(0xFF638559), + size: 16, ), - const SizedBox(width: 6), - const Text( - '\u201CVerified local agent\u201D', - style: TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF638559), - ), + ), + const SizedBox(width: 6), + const Text( + '\u201CVerified local agent\u201D', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF638559), ), - ], - ), + ), + ], + ), const SizedBox(height: 8), - // Row 4: Location - if (agent.location.isNotEmpty) ...[ + // Location + if (item.location.isNotEmpty) ...[ const Text( 'Location:', style: TextStyle( @@ -388,19 +388,19 @@ class _TopProfessionalsSectionState Row( children: [ SvgPicture.asset( - 'assets/icons/location_pin_icon.svg', - width: 14, - height: 14, + 'assets/icons/location_orange_icon.svg', + width: 16, + height: 16, placeholderBuilder: (_) => const Icon( Icons.location_on, color: AppColors.accentOrange, - size: 14, + size: 16, ), ), const SizedBox(width: 4), Expanded( child: Text( - agent.location, + item.location, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, @@ -415,8 +415,8 @@ class _TopProfessionalsSectionState const SizedBox(height: 8), ], - // Row 5: Expertise tags - if (expertise.isNotEmpty) ...[ + // Expertise tags + if (item.expertise.isNotEmpty) ...[ const Text( 'Expertise:', style: TextStyle( @@ -430,7 +430,7 @@ class _TopProfessionalsSectionState Wrap( spacing: 8, runSpacing: 6, - children: expertise + children: item.expertise .take(6) .map((tag) => _buildTag(tag)) .toList(), @@ -438,8 +438,8 @@ class _TopProfessionalsSectionState const SizedBox(height: 12), ], - // Experience text - if (experience.isNotEmpty) ...[ + // Experience + if (item.experience.isNotEmpty) ...[ RichText( text: TextSpan( children: [ @@ -453,7 +453,7 @@ class _TopProfessionalsSectionState ), ), TextSpan( - text: experience, + text: item.experience, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, @@ -468,9 +468,6 @@ class _TopProfessionalsSectionState ), const SizedBox(height: 8), ], - - // Star rating row at bottom - _buildStarRating(rating), ], ), ), @@ -557,48 +554,6 @@ class _TopProfessionalsSectionState ); } - Widget _buildStarRating(double rating) { - final fullStars = rating.floor(); - final hasHalfStar = (rating - fullStars) >= 0.3; - - return Row( - children: List.generate(5, (index) { - if (index < fullStars) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SvgPicture.asset( - 'assets/icons/star_rating_icon.svg', - width: 18, - height: 18, - placeholderBuilder: (_) => const Icon( - Icons.star, - color: Color(0xFFFFDE21), - size: 18, - ), - ), - ); - } else if (index == fullStars && hasHalfStar) { - return const Padding( - padding: EdgeInsets.only(right: 4), - child: Icon( - Icons.star_half, - color: Color(0xFFFFDE21), - size: 18, - ), - ); - } else { - return const Padding( - padding: EdgeInsets.only(right: 4), - child: Icon( - Icons.star_border, - color: Color(0xFFFFDE21), - size: 18, - ), - ); - } - }), - ); - } Widget _buildAvatarPlaceholder() { return Container( @@ -664,45 +619,6 @@ class _TopProfessionalsSectionState ); } - Widget _buildErrorState(String error) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 40), - child: Column( - children: [ - const Icon( - Icons.error_outline, - color: AppColors.accentOrange, - size: 48, - ), - const SizedBox(height: 12), - Text( - error, - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w400, - color: AppColors.primaryDark, - ), - ), - const SizedBox(height: 16), - TextButton( - onPressed: () => - ref.read(topProfessionalsProvider.notifier).refresh(), - child: const Text( - 'Retry', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.accentOrange, - ), - ), - ), - ], - ), - ); - } - Widget _buildEmptyState(String activeTab) { return Padding( padding: const EdgeInsets.symmetric(vertical: 40), diff --git a/lib/features/home/presentation/widgets/trust_stats_section.dart b/lib/features/home/presentation/widgets/trust_stats_section.dart index 09bf602..24ab9ed 100644 --- a/lib/features/home/presentation/widgets/trust_stats_section.dart +++ b/lib/features/home/presentation/widgets/trust_stats_section.dart @@ -31,6 +31,9 @@ const _defaultContent = TestimonialsContent( const _statIconMap = { 'cities': ('assets/icons/cities_icon.svg', Icons.location_city), 'properties': ('assets/icons/properties_icon.svg', Icons.home_work), + // CMS web-path keys + '/assets/icons/cities-icon.svg': ('assets/icons/cities_icon.svg', Icons.location_city), + '/assets/icons/properties-icon.svg': ('assets/icons/properties_icon.svg', Icons.home_work), }; class TrustStatsSection extends ConsumerWidget { @@ -49,6 +52,9 @@ class TrustStatsSection extends ConsumerWidget { data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle; final stats = data.stats.isNotEmpty ? data.stats : _defaultContent.stats; + final ratingInfo = data.ratingInfo.isNotEmpty + ? data.ratingInfo + : _defaultContent.ratingInfo; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), @@ -76,6 +82,17 @@ class TrustStatsSection extends ConsumerWidget { ), ), const SizedBox(height: 16), + Text( + ratingInfo, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 16), const Divider(color: AppColors.divider), const SizedBox(height: 16), // Stats (group centered, content left-aligned) @@ -114,10 +131,9 @@ class TrustStatsSection extends ConsumerWidget { svgPath, width: 27, height: 27, - placeholderBuilder: (_) => Icon( - fallbackIcon, - size: 27, - color: AppColors.accentOrange, + colorFilter: const ColorFilter.mode( + AppColors.accentOrange, + BlendMode.srcIn, ), ) : Icon(