feat: enhance profile subscription UI with new assets, font adjustments, and SVG support.

This commit is contained in:
pradeepkumar
2026-03-09 07:40:57 +05:30
parent ef1df39520
commit 88c71d6b6e
8 changed files with 485 additions and 303 deletions

4
.gitignore vendored
View File

@@ -43,3 +43,7 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Signing
/android/keystores/
/android/key.properties

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -6,6 +9,12 @@ plugins {
id("com.google.gms.google-services") id("com.google.gms.google-services")
} }
val keyProperties = Properties()
val keyPropertiesFile = rootProject.file("key.properties")
if (keyPropertiesFile.exists()) {
keyProperties.load(FileInputStream(keyPropertiesFile))
}
android { android {
namespace = "com.requestn.real_estate_mobile" namespace = "com.requestn.real_estate_mobile"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -29,6 +38,21 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
create("devRelease") {
storeFile = file(keyProperties.getProperty("devStoreFile", ""))
storePassword = keyProperties.getProperty("devStorePassword", "")
keyAlias = keyProperties.getProperty("devKeyAlias", "")
keyPassword = keyProperties.getProperty("devKeyPassword", "")
}
create("demoRelease") {
storeFile = file(keyProperties.getProperty("demoStoreFile", ""))
storePassword = keyProperties.getProperty("demoStorePassword", "")
keyAlias = keyProperties.getProperty("demoKeyAlias", "")
keyPassword = keyProperties.getProperty("demoKeyPassword", "")
}
}
flavorDimensions += "flavor-type" flavorDimensions += "flavor-type"
productFlavors { productFlavors {
@@ -41,11 +65,13 @@ android {
dimension = "flavor-type" dimension = "flavor-type"
applicationIdSuffix = ".demo" applicationIdSuffix = ".demo"
resValue("string", "app_name", "RE-QuestN Demo") resValue("string", "app_name", "RE-QuestN Demo")
signingConfig = signingConfigs.getByName("demoRelease")
} }
create("dev") { create("dev") {
dimension = "flavor-type" dimension = "flavor-type"
applicationIdSuffix = ".dev" applicationIdSuffix = ".dev"
resValue("string", "app_name", "RE-QuestN Dev") resValue("string", "app_name", "RE-QuestN Dev")
signingConfig = signingConfigs.getByName("devRelease")
} }
} }

View File

@@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.00025 5.49532H15.9652M12.224 1C12.4563 1 12.6854 1.05416 12.8932 1.15819C13.101 1.26223 13.2818 1.41328 13.4212 1.59938L15.6659 4.59625C15.8584 4.85336 15.9633 5.16562 15.9651 5.48696C15.9668 5.80831 15.8654 6.1217 15.6757 6.38089L9.69714 14.6118C9.55853 14.8047 9.37609 14.9619 9.16486 15.0702C8.95364 15.1786 8.7197 15.2352 8.48236 15.2352C8.24502 15.2352 8.01108 15.1786 7.79986 15.0702C7.58864 14.9619 7.40619 14.8047 7.26758 14.6118L1.28907 6.38089C1.09947 6.12162 0.99814 5.80818 1.00003 5.48684C1.00191 5.1655 1.10692 4.85328 1.29955 4.59625L3.5428 1.60162C3.68211 1.41491 3.863 1.2633 4.07109 1.15886C4.27917 1.05442 4.50873 1.00003 4.74149 1H12.224Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 856 B

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.97899 0C4.02782 0 0 4.03305 0 8.99063C0 13.9482 4.02782 17.9813 8.97899 17.9813C13.9301 17.9813 17.958 13.9482 17.958 8.99063H15.9377C15.9377 12.8326 12.816 15.9584 8.97899 15.9584C5.14197 15.9584 2.02027 12.8326 2.02027 8.99063C2.02027 5.14863 5.14197 2.02289 8.97899 2.02289V0ZM14.516 1.93523L8.90042 8.95617L6.27631 6.80291L4.77832 8.62801L8.32576 11.541C8.44628 11.6405 8.58538 11.715 8.73496 11.7602C8.88453 11.8053 9.04159 11.8202 9.19697 11.804C9.35235 11.7877 9.50295 11.7407 9.63998 11.6655C9.77701 11.5904 9.89773 11.4887 9.99511 11.3664L16.3582 3.41194L14.516 1.93523Z" fill="#43A047"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

