feat: Redesign home screen sections with updated testimonial card styles, refined trust stats layout, and spacing adjustments.
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M3 21V7L9 3L15 7V9H21V21H3ZM5 19H7V17H5V19ZM5 15H7V13H5V15ZM5 11H7V9H5V11ZM9 19H11V17H9V19ZM9 15H11V13H9V15ZM9 11H11V9H9V11ZM9 7H11V5.85L9 4.7V7ZM13 19H19V11H13V19ZM15 17V15H17V17H15ZM15 13V11H17V13H15Z" fill="currentColor"/>
|
<path d="M6.30556 9.09722L6.31944 9.08194M11.8611 9.09722L11.875 9.08194M6.30556 14.6528L6.31944 14.6375M11.8611 14.6528L11.875 14.6375M6.30556 20.2083L6.31944 20.1931M11.8611 20.2083L11.875 20.1931M17.4167 25.75H1.58333C1.36232 25.75 1.15036 25.6622 0.994078 25.5059C0.837797 25.3496 0.75 25.1377 0.75 24.9167V4.36111C0.75 4.1401 0.837797 3.92814 0.994078 3.77186C1.15036 3.61558 1.36232 3.52778 1.58333 3.52778H9.08333V1.58333C9.08333 1.36232 9.17113 1.15036 9.32741 0.994078C9.48369 0.837797 9.69565 0.75 9.91667 0.75H16.5833C16.8043 0.75 17.0163 0.837797 17.1726 0.994078C17.3289 1.15036 17.4167 1.36232 17.4167 1.58333V9.08333M17.4167 25.75H24.9167C25.1377 25.75 25.3496 25.6622 25.5059 25.5059C25.6622 25.3496 25.75 25.1377 25.75 24.9167V9.91667C25.75 9.69565 25.6622 9.48369 25.5059 9.32741C25.3496 9.17113 25.1377 9.08333 24.9167 9.08333H17.4167M17.4167 25.75V20.1944M17.4167 9.08333V14.6389M17.4167 20.1944V14.6389M17.4167 20.1944H20.1944M17.4167 14.6389H20.1944" stroke="#E58625" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,7 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10 20V14H14V20H19V12H22L12 3L2 12H5V20H10Z" fill="currentColor"/>
|
<path d="M24.46 6.47656L23.1484 23.4053H1.85156L0.540039 6.47656H24.46Z" stroke="#E58625"/>
|
||||||
|
<mask id="path-2-inside-1_49_6415" fill="white">
|
||||||
|
<path d="M6.94531 8.63233V0H18.0563V8.63233"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M6.94531 0V-1H5.94531V0H6.94531ZM18.0563 0H19.0563V-1H18.0563V0ZM6.94531 8.63233H7.94531V0H6.94531H5.94531V8.63233H6.94531ZM6.94531 0V1H18.0563V0V-1H6.94531V0ZM18.0563 0H17.0563V8.63233H18.0563H19.0563V0H18.0563Z" fill="#E58625" mask="url(#path-2-inside-1_49_6415)"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 576 B |
@@ -119,6 +119,14 @@ class PushNotificationService {
|
|||||||
|
|
||||||
Future<void> _getAndRegisterToken() async {
|
Future<void> _getAndRegisterToken() async {
|
||||||
try {
|
try {
|
||||||
|
// On iOS, wait for APNS token before requesting FCM token
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
final apnsToken = await _messaging!.getAPNSToken();
|
||||||
|
if (apnsToken == null) {
|
||||||
|
debugPrint('[FCM] APNS token not available yet (iOS simulator?), skipping FCM token registration');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
final token = await _messaging!.getToken();
|
final token = await _messaging!.getToken();
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
_currentToken = token;
|
_currentToken = token;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
// Trust & Stats Section
|
// Trust & Stats Section
|
||||||
const TrustStatsSection(),
|
const TrustStatsSection(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Testimonials Section
|
// Testimonials Section
|
||||||
const TestimonialsSection(),
|
const TestimonialsSection(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/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/data/models/landing_page_content.dart';
|
||||||
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
|
import 'package:real_estate_mobile/features/home/presentation/providers/home_provider.dart';
|
||||||
@@ -68,48 +69,14 @@ class TestimonialsSection extends ConsumerWidget {
|
|||||||
? cmsTestimonials
|
? cmsTestimonials
|
||||||
: _defaultContent;
|
: _defaultContent;
|
||||||
|
|
||||||
final title = data.title.isNotEmpty ? data.title : _defaultContent.title;
|
|
||||||
final subtitle =
|
|
||||||
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
|
|
||||||
final testimonials =
|
final testimonials =
|
||||||
data.testimonials.isNotEmpty ? data.testimonials : _defaultTestimonials;
|
data.testimonials.isNotEmpty ? data.testimonials : _defaultTestimonials;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Section header
|
// Testimonial cards carousel (header is in TrustStatsSection above)
|
||||||
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),
|
|
||||||
|
|
||||||
// Testimonial cards carousel
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 380,
|
height: 420,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
@@ -126,37 +93,38 @@ class TestimonialsSection extends ConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildTestimonialCard(TestimonialItem testimonial) {
|
Widget _buildTestimonialCard(TestimonialItem testimonial) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 315,
|
width: 300,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primaryDark,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Quote icon
|
// Quote icon SVG
|
||||||
const Text(
|
SvgPicture.asset(
|
||||||
'\u201C',
|
'assets/icons/quote_icon.svg',
|
||||||
style: TextStyle(
|
width: 23,
|
||||||
fontFamily: 'SourceSerif4',
|
height: 13,
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: AppColors.accentOrange,
|
|
||||||
height: 0.8,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
|
// Title
|
||||||
Text(
|
Text(
|
||||||
testimonial.title,
|
testimonial.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Fractul',
|
fontFamily: 'Fractul',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Colors.white,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
// Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
testimonial.content,
|
testimonial.content,
|
||||||
@@ -164,12 +132,12 @@ class TestimonialsSection extends ConsumerWidget {
|
|||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: Colors.white,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Stars
|
// Stars (yellow like Figma)
|
||||||
Row(
|
Row(
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
testimonial.rating,
|
testimonial.rating,
|
||||||
@@ -177,30 +145,32 @@ class TestimonialsSection extends ConsumerWidget {
|
|||||||
padding: EdgeInsets.only(right: 4),
|
padding: EdgeInsets.only(right: 4),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.star,
|
Icons.star,
|
||||||
color: AppColors.accentOrange,
|
color: Color(0xFFFFDE21),
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Author info
|
// Author name
|
||||||
Text(
|
Text(
|
||||||
testimonial.author,
|
testimonial.author,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
// Role
|
||||||
Text(
|
Text(
|
||||||
testimonial.role,
|
testimonial.role,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: AppColors.primaryDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -47,15 +47,9 @@ class TrustStatsSection extends ConsumerWidget {
|
|||||||
final title = data.title.isNotEmpty ? data.title : _defaultContent.title;
|
final title = data.title.isNotEmpty ? data.title : _defaultContent.title;
|
||||||
final subtitle =
|
final subtitle =
|
||||||
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
|
data.subtitle.isNotEmpty ? data.subtitle : _defaultContent.subtitle;
|
||||||
final ratingInfo = data.ratingInfo.isNotEmpty
|
|
||||||
? data.ratingInfo
|
|
||||||
: _defaultContent.ratingInfo;
|
|
||||||
final stats =
|
final stats =
|
||||||
data.stats.isNotEmpty ? data.stats : _defaultContent.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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -81,40 +75,23 @@ class TrustStatsSection extends ConsumerWidget {
|
|||||||
color: AppColors.primaryDark,
|
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 SizedBox(height: 16),
|
||||||
const Divider(color: AppColors.divider),
|
const Divider(color: AppColors.divider),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Stats row
|
// Stats (group centered, content left-aligned)
|
||||||
Row(
|
Align(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
alignment: Alignment.center,
|
||||||
children: [
|
child: IntrinsicWidth(
|
||||||
// Dynamic stats from CMS
|
child: Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: stats
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
.map((stat) => Padding(
|
||||||
children: stats
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
.map((stat) => Padding(
|
child: _buildStatItem(stat),
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
))
|
||||||
child: _buildStatItem(stat),
|
.toList(),
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// Rating visual
|
),
|
||||||
const SizedBox(width: 16),
|
|
||||||
_buildRatingVisual(ratingValue),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -127,32 +104,29 @@ class TrustStatsSection extends ConsumerWidget {
|
|||||||
final fallbackIcon = iconEntry?.$2 ?? Icons.info_outline;
|
final fallbackIcon = iconEntry?.$2 ?? Icons.info_outline;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (svgPath != null)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(right: 10, top: 2),
|
||||||
padding: const EdgeInsets.only(right: 8, top: 2),
|
child: svgPath != null
|
||||||
child: SvgPicture.asset(
|
? SvgPicture.asset(
|
||||||
svgPath,
|
svgPath,
|
||||||
width: 27,
|
width: 27,
|
||||||
height: 27,
|
height: 27,
|
||||||
placeholderBuilder: (_) => Icon(
|
placeholderBuilder: (_) => Icon(
|
||||||
fallbackIcon,
|
fallbackIcon,
|
||||||
size: 27,
|
size: 27,
|
||||||
color: AppColors.accentOrange,
|
color: AppColors.accentOrange,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: Icon(
|
||||||
else
|
fallbackIcon,
|
||||||
Padding(
|
size: 27,
|
||||||
padding: const EdgeInsets.only(right: 8, top: 2),
|
color: AppColors.accentOrange,
|
||||||
child: Icon(
|
),
|
||||||
fallbackIcon,
|
),
|
||||||
size: 27,
|
Flexible(
|
||||||
color: AppColors.accentOrange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
@@ -182,59 +156,4 @@ class TrustStatsSection extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user