feat: enhance profile subscription UI with new assets, font adjustments, and SVG support.
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.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/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
||||
@@ -1085,7 +1086,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
'Subscription & Payments',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 22,
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
@@ -1112,7 +1113,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
'Current Plan',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
@@ -1265,132 +1266,206 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
'Annual Membership Plans',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Plan card matching Figma
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||
width: 0.5,
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Annual Subscription badge
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accentOrange,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: const Text(
|
||||
'Annual Subscription',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Professional Annual Plan
|
||||
const Text(
|
||||
'Professional Annual Plan',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Complete solutions for real estate\nagents and lenders to grow their\nbusiness.',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Price
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: '\$499',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 35,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' / ',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Year',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Get Premium Access button + illustration
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 155,
|
||||
height: 43,
|
||||
child: ElevatedButton(
|
||||
onPressed: _startCheckout,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
side: const BorderSide(color: AppColors.accentOrange),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: const Text('Get Premium Access',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white)),
|
||||
),
|
||||
),
|
||||
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
|
||||
const Text(
|
||||
'Include Features',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._buildPlanFeatures(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Annual Subscription badge
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accentOrange,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
// "Best value" ribbon on top-right corner
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'Annual Subscription',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
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),
|
||||
// Professional Annual Plan
|
||||
const Text(
|
||||
'Professional Annual Plan',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Complete solutions for real estate agents and lenders to grow their business.',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Price
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: '\$499',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 35,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' / ',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Year',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Get Premium Access button
|
||||
SizedBox(
|
||||
width: 155,
|
||||
child: ElevatedButton(
|
||||
onPressed: _startCheckout,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: const Text('Get Premium Access',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Include Features
|
||||
const Text(
|
||||
'Include Features',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._buildPlanFeatures(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1398,7 +1473,8 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
|
||||
// Need Help section matching Figma
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: const EdgeInsets.fromLTRB(20, 30, 20, 0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
@@ -1419,7 +1495,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
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,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
@@ -1439,7 +1515,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'support@re-quest.co',
|
||||
'support@example.com',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
@@ -1449,15 +1525,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
decorationColor: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: 4),
|
||||
Transform.rotate(
|
||||
angle: 0.7, // ~40 degrees
|
||||
angle: 0.72, // ~41 degrees matching Figma
|
||||
child: const Icon(Icons.arrow_upward,
|
||||
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() {
|
||||
const features = [
|
||||
'Agent Co-Marketing',
|
||||
'Priority 24/7 Support',
|
||||
'Whitelabel Client Portals',
|
||||
'Advanced CRM Tools & Analytics',
|
||||
('Agent -Co-Marketing', true), // first uses diamond icon
|
||||
('Priority 24/7 Support', false),
|
||||
('Whitelabel Client Portals', false),
|
||||
('Advanced CRM Tools & Analytics', false),
|
||||
];
|
||||
return features
|
||||
.map((f) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.accentOrange, width: 1.5),
|
||||
),
|
||||
child: const Icon(Icons.check,
|
||||
size: 14, color: AppColors.accentOrange),
|
||||
SvgPicture.asset(
|
||||
f.$2
|
||||
? 'assets/icons/diamond_icon.svg'
|
||||
: 'assets/icons/feature_check_icon.svg',
|
||||
width: 18,
|
||||
height: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(f,
|
||||
Text(f.$1,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
@@ -1634,121 +1717,156 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
|
||||
children: [
|
||||
const Text(
|
||||
'Add Testimonials',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
// Title matching Figma
|
||||
const Center(
|
||||
child: Text(
|
||||
'Add Testimonials',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Generate a link for your clients to submit testimonials. Share it via email or messaging.',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
const SizedBox(height: 10),
|
||||
const Center(
|
||||
child: Text(
|
||||
'What people are saying about you',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Generate link section
|
||||
_buildSectionCard(
|
||||
title: 'Testimonial Collection Link',
|
||||
subtitle: 'Share this link with your clients',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_testimonialLink != null) ...[
|
||||
// Create Testimonials section
|
||||
const Center(
|
||||
child: Text(
|
||||
'Create Testimonials',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Generate Link button (full-width, orange)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 43,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isGeneratingLink ? null : _generateTestimonialLink,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: _isGeneratingLink
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Generate Link',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 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: 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)),
|
||||
),
|
||||
child: Text(
|
||||
_testimonialLink!,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 13,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
width: 1,
|
||||
height: 43,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
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)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isGeneratingLink ? null : _generateTestimonialLink,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
foregroundColor: AppColors.primaryDark,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: _isGeneratingLink
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: AppColors.primaryDark))
|
||||
: const Text('Generate Link',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Testimonials list header
|
||||
// Reviews header with sort
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Received Testimonials',
|
||||
'Reviews',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Sort toggle
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -1759,25 +1877,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.sort,
|
||||
size: 16,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.6)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_testimonialSort == 'latest' ? 'Latest' : 'Oldest',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 13,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.6),
|
||||
'Sort By : ${_testimonialSort == 'latest' ? 'Latest' : 'Oldest'}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
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)
|
||||
const Center(
|
||||
child: Padding(
|
||||
@@ -1786,7 +1905,11 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
))
|
||||
else if (_testimonials.isEmpty)
|
||||
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: Column(
|
||||
children: [
|
||||
@@ -1818,8 +1941,18 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
),
|
||||
)
|
||||
else
|
||||
..._testimonials
|
||||
.map((t) => _buildTestimonialCard(t)),
|
||||
Container(
|
||||
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,76 +1969,62 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
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,
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stars + date
|
||||
// Stars
|
||||
Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
5,
|
||||
(i) => Icon(
|
||||
i < rating ? Icons.star : Icons.star_border,
|
||||
size: 16,
|
||||
color: i < rating
|
||||
? AppColors.accentOrange
|
||||
: AppColors.primaryDark.withValues(alpha: 0.3),
|
||||
),
|
||||
children: List.generate(
|
||||
5,
|
||||
(i) => Icon(
|
||||
i < rating ? Icons.star : Icons.star_border,
|
||||
size: 16,
|
||||
color: i < rating
|
||||
? AppColors.accentOrange
|
||||
: 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),
|
||||
// Text
|
||||
// Review text
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
color: AppColors.primaryDark,
|
||||
color: Colors.black,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Author
|
||||
Text(
|
||||
authorName,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryDark,
|
||||
// Author line: "Name , Role · time ago"
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: authorName,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
if (authorRole.isNotEmpty || createdAt != null)
|
||||
TextSpan(
|
||||
text: ' , ${authorRole.isNotEmpty ? authorRole : ''}${createdAt != null ? ' · ${_formatRelativeTime(createdAt)}' : ''}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (authorRole.isNotEmpty)
|
||||
Text(
|
||||
authorRole,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 13,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -2526,3 +2645,27 @@ class _TabItem {
|
||||
final IconData 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user