feat: add static agent carousel with navigation dots to the FeaturesSection.
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user