import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/widgets/s3_image.dart'; // ── Data models (matching web CMS types) ── class _AboutHero { final String badge; final String headline; final String description; final String ctaButtonText; final String bannerImageUrl; final String bannerOverlayText; const _AboutHero({ this.badge = 'Our Mission', this.headline = 'Connecting You with Trusted Agents.', this.description = 'We believe finding the right home shouldn\u2019t be complicated. Our platform bridges the gap between buyers, sellers, and verified real estate agents for a smooth and transparent experience.', this.ctaButtonText = 'Find Your Agent', this.bannerImageUrl = '', this.bannerOverlayText = 'Building the future of property.', }); factory _AboutHero.fromJson(Map json) { return _AboutHero( badge: json['badge'] as String? ?? 'Our Mission', headline: json['headline'] as String? ?? 'Connecting You with Trusted Agents.', description: json['description'] as String? ?? 'We believe finding the right home shouldn\u2019t be complicated. Our platform bridges the gap between buyers, sellers, and verified real estate agents for a smooth and transparent experience.', ctaButtonText: json['ctaButtonText'] as String? ?? 'Find Your Agent', bannerImageUrl: json['bannerImageUrl'] as String? ?? '', bannerOverlayText: json['bannerOverlayText'] as String? ?? 'Building the future of property.', ); } } class _AboutStatItem { final String value; final String label; const _AboutStatItem({required this.value, required this.label}); factory _AboutStatItem.fromJson(Map json) { return _AboutStatItem( value: json['value'] as String? ?? '', label: json['label'] as String? ?? '', ); } } class _AboutFeatureItem { final String iconPath; final String title; final String description; const _AboutFeatureItem({ required this.iconPath, required this.title, required this.description, }); factory _AboutFeatureItem.fromJson(Map json) { return _AboutFeatureItem( iconPath: json['iconPath'] as String? ?? '', title: json['title'] as String? ?? '', description: json['description'] as String? ?? '', ); } } class _AboutTeamMember { final String name; final String role; final String imageUrl; const _AboutTeamMember({ required this.name, required this.role, required this.imageUrl, }); factory _AboutTeamMember.fromJson(Map json) { return _AboutTeamMember( name: json['name'] as String? ?? '', role: json['role'] as String? ?? '', imageUrl: json['imageUrl'] as String? ?? '', ); } } class _AboutCta { final String title; final String description; final String buttonText; const _AboutCta({ this.title = 'Ready to find a agent', this.description = 'Search and connect with verified real estate agents who match your needs, location, and goals.', this.buttonText = 'Find Agents Now', }); factory _AboutCta.fromJson(Map json) { return _AboutCta( title: json['title'] as String? ?? 'Ready to find a agent', description: json['description'] as String? ?? 'Search and connect with verified real estate agents who match your needs, location, and goals.', buttonText: json['buttonText'] as String? ?? 'Find Agents Now', ); } } // ── Defaults ── const _defaultStats = [ _AboutStatItem(value: '15k+', label: 'Verified Agents'), _AboutStatItem(value: '98%', label: 'Customer Satisfaction'), ]; const _defaultFeatures = [ _AboutFeatureItem( iconPath: '', title: 'Verified Agents', description: 'All agents on our platform are carefully verified to ensure trust, proven expertise, and consistent service, helping users connect with reliable professionals and enjoy a smooth, confident, and transparent property journey', ), _AboutFeatureItem( iconPath: '', title: 'Total Transparency', description: 'We maintain complete transparency by clearly displaying agent details, reviews, and activity, allowing users to make informed decisions with confidence, clarity, and trust throughout their entire property journey.', ), _AboutFeatureItem( iconPath: '', title: 'Simple Journey', description: 'Our platform simplifies every step of the property process, from discovering the right agent to closing with confidence, making the experience smooth, fast, and stress-free for users at every stage.', ), ]; const _defaultTeamMembers = [ _AboutTeamMember(name: 'Andrew', role: 'Founder & CEO', imageUrl: 'asset://professional-1'), _AboutTeamMember(name: 'Thomas', role: 'Co-Founder', imageUrl: 'asset://professional-2'), _AboutTeamMember(name: 'Darren', role: 'Advisor', imageUrl: 'asset://professional-3'), ]; // ── State ── class _AboutState { final bool isLoading; final _AboutHero hero; final List<_AboutStatItem> stats; final String featuresBadge; final List<_AboutFeatureItem> features; final String teamTitle; final String teamSubtitle; final List<_AboutTeamMember> teamMembers; final _AboutCta cta; final int activeTeamIndex; const _AboutState({ this.isLoading = true, this.hero = const _AboutHero(), this.stats = _defaultStats, this.featuresBadge = 'Why Choose Us', this.features = _defaultFeatures, this.teamTitle = 'Meet the minds behind the platform', this.teamSubtitle = 'We are a team of passionate innovators, real estate experts, and designers working together to redefine how people connect with trusted agents.', this.teamMembers = _defaultTeamMembers, this.cta = const _AboutCta(), this.activeTeamIndex = 0, }); _AboutState copyWith({ bool? isLoading, _AboutHero? hero, List<_AboutStatItem>? stats, String? featuresBadge, List<_AboutFeatureItem>? features, String? teamTitle, String? teamSubtitle, List<_AboutTeamMember>? teamMembers, _AboutCta? cta, int? activeTeamIndex, }) { return _AboutState( isLoading: isLoading ?? this.isLoading, hero: hero ?? this.hero, stats: stats ?? this.stats, featuresBadge: featuresBadge ?? this.featuresBadge, features: features ?? this.features, teamTitle: teamTitle ?? this.teamTitle, teamSubtitle: teamSubtitle ?? this.teamSubtitle, teamMembers: teamMembers ?? this.teamMembers, cta: cta ?? this.cta, activeTeamIndex: activeTeamIndex ?? this.activeTeamIndex, ); } } // ── Provider ── class _AboutNotifier extends StateNotifier<_AboutState> { _AboutNotifier() : super(const _AboutState()) { _load(); } Future _load() async { try { final response = await ApiClient.instance.dio.get('/cms/page/about'); final data = response.data['data'] as List?; if (data != null) { for (final record in data) { final map = record as Map; final sectionKey = map['sectionKey'] as String?; final content = map['content'] as Map?; if (content == null) continue; switch (sectionKey) { case 'hero': if (content['headline'] != null || content['description'] != null) { state = state.copyWith(hero: _AboutHero.fromJson(content)); } case 'stats': final statsList = content['stats'] as List?; if (statsList != null && statsList.isNotEmpty) { state = state.copyWith( stats: statsList .map((e) => _AboutStatItem.fromJson(e as Map)) .toList(), ); } case 'features': final badge = content['badge'] as String?; final featuresList = content['features'] as List?; if (badge != null) { state = state.copyWith(featuresBadge: badge); } if (featuresList != null && featuresList.isNotEmpty) { state = state.copyWith( features: featuresList .map((e) => _AboutFeatureItem.fromJson(e as Map)) .toList(), ); } case 'team': final title = content['title'] as String?; final subtitle = content['subtitle'] as String?; final membersList = content['members'] as List?; if (title != null) state = state.copyWith(teamTitle: title); if (subtitle != null) state = state.copyWith(teamSubtitle: subtitle); if (membersList != null && membersList.isNotEmpty) { state = state.copyWith( teamMembers: membersList .map((e) => _AboutTeamMember.fromJson(e as Map)) .toList(), ); } case 'cta': if (content['title'] != null || content['description'] != null) { state = state.copyWith(cta: _AboutCta.fromJson(content)); } } } } } catch (_) { // Fall through to defaults } state = state.copyWith(isLoading: false); } void setActiveTeamIndex(int index) { state = state.copyWith(activeTeamIndex: index); } } final _aboutProvider = StateNotifierProvider.autoDispose<_AboutNotifier, _AboutState>((ref) { return _AboutNotifier(); }); // ── Feature icon mapping (fallback when CMS has no iconPath) ── const _featureIconFallback = { 'Verified Agents': Icons.verified, 'Total Transparency': Icons.lock_outline, 'Simple Journey': Icons.check_circle_outline, }; // ── Screen ── class AboutScreen extends ConsumerWidget { const AboutScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(_aboutProvider); if (state.isLoading) { return const Center( child: CircularProgressIndicator( color: AppColors.accentOrange, ), ); } return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Column( children: [ const SizedBox(height: 18), _buildHeroTitle(state.hero.headline), const SizedBox(height: 16), _buildHeroSubtitle(state.hero.description), const SizedBox(height: 24), _buildBannerImage(state.hero.bannerImageUrl), const SizedBox(height: 10), _buildBannerCaption(state.hero.bannerOverlayText), const SizedBox(height: 16), _buildStatsRow(state.stats), const SizedBox(height: 32), _buildSectionTitle(state.featuresBadge), const SizedBox(height: 12), _buildWhyChooseUsSubtitle(), const SizedBox(height: 24), ...state.features.map((f) => Padding( padding: const EdgeInsets.only(bottom: 15), child: _buildFeatureCard(f), )), const SizedBox(height: 25), _buildTeamSection(context, ref, state), const SizedBox(height: 40), _buildCtaSection(context, state.cta), const SizedBox(height: 30), ], ), ); } // -- Hero Title -- Widget _buildHeroTitle(String text) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 25, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ); } // -- Hero Subtitle -- Widget _buildHeroSubtitle(String text) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ); } // -- Banner Image (from CMS or fallback to local asset) -- Widget _buildBannerImage(String imageUrl) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 57), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: SizedBox( height: 295, width: double.infinity, child: imageUrl.isNotEmpty ? S3Image( imageUrl: imageUrl, height: 295, fit: BoxFit.cover, errorWidget: (context) => Image.asset( 'assets/images/about_cityscape.png', height: 295, width: double.infinity, fit: BoxFit.cover, ), ) : Image.asset( 'assets/images/about_cityscape.png', height: 295, width: double.infinity, fit: BoxFit.cover, ), ), ), ); } // -- Banner Caption -- Widget _buildBannerCaption(String text) { return Center( child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ); } // -- Stats Row (dynamic) -- Widget _buildStatsRow(List<_AboutStatItem> stats) { if (stats.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: stats.map((s) => _buildStatItem(s.value, s.label)).toList(), ), ); } Widget _buildStatItem(String value, String label) { return Column( children: [ Text( value, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.accentOrange, ), ), const SizedBox(height: 2), Text( label, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ], ); } // -- Section Title -- Widget _buildSectionTitle(String text) { return Center( child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 25, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ); } // -- Why Choose Us Subtitle -- Widget _buildWhyChooseUsSubtitle() { return const Padding( padding: EdgeInsets.symmetric(horizontal: 65), child: Text( 'Our core principles guide every interaction, ensuring you feel confident and supported at every step.', textAlign: TextAlign.center, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ); } // -- Feature Card (dynamic with S3 icon support) -- Widget _buildFeatureCard(_AboutFeatureItem feature) { final fallbackIcon = _featureIconFallback[feature.title] ?? Icons.star_outline; return Padding( padding: const EdgeInsets.symmetric(horizontal: 57), child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 0.1, ), ), child: Column( children: [ if (feature.iconPath.isNotEmpty) SizedBox( width: 36, height: 36, child: S3Image( imageUrl: feature.iconPath, width: 36, height: 36, fit: BoxFit.contain, errorWidget: (_) => Icon( fallbackIcon, size: 36, color: AppColors.accentOrange, ), ), ) else Icon( fallbackIcon, size: 36, color: AppColors.accentOrange, ), const SizedBox(height: 10), Text( feature.title, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), Text( feature.description, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, height: 1.5, ), ), ], ), ), ); } // -- Team Section (dynamic with carousel) -- Widget _buildTeamSection( BuildContext context, WidgetRef ref, _AboutState state, ) { final members = state.teamMembers; if (members.isEmpty) return const SizedBox.shrink(); final activeIndex = state.activeTeamIndex.clamp(0, members.length - 1); final screenWidth = MediaQuery.of(context).size.width; final cardWidth = screenWidth - (69 * 2); return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 57), child: Text( state.teamTitle, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 25, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 57), child: Text( state.teamSubtitle, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 40), // Team carousel SizedBox( height: 330, child: PageView.builder( itemCount: members.length, controller: PageController( viewportFraction: cardWidth / screenWidth, ), onPageChanged: (index) { ref.read(_aboutProvider.notifier).setActiveTeamIndex(index); }, itemBuilder: (context, index) { final member = members[index]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 0.1, ), ), child: Column( children: [ const SizedBox(height: 19), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: SizedBox( height: 211, width: double.infinity, child: _buildTeamMemberImage(member.imageUrl), ), ), ), const SizedBox(height: 16), Text( member.name, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: Colors.black, ), ), const SizedBox(height: 4), Text( member.role, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w300, color: Colors.black, ), ), ], ), ), ); }, ), ), const SizedBox(height: 16), // Page indicator if (members.length > 1) Padding( padding: const EdgeInsets.symmetric(horizontal: 58), child: LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; final segmentWidth = totalWidth / members.length; return Stack( children: [ Container( height: 5, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 0.1, ), ), ), AnimatedPositioned( duration: const Duration(milliseconds: 200), left: segmentWidth * activeIndex, child: Container( height: 5, width: segmentWidth, decoration: BoxDecoration( color: AppColors.primaryDark, borderRadius: BorderRadius.circular(15), ), ), ), ], ); }, ), ), ], ); } // -- Team member image (handles asset://, S3 keys, and URLs) -- Widget _buildTeamMemberImage(String imageUrl) { const placeholder = _TeamMemberPlaceholder(); if (imageUrl.isEmpty) return placeholder; // Local asset images (defaults) if (imageUrl.startsWith('asset://')) { final name = imageUrl.replaceFirst('asset://', ''); return Image.asset( 'assets/images/$name.jpg', height: 211, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => placeholder, ); } // S3 key or URL from CMS return S3Image( imageUrl: imageUrl, height: 211, fit: BoxFit.cover, placeholder: (_) => placeholder, errorWidget: (_) => placeholder, ); } // -- CTA Section (dynamic) -- Widget _buildCtaSection(BuildContext context, _AboutCta cta) { return Column( children: [ Text( cta.title, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Fractul', fontSize: 25, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 83), child: Text( cta.description, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 24), // Illustration Padding( padding: const EdgeInsets.symmetric(horizontal: 34), child: ClipRRect( borderRadius: BorderRadius.circular(56), child: Image.asset( 'assets/images/about_find_agent.jpg', height: 241, width: double.infinity, fit: BoxFit.cover, ), ), ), const SizedBox(height: 24), // Find Agents Now button GestureDetector( onTap: () => context.push('/agents/search'), child: Container( height: 50, width: 210, decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(25), ), alignment: Alignment.center, child: Text( cta.buttonText, style: const TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), ), const SizedBox(height: 12), // Get Help button GestureDetector( onTap: () => context.push('/contact'), child: Container( height: 50, width: 210, decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), border: Border.all( color: AppColors.primaryDark, width: 1, ), ), alignment: Alignment.center, child: const Text( 'Get Help', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ), ), ], ); } } class _TeamMemberPlaceholder extends StatelessWidget { const _TeamMemberPlaceholder(); @override Widget build(BuildContext context) { return Container( color: const Color(0xFFC4D9D4), child: const Center( child: Icon(Icons.person, size: 60, color: AppColors.primaryDark), ), ); } }