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,28 +923,37 @@ 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.isNotEmpty
subtitle: state.availabilityType, ? state.availabilityType
items: state.availabilitySchedule, : 'Not specified',
), items: state.availabilitySchedule.isNotEmpty
if (workEnv.isNotEmpty) ...[ ? state.availabilitySchedule
const SizedBox(height: 16), : null,
_buildInfoCard( isEmpty: state.availabilityType.isEmpty,
icon: Icons.landscape_outlined, ),
description: workEnv, const SizedBox(height: 16),
), // Work environment (no title — matches web)
], _buildInfoCard(
if (bestExp.isNotEmpty) ...[ icon: Icons.landscape_outlined,
const SizedBox(height: 16), title: '',
_buildInfoCard( subtitle: '',
icon: Icons.star_rounded, description: workEnv,
description: bestExp, ),
), 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({ 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,37 +1002,43 @@ 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(
padding: const EdgeInsets.symmetric(vertical: 1), (item) => Padding(
child: Text( padding: const EdgeInsets.symmetric(vertical: 1),
item, child: Text(
style: const TextStyle( item,
fontFamily: 'SourceSerif4', style: const TextStyle(
fontSize: 14, fontFamily: 'SourceSerif4',
color: AppColors.primaryDark, fontSize: 14,
), 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();
@@ -1156,8 +1186,9 @@ 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,163 +1231,104 @@ 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,
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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( Expanded(
child: Container( child: Text(
width: double.infinity, text,
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), style: const TextStyle(
decoration: BoxDecoration( fontFamily: 'SourceSerif4',
color: Colors.white, fontSize: 14,
borderRadius: BorderRadius.circular(15), fontWeight: FontWeight.w400,
boxShadow: [ color: AppColors.primaryDark,
BoxShadow( height: 1.4,
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5),
offset: const Offset(0, 10),
blurRadius: 20,
),
],
), ),
child: Column( overflow: TextOverflow.fade,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ ),
// Quote icon const SizedBox(height: 12),
const Text( // Divider
'\u201C', Divider(
style: TextStyle( color: AppColors.primaryDark.withValues(alpha: 0.08),
fontFamily: 'Fractul', height: 1,
fontSize: 36, ),
fontWeight: FontWeight.w700, const SizedBox(height: 12),
color: AppColors.primaryDark, // Star rating
height: 0.8, Row(
), mainAxisSize: MainAxisSize.min,
), children: List.generate(
const SizedBox(height: 6), 5,
// Title snippet (i) => Padding(
Text( padding: const EdgeInsets.only(right: 2),
text.length > 35 child: Icon(
? '${text.substring(0, 35)} ..' i < rating ? Icons.star : Icons.star_border,
: text, size: 18,
style: const TextStyle( color: i < rating
fontFamily: 'Fractul', ? const Color(0xFFFFDE21)
fontSize: 14, : AppColors.primaryDark.withValues(alpha: 0.3),
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 const SizedBox(height: 8),
Padding( // Author name
padding: const EdgeInsets.only(left: 30), Text(
child: CustomPaint( authorName,
size: const Size(16, 10), style: const TextStyle(
painter: _TrianglePainter(color: Colors.white), fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
), ),
), ),
const SizedBox(height: 6), // Author role
// Stars + Author info if (authorRole.isNotEmpty) ...[
Padding( const SizedBox(height: 2),
padding: const EdgeInsets.only(left: 4), Text(
child: Row( authorRole,
children: [ style: const TextStyle(
// Stars fontFamily: 'Fractul',
Row( fontSize: 14,
mainAxisSize: MainAxisSize.min, fontWeight: FontWeight.w500,
children: List.generate( color: AppColors.primaryDark,
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;
}

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