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) {
// 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<String>? 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<String, dynamic> 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;
}

View File

@@ -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