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:go_router/go_router.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/hero_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 SizedBox(height: 40),
// Featured Professionals Carousel
const FeaturedProfessionalsSection(),
const SizedBox(height: 40),
// Top Professionals Section (Agents/Lenders tabs)
const TopProfessionalsSection(),
const SizedBox(height: 40),

View File

@@ -4,12 +4,11 @@ import 'package:flutter_svg/flutter_svg.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';
/// Featured professionals carousel shown above the agents/lenders tab section.
/// Now fetches REAL agent data from GET /agents API (like web does)
/// instead of using CMS/hardcoded fallback data.
/// Uses CMS data from the admin panel (Top Professionals section).
class FeaturedProfessionalsSection extends ConsumerStatefulWidget {
const FeaturedProfessionalsSection({super.key});
@@ -49,11 +48,13 @@ class _FeaturedProfessionalsSectionState
@override
Widget build(BuildContext context) {
// Real agent data from GET /agents API
final topProState = ref.watch(topProfessionalsProvider);
final professionals = topProState.agents;
final homeState = ref.watch(homeProvider);
final cmsContent = homeState.content?.topProfessionals;
if (topProState.isLoading) {
// Use CMS agents list for the featured carousel
final professionals = cmsContent?.agents ?? [];
if (homeState.isLoading) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Shimmer.fromColors(
@@ -89,23 +90,6 @@ class _FeaturedProfessionalsSectionState
Container(height: 14, width: 180, color: Colors.white),
const SizedBox(height: 12),
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) {
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;
Widget _buildFeaturedCard(ProfessionalItem item, int index) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
@@ -175,7 +154,16 @@ class _FeaturedProfessionalsSectionState
// Image
ClipRRect(
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
@@ -186,7 +174,7 @@ class _FeaturedProfessionalsSectionState
children: [
// Name
Text(
agent.fullName,
item.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
@@ -197,10 +185,10 @@ class _FeaturedProfessionalsSectionState
),
const SizedBox(height: 2),
// Subtitle/headline
if (subtitle.isNotEmpty)
// Subtitle
if (item.subtitle.isNotEmpty)
Text(
subtitle,
item.subtitle,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
@@ -212,72 +200,51 @@ class _FeaturedProfessionalsSectionState
),
const SizedBox(height: 6),
// Verified + Rating row
// Verified badge
Row(
children: [
if (agent.isVerified) ...[
SvgPicture.asset(
'assets/icons/verified_badge_icon.svg',
width: 16,
height: 16,
placeholderBuilder: (_) => const Icon(
Icons.verified,
color: Color(0xFF638559),
size: 16,
),
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: 4),
const Text(
'Verified Agent',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF638559),
),
),
const SizedBox(width: 4),
const Text(
'Verified Agent',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF638559),
),
],
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),
// Location row
if (agent.location.isNotEmpty)
if (item.location.isNotEmpty)
Row(
children: [
SvgPicture.asset(
'assets/icons/location_pin_icon.svg',
'assets/icons/location_orange_icon.svg',
width: 16,
height: 16,
placeholderBuilder: (_) => const Icon(
Icons.location_on,
color: AppColors.accentOrange,
size: 16,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
agent.location,
item.location,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
@@ -292,7 +259,7 @@ class _FeaturedProfessionalsSectionState
const SizedBox(height: 8),
// Experience
if (experience.isNotEmpty)
if (item.experience.isNotEmpty)
RichText(
text: TextSpan(
children: [
@@ -306,7 +273,7 @@ class _FeaturedProfessionalsSectionState
),
),
TextSpan(
text: experience,
text: item.experience,
style: const TextStyle(
fontFamily: 'SourceSerif4',
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) {
const colors = [
Color(0xFFC4D9D4),
@@ -354,8 +308,6 @@ class _FeaturedProfessionalsSectionState
);
}
// -- Dot indicators --
Widget _buildDotIndicators(int count) {
return Row(
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),
};
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});
@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 cmsFeatures = homeState.content?.features;
@@ -70,38 +148,76 @@ class FeaturesSection extends ConsumerWidget {
final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle;
return 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,
return Column(
children: [
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),
...data.features.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildFeatureCard(feature),
)),
],
),
),
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: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 24),
...data.features.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildFeatureCard(feature),
)),
],
),
),
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,
),
),
],
),
),
],
),
),
],
),
);
}
}