209 lines
6.4 KiB
Dart
209 lines
6.4 KiB
Dart
// lib/features/reviews/widgets/review_section.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
|
import '../../../core/storage/token_storage.dart';
|
|
import '../../../core/utils/error_utils.dart';
|
|
import '../models/review_models.dart';
|
|
import '../services/review_service.dart';
|
|
import 'review_summary.dart';
|
|
import 'review_form.dart';
|
|
import 'review_card.dart';
|
|
|
|
class ReviewSection extends StatefulWidget {
|
|
final int eventId;
|
|
|
|
const ReviewSection({Key? key, required this.eventId}) : super(key: key);
|
|
|
|
@override
|
|
State<ReviewSection> createState() => _ReviewSectionState();
|
|
}
|
|
|
|
class _ReviewSectionState extends State<ReviewSection> {
|
|
final ReviewService _service = ReviewService();
|
|
|
|
List<ReviewModel> _reviews = [];
|
|
ReviewStatsModel? _stats;
|
|
ReviewModel? _userReview;
|
|
String? _currentUsername;
|
|
bool _loading = true;
|
|
String? _error;
|
|
int _page = 1;
|
|
int _total = 0;
|
|
bool _loadingMore = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_init();
|
|
}
|
|
|
|
Future<void> _init() async {
|
|
_currentUsername = await TokenStorage.getUsername();
|
|
await _loadReviews();
|
|
}
|
|
|
|
Future<void> _loadReviews() async {
|
|
setState(() { _loading = true; _error = null; });
|
|
try {
|
|
final response = await _service.getReviews(widget.eventId, page: 1);
|
|
if (mounted) {
|
|
setState(() {
|
|
_reviews = response.reviews;
|
|
_stats = response.stats;
|
|
_userReview = response.userReview;
|
|
_total = response.total;
|
|
_page = 1;
|
|
_loading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() { _loading = false; _error = userFriendlyError(e); });
|
|
}
|
|
}
|
|
|
|
Future<void> _loadMore() async {
|
|
if (_loadingMore || _reviews.length >= _total) return;
|
|
setState(() => _loadingMore = true);
|
|
try {
|
|
final response = await _service.getReviews(widget.eventId, page: _page + 1);
|
|
if (mounted) {
|
|
setState(() {
|
|
_reviews.addAll(response.reviews);
|
|
_page = response.page;
|
|
_total = response.total;
|
|
_loadingMore = false;
|
|
});
|
|
}
|
|
} catch (_) {
|
|
if (mounted) setState(() => _loadingMore = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _handleSubmit(int rating, String? comment) async {
|
|
await _service.submitReview(widget.eventId, rating, comment);
|
|
await _loadReviews(); // Refresh to get updated stats + review list
|
|
}
|
|
|
|
Future<int> _handleHelpful(int reviewId) async {
|
|
return _service.markHelpful(reviewId);
|
|
}
|
|
|
|
Future<void> _handleFlag(int reviewId) async {
|
|
await _service.flagReview(reviewId);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section header
|
|
const Text(
|
|
'Reviews & Ratings',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
if (_loading)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(32),
|
|
child: CircularProgressIndicator(color: Color(0xFF0F45CF)),
|
|
),
|
|
)
|
|
else if (_error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
Text(_error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)),
|
|
const SizedBox(height: 8),
|
|
TextButton.icon(
|
|
onPressed: _loadReviews,
|
|
icon: const Icon(Icons.refresh, size: 16),
|
|
label: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else ...[
|
|
// Summary card
|
|
if (_stats != null && _stats!.reviewCount > 0) ...[
|
|
ReviewSummary(stats: _stats!),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Review form
|
|
ReviewForm(
|
|
eventId: widget.eventId,
|
|
existingReview: _userReview,
|
|
onSubmit: _handleSubmit,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Divider
|
|
if (_reviews.isNotEmpty)
|
|
const Divider(color: Color(0xFFF1F5F9), thickness: 1),
|
|
|
|
// Reviews list
|
|
if (_reviews.isEmpty && (_stats == null || _stats!.reviewCount == 0))
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24),
|
|
child: Center(
|
|
child: Text(
|
|
'No reviews yet. Be the first to share your experience!',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
const SizedBox(height: 12),
|
|
AnimationLimiter(
|
|
child: Column(
|
|
children: AnimationConfiguration.toStaggeredList(
|
|
duration: const Duration(milliseconds: 375),
|
|
childAnimationBuilder: (widget) => SlideAnimation(
|
|
verticalOffset: 50.0,
|
|
child: FadeInAnimation(child: widget),
|
|
),
|
|
children: List.generate(_reviews.length, (i) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: ReviewCard(
|
|
review: _reviews[i],
|
|
currentUsername: _currentUsername,
|
|
onHelpful: _handleHelpful,
|
|
onFlag: _handleFlag,
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
|
|
// Load more
|
|
if (_reviews.length < _total)
|
|
Center(
|
|
child: _loadingMore
|
|
? const Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF0F45CF))),
|
|
)
|
|
: TextButton(
|
|
onPressed: _loadMore,
|
|
child: const Text(
|
|
'Show more reviews',
|
|
style: TextStyle(color: Color(0xFF0F45CF), fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|