import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.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/profile/data/profile_repository.dart'; /// Provider that fetches the agent's own profile ID from /agents/profile/me final _agentMyProfileProvider = FutureProvider.autoDispose((ref) async { final repo = ProfileRepository(); try { final data = await repo.getMyProfile('AGENT'); return data['id'] as String?; } catch (_) { return null; } }); class AgentHomeScreen extends ConsumerWidget { const AgentHomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final myProfileAsync = ref.watch(_agentMyProfileProvider); return myProfileAsync.when( loading: () => const Center( child: CircularProgressIndicator( color: AppColors.accentOrange, strokeWidth: 2, ), ), error: (_, __) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.accentOrange), const SizedBox(height: 16), const Text('Failed to load profile', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primaryDark)), const SizedBox(height: 12), TextButton( onPressed: () => ref.invalidate(_agentMyProfileProvider), child: const Text('Retry', style: TextStyle(color: AppColors.accentOrange)), ), ], ), ), data: (agentProfileId) { if (agentProfileId == null) { return const Center( child: Text('No agent profile found', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, color: AppColors.primaryDark)), ); } return _AgentHomeContent(agentProfileId: agentProfileId); }, ); } } class _AgentHomeContent extends ConsumerStatefulWidget { final String agentProfileId; const _AgentHomeContent({required this.agentProfileId}); @override ConsumerState<_AgentHomeContent> createState() => _AgentHomeContentState(); } class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { bool _isUploadingAvatar = false; bool _isTogglingAvailability = false; File? _localAvatarFile; Future _pickAndUploadAvatar() async { final picker = ImagePicker(); final picked = await picker.pickImage( source: ImageSource.gallery, maxWidth: 1200, maxHeight: 1200, imageQuality: 85, ); if (picked == null) return; final file = File(picked.path); final fileName = picked.name; final ext = fileName.split('.').last.toLowerCase(); // Validate file type const allowedTypes = ['jpg', 'jpeg', 'png', 'gif']; if (!allowedTypes.contains(ext)) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Only JPG, PNG, and GIF images are allowed')), ); } return; } // Validate file size (2MB max) final fileSize = await file.length(); if (fileSize > 2 * 1024 * 1024) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Image must be smaller than 2MB')), ); } return; } final contentType = ext == 'png' ? 'image/png' : ext == 'gif' ? 'image/gif' : 'image/jpeg'; setState(() { _localAvatarFile = file; _isUploadingAvatar = true; }); try { final repo = ProfileRepository(); // 1. Get presigned upload URL final uploadData = await repo.getAvatarUploadUrl('AGENT', fileName, contentType); final uploadUrl = uploadData['uploadUrl']!; final key = uploadData['key']!; // 2. Upload to S3 final fileBytes = await file.readAsBytes(); await repo.uploadToS3(uploadUrl, fileBytes, contentType); // 3. Update profile with new avatar key await repo.updateProfile('AGENT', {'avatar': key}); // 4. Refresh the agent detail provider ref.invalidate(agentDetailProvider(widget.agentProfileId)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Photo updated successfully')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to upload photo: ${e.toString()}')), ); } } finally { if (mounted) { setState(() { _isUploadingAvatar = false; _localAvatarFile = null; }); } } } Future _toggleAvailability(bool newValue) async { if (_isTogglingAvailability) return; setState(() => _isTogglingAvailability = true); try { final repo = ProfileRepository(); await repo.updateProfile('AGENT', {'isAvailable': newValue}); // Refresh agent detail to reflect new status ref.invalidate(agentDetailProvider(widget.agentProfileId)); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to update availability: $e')), ); } } finally { if (mounted) { setState(() => _isTogglingAvailability = false); } } } @override Widget build(BuildContext context) { final state = ref.watch(agentDetailProvider(widget.agentProfileId)); 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: 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: () => ref.invalidate(agentDetailProvider(widget.agentProfileId)), child: const Text('Retry', style: TextStyle(color: AppColors.accentOrange)), ), ], ), ); } Widget _buildContent(AgentDetailState state) { final agent = state.agent!; return SingleChildScrollView( child: Column( children: [ _buildCoverAndAvatar(agent), const SizedBox(height: 16), _buildStatusSection(agent), const SizedBox(height: 12), _buildContactInfo(state), const SizedBox(height: 16), _buildProfileInfo(agent), const SizedBox(height: 24), _buildExperienceSection(state), const SizedBox(height: 24), _buildInfoCards(state), const SizedBox(height: 24), _buildSpecializationSection(state), const SizedBox(height: 24), _buildTestimonialsSection(state), const SizedBox(height: 24), ], ), ); } // ── Agent profile photo as cover image ── Widget _buildCoverAndAvatar(AgentProfile agent) { final screenWidth = MediaQuery.of(context).size.width; final coverWidth = screenWidth * 0.877; // 377/430 from Figma return Center( child: Stack( children: [ // Agent photo as the cover ClipRRect( borderRadius: BorderRadius.circular(20), child: SizedBox( width: coverWidth, height: 346, child: _localAvatarFile != null ? Image.file( _localAvatarFile!, width: coverWidth, height: 346, fit: BoxFit.cover, ) : agent.avatarUrl != null && agent.avatarUrl!.isNotEmpty ? S3Image( imageUrl: agent.avatarUrl!, width: coverWidth, height: 346, fit: BoxFit.cover, ) : Container( color: const Color(0xFFC4D9D4), alignment: Alignment.center, child: Icon(Icons.person, size: 80, color: AppColors.primaryDark), ), ), ), // Loading overlay during upload if (_isUploadingAvatar) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Container( color: Colors.black.withValues(alpha: 0.4), child: const Center( child: CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ), ), ), ), // Orange edit icon top-right (tabler:edit style) Positioned( top: 8, right: 6, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _isUploadingAvatar ? null : _pickAndUploadAvatar, child: Padding( padding: const EdgeInsets.all(8), child: Container( width: 28, height: 26, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(7), ), alignment: Alignment.center, child: SvgPicture.asset( 'assets/icons/edit_icon.svg', width: 20, height: 20, ), ), ), ), ), ], ), ); } // ── Available / Unavailable status toggle (matches web design) ── Widget _buildStatusSection(AgentProfile agent) { final isAvailable = agent.isAvailable; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ _buildStatusOption( label: 'Available.', isActive: isAvailable, dotColor: const Color(0xFF4CAF50), activeColor: const Color(0xFFE8F5E9), activeBorderColor: const Color(0xFF4CAF50), onTap: isAvailable ? null : () => _toggleAvailability(true), ), const SizedBox(height: 12), _buildStatusOption( label: 'Unavailable.', isActive: !isAvailable, dotColor: const Color(0xFF00293D), activeColor: const Color(0xFFE8EDF2), activeBorderColor: const Color(0xFF00293D), onTap: !isAvailable ? null : () => _toggleAvailability(false), ), ], ), ); } Widget _buildStatusOption({ required String label, required bool isActive, required Color dotColor, required Color activeColor, required Color activeBorderColor, VoidCallback? onTap, }) { return GestureDetector( onTap: _isTogglingAvailability ? null : onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 50, padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: isActive ? activeColor : Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: isActive ? activeBorderColor : const Color(0xFFE0E0E0), width: isActive ? 1.5 : 1, ), ), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? dotColor : Colors.white, border: Border.all( color: isActive ? dotColor : const Color(0xFFD0D0D0), width: 1.5, ), ), ), const SizedBox(width: 10), Text( label, style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, color: AppColors.primaryDark, ), ), const Spacer(), if (_isTogglingAvailability && !isActive) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.accentOrange, ), ) else if (isActive) Text( 'Active', style: TextStyle( fontFamily: 'Fractul', fontSize: 13, fontWeight: FontWeight.w500, color: activeBorderColor, ), ), ], ), ), ); } // ── Contact Info ── Widget _buildContactInfo(AgentDetailState state) { final email = state.contactEmail ?? ''; final phone = state.contactPhone ?? ''; return Container( margin: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Text('Email:', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(width: 16), Expanded( child: Text( email.isNotEmpty ? state.maskEmail(email) : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), ), ), const Icon(Icons.visibility_off_outlined, size: 18, color: AppColors.primaryDark), ], ), const SizedBox(height: 14), Row( children: [ const Text('Ph.No:', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(width: 16), Expanded( child: Text( phone.isNotEmpty ? state.maskPhone(phone) : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), ), ), const Icon(Icons.visibility_off_outlined, size: 18, color: AppColors.primaryDark), ], ), ], ), ); } // ── Name, verified badge, title, location, member since, bio ── Widget _buildProfileInfo(AgentProfile agent) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 38), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Name + Verified badge + (Verified Expert) on same line 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), if (agent.isVerified) ...[ const Padding( padding: EdgeInsets.only(bottom: 1), child: Icon(Icons.verified, size: 16, color: Color(0xFF2196F3)), ), 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 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 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (agent.location.isNotEmpty) ...[ Icon(Icons.location_on, size: 16, color: AppColors.primaryDark.withValues(alpha: 0.7)), const SizedBox(width: 4), Text( agent.location, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 20), ], Icon(Icons.calendar_today, size: 14, color: AppColors.primaryDark.withValues(alpha: 0.7)), const SizedBox(width: 4), Text( agent.memberSinceText, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ], ), // Bio if (agent.description.isNotEmpty) ...[ const SizedBox(height: 16), SizedBox( width: 338, child: Text( agent.description, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.43, ), textAlign: TextAlign.center, ), ), ], ], ), // Edit icon positioned at top-right Positioned( right: 0, top: 0, child: GestureDetector( onTap: () {}, child: SvgPicture.asset( 'assets/icons/edit_icon.svg', width: 22, height: 22, ), ), ), ], ), ); } // ── Experience Section ── Widget _buildExperienceSection(AgentDetailState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('Experience', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 16), _buildExperienceRow( 'Years in Experience', state.yearsInExperience), const Divider(height: 24, thickness: 0.2), _buildExperienceRow( 'Number of contract closed', state.contractsClosed), const Divider(height: 24, thickness: 0.2), if (state.licensingAreas.isNotEmpty) ...[ const Align( alignment: Alignment.centerLeft, child: Text('Licensing & Areas', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), _buildTagsWrap(state.licensingAreas), const SizedBox(height: 16), ], if (state.expertiseYears.isNotEmpty) ...[ const Align( alignment: Alignment.centerLeft, child: Text('Areas in expertise & Years', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), _buildTagsWrap(state.expertiseYears .map((e) => e['years']!.isNotEmpty ? '${e['area']} – ${e['years']}' : e['area']!) .toList()), ], if (state.certifications.isNotEmpty) ...[ const SizedBox(height: 16), const Align( alignment: Alignment.centerLeft, child: Text('Certifications', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), ...state.certifications.map((cert) => Padding( padding: const EdgeInsets.only(bottom: 12, left: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const 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: 16), child: Opacity( opacity: 0.5, child: Text(cert['org']!, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark)), ), ), ], ), )), ], ], ), ); } Widget _buildExperienceRow(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 6), Padding( padding: const EdgeInsets.only(left: 16), child: Row( children: [ const Text('\u2022 ', style: TextStyle( fontSize: 15, color: AppColors.primaryDark)), Text(value, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.primaryDark)), ], ), ), ], ); } Widget _buildTagsWrap(List tags) { final displayTags = tags.take(6).toList(); final remaining = tags.length - 6; return Wrap( spacing: 8, runSpacing: 8, children: [ ...displayTags.map((tag) => Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 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.w500, color: AppColors.primaryDark)), )), if (remaining > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.1), ), child: Text('+$remaining More', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 15, fontWeight: FontWeight.w300, color: AppColors.primaryDark)), ), ], ); } // ── Info Cards (Availability, Work Environment, Experience) ── Widget _buildInfoCards(AgentDetailState state) { // Extract 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 if (state.availabilityType.isNotEmpty) _buildInfoCard( icon: Icons.access_time_filled, title: 'Availability', subtitle: state.availabilityType, items: state.availabilitySchedule, ), if (workEnv.isNotEmpty) ...[ const SizedBox(height: 16), _buildInfoCard( icon: Icons.landscape_outlined, description: workEnv, ), ], if (bestExp.isNotEmpty) ...[ const SizedBox(height: 16), _buildInfoCard( icon: Icons.star_rounded, description: bestExp, ), ], ], ), ); } Widget _buildInfoCard({ required IconData icon, String title = '', String subtitle = '', List? items, String? description, }) { 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: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: 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 && description.isNotEmpty) ...[ const SizedBox(height: 8), Text( description, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), ], ], ), ); } // ── Specialization Section (grey background) ── Widget _buildSpecializationSection(AgentDetailState state) { final cards = state.specializationCards; if (cards.isEmpty) return const SizedBox.shrink(); 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), ...cards.map((card) => _buildSpecCard(card)), ], ), ); } Widget _buildSpecCard(SpecializationCard card) { final displayName = AgentDetailState.titleCase(card.name); return Container( margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8), width: 298, constraints: const BoxConstraints(minHeight: 236), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.7), ), child: Column( children: [ const Icon(Icons.category_outlined, size: 28, color: AppColors.accentOrange), const SizedBox(height: 10), Text(displayName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 12), ...card.values.take(5).map((val) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text( AgentDetailState.titleCase(val), textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.8), ), )), if (card.values.length > 5) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.accentOrange, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Show More', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.accentOrange)), const SizedBox(width: 4), Icon(Icons.keyboard_arrow_down, size: 14, color: AppColors.accentOrange), ], ), ), ], ], ), ); } // ── Testimonials Section ── Widget _buildTestimonialsSection(AgentDetailState state) { if (state.testimonials.isEmpty) return const SizedBox.shrink(); 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: 320, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.testimonials.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (_, i) => _TestimonialCard(data: state.testimonials[i]), ), ), ], ), ); } } // ── Testimonial Card (same as agent detail screen) ── 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 SizedBox( width: 320, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Main card body with speech bubble shape Expanded( child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: const Color(0xFFD9D9D9).withValues(alpha: 0.5), offset: const Offset(0, 10), blurRadius: 20, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Quote icon const Text( '\u201C', style: TextStyle( fontFamily: 'Fractul', fontSize: 36, fontWeight: FontWeight.w700, color: AppColors.primaryDark, height: 0.8, ), ), const SizedBox(height: 6), // Title snippet Text( text.length > 35 ? '${text.substring(0, 35)} ..' : text, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 10), // Full text Expanded( child: Text( text, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.5, ), overflow: TextOverflow.fade, ), ), ], ), ), ), // Speech bubble triangle Padding( padding: const EdgeInsets.only(left: 30), child: CustomPaint( size: const Size(16, 10), painter: _TrianglePainter(color: Colors.white), ), ), const SizedBox(height: 6), // Stars + Author info Padding( padding: const EdgeInsets.only(left: 4), child: Row( children: [ // Stars Row( mainAxisSize: MainAxisSize.min, children: List.generate( 5, (i) => Icon( i < rating ? Icons.star : Icons.star_border, size: 16, color: i < rating ? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.3), ), ), ), const SizedBox(width: 10), // Author Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( authorName, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), if (authorRole.isNotEmpty) Text( authorRole, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), ], ), ), ], ), ), ], ), ); } } // ── Triangle painter for speech bubble ── class _TrianglePainter extends CustomPainter { final Color color; _TrianglePainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; final shadowPaint = Paint() ..color = const Color(0xFFD9D9D9).withValues(alpha: 0.16) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4); final path = Path() ..moveTo(0, 0) ..lineTo(size.width / 2, size.height) ..lineTo(size.width, 0) ..close(); canvas.drawPath(path, shadowPaint); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }