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

241 lines
7.4 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:real_estate_mobile/core/constants/app_colors.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 content — matches web's hardcoded defaultContent fallback.
const _defaultContent = TestimonialsContent(
title: "Our Clients' Trust Is Our Foundation",
subtitle:
'We help buyers, sellers, and investors close successful real estate deals with confidence in every transaction.',
ratingInfo:
'Clients rate our real estate services 4.9 out of 5 on average, based on recent client reviews.',
stats: [
StatItem(
iconPath: 'cities',
boldText: '25+ Cities &',
normalText: 'neighborhoods served',
),
StatItem(
iconPath: 'properties',
boldText: '1,000+ Properties',
normalText: 'bought and sold for our clients.',
),
],
testimonials: [],
);
/// Map of stat icon keys to local SVG asset paths and Material fallback icons.
const _statIconMap = {
'cities': ('assets/icons/cities_icon.svg', Icons.location_city),
'properties': ('assets/icons/properties_icon.svg', Icons.home_work),
};
class TrustStatsSection extends ConsumerWidget {
const TrustStatsSection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final homeState = ref.watch(homeProvider);
final cmsTestimonials = homeState.content?.testimonials;
// Use CMS data if available, otherwise fallback (same pattern as web)
final data = cmsTestimonials ?? _defaultContent;
final title = data.title.isNotEmpty ? data.title : _defaultContent.title;
final subtitle =
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
final ratingInfo = data.ratingInfo.isNotEmpty
? data.ratingInfo
: _defaultContent.ratingInfo;
final stats =
data.stats.isNotEmpty ? data.stats : _defaultContent.stats;
// Extract rating number from ratingInfo string (e.g. "4.9" from "...4.9 out of 5...")
final ratingValue = _extractRating(ratingInfo);
return 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: 16),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Text(
ratingInfo,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
const Divider(color: AppColors.divider),
const SizedBox(height: 16),
// Stats row
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Dynamic stats from CMS
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: stats
.map((stat) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildStatItem(stat),
))
.toList(),
),
),
// Rating visual
const SizedBox(width: 16),
_buildRatingVisual(ratingValue),
],
),
],
),
);
}
Widget _buildStatItem(StatItem stat) {
final iconEntry = _statIconMap[stat.iconPath];
final svgPath = iconEntry?.$1;
final fallbackIcon = iconEntry?.$2 ?? Icons.info_outline;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (svgPath != null)
Padding(
padding: const EdgeInsets.only(right: 8, top: 2),
child: SvgPicture.asset(
svgPath,
width: 27,
height: 27,
placeholderBuilder: (_) => Icon(
fallbackIcon,
size: 27,
color: AppColors.accentOrange,
),
),
)
else
Padding(
padding: const EdgeInsets.only(right: 8, top: 2),
child: Icon(
fallbackIcon,
size: 27,
color: AppColors.accentOrange,
),
),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${stat.boldText}\n',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
TextSpan(
text: stat.normalText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
),
],
);
}
Widget _buildRatingVisual(double rating) {
final fullStars = rating.floor();
final hasHalfStar = (rating - fullStars) >= 0.3;
final totalStars = 5;
return Column(
children: [
Text(
rating.toStringAsFixed(1),
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 32,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
Row(
children: List.generate(
totalStars,
(index) {
if (index < fullStars) {
return const Icon(
Icons.star,
color: AppColors.accentOrange,
size: 18,
);
} else if (index == fullStars && hasHalfStar) {
return const Icon(
Icons.star_half,
color: AppColors.accentOrange,
size: 18,
);
} else {
return const Icon(
Icons.star_border,
color: AppColors.accentOrange,
size: 18,
);
}
},
),
),
],
);
}
/// Extract numeric rating from ratingInfo string.
/// e.g. "Clients rate our real estate services 4.9 out of 5..." → 4.9
double _extractRating(String ratingInfo) {
final match = RegExp(r'(\d+\.?\d*)\s*out\s*of\s*\d+').firstMatch(ratingInfo);
if (match != null) {
return double.tryParse(match.group(1)!) ?? 4.9;
}
return 4.9;
}
}