View File

@@ -343,11 +343,13 @@ class _AgentEditProfileScreenState
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
body: Column( body: SafeArea(
bottom: false,
child: Column(
children: [ children: [
// Header with circular back button (SVG) matching Figma // Header with circular back button (SVG) matching Figma
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(
@@ -374,6 +376,7 @@ class _AgentEditProfileScreenState
Expanded(child: _buildBody(asyncData)), Expanded(child: _buildBody(asyncData)),
], ],
), ),
),
); );
} }

View File

@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart';
@@ -1085,7 +1086,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Subscription & Payments', 'Subscription & Payments',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 22, fontSize: 25,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
@@ -1112,7 +1113,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Current Plan', 'Current Plan',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 18, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
@@ -1265,15 +1266,18 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Annual Membership Plans', 'Annual Membership Plans',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 18, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Plan card matching Figma // Plan card matching Figma
Stack(
clipBehavior: Clip.none,
children: [
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(
@@ -1314,7 +1318,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'Complete solutions for real estate agents and lenders to grow their business.', 'Complete solutions for real estate\nagents and lenders to grow their\nbusiness.',
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1332,7 +1336,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 35, fontSize: 35,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: Colors.black,
), ),
), ),
const TextSpan( const TextSpan(
@@ -1341,7 +1345,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: Colors.black,
), ),
), ),
TextSpan( TextSpan(
@@ -1350,33 +1354,51 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark.withValues(alpha: 0.5), color: Colors.black.withValues(alpha: 0.5),
), ),
), ),
], ],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Get Premium Access button // Get Premium Access button + illustration
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox( SizedBox(
width: 155, width: 155,
height: 43,
child: ElevatedButton( child: ElevatedButton(
onPressed: _startCheckout, onPressed: _startCheckout,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange, backgroundColor: AppColors.accentOrange,
padding: const EdgeInsets.symmetric(vertical: 12), side: const BorderSide(color: AppColors.accentOrange),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15)),
), ),
child: const Text('Get Premium Access', child: const Text('Get Premium Access',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 13, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Colors.white)), color: Colors.white)),
), ),
), ),
const SizedBox(height: 24), const Spacer(),
// Wallet illustration
ClipRRect(
borderRadius: BorderRadius.circular(70),
child: Image.asset(
'assets/icons/wallet_illustration.jpg',
width: 120,
height: 120,
fit: BoxFit.cover,
),
),
],
),
const SizedBox(height: 16),
// Include Features // Include Features
const Text( const Text(
'Include Features', 'Include Features',
@@ -1392,13 +1414,67 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
], ],
), ),
), ),
// "Best value" ribbon on top-right corner
Positioned(
top: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
),
child: SizedBox(
width: 62,
height: 134,
child: Stack(
children: [
CustomPaint(
size: const Size(62, 134),
painter: _RibbonPainter(),
),
// Orange circle at top of ribbon
Positioned(
top: 6,
left: 12,
child: Container(
width: 15,
height: 14,
decoration: const BoxDecoration(
color: AppColors.accentOrange,
shape: BoxShape.circle,
),
),
),
// "Best value" text rotated ~67 degrees
Positioned(
top: 24,
left: 6,
child: Transform.rotate(
angle: 1.17,
child: const Text(
'Best value',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
color: Colors.white,
),
),
),
),
],
),
),
),
),
],
),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
// Need Help section matching Figma // Need Help section matching Figma
Container( Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.fromLTRB(20, 30, 20, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all( border: Border.all(
@@ -1419,7 +1495,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text( const Text(
'Our billing experts are here to help you\nwith any questions about your plan.', 'Our billing experts are here to help you\nwith any questions\n about your plan.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
@@ -1439,7 +1515,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'support@re-quest.co', 'support@example.com',
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1449,15 +1525,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
decorationColor: AppColors.accentOrange, decorationColor: AppColors.accentOrange,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 4),
Transform.rotate( Transform.rotate(
angle: 0.7, // ~40 degrees angle: 0.72, // ~41 degrees matching Figma
child: const Icon(Icons.arrow_upward, child: const Icon(Icons.arrow_upward,
size: 16, color: AppColors.accentOrange), size: 16, color: AppColors.accentOrange),
), ),
], ],
), ),
), ),
const SizedBox(height: 20),
// Businessman illustration
ClipRRect(
borderRadius: BorderRadius.circular(57),
child: Image.asset(
'assets/icons/need_help_illustration.jpg',
width: 261,
height: 261,
fit: BoxFit.cover,
),
),
], ],
), ),
), ),
@@ -1467,29 +1554,25 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
List<Widget> _buildPlanFeatures() { List<Widget> _buildPlanFeatures() {
const features = [ const features = [
'Agent Co-Marketing', ('Agent -Co-Marketing', true), // first uses diamond icon
'Priority 24/7 Support', ('Priority 24/7 Support', false),
'Whitelabel Client Portals', ('Whitelabel Client Portals', false),
'Advanced CRM Tools & Analytics', ('Advanced CRM Tools & Analytics', false),
]; ];
return features return features
.map((f) => Padding( .map((f) => Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 20),
child: Row( child: Row(
children: [ children: [
Container( SvgPicture.asset(
width: 22, f.$2
height: 22, ? 'assets/icons/diamond_icon.svg'
decoration: BoxDecoration( : 'assets/icons/feature_check_icon.svg',
shape: BoxShape.circle, width: 18,
border: Border.all( height: 18,
color: AppColors.accentOrange, width: 1.5),
),
child: const Icon(Icons.check,
size: 14, color: AppColors.accentOrange),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(f, Text(f.$1,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1634,18 +1717,23 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
return ListView( return ListView(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30), padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
children: [ children: [
const Text( // Title matching Figma
const Center(
child: Text(
'Add Testimonials', 'Add Testimonials',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 15, fontSize: 25,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
const SizedBox(height: 8), ),
const Text( const SizedBox(height: 10),
'Generate a link for your clients to submit testimonials. Share it via email or messaging.', const Center(
child: Text(
'What people are saying about you',
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1653,66 +1741,32 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
const SizedBox(height: 24),
// Generate link section
_buildSectionCard(
title: 'Testimonial Collection Link',
subtitle: 'Share this link with your clients',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_testimonialLink != null) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primaryDark.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1)),
), ),
const SizedBox(height: 20),
// Create Testimonials section
const Center(
child: Text( child: Text(
_testimonialLink!, 'Create Testimonials',
style: const TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'Fractul',
fontSize: 13, fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
SizedBox(
width: double.infinity, // Generate Link button (full-width, orange)
child: OutlinedButton.icon(
onPressed: () {
Clipboard.setData(
ClipboardData(text: _testimonialLink!));
setState(() => _linkCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _linkCopied = false);
});
},
icon: Icon(
_linkCopied ? Icons.check : Icons.copy, size: 16),
label: Text(_linkCopied ? 'Copied!' : 'Copy Link'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.accentOrange,
side: const BorderSide(color: AppColors.accentOrange),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
] else ...[
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 43,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isGeneratingLink ? null : _generateTestimonialLink, onPressed: _isGeneratingLink ? null : _generateTestimonialLink,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange, backgroundColor: AppColors.accentOrange,
foregroundColor: AppColors.primaryDark, padding: const EdgeInsets.symmetric(vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15)),
), ),
@@ -1721,34 +1775,98 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, color: AppColors.primaryDark)) strokeWidth: 2, color: Colors.white))
: const Text('Generate Link', : const Text('Generate Link',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700)), fontWeight: FontWeight.w700,
color: Colors.white)),
), ),
), ),
], const SizedBox(height: 16),
],
),
),
const SizedBox(height: 24),
// Testimonials list header // Link URL field with Copy button
if (_testimonialLink != null)
Container(
height: 43,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
_testimonialLink!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: Colors.black,
),
),
),
),
Container(
width: 1,
height: 43,
color: AppColors.primaryDark.withValues(alpha: 0.1),
),
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: _testimonialLink!));
setState(() => _linkCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _linkCopied = false);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
Icon(
_linkCopied ? Icons.check : Icons.copy_outlined,
size: 18,
color: Colors.black,
),
const SizedBox(width: 4),
Text(
_linkCopied ? 'Copied!' : 'Copy',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Colors.black,
),
),
],
),
),
),
],
),
),
const SizedBox(height: 30),
// Reviews header with sort
Row( Row(
children: [ children: [
const Text( const Text(
'Received Testimonials', 'Reviews',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 15, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w500,
color: AppColors.primaryDark, color: Colors.black,
), ),
), ),
const Spacer(), const Spacer(),
// Sort toggle
GestureDetector( GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -1759,25 +1877,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
}, },
child: Row( child: Row(
children: [ children: [
Icon(Icons.sort,
size: 16,
color: AppColors.primaryDark.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text( Text(
_testimonialSort == 'latest' ? 'Latest' : 'Oldest', 'Sort By : ${_testimonialSort == 'latest' ? 'Latest' : 'Oldest'}',
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'Fractul',
fontSize: 13, fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.6), fontWeight: FontWeight.w500,
color: Colors.black,
), ),
), ),
const SizedBox(width: 4),
const Icon(Icons.arrow_downward,
size: 14, color: Colors.black),
], ],
), ),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
// Reviews list in grey container
if (_isLoadingTestimonials) if (_isLoadingTestimonials)
const Center( const Center(
child: Padding( child: Padding(
@@ -1786,7 +1905,11 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
)) ))
else if (_testimonials.isEmpty) else if (_testimonials.isEmpty)
Container( Container(
padding: const EdgeInsets.symmetric(vertical: 40), padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(7),
),
child: Center( child: Center(
child: Column( child: Column(
children: [ children: [
@@ -1818,8 +1941,18 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
), ),
) )
else else
..._testimonials Container(
.map((t) => _buildTestimonialCard(t)), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(7),
),
child: Column(
children: _testimonials
.map((t) => _buildTestimonialCard(t))
.toList(),
),
),
], ],
); );
} }
@@ -1836,24 +1969,14 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1)),
boxShadow: [
BoxShadow(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.3),
offset: const Offset(0, 4),
blurRadius: 10,
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Stars + date // Stars
Row( Row(
children: [ children: List.generate(
...List.generate(
5, 5,
(i) => Icon( (i) => Icon(
i < rating ? Icons.star : Icons.star_border, i < rating ? Icons.star : Icons.star_border,
@@ -1863,47 +1986,43 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
: AppColors.primaryDark.withValues(alpha: 0.3), : AppColors.primaryDark.withValues(alpha: 0.3),
), ),
), ),
const Spacer(),
if (createdAt != null)
Text(
_formatRelativeTime(createdAt),
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Text // Review text
Text( Text(
text, text,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
color: AppColors.primaryDark, color: Colors.black,
height: 1.5, height: 1.5,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Author // Author line: "Name , Role · time ago"
Text( RichText(
authorName, text: TextSpan(
children: [
TextSpan(
text: authorName,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
if (authorRole.isNotEmpty) if (authorRole.isNotEmpty || createdAt != null)
Text( TextSpan(
authorRole, text: ' , ${authorRole.isNotEmpty ? authorRole : ''}${createdAt != null ? ' · ${_formatRelativeTime(createdAt)}' : ''}',
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'Fractul',
fontSize: 13, fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.6), fontWeight: FontWeight.w300,
color: AppColors.primaryDark,
),
),
],
), ),
), ),
], ],
@@ -2526,3 +2645,27 @@ class _TabItem {
final IconData icon; final IconData icon;
const _TabItem({required this.label, required this.icon}); const _TabItem({required this.label, required this.icon});
} }
/// Custom painter for the "Best value" ribbon triangle (matches Figma SVG).
/// Ribbon painter matching Figma SVG: M28.5 0H0L62 134V73.99L28.5 0Z
/// Shape: a pennant/flag anchored top-right of the plan card.
class _RibbonPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = AppColors.accentOrange;
// Figma SVG path within 62×134 viewport:
// M28.5,0 → H0 → L62,134 → V73.99 → L28.5,0 → Z
final w = size.width; // 62
final h = size.height; // 134
final path = Path()
..moveTo(w * (28.5 / 62), 0) // (28.5, 0) top point of ribbon
..lineTo(0, 0) // (0, 0) top-left corner
..lineTo(w, h) // (62, 134) bottom-right tip
..lineTo(w, h * (73.99 / 134)) // (62, ~74) right edge notch
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}