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>
181 lines
7.0 KiB
Dart
181 lines
7.0 KiB
Dart
// lib/features/reviews/widgets/review_form.dart
|
|
import 'package:flutter/material.dart';
|
|
import '../../../core/storage/token_storage.dart';
|
|
import '../../../core/utils/error_utils.dart';
|
|
import '../models/review_models.dart';
|
|
import 'star_rating_input.dart';
|
|
|
|
class ReviewForm extends StatefulWidget {
|
|
final int eventId;
|
|
final ReviewModel? existingReview;
|
|
final Future<void> Function(int rating, String? comment) onSubmit;
|
|
|
|
const ReviewForm({
|
|
Key? key,
|
|
required this.eventId,
|
|
this.existingReview,
|
|
required this.onSubmit,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<ReviewForm> createState() => _ReviewFormState();
|
|
}
|
|
|
|
enum _FormState { idle, loading, success }
|
|
|
|
class _ReviewFormState extends State<ReviewForm> {
|
|
int _rating = 0;
|
|
final _commentController = TextEditingController();
|
|
_FormState _state = _FormState.idle;
|
|
bool _isLoggedIn = false;
|
|
String? _error;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_checkAuth();
|
|
if (widget.existingReview != null) {
|
|
_rating = widget.existingReview!.rating;
|
|
_commentController.text = widget.existingReview!.comment ?? '';
|
|
}
|
|
}
|
|
|
|
Future<void> _checkAuth() async {
|
|
final token = await TokenStorage.getToken();
|
|
final username = await TokenStorage.getUsername();
|
|
if (mounted) setState(() => _isLoggedIn = token != null && username != null);
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
if (_rating == 0) {
|
|
setState(() => _error = 'Please select a rating');
|
|
return;
|
|
}
|
|
setState(() { _state = _FormState.loading; _error = null; });
|
|
try {
|
|
await widget.onSubmit(_rating, _commentController.text);
|
|
if (mounted) {
|
|
setState(() => _state = _FormState.success);
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
if (mounted) setState(() => _state = _FormState.idle);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() { _state = _FormState.idle; _error = userFriendlyError(e); });
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_commentController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_isLoggedIn) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.login, color: Color(0xFF64748B), size: 20),
|
|
const SizedBox(width: 8),
|
|
const Expanded(
|
|
child: Text('Log in to write a review', style: TextStyle(color: Color(0xFF64748B), fontSize: 14)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 300),
|
|
child: _state == _FormState.success
|
|
? Container(
|
|
key: const ValueKey('success'),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF0FDF4),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF86EFAC)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: const [
|
|
Icon(Icons.check_circle, color: Color(0xFF10B981), size: 24),
|
|
SizedBox(width: 8),
|
|
Text('Review submitted!', style: TextStyle(color: Color(0xFF10B981), fontWeight: FontWeight.w600, fontSize: 15)),
|
|
],
|
|
),
|
|
)
|
|
: Container(
|
|
key: const ValueKey('form'),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2)),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.existingReview != null ? 'Update your review' : 'Write a review',
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1E293B)),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Center(child: StarRatingInput(rating: _rating, onRatingChanged: (r) => setState(() { _rating = r; _error = null; }))),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _commentController,
|
|
maxLength: 500,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: 'Share your experience (optional)',
|
|
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
|
|
filled: true,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFE2E8F0))),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFE2E8F0))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF0F45CF), width: 1.5)),
|
|
counterStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
|
),
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(_error!, style: const TextStyle(color: Color(0xFFEF4444), fontSize: 12)),
|
|
],
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 46,
|
|
child: ElevatedButton(
|
|
onPressed: _state == _FormState.loading ? null : _handleSubmit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF0F45CF),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
disabledBackgroundColor: const Color(0xFF0F45CF).withValues(alpha: 0.5),
|
|
),
|
|
child: _state == _FormState.loading
|
|
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
|
: Text(
|
|
widget.existingReview != null ? 'Update Review' : 'Submit Review',
|
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|