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:
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user