From fc2c099c085588632204a655c80615c68c3078ec Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Thu, 19 Mar 2026 03:11:34 +0530 Subject: [PATCH] feat: Refactor agent info cards to include testimonial previews and redesign the testimonial card to match web and Figma specifications. --- .../screens/agent_home_screen.dart | 361 ++++++++---------- pubspec.yaml | 2 +- 2 files changed, 168 insertions(+), 195 deletions(-) diff --git a/lib/features/agents/presentation/screens/agent_home_screen.dart b/lib/features/agents/presentation/screens/agent_home_screen.dart index 27c82d3..26e0e3d 100644 --- a/lib/features/agents/presentation/screens/agent_home_screen.dart +++ b/lib/features/agents/presentation/screens/agent_home_screen.dart @@ -884,11 +884,10 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ); } - // ── Info Cards (Availability, Work Environment, Experience) ── + // ── Info Cards (Availability, Work Env, Testimonial) — matches web ── Widget _buildInfoCards(AgentDetailState state) { - // Extract work environment and best experience from fieldValues + // Work environment — try fieldValues first, fallback to mock (same as web) String workEnv = ''; - String bestExp = ''; for (final fv in state.fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug.contains('work_environment') || @@ -899,9 +898,24 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { (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!; + } + // Fallback mock data (same as web dashboard) + if (workEnv.isEmpty) { + workEnv = + 'Preferred work environment includes on-site property visits, ' + 'in-depth market research, client consultations, property tours, ' + 'and active involvement in negotiations to ensure the best outcomes'; + } + + // Extract first testimonial for preview + String testimonialText = ''; + if (state.testimonials.isNotEmpty) { + final first = state.testimonials.first; + testimonialText = + (first['text'] ?? first['content'] ?? first['review'] ?? '') + .toString(); + if (testimonialText.length > 150) { + testimonialText = '${testimonialText.substring(0, 150)}...'; } } @@ -909,28 +923,37 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { 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, - ), - ], + // Availability card (with title — matches web) + _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 (no title — matches web) + _buildInfoCard( + icon: Icons.landscape_outlined, + title: '', + subtitle: '', + description: workEnv, + ), + const SizedBox(height: 16), + // Testimonial preview (no title — matches web) + _buildInfoCard( + icon: Icons.star_rounded, + title: '', + subtitle: '', + description: testimonialText.isNotEmpty + ? '\u201C$testimonialText\u201D' + : 'No testimonials yet', + isEmpty: testimonialText.isEmpty, + ), ], ), ); @@ -938,10 +961,11 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { Widget _buildInfoCard({ required IconData icon, - String title = '', - String subtitle = '', + required String title, + required String subtitle, List? items, String? description, + bool isEmpty = false, }) { return Container( width: double.infinity, @@ -978,37 +1002,43 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { const SizedBox(height: 2), Text( subtitle, - style: const TextStyle( + style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.primaryDark, + 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, - ), + ...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) ...[ + if (description != null) ...[ const SizedBox(height: 8), Text( description, - style: const TextStyle( + style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.primaryDark, + color: isEmpty + ? AppColors.primaryDark.withValues(alpha: 0.4) + : AppColors.primaryDark, ), textAlign: TextAlign.center, ), @@ -1137,7 +1167,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ); } - // ── Testimonials Section ── + // ── Testimonials ── Widget _buildTestimonialsSection(AgentDetailState state) { if (state.testimonials.isEmpty) return const SizedBox.shrink(); @@ -1156,8 +1186,9 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ), const SizedBox(height: 8), Divider( - color: AppColors.primaryDark.withValues(alpha: 0.15), - height: 1), + 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.', @@ -1170,7 +1201,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ), const SizedBox(height: 16), SizedBox( - height: 320, + height: 380, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.testimonials.length, @@ -1186,7 +1217,8 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { } -// ── Testimonial Card (same as agent detail screen) ── +// ── Testimonial Card (matches Figma & web TestimonialCard) ── + class _TestimonialCard extends StatelessWidget { final Map data; @@ -1199,163 +1231,104 @@ class _TestimonialCard extends StatelessWidget { final authorRole = data['authorRole'] as String? ?? ''; final rating = (data['rating'] as num?)?.toInt() ?? 5; - return SizedBox( - width: 320, + 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: [ - // Main card body with speech bubble shape + // 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: 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: Text( + text, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + height: 1.4, ), - 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, - ), - ), - ], + 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), + ), ), ), ), - // 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: 8), + // Author name + Text( + authorName, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, ), ), - 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, - ), - ), - ], - ), - ), - ], + // Author role + if (authorRole.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + authorRole, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w500, + 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; -} diff --git a/pubspec.yaml b/pubspec.yaml index 04fd231..91cb235 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: real_estate_mobile description: "RE-Quest Real Estate Mobile App" publish_to: 'none' -version: 1.0.0+2 +version: 1.0.0+3 environment: sdk: ^3.10.1