feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
// lib/features/reviews/widgets/review_form.dart
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../../../core/storage/token_storage.dart';
|
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
|
|
|
import '../../../core/utils/error_utils.dart';
|
feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
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) {
|
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
|
|
|
if (mounted) setState(() { _state = _FormState.idle; _error = userFriendlyError(e); });
|
feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|