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/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart'; /// Local fallback images for professionals. const _fallbackImages = [ 'assets/images/professional-1.jpg', 'assets/images/professional-2.jpg', 'assets/images/professional-3.jpg', ]; /// 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. class FeaturedProfessionalsSection extends ConsumerStatefulWidget { const FeaturedProfessionalsSection({super.key}); @override ConsumerState createState() => _FeaturedProfessionalsSectionState(); } class _FeaturedProfessionalsSectionState extends ConsumerState { int _currentPage = 0; late ScrollController _scrollController; @override void initState() { super.initState(); _scrollController = ScrollController(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (!_scrollController.hasClients) return; final cardWidth = MediaQuery.of(context).size.width - 80; if (cardWidth <= 0) return; final page = (_scrollController.offset / cardWidth).round(); if (page != _currentPage) { setState(() => _currentPage = page); } } @override Widget build(BuildContext context) { // Real agent data from GET /agents API final topProState = ref.watch(topProfessionalsProvider); final professionals = topProState.agents; if (topProState.isLoading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 40), child: Center( child: CircularProgressIndicator(color: AppColors.accentOrange), ), ); } if (professionals.isEmpty) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ // Card carousel SizedBox( height: 370, child: ListView.builder( controller: _scrollController, scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: professionals.length, itemBuilder: (context, index) { final cardWidth = MediaQuery.of(context).size.width - 80; return SizedBox( width: cardWidth, child: Padding( padding: EdgeInsets.only( left: index == 0 ? 0 : 8, right: index == professionals.length - 1 ? 0 : 8, ), child: _buildFeaturedCard(professionals[index], index), ), ); }, ), ), const SizedBox(height: 16), // Dot indicators _buildDotIndicators(professionals.length), ], ), ); } 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; 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, mainAxisSize: MainAxisSize.min, children: [ // Image ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), child: _buildCardImage(agent, index), ), // Content Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Name Text( agent.fullName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), // Subtitle/headline if (subtitle.isNotEmpty) Text( 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: [ 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, ), ), const SizedBox(width: 6), const Text( '\u201CVerified local agent\u201D', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF638559), ), ), 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) 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( agent.location, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), // Experience if (experience.isNotEmpty) RichText( text: TextSpan( children: [ const TextSpan( text: 'Experience: ', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), TextSpan( text: experience, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } // -- Image helpers -- Widget _buildCardImage(AgentProfile agent, int index) { final resolvedUrl = _resolveImageUrl(agent.avatarUrl); 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 == null || 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), ), ); }), ); } }