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({
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<dynamic>?)
?.map((e) =>
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: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<TopProfessionalsSection> {
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),

View File

@@ -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(