feat: Fetch real agent data for featured professionals and update associated icons and image placeholders.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user