import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.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/data/models/agent_type.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart'; import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart'; class AgentSearchScreen extends ConsumerStatefulWidget { final String? initialQuery; final String? initialAgentTypeId; final String? initialLocation; final String? initialCategory; const AgentSearchScreen({ super.key, this.initialQuery, this.initialAgentTypeId, this.initialLocation, this.initialCategory, }); @override ConsumerState createState() => _AgentSearchScreenState(); } class _AgentSearchScreenState extends ConsumerState { late TextEditingController _searchController; final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); _searchController = TextEditingController(text: widget.initialQuery ?? ''); _scrollController.addListener(_onScroll); WidgetsBinding.instance.addPostFrameCallback((_) { final notifier = ref.read(searchAgentsProvider.notifier); // Apply initial agent type filter if provided if (widget.initialAgentTypeId != null) { notifier.filterByAgentType(widget.initialAgentTypeId); } // Apply location and category filters if provided final initialFilters = >{}; if (widget.initialLocation != null) { initialFilters['state'] = [widget.initialLocation!]; } if (widget.initialCategory != null) { initialFilters['specialization'] = [widget.initialCategory!]; } if (initialFilters.isNotEmpty) { notifier.applyFilters(initialFilters); } // Trigger initial search if query provided if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { notifier.search(widget.initialQuery!); } }); } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { ref.read(searchAgentsProvider.notifier).loadMore(); } } void _openFilterSheet() { final state = ref.read(searchAgentsProvider); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => AgentFilterSheet( filterFields: state.filterFields, agentTypes: state.agentTypes, currentFilters: state.activeFilters, selectedAgentTypeId: state.selectedAgentTypeId, onApply: (filters) { ref.read(searchAgentsProvider.notifier).applyFilters(filters); }, onClearAll: () { ref.read(searchAgentsProvider.notifier).clearFilters(); }, onListingTypeChange: (agentTypeId) { ref .read(searchAgentsProvider.notifier) .filterByAgentType(agentTypeId); }, ), ); } @override Widget build(BuildContext context) { final state = ref.watch(searchAgentsProvider); return Column( children: [ _buildSearchBar(), const SizedBox(height: 12), _buildFilterChips(state.agentTypes, state.selectedAgentTypeId), const SizedBox(height: 12), Expanded(child: _buildAgentList(state)), ], ); } Widget _buildSearchBar() { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 24, 0), child: SizedBox( height: 42, child: Row( children: [ GestureDetector( onTap: () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } else { context.go('/home'); } }, child: const Padding( padding: EdgeInsets.only(right: 8), child: Icon( Icons.arrow_back_ios_new_rounded, color: AppColors.primaryDark, size: 20, ), ), ), Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all( color: AppColors.primaryDark, width: 0.1, ), borderRadius: BorderRadius.circular(7), ), child: Theme( data: Theme.of(context).copyWith( inputDecorationTheme: const InputDecorationTheme( filled: false, border: InputBorder.none, ), ), child: TextField( controller: _searchController, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), decoration: InputDecoration( hintText: 'Search Agents', hintStyle: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black.withValues(alpha: 0.5), ), filled: false, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), ), onSubmitted: (value) { ref.read(searchAgentsProvider.notifier).search(value); }, ), ), ), ), const SizedBox(width: 0), GestureDetector( onTap: () { ref .read(searchAgentsProvider.notifier) .search(_searchController.text); }, child: Container( width: 62, height: 42, decoration: BoxDecoration( color: AppColors.accentOrange, border: Border.all( color: AppColors.primaryDark, width: 0.1, ), borderRadius: BorderRadius.circular(7), ), child: Center( child: SvgPicture.string( '', width: 24, height: 24, ), ), ), ), ], ), ), ); } Widget _buildFilterChips( List agentTypes, String? selectedTypeId) { return SizedBox( height: 32, child: Padding( padding: const EdgeInsets.only(left: 24), child: Row( children: [ Expanded( child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: agentTypes.length, separatorBuilder: (_, __) => const SizedBox(width: 4), itemBuilder: (context, index) { final type = agentTypes[index]; final isSelected = type.id == selectedTypeId; return GestureDetector( onTap: () { ref .read(searchAgentsProvider.notifier) .filterByAgentType( isSelected ? null : type.id); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4), decoration: BoxDecoration( color: isSelected ? AppColors.accentOrange : Colors.transparent, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark, width: 0.1, ), ), child: Center( child: Text( type.name, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: isSelected ? Colors.white : AppColors.primaryDark, ), ), ), ), ); }, ), ), const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(right: 24), child: GestureDetector( onTap: () => _openFilterSheet(), child: Container( width: 39, height: 27, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark, width: 0.1, ), ), child: const Icon( Icons.tune, color: AppColors.primaryDark, size: 20, ), ), ), ), ], ), ), ); } Widget _buildShimmerList() { return Shimmer.fromColors( baseColor: const Color(0xFFE8E8E8), highlightColor: const Color(0xFFF5F5F5), child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 24), physics: const NeverScrollableScrollPhysics(), itemCount: 3, itemBuilder: (_, __) => Padding( padding: const EdgeInsets.only(bottom: 16), child: Container( height: 300, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), ), ), ), ); } Widget _buildAgentList(SearchAgentsState state) { if (state.isLoading) { return _buildShimmerList(); } if (state.error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: AppColors.accentOrange, size: 48), const SizedBox(height: 12), Text( state.error!, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), TextButton( onPressed: () => ref.read(searchAgentsProvider.notifier).refresh(), child: const Text( 'Retry', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.accentOrange, ), ), ), ], ), ); } if (state.agents.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people_outline, color: AppColors.hintText, size: 48), SizedBox(height: 12), Text( 'No agents found', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, color: AppColors.hintText, ), ), ], ), ); } return ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 24), itemCount: state.agents.length + (state.isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (index == state.agents.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Center( child: CircularProgressIndicator(color: AppColors.accentOrange), ), ); } return Padding( padding: const EdgeInsets.only(bottom: 16), child: GestureDetector( onTap: () => context.push('/agents/detail/${state.agents[index].id}'), child: _AgentCard(agent: state.agents[index]), ), ); }, ); } } // --- Agent Card Widget matching Figma design --- class _AgentCard extends StatefulWidget { final AgentProfile agent; const _AgentCard({required this.agent}); @override State<_AgentCard> createState() => _AgentCardState(); } class _AgentCardState extends State<_AgentCard> { bool _bioExpanded = false; bool _tagsExpanded = false; @override Widget build(BuildContext context) { final agent = widget.agent; final specializations = agent.specializations; final bio = agent.description; final matchPercent = agent.matchPercentage != null ? '${agent.matchPercentage}%' : null; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.primaryDark.withValues(alpha: 0.1)), boxShadow: [ BoxShadow( color: const Color(0xFFD9D9D9).withValues(alpha: 0.5), offset: const Offset(0, 10), blurRadius: 20, ), ], ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cover image with match badge Stack( children: [ S3Image( imageUrl: agent.avatarUrl, width: double.infinity, height: 265, alignment: Alignment.topCenter, errorWidget: (_) => Container( width: double.infinity, height: 265, color: AppColors.gradientStart, child: const Icon(Icons.person, size: 80, color: AppColors.primaryDark), ), ), if (matchPercent != null) Positioned( top: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), decoration: const BoxDecoration( color: Color(0xFF638559), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), ), ), child: Text( matchPercent, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: Color(0xFFF0F5FC), ), ), ), ), ], ), // Card content Padding( padding: const EdgeInsets.fromLTRB(34, 16, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Name Text.rich( TextSpan( children: [ TextSpan( text: agent.firstName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), TextSpan( text: ' ${agent.lastName}', style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), // Verified badge if (agent.isVerified) ...[ const SizedBox(height: 4), Row( children: [ SvgPicture.asset( 'assets/icons/verified_badge_icon.svg', width: 16, height: 16, placeholderBuilder: (_) => const Icon( Icons.verified, color: Color(0xFF5D8B5A), size: 16, ), ), const SizedBox(width: 4), const Text( '(Verified Expert)', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF5D8B5A), ), ), ], ), ], const SizedBox(height: 4), // Headline / title Text( agent.agentType?.name ?? agent.headline ?? 'Professional', style: const TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), const SizedBox(height: 10), // Location + Member Since row Row( children: [ if (agent.location.isNotEmpty) ...[ SvgPicture.asset( 'assets/icons/location_pin_icon.svg', width: 14, height: 14, placeholderBuilder: (_) => const Icon( Icons.location_on, color: AppColors.accentOrange, size: 14, ), ), const SizedBox(width: 4), Builder(builder: (_) { final allParts = []; // Format raw slug/label. For cities, strip 2-letter // state prefix ("nd_hurley" → "Hurley"). For states, // uppercase 2-letter codes ("nd" → "ND"). Matches // web's stripCityPrefix + extractAllValues. String formatPart(String raw, {required bool isCity}) { var s = raw.trim(); if (isCity) { s = s.replaceFirst( RegExp(r'^[a-z]{2}_', caseSensitive: false), ''); } final titled = s .split('_') .map((w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}' : '') .join(' '); if (!isCity && titled.length == 2) { return titled.toUpperCase(); } return titled; } List extractValues(dynamic fv, {required bool isCity}) { final out = []; if (fv.jsonValue is List) { for (final v in (fv.jsonValue as List)) { final s = v.toString().trim(); if (s.isEmpty) continue; out.add(formatPart(s, isCity: isCity)); } } else if (fv.textValue != null && (fv.textValue as String).isNotEmpty) { out.add(formatPart(fv.textValue as String, isCity: isCity)); } return out; } void addUnique(String value) { if (!allParts.any((p) => p.toLowerCase() == value.toLowerCase())) { allParts.add(value); } } // 1. States first for (final fv in agent.fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug == 'state' || slug == 'state_name') { for (final v in extractValues(fv, isCity: false)) { addUnique(v); } } } // 2. Cities next for (final fv in agent.fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug == 'city' || slug == 'city_name') { for (final v in extractValues(fv, isCity: true)) { addUnique(v); } } } // Fallback to agent.location + serviceAreas if (allParts.isEmpty) { final locParts = agent.location .split(',') .map((s) => s.trim()) .where((s) => s.isNotEmpty); allParts.addAll(locParts); for (final area in agent.serviceAreas) { final trimmed = area.trim(); if (trimmed.isNotEmpty && !allParts.any((p) => p.toLowerCase() == trimmed.toLowerCase())) { allParts.add(trimmed); } } } final firstPart = allParts.isNotEmpty ? allParts.first : agent.location; final remaining = allParts.length - 1; return Row( mainAxisSize: MainAxisSize.min, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 90), child: Text( firstPart, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), if (remaining > 0) GestureDetector( onTap: () => _showLocationModal(context, allParts), child: Text( ' +$remaining more', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.accentOrange, ), ), ), ], ); }), const SizedBox(width: 16), ], SvgPicture.asset( 'assets/icons/calendar_icon.svg', width: 14, height: 14, placeholderBuilder: (_) => const Icon( Icons.calendar_today, color: AppColors.accentOrange, size: 14, ), ), const SizedBox(width: 4), Flexible( child: Text( agent.memberSinceText, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 12), // Expertise tags if (specializations.isNotEmpty) ...[ Wrap( alignment: WrapAlignment.start, spacing: 6, runSpacing: 6, children: [ ...(_tagsExpanded ? specializations : specializations.take(6)) .map((tag) => _buildTag(tag)), if (specializations.length > 6) GestureDetector( onTap: () => setState(() => _tagsExpanded = !_tagsExpanded), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: AppColors.accentOrange .withValues(alpha: 0.1), border: Border.all( color: AppColors.accentOrange, width: 0.7), ), child: Text( _tagsExpanded ? 'Show Less' : '+${specializations.length - 6} more', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.accentOrange, ), ), ), ), ], ), const SizedBox(height: 14), ], // Bio if (bio.isNotEmpty) ...[ Text.rich( TextSpan( children: [ TextSpan( text: _bioExpanded || bio.length <= 150 ? bio : '${bio.substring(0, 150)}... ', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), if (bio.length > 150) WidgetSpan( child: GestureDetector( onTap: () => setState(() => _bioExpanded = !_bioExpanded), child: Text( _bioExpanded ? 'Show Less.' : 'Show More.', style: const TextStyle( fontFamily: 'Fractul', fontSize: 10, fontWeight: FontWeight.w600, color: AppColors.primaryDark, decoration: TextDecoration.underline, ), ), ), ), ], ), ), ], ], ), ), ], ), ); } void _showLocationModal(BuildContext context, List parts) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), backgroundColor: Colors.white, builder: (_) => Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'All Locations', style: TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.close, size: 20), ), ], ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: parts .map((p) => Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.2), ), ), child: Text( p, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), )) .toList(), ), const SizedBox(height: 20), ], ), ), ); } Widget _buildTag(String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.primaryDark, width: 0.7), ), child: Text( label, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ); } }