diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index eeaf3c2..f634fea 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.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/home_header.dart'; @@ -67,7 +68,11 @@ class _HomeScreenState extends State { const FeaturesSection(), const SizedBox(height: 40), - // Top Professionals Section + // 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 new file mode 100644 index 0000000..11f6006 --- /dev/null +++ b/lib/features/home/presentation/widgets/featured_professionals_section.dart @@ -0,0 +1,343 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:real_estate_mobile/config/app_config.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.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'; + +/// Local fallback images for professionals. +const _fallbackImages = [ + 'assets/images/professional-1.jpg', + 'assets/images/professional-2.jpg', + 'assets/images/professional-3.jpg', +]; + +/// Default agent data — matches web's hardcoded agentsData fallback. +/// Used when CMS returns no data from admin. +const _defaultAgents = [ + ProfessionalItem( + name: 'Arjun Mehta', + subtitle: 'Residential Property Expert', + location: 'San Francisco, CA', + experience: '10+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], + ), + ProfessionalItem( + name: 'Anderson', + subtitle: 'Rental & Investment Consultant', + location: 'New York', + experience: '7+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial'], + ), + ProfessionalItem( + name: 'Sarah Johnson', + subtitle: 'Luxury Real Estate Specialist', + location: 'Los Angeles, CA', + experience: '12+ years in the real estate industry.', + expertise: ['Luxury', 'Residential', 'Investment'], + ), +]; + +/// Featured professionals carousel shown above the agents/lenders tab section. +/// Matches Figma node 49:6248 — compact card with image, name, subtitle, +/// verified+rating row, location, and experience. +/// +/// Data flow (same as web pattern): +/// CMS admin → GET /cms/page/landing → topProfessionals.agents +/// Fallback → _defaultAgents (hardcoded, like web's agentsData) +class FeaturedProfessionalsSection extends ConsumerStatefulWidget { + const FeaturedProfessionalsSection({super.key}); + + @override + ConsumerState createState() => + _FeaturedProfessionalsSectionState(); +} + +class _FeaturedProfessionalsSectionState + extends ConsumerState { + int _currentPage = 0; + late PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController(); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final homeState = ref.watch(homeProvider); + final topProfessionals = homeState.content?.topProfessionals; + + // Use CMS agents if available, otherwise fallback to defaults (same as web) + final cmsAgents = topProfessionals?.agents ?? []; + final professionals = cmsAgents.isNotEmpty ? cmsAgents : _defaultAgents; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + children: [ + // Card carousel + SizedBox( + height: 370, + child: PageView.builder( + controller: _pageController, + itemCount: professionals.length, + onPageChanged: (index) { + setState(() => _currentPage = index); + }, + itemBuilder: (context, index) { + return _buildFeaturedCard(professionals[index], index); + }, + ), + ), + const SizedBox(height: 16), + + // Dot indicators + _buildDotIndicators(professionals.length), + ], + ), + ); + } + + Widget _buildFeaturedCard(ProfessionalItem professional, int index) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image + ClipRRect( + borderRadius: + const BorderRadius.vertical(top: Radius.circular(15)), + child: _buildCardImage(professional, index), + ), + + // Content + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name (Fractul Bold 16px) + Text( + professional.name, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + + // Subtitle (e.g. "Rental & Investment Consultant") + if (professional.subtitle.isNotEmpty) + Text( + professional.subtitle, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + + // Verified + Rating row + Row( + children: [ + SvgPicture.asset( + 'assets/icons/verified_badge_icon.svg', + width: 19, + height: 19, + placeholderBuilder: (_) => const Icon( + Icons.verified, + color: Color(0xFF1DA1F2), + size: 19, + ), + ), + const SizedBox(width: 6), + const Text( + 'Verified Agent', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 16), + SvgPicture.asset( + 'assets/icons/star_rating_icon.svg', + width: 19, + height: 19, + placeholderBuilder: (_) => const Icon( + Icons.star, + color: Color(0xFFFFDE21), + size: 19, + ), + ), + const SizedBox(width: 4), + const Text( + '4.9 Rating', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + const SizedBox(height: 6), + + // Location row with filled icon + if (professional.location.isNotEmpty) + Row( + children: [ + SvgPicture.asset( + 'assets/icons/location_filled_icon.svg', + width: 19, + height: 19, + placeholderBuilder: (_) => const Icon( + Icons.location_on, + color: AppColors.accentOrange, + size: 19, + ), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + professional.location, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const Spacer(), + + // Experience + if (professional.experience.isNotEmpty) + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: 'Experience: ', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + TextSpan( + text: professional.experience, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ], + ), + ); + } + + // ── Image helpers ── + + Widget _buildCardImage(ProfessionalItem professional, int index) { + final resolvedUrl = _resolveImageUrl(professional.imageUrl); + if (resolvedUrl != null) { + return CachedNetworkImage( + imageUrl: resolvedUrl, + width: double.infinity, + height: 192, + fit: BoxFit.cover, + placeholder: (context, url) => _buildImagePlaceholder(index), + errorWidget: (context, url, error) => _buildImagePlaceholder(index), + ); + } + return _buildImagePlaceholder(index); + } + + String? _resolveImageUrl(String imageUrl) { + if (imageUrl.isEmpty) return null; + if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { + return imageUrl; + } + final baseUrl = AppConfig.apiBaseUrl; + if (baseUrl.isNotEmpty) return '$baseUrl$imageUrl'; + return null; + } + + Widget _buildImagePlaceholder(int index) { + final assetPath = _fallbackImages[index % _fallbackImages.length]; + return Image.asset( + assetPath, + width: double.infinity, + height: 192, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: double.infinity, + height: 192, + color: AppColors.gradientStart, + child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark), + ), + ); + } + + // ── Dot indicators ── + + Widget _buildDotIndicators(int count) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(count, (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), + ), + ); + }), + ); + } +} diff --git a/lib/features/home/presentation/widgets/top_professionals_section.dart b/lib/features/home/presentation/widgets/top_professionals_section.dart index 342cf4e..4ab2876 100644 --- a/lib/features/home/presentation/widgets/top_professionals_section.dart +++ b/lib/features/home/presentation/widgets/top_professionals_section.dart @@ -14,6 +14,65 @@ const _fallbackImages = [ 'assets/images/professional-3.jpg', ]; +/// Default agents data — matches web's hardcoded agentsData fallback. +const _defaultAgents = [ + ProfessionalItem( + name: 'Arjun Mehta', + subtitle: 'Residential Property Expert', + location: 'San Francisco, CA', + experience: '10+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial', 'Inspection', 'Land', 'Rental'], + ), + ProfessionalItem( + name: 'Anderson', + subtitle: 'Rental & Investment Consultant', + location: 'New York', + experience: '7+ years in the real estate industry.', + expertise: ['Residential', 'Rental', 'Commercial'], + ), + ProfessionalItem( + name: 'Sarah Johnson', + subtitle: 'Luxury Real Estate Specialist', + location: 'Los Angeles, CA', + experience: '12+ years in the real estate industry.', + expertise: ['Luxury', 'Residential', 'Investment'], + ), +]; + +/// Default lenders data — matches web's hardcoded lendersData fallback. +const _defaultLenders = [ + ProfessionalItem( + name: 'Sarah Johnson', + subtitle: 'Mortgage Specialist', + location: 'Los Angeles, CA', + experience: '10+ years in mortgage lending.', + expertise: ['FHA Loans', 'Conventional', 'VA Loans', 'Refinancing', 'Jumbo'], + ), + ProfessionalItem( + name: 'Michael Chen', + subtitle: 'Senior Loan Officer', + location: 'Seattle, WA', + experience: '7+ years in lending.', + expertise: ['Jumbo Loans', 'Refinancing', 'Investment', 'First-time', 'USDA'], + ), + ProfessionalItem( + name: 'Emily Davis', + subtitle: 'Home Loan Advisor', + location: 'Austin, TX', + experience: '5+ years in mortgage industry.', + expertise: ['First-time Buyers', 'FHA', 'USDA Loans', 'VA Loans', 'Conventional'], + ), +]; + +/// Default content — matches web's defaultContent pattern. +const _defaultContent = TopProfessionalsContent( + title: '"Meet top real estate professionals"', + ctaText: 'Discover 5,000+ Top Real Estate Agents in Our Network.', + ctaButtonText: 'See All Agents', + agents: _defaultAgents, + lenders: _defaultLenders, +); + class TopProfessionalsSection extends ConsumerStatefulWidget { const TopProfessionalsSection({super.key}); @@ -43,18 +102,18 @@ class _TopProfessionalsSectionState @override Widget build(BuildContext context) { final homeState = ref.watch(homeProvider); - final topProfessionals = homeState.content?.topProfessionals; - final activeList = _activeTab == 'agents' - ? (topProfessionals?.agents ?? []) - : (topProfessionals?.lenders ?? []); + // Use CMS data if available, otherwise fallback to defaults (same as web) + final data = homeState.content?.topProfessionals ?? _defaultContent; + + final activeList = _activeTab == 'agents' ? data.agents : data.lenders; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ Text( - topProfessionals?.title ?? '"Meet top real estate professionals"', + data.title, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', @@ -104,7 +163,7 @@ class _TopProfessionalsSectionState const SizedBox(height: 32), // See All Agents CTA - _buildSeeAllAgentsCta(topProfessionals), + _buildSeeAllAgentsCta(data), ], ), ); @@ -659,7 +718,7 @@ class _TopProfessionalsSectionState ); } - Widget _buildSeeAllAgentsCta(TopProfessionalsContent? content) { + Widget _buildSeeAllAgentsCta(TopProfessionalsContent content) { return Container( width: double.infinity, padding: const EdgeInsets.all(24), @@ -693,8 +752,7 @@ class _TopProfessionalsSectionState ), const SizedBox(height: 12), Text( - content?.ctaText ?? - 'Discover 5,000+ Top Real Estate Agents in Our Network.', + content.ctaText, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', @@ -717,7 +775,7 @@ class _TopProfessionalsSectionState ), ), child: Text( - content?.ctaButtonText ?? 'See All Agents', + content.ctaButtonText, style: const TextStyle( fontFamily: 'Fractul', fontSize: 16,