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: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/agent_detail_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; class AgentDetailScreen extends ConsumerStatefulWidget { final String agentId; const AgentDetailScreen({super.key, required this.agentId}); @override ConsumerState createState() => _AgentDetailScreenState(); } class _AgentDetailScreenState extends ConsumerState { bool _emailVisible = false; bool _phoneVisible = false; bool _bioExpanded = false; @override void initState() { super.initState(); _loadConnectionIfAuth(); } void _loadConnectionIfAuth() { WidgetsBinding.instance.addPostFrameCallback((_) { final authState = ref.read(authProvider); if (authState.status == AuthStatus.authenticated) { ref .read(agentDetailProvider(widget.agentId).notifier) .loadConnectionStatus(); } }); } @override Widget build(BuildContext context) { final state = ref.watch(agentDetailProvider(widget.agentId)); if (state.isLoading) { return const Center( child: CircularProgressIndicator(color: AppColors.accentOrange), ); } if (state.error != null) { return _buildError(state.error!); } return _buildContent(state); } Widget _buildError(String error) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, size: 48, color: AppColors.accentOrange, ), const SizedBox(height: 16), const Text( 'Unable to Load Profile', style: TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 12), TextButton( onPressed: () => context.pop(), child: const Text( 'Go Back', style: TextStyle(color: AppColors.accentOrange), ), ), ], ), ), ); } Widget _buildContent(AgentDetailState state) { final agent = state.agent!; return SingleChildScrollView( child: Column( children: [ // ── Avatar Section ── const SizedBox(height: 16), _buildAvatarSection(agent), // ── Status + Connect Buttons ── const SizedBox(height: 16), _buildStatusButtons(state), // ── Contact Info ── const SizedBox(height: 12), _buildContactInfo(state), // ── Profile Info ── const SizedBox(height: 16), _buildProfileInfo(agent, state), // ── Experience Section (always show, like web) ── const SizedBox(height: 24), _buildExperienceSection(state), // ── Info Cards (Availability, Work Env, Best Experience) ── const SizedBox(height: 24), _buildInfoCards(state), // ── Specialization Section (always show, like web) ── const SizedBox(height: 24), _buildSpecializationSection(state), // ── Testimonials Section ── if (state.testimonials.isNotEmpty) ...[ const SizedBox(height: 24), _buildTestimonialsSection(state), ], const SizedBox(height: 30), ], ), ); } // ── Avatar ── Widget _buildAvatarSection(AgentProfile agent) { final screenWidth = MediaQuery.of(context).size.width; final avatarWidth = screenWidth * 0.877; // 377/430 from Figma return Center( child: ClipRRect( borderRadius: BorderRadius.circular(20), child: SizedBox( width: avatarWidth, height: 346, child: Stack( fit: StackFit.expand, children: [ S3Image( imageUrl: agent.avatarUrl, width: avatarWidth, height: 346, fit: BoxFit.cover, errorWidget: (_) => Container( color: const Color(0xFFC4D9D4), child: const Icon( Icons.person, size: 80, color: AppColors.primaryDark, ), ), ), // Gradient overlay Positioned( bottom: 0, left: 0, right: 0, height: 100, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withValues(alpha: 0.5), Colors.transparent, ], ), ), ), ), ], ), ), ), ); } // ── Status + Connect (single row matching web public view) ── Widget _buildStatusButtons(AgentDetailState state) { final agent = state.agent!; final isAvailable = agent.isAvailable; final connState = state.connectionState; return Padding( padding: const EdgeInsets.symmetric(horizontal: 38), child: Container( height: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 1, ), ), child: Row( children: [ const SizedBox(width: 16), // Status dot Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: isAvailable ? const Color(0xFF22C55E) : Colors.white, border: Border.all( color: isAvailable ? const Color(0xFF22C55E) : AppColors.primaryDark.withValues(alpha: 0.3), width: 1.5, ), ), ), const SizedBox(width: 8), Text( isAvailable ? 'Available.' : 'Unavailable.', style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const Spacer(), // Connect/Pending/Unlink button _buildConnectButton( connState, isAvailable, () => _handleConnect(state), ), const SizedBox(width: 4), ], ), ), ); } Widget _buildConnectButton( String connState, bool isAvailable, VoidCallback? onConnect, ) { Color bgColor; Color textColor; String label; bool enabled; switch (connState) { case 'pending': bgColor = const Color(0xFFFEF3C7); // yellow-100 textColor = const Color(0xFF92600F); label = 'Pending'; enabled = false; break; case 'accepted': bgColor = const Color(0xFFEF4444); // red-500 textColor = Colors.white; label = 'Unlink'; enabled = true; break; default: bgColor = isAvailable ? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.7); textColor = Colors.white; label = 'Connect'; enabled = isAvailable; } return GestureDetector( onTap: enabled ? onConnect : null, child: Container( height: 55, width: 140, decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(15), ), alignment: Alignment.center, child: Text( label, style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: textColor, ), ), ), ); } void _handleConnect(AgentDetailState state) { final authState = ref.read(authProvider); if (authState.status != AuthStatus.authenticated) { context.push('/login'); return; } if (state.connectionState == 'accepted') { ref.read(agentDetailProvider(widget.agentId).notifier).cancelConnection(); return; } _showConnectModal(state); } void _showConnectModal(AgentDetailState state) { final controller = TextEditingController(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => _ConnectModalContent( agentName: state.agent?.fullName ?? '', controller: controller, onSend: (message) async { final success = await ref .read(agentDetailProvider(widget.agentId).notifier) .connect(message: message); return success; }, ), ); } // ── Location Modal (matching web "Service Areas" modal) ── void _showLocationModal(List locations) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.fromLTRB( 24, 24, 24, 24 + MediaQuery.of(ctx).padding.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Service Areas', style: TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), GestureDetector( onTap: () => Navigator.of(ctx).pop(), child: Icon( Icons.close, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), ], ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: locations .map((loc) => Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color(0xFFE8E8E8), borderRadius: BorderRadius.circular(10), ), child: Text( loc, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark, ), ), )) .toList(), ), ], ), ), ); } // ── Contact Info ── Widget _buildContactInfo(AgentDetailState state) { final email = state.contactEmail; final phone = state.contactPhone; if (email == null && phone == null) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 38), child: Container( height: 98, padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.15), width: 1, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (email != null) _buildContactRow( label: 'Email:', value: _emailVisible ? email : state.maskEmail(email), onToggle: () => setState(() => _emailVisible = !_emailVisible), visible: _emailVisible, ), if (email != null && phone != null) const SizedBox(height: 12), if (phone != null) _buildContactRow( label: 'Ph.No:', value: _phoneVisible ? phone : state.maskPhone(phone), onToggle: () => setState(() => _phoneVisible = !_phoneVisible), visible: _phoneVisible, ), ], ), ), ); } Widget _buildContactRow({ required String label, required String value, required VoidCallback onToggle, required bool visible, }) { return Row( children: [ Text( label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 8), Expanded( child: Text( value, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), ), ), const SizedBox(width: 8), GestureDetector( onTap: onToggle, child: Icon( visible ? Icons.visibility : Icons.visibility_off, size: 18, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), ], ); } // ── Profile Info (Name, Title, Location, Bio) ── Widget _buildProfileInfo(AgentProfile agent, AgentDetailState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 38), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Name + Verified badge + (Verified Expert) — badge after last name Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( agent.firstName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 4), Text( agent.lastName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), if (agent.isVerified) ...[ const SizedBox(width: 6), const Padding( padding: EdgeInsets.only(bottom: 1), child: Icon( Icons.verified, size: 16, color: Color(0xFF2196F3), ), ), const SizedBox(width: 6), const Text( '(Verified Expert)', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF638559), ), ), ], ], ), // Title if (agent.agentType != null) ...[ const SizedBox(height: 6), Text( agent.headline ?? agent.agentType!.name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), ], const SizedBox(height: 8), // Location + Member Since (same line, matching web ProfileCard) Builder(builder: (_) { final parts = state.locationParts; const maxVisible = 1; final firstPart = parts.isNotEmpty ? parts.first : ''; final remaining = parts.length - maxVisible; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (parts.isNotEmpty) ...[ SvgPicture.asset( 'assets/icons/location_pin_icon.svg', width: 18, height: 18, ), const SizedBox(width: 4), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 120), child: Text( firstPart, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), if (remaining > 0) GestureDetector( onTap: () => _showLocationModal(parts), child: Text( ' +$remaining more', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.accentOrange, ), ), ), const SizedBox(width: 12), ], SvgPicture.asset( 'assets/icons/calendar_icon.svg', width: 18, height: 18, ), const SizedBox(width: 4), Flexible( child: Text( agent.memberSinceText, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ], ); }), // Expertise tags (matching web ProfileCard) if (state.expertiseTags.isNotEmpty) ...[ const SizedBox(height: 14), Wrap( alignment: WrapAlignment.center, spacing: 8, runSpacing: 8, children: state.expertiseTags .map((tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark, width: 0.7, ), ), child: Text( tag, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), )) .toList(), ), ], // Bio with Show More / Show Less if (state.descriptionText.isNotEmpty) ...[ const SizedBox(height: 16), SizedBox( width: 338, child: Text.rich( TextSpan( children: [ TextSpan( text: _bioExpanded || state.descriptionText.length <= 150 ? state.descriptionText : '${state.descriptionText.substring(0, 150)}... ', ), if (state.descriptionText.length > 150) WidgetSpan( child: GestureDetector( onTap: () => setState(() => _bioExpanded = !_bioExpanded), child: Text( _bioExpanded ? ' Show Less' : 'Show More', style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, decoration: TextDecoration.underline, ), ), ), ), ], style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.43, ), ), textAlign: TextAlign.center, ), ), ], ], ), ); } // ── Experience ── Widget _buildExperienceSection(AgentDetailState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 17), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Center( child: Text( 'Experience', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 22), // Years (always show, matching web) _buildExperienceItem('Years in Experience', [ state.yearsInExperience, ]), // Contracts (always show, matching web) _buildExperienceItem('Number of contracts closed', [ state.contractsClosed, ]), // Licensing Areas (always show) state.licensingAreas.isNotEmpty ? _buildLicensingAreas(state.licensingAreas) : _buildEmptySection('Licensing & Areas', 'No licensed areas added'), // Expertise Years (always show) state.expertiseYears.isNotEmpty ? _buildExpertiseYears(state.expertiseYears) : _buildEmptySection('Areas in expertise & Years', 'No expertise areas added'), // Certifications (always show) state.certifications.isNotEmpty ? _buildCertifications(state.certifications) : _buildEmptySection('Certifications', 'No certifications added'), ], ), ); } Widget _buildExperienceItem(String title, List items) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 14), ...items.map( (item) => Padding( padding: const EdgeInsets.only(left: 10, bottom: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(top: 2), child: Text( '\u2022 ', style: TextStyle( fontSize: 15, color: AppColors.primaryDark, ), ), ), Expanded( child: Text( item == '-' ? 'Not specified' : item, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 15, fontWeight: FontWeight.w500, color: item == '-' ? AppColors.primaryDark.withValues(alpha: 0.4) : AppColors.primaryDark, ), ), ), ], ), ), ), const SizedBox(height: 16), Divider( color: AppColors.primaryDark.withValues(alpha: 0.15), height: 1, ), const SizedBox(height: 10), ], ); } Widget _buildEmptySection(String title, String emptyText) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 14), Padding( padding: const EdgeInsets.only(left: 10), child: Text( emptyText, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark.withValues(alpha: 0.4), ), ), ), const SizedBox(height: 16), Divider( color: AppColors.primaryDark.withValues(alpha: 0.15), height: 1, ), const SizedBox(height: 10), ], ); } Widget _buildLicensingAreas(List areas) { return _ExpandableChipsSection( title: 'Licensing & Areas', items: areas, initialCount: 6, ); } Widget _buildExpertiseYears(List> items) { final chips = items.map((e) { final years = e['years'] ?? ''; return years.isNotEmpty ? '${e['area']} – $years' : e['area'] ?? ''; }).toList(); return _ExpandableChipsSection( title: 'Areas in expertise & Years', items: chips, initialCount: 4, ); } Widget _buildCertifications(List> certs) { return _ExpandableCertificationsSection(certs: certs); } // ── Info Cards (Availability, Work Env, Best Experience) ── Widget _buildInfoCards(AgentDetailState state) { // Work environment and best experience from fieldValues String workEnv = ''; String bestExp = ''; for (final fv in state.fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug.contains('work_environment') || slug.contains('preferred_environment')) { if (fv.textValue != null) workEnv = fv.textValue!; if (fv.jsonValue is List) { workEnv = (fv.jsonValue as List).map((e) => e.toString()).join(', '); } } if (slug.contains('best_experience') || slug.contains('amazing_experience')) { if (fv.textValue != null) bestExp = fv.textValue!; } } return Padding( padding: const EdgeInsets.symmetric(horizontal: 36), child: Column( children: [ // Availability card (always show) _buildInfoCard( icon: Icons.access_time_filled, title: 'Availability', subtitle: state.availabilityType.isNotEmpty ? state.availabilityType : 'Not specified', items: state.availabilitySchedule.isNotEmpty ? state.availabilitySchedule : null, isEmpty: state.availabilityType.isEmpty, ), const SizedBox(height: 16), // Work environment (always show) _buildInfoCard( icon: Icons.landscape_outlined, title: 'Work Environment', subtitle: '', description: workEnv.isNotEmpty ? workEnv : 'Not specified', isEmpty: workEnv.isEmpty, ), const SizedBox(height: 16), // Best experience (always show) _buildInfoCard( icon: Icons.star_rounded, title: 'Best Experience', subtitle: '', description: bestExp.isNotEmpty ? bestExp : 'Not specified', isEmpty: bestExp.isEmpty, ), ], ), ); } Widget _buildInfoCard({ required IconData icon, required String title, required String subtitle, List? items, String? description, bool isEmpty = false, }) { return Container( width: double.infinity, constraints: const BoxConstraints(minHeight: 217), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: const Color(0xFFD9D9D9).withValues(alpha: 0.5), offset: const Offset(0, 10), blurRadius: 20, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(icon, size: 36, color: AppColors.accentOrange), if (title.isNotEmpty) ...[ const SizedBox(height: 8), Text( title, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), ], if (subtitle.isNotEmpty) ...[ const SizedBox(height: 2), Text( subtitle, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: isEmpty ? AppColors.primaryDark.withValues(alpha: 0.4) : AppColors.primaryDark, ), ), ], if (items != null && items.isNotEmpty) ...[ const SizedBox(height: 8), ...items.map( (item) => Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Text( item, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), ), ), ), ], if (description != null) ...[ const SizedBox(height: 8), Text( description, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: isEmpty ? AppColors.primaryDark.withValues(alpha: 0.4) : AppColors.primaryDark, ), textAlign: TextAlign.center, ), ], ], ), ); } // ── Specialization Section ── Widget _buildSpecializationSection(AgentDetailState state) { return Container( width: double.infinity, color: const Color(0xFFE6E6E6), padding: const EdgeInsets.symmetric(vertical: 24), child: Column( children: [ const Text( 'Specialization', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 4), const Text( 'Area Of Expertise and Focus', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), const SizedBox(height: 16), if (state.specializationCards.isNotEmpty) ...state.specializationCards.map( (card) => Padding( padding: const EdgeInsets.only(bottom: 12), child: _SpecializationCardWidget(card: card), ), ) else Text( 'No specialization data added', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark.withValues(alpha: 0.4), ), ), ], ), ); } // ── Testimonials ── Widget _buildTestimonialsSection(AgentDetailState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ const Text( 'Testimonials', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), Divider( color: AppColors.primaryDark.withValues(alpha: 0.15), height: 1, ), const SizedBox(height: 8), const Text( 'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), SizedBox( height: 380, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.testimonials.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (_, i) => _TestimonialCard(data: state.testimonials[i]), ), ), ], ), ); } } // ── Expandable Chips Section ── class _ExpandableChipsSection extends StatefulWidget { final String title; final List items; final int initialCount; const _ExpandableChipsSection({ required this.title, required this.items, this.initialCount = 6, }); @override State<_ExpandableChipsSection> createState() => _ExpandableChipsSectionState(); } class _ExpandableChipsSectionState extends State<_ExpandableChipsSection> { bool _expanded = false; @override Widget build(BuildContext context) { final showItems = _expanded ? widget.items : widget.items.take(widget.initialCount).toList(); final hasMore = widget.items.length > widget.initialCount; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.title, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 14), Wrap( spacing: 10, runSpacing: 10, children: [ ...showItems.map( (item) => IntrinsicWidth( child: Container( height: 28, padding: const EdgeInsets.symmetric(horizontal: 10), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark, width: 0.7, ), ), child: Text( item, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ), ), ), if (hasMore || _expanded) GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: IntrinsicWidth( child: Container( height: 28, padding: const EdgeInsets.symmetric(horizontal: 10), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: _expanded ? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.15), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _expanded ? 'Show Less' : '+${widget.items.length - widget.initialCount} More', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: _expanded ? 13 : 15, fontWeight: _expanded ? FontWeight.w700 : FontWeight.w300, color: _expanded ? AppColors.accentOrange : AppColors.primaryDark, ), ), if (_expanded) ...[ const SizedBox(width: 2), const Icon( Icons.keyboard_arrow_up, size: 14, color: AppColors.accentOrange, ), ], ], ), ), ), ), ], ), const SizedBox(height: 14), Divider( color: AppColors.primaryDark.withValues(alpha: 0.15), height: 1, ), const SizedBox(height: 10), ], ); } } // ── Expandable Certifications Section ── class _ExpandableCertificationsSection extends StatefulWidget { final List> certs; final int initialCount; const _ExpandableCertificationsSection({ required this.certs, this.initialCount = 2, }); @override State<_ExpandableCertificationsSection> createState() => _ExpandableCertificationsSectionState(); } class _ExpandableCertificationsSectionState extends State<_ExpandableCertificationsSection> { bool _expanded = false; @override Widget build(BuildContext context) { final showCerts = _expanded ? widget.certs : widget.certs.take(widget.initialCount).toList(); final hasMore = widget.certs.length > widget.initialCount; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Certifications', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 16), ...showCerts.map( (cert) => Padding( padding: const EdgeInsets.only(left: 14, bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(top: 2), child: Text( '\u2022 ', style: TextStyle( fontSize: 14, color: AppColors.primaryDark, ), ), ), Expanded( child: Text( cert['name'] ?? '', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), ), ), ], ), if ((cert['org'] ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only(left: 20, top: 2), child: Text( cert['org']!.toUpperCase(), style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), ), ], ), ), ), if (hasMore || _expanded) Center( child: GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 4, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.accentOrange, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _expanded ? 'Show Less' : '+${widget.certs.length - widget.initialCount} More', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.accentOrange, ), ), const SizedBox(width: 2), Icon( _expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 14, color: AppColors.accentOrange, ), ], ), ), ), ), const SizedBox(height: 4), ], ); } } // ── Specialization Card ── class _SpecializationCardWidget extends StatefulWidget { final SpecializationCard card; const _SpecializationCardWidget({required this.card}); @override State<_SpecializationCardWidget> createState() => _SpecializationCardWidgetState(); } class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> { bool _expanded = false; static const _initialCount = 3; IconData get _icon { final slug = widget.card.slug.toLowerCase(); if (slug.contains('loan') || slug.contains('price')) { return Icons.attach_money; } if (slug.contains('property')) return Icons.apartment; if (slug.contains('hobby') || slug.contains('interest')) { return Icons.favorite_border; } return Icons.home_outlined; } @override Widget build(BuildContext context) { final values = _expanded ? widget.card.values : widget.card.values.take(_initialCount).toList(); final hasMore = widget.card.values.length > _initialCount; return Container( width: 298, constraints: const BoxConstraints(minHeight: 236), padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.7), width: 0.7, ), ), child: Column( children: [ Icon(_icon, size: 32, color: AppColors.accentOrange), const SizedBox(height: 8), Text( widget.card.name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 12), ...values.map( (v) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text( AgentDetailState.titleCase(v), style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.6, ), textAlign: TextAlign.center, ), ), ), if (hasMore) ...[ const SizedBox(height: 10), GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 4, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.accentOrange, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _expanded ? 'Show Less' : 'Show More', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.accentOrange, ), ), const SizedBox(width: 2), Icon( _expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 14, color: AppColors.accentOrange, ), ], ), ), ), ], ], ), ); } } // ── Testimonial Card (matches Figma & web TestimonialCard) ── class _TestimonialCard extends StatelessWidget { final Map data; const _TestimonialCard({required this.data}); @override Widget build(BuildContext context) { final text = data['text'] as String? ?? ''; final authorName = data['authorName'] as String? ?? ''; final authorRole = data['authorRole'] as String? ?? ''; final rating = (data['rating'] as num?)?.toInt() ?? 5; return Container( width: 300, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Quote icon SvgPicture.asset( 'assets/icons/quote_icon.svg', width: 23, height: 13, ), const SizedBox(height: 12), // Title snippet (bold) Text( text.length > 35 ? '${text.substring(0, 35)} ..' : text, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), // Full text Expanded( child: Text( text, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, height: 1.4, ), overflow: TextOverflow.fade, ), ), const SizedBox(height: 12), // Divider Divider( color: AppColors.primaryDark.withValues(alpha: 0.08), height: 1, ), const SizedBox(height: 12), // Star rating Row( mainAxisSize: MainAxisSize.min, children: List.generate( 5, (i) => Padding( padding: const EdgeInsets.only(right: 2), child: Icon( i < rating ? Icons.star : Icons.star_border, size: 18, color: i < rating ? const Color(0xFFFFDE21) : AppColors.primaryDark.withValues(alpha: 0.3), ), ), ), ), const SizedBox(height: 8), // Author name Text( authorName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), // Author role if (authorRole.isNotEmpty) ...[ const SizedBox(height: 2), Text( authorRole, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ], ], ), ); } } // ── Connect Modal Content (matches web ConnectRequestModal) ── class _ConnectModalContent extends StatefulWidget { final String agentName; final TextEditingController controller; final Future Function(String? message) onSend; const _ConnectModalContent({ required this.agentName, required this.controller, required this.onSend, }); @override State<_ConnectModalContent> createState() => _ConnectModalContentState(); } class _ConnectModalContentState extends State<_ConnectModalContent> { bool _isSubmitting = false; bool _success = false; String? _error; void _closeModal() { if (mounted) { Navigator.of(context, rootNavigator: false).pop(); } } Future _handleSend() async { setState(() { _isSubmitting = true; _error = null; }); try { final success = await widget.onSend(widget.controller.text.trim()); if (success) { setState(() => _success = true); // Auto-close after 2 seconds (matching web) Future.delayed(const Duration(seconds: 2), () { _closeModal(); }); } else { setState(() => _error = 'Failed to send connection request'); } } catch (e) { setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _isSubmitting = false); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.fromLTRB( 24, 24, 24, 24 + MediaQuery.of(context).padding.bottom, ), child: SingleChildScrollView( child: _success ? _buildSuccessContent() : _buildFormContent(), ), ), ); } Widget _buildSuccessContent() { return Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 16), Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFFDCFCE7), // green-100 shape: BoxShape.circle, ), child: const Icon( Icons.check, size: 32, color: Color(0xFF22C55E), // green-500 ), ), const SizedBox(height: 16), const Text( 'Request Sent!', style: TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), Text( 'Your connection request has been sent to ${widget.agentName}.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 24), ], ); } Widget _buildFormContent() { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( 'Connect with ${widget.agentName}', style: const TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), GestureDetector( onTap: _closeModal, child: Icon( Icons.close, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), ], ), const SizedBox(height: 4), Text( 'Send a connection request to start communicating with this agent.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.7), ), ), const SizedBox(height: 16), const Text( 'Message (Optional)', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), TextField( controller: widget.controller, maxLines: 4, maxLength: 1000, decoration: InputDecoration( hintText: 'Introduce yourself and explain what you\'re looking for...', hintStyle: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.4), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.2), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: AppColors.accentOrange), ), ), ), if (_error != null) ...[ const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFEF2F2), // red-50 borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFECACA)), // red-200 ), child: Text( _error!, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: Color(0xFFDC2626), // red-600 ), ), ), ], const SizedBox(height: 16), Row( children: [ // Cancel button Expanded( child: SizedBox( height: 48, child: OutlinedButton( onPressed: _closeModal, style: OutlinedButton.styleFrom( side: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.2), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), child: const Text( 'Cancel', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), ), ), ), const SizedBox(width: 12), // Send Request button Expanded( child: SizedBox( height: 48, child: ElevatedButton( onPressed: _isSubmitting ? null : _handleSend, style: ElevatedButton.styleFrom( backgroundColor: AppColors.accentOrange, disabledBackgroundColor: AppColors.accentOrange.withValues(alpha: 0.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), child: Text( _isSubmitting ? 'Sending...' : 'Send Request', style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), ], ), ], ); } }