feat: Fetch real agent data for featured professionals and update associated icons and image placeholders.

This commit is contained in:
pradeepkumar
2026-03-14 21:30:37 +05:30
parent ddcc04a446
commit 751388ed00
9 changed files with 124 additions and 181 deletions

View File

@@ -72,7 +72,8 @@ class TopProfessionalsNotifier extends StateNotifier<TopProfessionalsState> {
final name = type.name.toLowerCase();
if (name.contains('lender') || name.contains('lending')) {
lenderTypeId = type.id;
} else if (name.contains('agent') ||
} else if (name.contains('professional') ||
name.contains('agent') ||
name.contains('realtor') ||
name.contains('broker')) {
agentTypeId = type.id;

View File

@@ -1045,7 +1045,7 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
),
const SizedBox(height: 16),
SizedBox(
height: 320,
height: 380,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.testimonials.length,
@@ -1130,7 +1130,7 @@ class _ExpandableChipsSectionState extends State<_ExpandableChipsSection> {
),
),
),
if (hasMore)
if (hasMore || _expanded)
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: IntrinsicWidth(
@@ -1199,7 +1199,7 @@ class _ExpandableCertificationsSection extends StatefulWidget {
const _ExpandableCertificationsSection({
required this.certs,
this.initialCount = 3,
this.initialCount = 2,
});
@override
@@ -1278,7 +1278,7 @@ class _ExpandableCertificationsSectionState
),
),
),
if (hasMore)
if (hasMore || _expanded)
Center(
child: GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
@@ -1444,7 +1444,7 @@ class _SpecializationCardWidgetState extends State<_SpecializationCardWidget> {
}
}
// ── Testimonial Card ──
// ── Testimonial Card (matches Figma & web TestimonialCard) ──
class _TestimonialCard extends StatelessWidget {
final Map<String, dynamic> data;
@@ -1458,165 +1458,108 @@ 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).withAlpha(40)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4);
final path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..close();
canvas.drawPath(path, shadowPaint);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// ── Connect Modal Content (matches web ConnectRequestModal) ──
class _ConnectModalContent extends StatefulWidget {
@@ -1683,7 +1626,9 @@ class _ConnectModalContentState extends State<_ConnectModalContent> {
24,
24 + MediaQuery.of(context).padding.bottom,
),
child: _success ? _buildSuccessContent() : _buildFormContent(),
child: SingleChildScrollView(
child: _success ? _buildSuccessContent() : _buildFormContent(),
),
),
);
}

View File

@@ -7,13 +7,6 @@ import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart';
/// Local fallback images for professionals.
const _fallbackImages = [
'assets/images/professional-1.jpg',
'assets/images/professional-2.jpg',
'assets/images/professional-3.jpg',
];
/// Featured professionals carousel shown above the agents/lenders tab section.
/// Now fetches REAL agent data from GET /agents API (like web does)
/// instead of using CMS/hardcoded fallback data.
@@ -148,7 +141,7 @@ class _FeaturedProfessionalsSectionState
left: index == 0 ? 0 : 8,
right: index == professionals.length - 1 ? 0 : 8,
),
child: Container(),
child: _buildFeaturedCard(professionals[index], index),
),
);
},
@@ -348,18 +341,16 @@ class _FeaturedProfessionalsSectionState
}
Widget _buildImagePlaceholder(int index) {
final assetPath = _fallbackImages[index % _fallbackImages.length];
return Image.asset(
assetPath,
const colors = [
Color(0xFFC4D9D4),
Color(0xFFD4C9B8),
Color(0xFFB8C4D4),
];
return Container(
width: double.infinity,
height: 192,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
width: double.infinity,
height: 192,
color: AppColors.gradientStart,
child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark),
),
color: colors[index % colors.length],
child: const Icon(Icons.person, size: 80, color: AppColors.primaryDark),
);
}

View File

@@ -39,11 +39,18 @@ const _defaultFeatures = FeaturesContent(
);
/// Map of feature icon keys to local SVG asset paths and Material fallback icons.
/// Supports both default slug keys and CMS web-style icon paths.
const _iconMap = {
// Default slug-based keys
'hire_quickly': ('assets/icons/hire_quickly_icon.svg', Icons.bolt),
'verified': ('assets/icons/verified_icon.svg', Icons.verified_user_outlined),
'top_rated': ('assets/icons/top_rated_icon.svg', Icons.star_outline),
'trusted': ('assets/icons/trusted_icon.svg', Icons.people_outline),
// CMS web-path keys (from CMS iconPath field)
'/assets/icons/hourglass-icon.svg': ('assets/icons/hire_quickly_icon.svg', Icons.bolt),
'/assets/icons/verified-badge-icon.svg': ('assets/icons/verified_icon.svg', Icons.verified_user_outlined),
'/assets/icons/star-orange-icon.svg': ('assets/icons/top_rated_icon.svg', Icons.star_outline),
'/assets/icons/trusted-people-icon.svg': ('assets/icons/trusted_icon.svg', Icons.people_outline),
};
class FeaturesSection extends ConsumerWidget {