Files
mobile-app/lib/features/profile/presentation/widgets/testimonials_tab.dart

392 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
class TestimonialsTab extends ConsumerStatefulWidget {
const TestimonialsTab({super.key});
@override
ConsumerState<TestimonialsTab> createState() => _TestimonialsTabState();
}
class _TestimonialsTabState extends ConsumerState<TestimonialsTab> {
List<Map<String, dynamic>> _testimonials = [];
bool _isLoadingTestimonials = false;
String _testimonialSort = 'latest';
String? _testimonialLink;
bool _isGeneratingLink = false;
bool _linkCopied = false;
@override
void initState() {
super.initState();
_loadTestimonials();
}
Future<void> _loadTestimonials() async {
if (_isLoadingTestimonials) return;
setState(() => _isLoadingTestimonials = true);
try {
final repo = ref.read(profileRepositoryProvider);
final data = await repo.getMyTestimonials(sort: _testimonialSort);
setState(() {
_testimonials = data;
_isLoadingTestimonials = false;
});
} catch (_) {
setState(() => _isLoadingTestimonials = false);
}
}
Future<void> _generateTestimonialLink() async {
setState(() => _isGeneratingLink = true);
try {
final repo = ref.read(profileRepositoryProvider);
final token = await repo.generateTestimonialLink();
setState(() {
_testimonialLink = 'https://re-quest.co/testimonials/$token';
_isGeneratingLink = false;
});
} catch (e) {
setState(() => _isGeneratingLink = false);
if (mounted) _showSnackBar('Failed to generate link: $e', isError: true);
}
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
children: [
const Center(
child: Text(
'Add Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
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: 20),
const Center(
child: Text(
'Create Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 16),
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),
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),
Row(
children: [
const Text(
'Reviews',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
const Spacer(),
GestureDetector(
onTap: () {
setState(() {
_testimonialSort =
_testimonialSort == 'latest' ? 'oldest' : 'latest';
});
_loadTestimonials();
},
child: Row(
children: [
Text(
'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: 16),
if (_isLoadingTestimonials)
const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: CircularProgressIndicator(color: AppColors.accentOrange),
))
else if (_testimonials.isEmpty)
Container(
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: [
Icon(Icons.rate_review_outlined,
size: 48,
color: AppColors.primaryDark.withValues(alpha: 0.3)),
const SizedBox(height: 12),
const Text(
'No testimonials yet',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 6),
Text(
'Share your testimonial link with clients to start collecting reviews.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.6),
),
),
],
),
),
)
else
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(),
),
),
],
);
}
Widget _buildTestimonialCard(Map<String, dynamic> testimonial) {
final rating = (testimonial['rating'] as num?)?.toInt() ?? 5;
final text = testimonial['text'] as String? ?? '';
final authorName = testimonial['authorName'] as String? ?? '';
final authorRole = testimonial['authorRole'] as String? ?? '';
final createdAt = testimonial['createdAt'] as String?;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
),
),
),
const SizedBox(height: 10),
Text(
text,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Colors.black,
height: 1.5,
),
),
const SizedBox(height: 12),
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,
),
),
],
),
),
],
),
);
}
String _formatRelativeTime(String dateStr) {
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inDays == 0) return 'Today';
if (diff.inDays == 1) return 'Yesterday';
if (diff.inDays < 30) return '${diff.inDays} days ago';
if (diff.inDays < 365) return '${diff.inDays ~/ 30} months ago';
return DateFormat('MMM d, y').format(date);
} catch (_) {
return '';
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : const Color(0xFF638559),
),
);
}
}