diff --git a/assets/images/agent-anderson.jpg b/assets/images/agent-anderson.jpg new file mode 100644 index 0000000..b984fd3 Binary files /dev/null and b/assets/images/agent-anderson.jpg differ diff --git a/assets/images/agent-daniel.jpg b/assets/images/agent-daniel.jpg new file mode 100644 index 0000000..b6c4e31 Binary files /dev/null and b/assets/images/agent-daniel.jpg differ diff --git a/assets/images/agent-deepak.jpg b/assets/images/agent-deepak.jpg new file mode 100644 index 0000000..45c8145 Binary files /dev/null and b/assets/images/agent-deepak.jpg differ diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index de33005..6575a76 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -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), diff --git a/lib/features/home/presentation/widgets/featured_professionals_section.dart b/lib/features/home/presentation/widgets/featured_professionals_section.dart index b1b96fc..886c9db 100644 --- a/lib/features/home/presentation/widgets/featured_professionals_section.dart +++ b/lib/features/home/presentation/widgets/featured_professionals_section.dart @@ -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, diff --git a/lib/features/home/presentation/widgets/features_section.dart b/lib/features/home/presentation/widgets/features_section.dart index 19728d9..6801d4e 100644 --- a/lib/features/home/presentation/widgets/features_section.dart +++ b/lib/features/home/presentation/widgets/features_section.dart @@ -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 createState() => _FeaturesSectionState(); +} + +class _FeaturesSectionState extends ConsumerState { + 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, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } }