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

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,
),
),
],
),
),
],
),
),
],
),
);
}
}