392 lines
13 KiB
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),
|
|
),
|
|
);
|
|
}
|
|
}
|