feat: Refactor agent info cards to include testimonial previews and redesign the testimonial card to match web and Figma specifications.

This commit is contained in:
pradeepkumar
2026-03-19 03:11:34 +05:30
parent a4aa9bfe25
commit fc2c099c08
2 changed files with 168 additions and 195 deletions

View File

@@ -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) { 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 workEnv = '';
String bestExp = '';
for (final fv in state.fieldValues) { for (final fv in state.fieldValues) {
final slug = fv.fieldSlug.toLowerCase(); final slug = fv.fieldSlug.toLowerCase();
if (slug.contains('work_environment') || if (slug.contains('work_environment') ||
@@ -899,9 +898,24 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
(fv.jsonValue as List).map((e) => e.toString()).join(', '); (fv.jsonValue as List).map((e) => e.toString()).join(', ');
} }
} }
if (slug.contains('best_experience') || }
slug.contains('amazing_experience')) { // Fallback mock data (same as web dashboard)
if (fv.textValue != null) bestExp = fv.textValue!; 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,39 +923,49 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
padding: const EdgeInsets.symmetric(horizontal: 36), padding: const EdgeInsets.symmetric(horizontal: 36),
child: Column( child: Column(
children: [ children: [
// Availability card // Availability card (with title — matches web)
if (state.availabilityType.isNotEmpty)
_buildInfoCard( _buildInfoCard(
icon: Icons.access_time_filled, icon: Icons.access_time_filled,
title: 'Availability', title: 'Availability',
subtitle: state.availabilityType, subtitle: state.availabilityType.isNotEmpty
items: state.availabilitySchedule, ? state.availabilityType
: 'Not specified',
items: state.availabilitySchedule.isNotEmpty
? state.availabilitySchedule
: null,
isEmpty: state.availabilityType.isEmpty,
), ),
if (workEnv.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
// Work environment (no title — matches web)
_buildInfoCard( _buildInfoCard(
icon: Icons.landscape_outlined, icon: Icons.landscape_outlined,
title: '',
subtitle: '',
description: workEnv, description: workEnv,
), ),
],
if (bestExp.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
// Testimonial preview (no title — matches web)
_buildInfoCard( _buildInfoCard(
icon: Icons.star_rounded, icon: Icons.star_rounded,
description: bestExp, title: '',
subtitle: '',
description: testimonialText.isNotEmpty
? '\u201C$testimonialText\u201D'
: 'No testimonials yet',
isEmpty: testimonialText.isEmpty,
), ),
], ],
],
), ),
); );
} }
Widget _buildInfoCard({ Widget _buildInfoCard({
required IconData icon, required IconData icon,
String title = '', required String title,
String subtitle = '', required String subtitle,
List<String>? items, List<String>? items,
String? description, String? description,
bool isEmpty = false,
}) { }) {
return Container( return Container(
width: double.infinity, width: double.infinity,
@@ -978,17 +1002,20 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
subtitle, subtitle,
style: const TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryDark, color: isEmpty
? AppColors.primaryDark.withValues(alpha: 0.4)
: AppColors.primaryDark,
), ),
), ),
], ],
if (items != null && items.isNotEmpty) ...[ if (items != null && items.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
...items.map((item) => Padding( ...items.map(
(item) => Padding(
padding: const EdgeInsets.symmetric(vertical: 1), padding: const EdgeInsets.symmetric(vertical: 1),
child: Text( child: Text(
item, item,
@@ -998,17 +1025,20 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
)), ),
),
], ],
if (description != null && description.isNotEmpty) ...[ if (description != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
description, description,
style: const TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryDark, color: isEmpty
? AppColors.primaryDark.withValues(alpha: 0.4)
: AppColors.primaryDark,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -1137,7 +1167,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
); );
} }
// ── Testimonials Section ── // ── Testimonials ──
Widget _buildTestimonialsSection(AgentDetailState state) { Widget _buildTestimonialsSection(AgentDetailState state) {
if (state.testimonials.isEmpty) return const SizedBox.shrink(); if (state.testimonials.isEmpty) return const SizedBox.shrink();
@@ -1157,7 +1187,8 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
const SizedBox(height: 8), const SizedBox(height: 8),
Divider( Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15), color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1), height: 1,
),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.', '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), const SizedBox(height: 16),
SizedBox( SizedBox(
height: 320, height: 380,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: state.testimonials.length, 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 { class _TestimonialCard extends StatelessWidget {
final Map<String, dynamic> data; final Map<String, dynamic> data;
@@ -1199,57 +1231,40 @@ class _TestimonialCard extends StatelessWidget {
final authorRole = data['authorRole'] as String? ?? ''; final authorRole = data['authorRole'] as String? ?? '';
final rating = (data['rating'] as num?)?.toInt() ?? 5; final rating = (data['rating'] as num?)?.toInt() ?? 5;
return SizedBox( return Container(
width: 320, width: 300,
child: Column( padding: const EdgeInsets.all(16),
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( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
boxShadow: [ border: Border.all(
BoxShadow( color: AppColors.primaryDark.withValues(alpha: 0.1),
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5), width: 1,
offset: const Offset(0, 10),
blurRadius: 20,
), ),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Quote icon // Quote icon
const Text( SvgPicture.asset(
'\u201C', 'assets/icons/quote_icon.svg',
style: TextStyle( width: 23,
fontFamily: 'Fractul', height: 13,
fontSize: 36,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
height: 0.8,
), ),
), const SizedBox(height: 12),
const SizedBox(height: 6), // Title snippet (bold)
// Title snippet
Text( Text(
text.length > 35 text.length > 35 ? '${text.substring(0, 35)} ..' : text,
? '${text.substring(0, 35)} ..'
: text,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w500,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 10), const SizedBox(height: 12),
// Full text // Full text
Expanded( Expanded(
child: Text( child: Text(
@@ -1257,105 +1272,63 @@ class _TestimonialCard extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark, color: AppColors.primaryDark,
height: 1.5, height: 1.4,
), ),
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
// 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( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: List.generate( children: List.generate(
5, 5,
(i) => Icon( (i) => Padding(
padding: const EdgeInsets.only(right: 2),
child: Icon(
i < rating ? Icons.star : Icons.star_border, i < rating ? Icons.star : Icons.star_border,
size: 16, size: 18,
color: i < rating color: i < rating
? AppColors.accentOrange ? const Color(0xFFFFDE21)
: AppColors.primaryDark.withValues(alpha: 0.3), : AppColors.primaryDark.withValues(alpha: 0.3),
), ),
), ),
), ),
const SizedBox(width: 10), ),
// Author const SizedBox(height: 8),
Expanded( // Author name
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
authorName, authorName,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', 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, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppColors.primaryDark, 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;
}

View File

@@ -2,7 +2,7 @@ name: real_estate_mobile
description: "RE-Quest Real Estate Mobile App" description: "RE-Quest Real Estate Mobile App"
publish_to: 'none' publish_to: 'none'
version: 1.0.0+2 version: 1.0.0+3
environment: environment:
sdk: ^3.10.1 sdk: ^3.10.1