feat: add static agent carousel with navigation dots to the FeaturesSection.
This commit is contained in:
BIN
assets/images/agent-anderson.jpg
Normal file
BIN
assets/images/agent-anderson.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 MiB |
BIN
assets/images/agent-daniel.jpg
Normal file
BIN
assets/images/agent-daniel.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
BIN
assets/images/agent-deepak.jpg
Normal file
BIN
assets/images/agent-deepak.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
@@ -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),
|
||||
|
||||
@@ -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,10 +200,9 @@ 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,
|
||||
@@ -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),
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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,7 +148,9 @@ class FeaturesSection extends ConsumerWidget {
|
||||
final subtitle =
|
||||
data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle;
|
||||
|
||||
return Padding(
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user