2026-02-24 03:19:55 +05:30
import ' package:flutter/material.dart ' ;
2026-02-25 07:48:02 +05:30
import ' package:flutter_riverpod/flutter_riverpod.dart ' ;
2026-02-24 03:19:55 +05:30
import ' package:flutter_svg/flutter_svg.dart ' ;
2026-04-24 12:50:49 +05:30
import ' package:go_router/go_router.dart ' ;
2026-02-24 03:19:55 +05:30
import ' package:real_estate_mobile/core/constants/app_colors.dart ' ;
2026-03-23 11:26:11 +05:30
import ' package:real_estate_mobile/core/network/api_client.dart ' ;
import ' package:real_estate_mobile/core/utils/image_url_resolver.dart ' ;
2026-02-25 07:48:02 +05:30
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 ' ;
2026-02-24 03:19:55 +05:30
2026-02-25 07:48:02 +05:30
/// 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.
2026-03-14 21:30:37 +05:30
/// Supports both default slug keys and CMS web-style icon paths.
2026-02-25 07:48:02 +05:30
const _iconMap = {
2026-03-14 21:30:37 +05:30
// Default slug-based keys
2026-02-25 07:48:02 +05:30
' 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 ) ,
2026-03-14 21:30:37 +05:30
// 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 ) ,
2026-02-25 07:48:02 +05:30
} ;
2026-03-14 23:33:32 +05:30
/// 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 {
2026-04-24 12:50:49 +05:30
final String id ;
2026-03-14 23:33:32 +05:30
final String name ;
final String role ;
final String rating ;
final String location ;
final String experience ;
final String image ;
2026-03-23 11:26:11 +05:30
final bool isVerified ;
2026-03-14 23:33:32 +05:30
const _StaticAgent ( {
2026-04-24 12:50:49 +05:30
this . id = ' ' ,
2026-03-14 23:33:32 +05:30
required this . name ,
required this . role ,
required this . rating ,
required this . location ,
required this . experience ,
required this . image ,
2026-03-23 11:26:11 +05:30
this . isVerified = true ,
2026-03-14 23:33:32 +05:30
} ) ;
}
class FeaturesSection extends ConsumerStatefulWidget {
2026-02-24 03:19:55 +05:30
const FeaturesSection ( { super . key } ) ;
@ override
2026-03-14 23:33:32 +05:30
ConsumerState < FeaturesSection > createState ( ) = > _FeaturesSectionState ( ) ;
}
class _FeaturesSectionState extends ConsumerState < FeaturesSection > {
int _currentPage = 0 ;
late ScrollController _agentScrollController ;
2026-03-23 11:26:11 +05:30
List < _StaticAgent > _agents = List . from ( _staticAgents ) ;
bool _agentsLoaded = false ;
2026-03-14 23:33:32 +05:30
@ override
void initState ( ) {
super . initState ( ) ;
_agentScrollController = ScrollController ( ) ;
_agentScrollController . addListener ( _onAgentScroll ) ;
2026-03-23 11:26:11 +05:30
_fetchFeaturedAgents ( ) ;
2026-03-14 23:33:32 +05:30
}
@ override
void dispose ( ) {
_agentScrollController . removeListener ( _onAgentScroll ) ;
_agentScrollController . dispose ( ) ;
super . dispose ( ) ;
}
2026-03-23 11:26:11 +05:30
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 (
2026-04-24 12:50:49 +05:30
id: agent [ ' id ' ] as String ? ? ? ' ' ,
2026-03-23 11:26:11 +05:30
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
}
}
2026-03-14 23:33:32 +05:30
void _onAgentScroll ( ) {
if ( ! _agentScrollController . hasClients ) return ;
2026-03-23 11:26:11 +05:30
const cardWidth = 254.0 + 16.0 ;
2026-03-14 23:33:32 +05:30
final page = ( _agentScrollController . offset / cardWidth ) . round ( ) ;
2026-03-23 11:26:11 +05:30
if ( page ! = _currentPage & & page > = 0 & & page < _agents . length ) {
2026-03-14 23:33:32 +05:30
setState ( ( ) = > _currentPage = page ) ;
}
}
@ override
Widget build ( BuildContext context ) {
final ref = this . ref ;
2026-02-25 07:48:02 +05:30
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 ;
2026-03-14 23:33:32 +05:30
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 ) ,
) ) ,
] ,
2026-02-24 03:19:55 +05:30
) ,
2026-03-14 23:33:32 +05:30
) ,
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 ) ,
2026-03-23 11:26:11 +05:30
itemCount: _agents . length ,
2026-03-14 23:33:32 +05:30
itemBuilder: ( context , index ) = > Padding (
padding: const EdgeInsets . only ( right: 16 ) ,
2026-03-23 11:26:11 +05:30
child: _buildStaticAgentCard ( _agents [ index ] ) ,
2026-02-24 03:19:55 +05:30
) ,
) ,
2026-03-14 23:33:32 +05:30
) ,
const SizedBox ( height: 16 ) ,
// Dot indicators
Row (
mainAxisAlignment: MainAxisAlignment . center ,
2026-03-23 11:26:11 +05:30
children: List . generate ( _agents . length , ( index ) {
2026-03-14 23:33:32 +05:30
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 ) ,
) ,
) ;
} ) ,
) ,
] ,
2026-02-24 03:19:55 +05:30
) ;
}
2026-02-25 07:48:02 +05:30
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 ;
2026-02-24 03:19:55 +05:30
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 (
2026-02-25 07:48:02 +05:30
svgPath ,
2026-02-24 03:19:55 +05:30
width: 35 ,
height: 35 ,
placeholderBuilder: ( _ ) = > Icon (
fallbackIcon ,
size: 35 ,
color: AppColors . accentOrange ,
) ,
) ,
const SizedBox ( width: 12 ) ,
2026-02-25 07:48:02 +05:30
Expanded (
child: Text (
feature . title ,
style: const TextStyle (
fontFamily: ' Fractul ' ,
fontSize: 20 ,
fontWeight: FontWeight . w600 ,
color: AppColors . primaryDark ,
) ,
2026-02-24 03:19:55 +05:30
) ,
) ,
] ,
) ,
const SizedBox ( height: 12 ) ,
Text (
2026-02-25 07:48:02 +05:30
feature . description ,
2026-02-24 03:19:55 +05:30
style: const TextStyle (
fontFamily: ' SourceSerif4 ' ,
fontSize: 14 ,
fontWeight: FontWeight . w400 ,
color: AppColors . primaryDark ,
) ,
) ,
] ,
) ,
) ;
}
2026-03-14 23:33:32 +05:30
Widget _buildStaticAgentCard ( _StaticAgent agent ) {
2026-04-24 12:50:49 +05:30
final card = Container (
2026-03-14 23:33:32 +05:30
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 ) ) ,
2026-03-23 11:26:11 +05:30
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 ) ,
) ,
) ,
2026-03-14 23:33:32 +05:30
) ,
// 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 (
children: [
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 ) ,
Text (
agent . location ,
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 ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
) ;
2026-04-24 12:50:49 +05:30
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 ;
2026-03-14 23:33:32 +05:30
}
2026-02-24 03:19:55 +05:30
}