Files
Eventify-frontend/lib/features/reviews/widgets/review_section.dart
Sicherhaven bc12fe70aa security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.

Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)

Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30

197 lines
5.8 KiB
Dart

// lib/features/reviews/widgets/review_section.dart
import 'package:flutter/material.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),
...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),
),
),
),
],
],
);
}
}