Files
mobile-app/lib/features/home/presentation/widgets/features_section.dart

559 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/network/api_client.dart';
import 'package:real_estate_mobile/core/utils/image_url_resolver.dart';
import 'package:real_estate_mobile/features/home/data/models/landing_page_content.dart';
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
/// Default features content — matches web's hardcoded defaultContent fallback.
const _defaultFeatures = FeaturesContent(
title: 'Find Trusted Real Estate Professionals On Demand',
subtitle:
'Quickly connect with the right agents exactly when you need them.',
features: [
FeatureItem(
iconPath: 'hire_quickly',
title: 'Hire Quickly',
description:
'Connect with experienced local real estate agents who match your needs and preferences. Our simple process helps you get started quickly and move forward without delays or unnecessary complexity.',
),
FeatureItem(
iconPath: 'verified',
title: 'Verified Agents',
description:
'All agents are carefully identity-verified and background-checked to ensure trust and safety. You can confidently work with professionals who meet quality and reliability standards.',
),
FeatureItem(
iconPath: 'top_rated',
title: 'Top Rated',
description:
'Work with highly rated agents known for strong expertise and positive client feedback. Their consistent performance helps you make informed and confident property decisions.',
),
FeatureItem(
iconPath: 'trusted',
title: 'Trusted by Thousands',
description:
'Thousands of buyers, sellers, and renters trust our platform to find reliable agents. Our professionals help people navigate property journeys with confidence and ease.',
),
],
);
/// 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),
};
/// Static agent cards — matches web's hardcoded agents in FeaturesSection.
const _staticAgents = [
_StaticAgent(
name: 'Anderson',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-anderson.jpg',
),
_StaticAgent(
name: 'Deepak',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-deepak.jpg',
),
_StaticAgent(
name: 'Daniel',
role: 'Rental & Investment Consultant',
rating: '4.9',
location: 'New York',
experience: '7+ years in the real estate industry.',
image: 'assets/images/agent-daniel.jpg',
),
];
class _StaticAgent {
final String id;
final String name;
final String role;
final String rating;
final String location;
final String experience;
final String image;
final bool isVerified;
const _StaticAgent({
this.id = '',
required this.name,
required this.role,
required this.rating,
required this.location,
required this.experience,
required this.image,
this.isVerified = true,
});
}
class FeaturesSection extends ConsumerStatefulWidget {
const FeaturesSection({super.key});
@override
ConsumerState<FeaturesSection> createState() => _FeaturesSectionState();
}
class _FeaturesSectionState extends ConsumerState<FeaturesSection> {
int _currentPage = 0;
late ScrollController _agentScrollController;
List<_StaticAgent> _agents = List.from(_staticAgents);
bool _agentsLoaded = false;
@override
void initState() {
super.initState();
_agentScrollController = ScrollController();
_agentScrollController.addListener(_onAgentScroll);
_fetchFeaturedAgents();
}
@override
void dispose() {
_agentScrollController.removeListener(_onAgentScroll);
_agentScrollController.dispose();
super.dispose();
}
Future<void> _fetchFeaturedAgents() async {
try {
final dio = ApiClient.instance.dio;
final response = await dio.get('/cms/page/landing');
final sections = response.data['data'] as List<dynamic>?;
if (sections == null) return;
for (final section in sections) {
if (section['sectionKey'] == 'features') {
final content = section['content'] as Map<String, dynamic>?;
final featuredAgents = content?['featuredAgents'] as List<dynamic>?;
if (featuredAgents == null || featuredAgents.isEmpty) return;
final List<_StaticAgent> fetched = [];
for (final agent in featuredAgents) {
String image = agent['imageUrl'] as String? ?? '';
if (image.isNotEmpty && !image.startsWith('http') && !image.startsWith('/')) {
try {
image = await ImageUrlResolver.instance.resolve(image) ?? '';
} catch (_) {}
}
fetched.add(_StaticAgent(
id: agent['id'] as String? ?? '',
name: agent['name'] as String? ?? '',
role: agent['role'] as String? ?? '',
rating: agent['rating'] as String? ?? '',
location: agent['location'] as String? ?? '',
experience: agent['experience'] as String? ?? '',
image: image,
));
}
if (fetched.isNotEmpty && mounted) {
setState(() {
_agents = fetched;
_agentsLoaded = true;
});
}
break;
}
}
} catch (_) {
// Keep fallback static agents
}
}
void _onAgentScroll() {
if (!_agentScrollController.hasClients) return;
const cardWidth = 254.0 + 16.0;
final page = (_agentScrollController.offset / cardWidth).round();
if (page != _currentPage && page >= 0 && page < _agents.length) {
setState(() => _currentPage = page);
}
}
@override
Widget build(BuildContext context) {
final ref = this.ref;
final homeState = ref.watch(homeProvider);
final cmsFeatures = homeState.content?.features;
// Use CMS data if available with non-empty features, otherwise fallback
final data = (cmsFeatures != null && cmsFeatures.features.isNotEmpty)
? cmsFeatures
: _defaultFeatures;
final title = data.title.isNotEmpty ? data.title : _defaultFeatures.title;
final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultFeatures.subtitle;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 24),
...data.features.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildFeatureCard(feature),
)),
],
),
),
const SizedBox(height: 24),
// Static agent cards carousel (same as web)
SizedBox(
height: 370,
child: ListView.builder(
controller: _agentScrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 24),
itemCount: _agents.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 16),
child: _buildStaticAgentCard(_agents[index]),
),
),
),
const SizedBox(height: 16),
// Dot indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_agents.length, (index) {
final isActive = index == _currentPage;
return Container(
width: isActive ? 10 : 8,
height: isActive ? 10 : 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? AppColors.primaryDark
: AppColors.primaryDark.withValues(alpha: 0.3),
),
);
}),
),
],
);
}
Widget _buildFeatureCard(FeatureItem feature) {
// Resolve icon: try icon map first, then default
final iconEntry = _iconMap[feature.iconPath];
final svgPath = iconEntry?.$1 ?? 'assets/icons/hire_quickly_icon.svg';
final fallbackIcon = iconEntry?.$2 ?? Icons.bolt;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark,
width: 0.1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SvgPicture.asset(
svgPath,
width: 35,
height: 35,
placeholderBuilder: (_) => Icon(
fallbackIcon,
size: 35,
color: AppColors.accentOrange,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
feature.title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
),
],
),
const SizedBox(height: 12),
Text(
feature.description,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
);
}
Widget _buildStaticAgentCard(_StaticAgent agent) {
final card = Container(
width: 254,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x33D9D9D9),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image
ClipRRect(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(15)),
child: agent.image.startsWith('http')
? Image.network(
agent.image,
width: 254,
height: 161,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 254,
height: 161,
color: const Color(0xFFC4D9D4),
child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark),
),
)
: Image.asset(
(agent.image.isNotEmpty ? agent.image : 'assets/images/agent-anderson.jpg').replaceFirst(RegExp(r'^/'), ''),
width: 254,
height: 161,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 254,
height: 161,
color: const Color(0xFFC4D9D4),
child: const Icon(Icons.person, size: 60, color: AppColors.primaryDark),
),
),
),
// Content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name
Text(
agent.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 2),
// Role
Text(
agent.role,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
// Verified + Rating row
Wrap(
spacing: 12,
runSpacing: 4,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/icons/verified_badge_icon.svg',
width: 14,
height: 14,
placeholderBuilder: (_) => const Icon(
Icons.verified,
color: Color(0xFF638559),
size: 14,
),
),
const SizedBox(width: 4),
const Text(
'Verified Agent',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/icons/star_rating_icon.svg',
width: 14,
height: 14,
placeholderBuilder: (_) => const Icon(
Icons.star,
color: Color(0xFFFFDE21),
size: 14,
),
),
const SizedBox(width: 4),
Text(
'${agent.rating} Rating',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
],
),
const SizedBox(height: 8),
// Location
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: SvgPicture.asset(
'assets/icons/location_filled_icon.svg',
width: 15,
height: 15,
placeholderBuilder: (_) => const Icon(
Icons.location_on,
color: AppColors.accentOrange,
size: 15,
),
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
agent.location,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
),
],
),
const SizedBox(height: 8),
// Experience
RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'Experience: ',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
TextSpan(
text: agent.experience,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
],
),
),
],
),
);
if (agent.id.isNotEmpty) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(15),
onTap: () => context.push('/agents/detail/${agent.id}'),
child: card,
),
);
}
return card;
}
}