refactor: update Top Professionals section to source agent and lender data from CMS content and adjust CTA button text.

This commit is contained in:
pradeepkumar
2026-03-14 23:26:56 +05:30
parent 29c9277bb7
commit 630fedc78a
3 changed files with 96 additions and 164 deletions

View File

@@ -37,7 +37,7 @@ class TopProfessionalsContent {
const TopProfessionalsContent({ const TopProfessionalsContent({
this.title = '"Meet top real estate professionals"', this.title = '"Meet top real estate professionals"',
this.ctaText = 'Discover 5,000+ Top Real Estate Agents in Our Network.', 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.agents = const [],
this.lenders = const [], this.lenders = const [],
}); });
@@ -47,7 +47,7 @@ class TopProfessionalsContent {
title: json['title'] as String? ?? '"Meet top real estate professionals"', title: json['title'] as String? ?? '"Meet top real estate professionals"',
ctaText: json['ctaText'] as String? ?? ctaText: json['ctaText'] as String? ??
'Discover 5,000+ Top Real Estate Agents in Our Network.', '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<dynamic>?) agents: (json['agents'] as List<dynamic>?)
?.map((e) => ?.map((e) =>
ProfessionalItem.fromJson(e as Map<String, dynamic>)) ProfessionalItem.fromJson(e as Map<String, dynamic>))

View File

@@ -5,8 +5,6 @@ import 'package:go_router/go_router.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.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/core/widgets/s3_image.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/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart'; import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
@@ -37,6 +35,7 @@ class TopProfessionalsSection extends ConsumerStatefulWidget {
class _TopProfessionalsSectionState class _TopProfessionalsSectionState
extends ConsumerState<TopProfessionalsSection> { extends ConsumerState<TopProfessionalsSection> {
int _currentPage = 0; int _currentPage = 0;
String _activeTab = 'agents';
late ScrollController _scrollController; late ScrollController _scrollController;
@override @override
@@ -65,13 +64,14 @@ class _TopProfessionalsSectionState
@override @override
Widget build(BuildContext context) { 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 homeState = ref.watch(homeProvider);
final cmsContent = homeState.content?.topProfessionals ?? _defaultCmsContent; final cmsContent = homeState.content?.topProfessionals ?? _defaultCmsContent;
// Real agent data from GET /agents API (like web does) // Use CMS agents/lenders list based on active tab
final topProState = ref.watch(topProfessionalsProvider); final activeList = _activeTab == 'agents'
final activeList = topProState.activeList; ? cmsContent.agents
: cmsContent.lenders;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
@@ -92,16 +92,14 @@ class _TopProfessionalsSectionState
const SizedBox(height: 24), const SizedBox(height: 24),
// Agents / Lenders Tab // Agents / Lenders Tab
_buildCategoryTab(topProState.activeTab), _buildCategoryTab(_activeTab),
const SizedBox(height: 24), const SizedBox(height: 24),
// Content // Content
if (topProState.isLoading) if (homeState.isLoading)
_buildShimmerCards() _buildShimmerCards()
else if (topProState.error != null)
_buildErrorState(topProState.error!)
else if (activeList.isEmpty) else if (activeList.isEmpty)
_buildEmptyState(topProState.activeTab) _buildEmptyState(_activeTab)
else ...[ else ...[
// Professional Cards - horizontal scroll // Professional Cards - horizontal scroll
SizedBox( SizedBox(
@@ -120,7 +118,7 @@ class _TopProfessionalsSectionState
left: index == 0 ? 0 : 8, left: index == 0 ? 0 : 8,
right: index == activeList.length - 1 ? 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( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
ref.read(topProfessionalsProvider.notifier).setActiveTab('agents'); setState(() {
setState(() => _currentPage = 0); _activeTab = 'agents';
_currentPage = 0;
});
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
_scrollController.jumpTo(0); _scrollController.jumpTo(0);
} }
@@ -209,8 +209,10 @@ class _TopProfessionalsSectionState
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
ref.read(topProfessionalsProvider.notifier).setActiveTab('lenders'); setState(() {
setState(() => _currentPage = 0); _activeTab = 'lenders';
_currentPage = 0;
});
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
_scrollController.jumpTo(0); _scrollController.jumpTo(0);
} }
@@ -277,12 +279,9 @@ class _TopProfessionalsSectionState
); );
} }
// -- Professional Card using real AgentProfile data -- // -- Professional Card using CMS ProfessionalItem data --
Widget _buildProfessionalCard(AgentProfile agent, {int index = 0}) { Widget _buildCmsProfessionalCard(ProfessionalItem item, {int index = 0}) {
final rating = agent.averageRating ?? 0.0; final imageUrl = item.imageUrl;
final expertise = agent.specializations;
final experience = agent.experienceText;
final subtitle = agent.agentType?.name ?? agent.headline ?? '';
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -294,20 +293,22 @@ class _TopProfessionalsSectionState
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Image expands to fill available space above the content // Image
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
borderRadius: borderRadius:
const BorderRadius.vertical(top: Radius.circular(15)), const BorderRadius.vertical(top: Radius.circular(15)),
child: S3Image( child: imageUrl.isNotEmpty
imageUrl: agent.avatarUrl, ? S3Image(
width: double.infinity, imageUrl: imageUrl,
height: double.infinity, width: double.infinity,
fit: BoxFit.cover, height: double.infinity,
alignment: Alignment.topCenter, fit: BoxFit.cover,
placeholder: (_) => _buildImagePlaceholder(index), alignment: Alignment.topCenter,
errorWidget: (_) => _buildImagePlaceholder(index), placeholder: (_) => _buildImagePlaceholder(index),
), errorWidget: (_) => _buildImagePlaceholder(index),
)
: _buildImagePlaceholder(index),
), ),
), ),
@@ -317,9 +318,9 @@ class _TopProfessionalsSectionState
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Row 1: Name only (matches Figma 49:6310) // Name
Text( Text(
agent.fullName, item.name,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
@@ -330,10 +331,10 @@ class _TopProfessionalsSectionState
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// Row 2: Subtitle/headline // Subtitle
if (subtitle.isNotEmpty) if (item.subtitle.isNotEmpty)
Text( Text(
'($subtitle)', item.subtitle,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 10, fontSize: 10,
@@ -345,36 +346,35 @@ class _TopProfessionalsSectionState
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// Row 3: Verified badge + "Verified local agent" in green (Figma 49:6319) // Verified badge
if (agent.isVerified) Row(
Row( children: [
children: [ SvgPicture.asset(
SvgPicture.asset( 'assets/icons/verified_badge_icon.svg',
'assets/icons/verified_badge_icon.svg', width: 16,
width: 16, height: 16,
height: 16, placeholderBuilder: (_) => const Icon(
placeholderBuilder: (_) => const Icon( Icons.verified,
Icons.verified, color: Color(0xFF638559),
color: Color(0xFF638559), size: 16,
size: 16,
),
), ),
const SizedBox(width: 6), ),
const Text( const SizedBox(width: 6),
'\u201CVerified local agent\u201D', const Text(
style: TextStyle( '\u201CVerified local agent\u201D',
fontFamily: 'SourceSerif4', style: TextStyle(
fontSize: 14, fontFamily: 'SourceSerif4',
fontWeight: FontWeight.w500, fontSize: 14,
color: Color(0xFF638559), fontWeight: FontWeight.w500,
), color: Color(0xFF638559),
), ),
], ),
), ],
),
const SizedBox(height: 8), const SizedBox(height: 8),
// Row 4: Location // Location
if (agent.location.isNotEmpty) ...[ if (item.location.isNotEmpty) ...[
const Text( const Text(
'Location:', 'Location:',
style: TextStyle( style: TextStyle(
@@ -388,19 +388,19 @@ class _TopProfessionalsSectionState
Row( Row(
children: [ children: [
SvgPicture.asset( SvgPicture.asset(
'assets/icons/location_pin_icon.svg', 'assets/icons/location_orange_icon.svg',
width: 14, width: 16,
height: 14, height: 16,
placeholderBuilder: (_) => const Icon( placeholderBuilder: (_) => const Icon(
Icons.location_on, Icons.location_on,
color: AppColors.accentOrange, color: AppColors.accentOrange,
size: 14, size: 16,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
agent.location, item.location,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 10, fontSize: 10,
@@ -415,8 +415,8 @@ class _TopProfessionalsSectionState
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
// Row 5: Expertise tags // Expertise tags
if (expertise.isNotEmpty) ...[ if (item.expertise.isNotEmpty) ...[
const Text( const Text(
'Expertise:', 'Expertise:',
style: TextStyle( style: TextStyle(
@@ -430,7 +430,7 @@ class _TopProfessionalsSectionState
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 6, runSpacing: 6,
children: expertise children: item.expertise
.take(6) .take(6)
.map((tag) => _buildTag(tag)) .map((tag) => _buildTag(tag))
.toList(), .toList(),
@@ -438,8 +438,8 @@ class _TopProfessionalsSectionState
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
// Experience text // Experience
if (experience.isNotEmpty) ...[ if (item.experience.isNotEmpty) ...[
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
@@ -453,7 +453,7 @@ class _TopProfessionalsSectionState
), ),
), ),
TextSpan( TextSpan(
text: experience, text: item.experience,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -468,9 +468,6 @@ class _TopProfessionalsSectionState
), ),
const SizedBox(height: 8), 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() { Widget _buildAvatarPlaceholder() {
return Container( 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) { Widget _buildEmptyState(String activeTab) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 40), padding: const EdgeInsets.symmetric(vertical: 40),

View File

@@ -31,6 +31,9 @@ const _defaultContent = TestimonialsContent(
const _statIconMap = { const _statIconMap = {
'cities': ('assets/icons/cities_icon.svg', Icons.location_city), 'cities': ('assets/icons/cities_icon.svg', Icons.location_city),
'properties': ('assets/icons/properties_icon.svg', Icons.home_work), '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 { class TrustStatsSection extends ConsumerWidget {
@@ -49,6 +52,9 @@ class TrustStatsSection extends ConsumerWidget {
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle; data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
final stats = final stats =
data.stats.isNotEmpty ? data.stats : _defaultContent.stats; data.stats.isNotEmpty ? data.stats : _defaultContent.stats;
final ratingInfo = data.ratingInfo.isNotEmpty
? data.ratingInfo
: _defaultContent.ratingInfo;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
@@ -76,6 +82,17 @@ class TrustStatsSection extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 16), 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 Divider(color: AppColors.divider),
const SizedBox(height: 16), const SizedBox(height: 16),
// Stats (group centered, content left-aligned) // Stats (group centered, content left-aligned)
@@ -114,10 +131,9 @@ class TrustStatsSection extends ConsumerWidget {
svgPath, svgPath,
width: 27, width: 27,
height: 27, height: 27,
placeholderBuilder: (_) => Icon( colorFilter: const ColorFilter.mode(
fallbackIcon, AppColors.accentOrange,
size: 27, BlendMode.srcIn,
color: AppColors.accentOrange,
), ),
) )
: Icon( : Icon(