feat: add static agent carousel with navigation dots to the FeaturesSection.

This commit is contained in:
pradeepkumar
2026-03-14 23:33:32 +05:30
parent 630fedc78a
commit 04a57dc137
6 changed files with 386 additions and 138 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.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/presentation/widgets/featured_professionals_section.dart';
import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart';
import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart';
import 'package:real_estate_mobile/features/home/presentation/widgets/testimonials_section.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/testimonials_section.dart';
@@ -25,10 +24,6 @@ class HomeScreen extends ConsumerWidget {
const FeaturesSection(), const FeaturesSection(),
const SizedBox(height: 40), const SizedBox(height: 40),
// Featured Professionals Carousel
const FeaturedProfessionalsSection(),
const SizedBox(height: 40),
// Top Professionals Section (Agents/Lenders tabs) // Top Professionals Section (Agents/Lenders tabs)
const TopProfessionalsSection(), const TopProfessionalsSection(),
const SizedBox(height: 40), const SizedBox(height: 40),

View File

@@ -4,12 +4,11 @@ import 'package:flutter_svg/flutter_svg.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/home/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart'; import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
/// Featured professionals carousel shown above the agents/lenders tab section. /// Featured professionals carousel shown above the agents/lenders tab section.
/// Now fetches REAL agent data from GET /agents API (like web does) /// Uses CMS data from the admin panel (Top Professionals section).
/// instead of using CMS/hardcoded fallback data.
class FeaturedProfessionalsSection extends ConsumerStatefulWidget { class FeaturedProfessionalsSection extends ConsumerStatefulWidget {
const FeaturedProfessionalsSection({super.key}); const FeaturedProfessionalsSection({super.key});
@@ -49,11 +48,13 @@ class _FeaturedProfessionalsSectionState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Real agent data from GET /agents API final homeState = ref.watch(homeProvider);
final topProState = ref.watch(topProfessionalsProvider); final cmsContent = homeState.content?.topProfessionals;
final professionals = topProState.agents;
if (topProState.isLoading) { // Use CMS agents list for the featured carousel
final professionals = cmsContent?.agents ?? [];
if (homeState.isLoading) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(horizontal: 40),
child: Shimmer.fromColors( child: Shimmer.fromColors(
@@ -89,23 +90,6 @@ class _FeaturedProfessionalsSectionState
Container(height: 14, width: 180, color: Colors.white), Container(height: 14, width: 180, color: Colors.white),
const SizedBox(height: 12), const SizedBox(height: 12),
Container(height: 12, width: 140, color: Colors.white), Container(height: 12, width: 140, color: Colors.white),
const SizedBox(height: 12),
Row(
children: List.generate(
5,
(_) => Padding(
padding: const EdgeInsets.only(right: 4),
child: Container(
height: 18,
width: 18,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
),
], ],
), ),
), ),
@@ -156,12 +140,7 @@ class _FeaturedProfessionalsSectionState
); );
} }
Widget _buildFeaturedCard(AgentProfile agent, int index) { Widget _buildFeaturedCard(ProfessionalItem item, int index) {
final rating = agent.averageRating ?? 0.0;
final ratingText = rating > 0 ? '${rating.toStringAsFixed(1)} Rating' : '';
final subtitle = agent.agentType?.name ?? agent.headline ?? '';
final experience = agent.experienceText;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@@ -175,7 +154,16 @@ class _FeaturedProfessionalsSectionState
// Image // Image
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
child: _buildCardImage(agent, index), child: item.imageUrl.isNotEmpty
? S3Image(
imageUrl: item.imageUrl,
width: double.infinity,
height: 192,
alignment: Alignment.topCenter,
placeholder: (_) => _buildImagePlaceholder(index),
errorWidget: (_) => _buildImagePlaceholder(index),
)
: _buildImagePlaceholder(index),
), ),
// Content // Content
@@ -186,7 +174,7 @@ class _FeaturedProfessionalsSectionState
children: [ children: [
// Name // Name
Text( Text(
agent.fullName, item.name,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 16, fontSize: 16,
@@ -197,10 +185,10 @@ class _FeaturedProfessionalsSectionState
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// Subtitle/headline // Subtitle
if (subtitle.isNotEmpty) if (item.subtitle.isNotEmpty)
Text( Text(
subtitle, item.subtitle,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
@@ -212,10 +200,9 @@ class _FeaturedProfessionalsSectionState
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// Verified + Rating row // Verified badge
Row( Row(
children: [ children: [
if (agent.isVerified) ...[
SvgPicture.asset( SvgPicture.asset(
'assets/icons/verified_badge_icon.svg', 'assets/icons/verified_badge_icon.svg',
width: 16, width: 16,
@@ -237,47 +224,27 @@ class _FeaturedProfessionalsSectionState
), ),
), ),
], ],
if (agent.isVerified && ratingText.isNotEmpty)
const SizedBox(width: 16),
if (ratingText.isNotEmpty) ...[
SvgPicture.asset(
'assets/icons/star_rating_icon.svg',
width: 16,
height: 16,
placeholderBuilder: (_) => const Icon(
Icons.star,
color: Color(0xFFFFDE21),
size: 16,
),
),
const SizedBox(width: 4),
Text(
ratingText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// Location row // Location row
if (agent.location.isNotEmpty) if (item.location.isNotEmpty)
Row( Row(
children: [ children: [
SvgPicture.asset( SvgPicture.asset(
'assets/icons/location_pin_icon.svg', 'assets/icons/location_orange_icon.svg',
width: 16, width: 16,
height: 16, height: 16,
placeholderBuilder: (_) => const Icon(
Icons.location_on,
color: AppColors.accentOrange,
size: 16,
),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
agent.location, item.location,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -292,7 +259,7 @@ class _FeaturedProfessionalsSectionState
const SizedBox(height: 8), const SizedBox(height: 8),
// Experience // Experience
if (experience.isNotEmpty) if (item.experience.isNotEmpty)
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
@@ -306,7 +273,7 @@ class _FeaturedProfessionalsSectionState
), ),
), ),
TextSpan( TextSpan(
text: experience, text: item.experience,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -327,19 +294,6 @@ class _FeaturedProfessionalsSectionState
); );
} }
// -- Image helpers --
Widget _buildCardImage(AgentProfile agent, int index) {
return S3Image(
imageUrl: agent.avatarUrl,
width: double.infinity,
height: 192,
alignment: Alignment.topCenter,
placeholder: (_) => _buildImagePlaceholder(index),
errorWidget: (_) => _buildImagePlaceholder(index),
);
}
Widget _buildImagePlaceholder(int index) { Widget _buildImagePlaceholder(int index) {
const colors = [ const colors = [
Color(0xFFC4D9D4), Color(0xFFC4D9D4),
@@ -354,8 +308,6 @@ class _FeaturedProfessionalsSectionState
); );
} }
// -- Dot indicators --
Widget _buildDotIndicators(int count) { Widget _buildDotIndicators(int count) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -53,11 +53,89 @@ const _iconMap = {
'/assets/icons/trusted-people-icon.svg': ('assets/icons/trusted_icon.svg', Icons.people_outline), '/assets/icons/trusted-people-icon.svg': ('assets/icons/trusted_icon.svg', Icons.people_outline),
}; };
class FeaturesSection extends ConsumerWidget { /// Static agent cards — matches web's hardcoded agents in FeaturesSection.
const _staticAgents = [
_StaticAgent(
name: 'Anderson',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-anderson.jpg',
),
_StaticAgent(
name: 'Deepak',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-deepak.jpg',
),
_StaticAgent(
name: 'Daniel',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-daniel.jpg',
),
];
class _StaticAgent {
final String name;
final String role;
final String rating;
final String location;
final String experience;
final String image;
const _StaticAgent({
required this.name,
required this.role,
required this.rating,
required this.location,
required this.experience,
required this.image,
});
}
class FeaturesSection extends ConsumerStatefulWidget {
const FeaturesSection({super.key}); const FeaturesSection({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<FeaturesSection> createState() => _FeaturesSectionState();
}
class _FeaturesSectionState extends ConsumerState<FeaturesSection> {
int _currentPage = 0;
late ScrollController _agentScrollController;
@override
void initState() {
super.initState();
_agentScrollController = ScrollController();
_agentScrollController.addListener(_onAgentScroll);
}
@override
void dispose() {
_agentScrollController.removeListener(_onAgentScroll);
_agentScrollController.dispose();
super.dispose();
}
void _onAgentScroll() {
if (!_agentScrollController.hasClients) return;
const cardWidth = 254.0 + 16.0; // card width + padding
final page = (_agentScrollController.offset / cardWidth).round();
if (page != _currentPage && page >= 0 && page < _staticAgents.length) {
setState(() => _currentPage = page);
}
}
@override
Widget build(BuildContext context) {
final ref = this.ref;
final homeState = ref.watch(homeProvider); final homeState = ref.watch(homeProvider);
final cmsFeatures = homeState.content?.features; final cmsFeatures = homeState.content?.features;
@@ -70,7 +148,9 @@ class FeaturesSection extends ConsumerWidget {
final subtitle = final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle; data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle;
return Padding( return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column( child: Column(
children: [ children: [
@@ -102,6 +182,42 @@ class FeaturesSection extends ConsumerWidget {
)), )),
], ],
), ),
),
const SizedBox(height: 24),
// Static agent cards carousel (same as web)
SizedBox(
height: 370,
child: ListView.builder(
controller: _agentScrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 24),
itemCount: _staticAgents.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 16),
child: _buildStaticAgentCard(_staticAgents[index]),
),
),
),
const SizedBox(height: 16),
// Dot indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_staticAgents.length, (index) {
final isActive = index == _currentPage;
return Container(
width: isActive ? 10 : 8,
height: isActive ? 10 : 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? AppColors.primaryDark
: AppColors.primaryDark.withValues(alpha: 0.3),
),
);
}),
),
],
); );
} }
@@ -164,4 +280,189 @@ class FeaturesSection extends ConsumerWidget {
), ),
); );
} }
Widget _buildStaticAgentCard(_StaticAgent agent) {
return Container(
width: 254,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x33D9D9D9),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image
ClipRRect(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(15)),
child: Image.asset(
agent.image,
width: 254,
height: 161,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 254,
height: 161,
color: const Color(0xFFC4D9D4),
child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark),
),
),
),
// Content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name
Text(
agent.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 2),
// Role
Text(
agent.role,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
// Verified + Rating row
Wrap(
spacing: 12,
runSpacing: 4,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/icons/verified_badge_icon.svg',
width: 14,
height: 14,
placeholderBuilder: (_) => const Icon(
Icons.verified,
color: Color(0xFF638559),
size: 14,
),
),
const SizedBox(width: 4),
const Text(
'Verified Agent',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/icons/star_rating_icon.svg',
width: 14,
height: 14,
placeholderBuilder: (_) => const Icon(
Icons.star,
color: Color(0xFFFFDE21),
size: 14,
),
),
const SizedBox(width: 4),
Text(
'${agent.rating} Rating',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
],
),
const SizedBox(height: 8),
// Location
Row(
children: [
SvgPicture.asset(
'assets/icons/location_filled_icon.svg',
width: 15,
height: 15,
placeholderBuilder: (_) => const Icon(
Icons.location_on,
color: AppColors.accentOrange,
size: 15,
),
),
const SizedBox(width: 4),
Text(
agent.location,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
),
const SizedBox(height: 8),
// Experience
RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'Experience: ',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
TextSpan(
text: agent.experience,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
],
),
),
],
),
);
}
} }