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>
This commit is contained in:
179
lib/features/reviews/widgets/review_form.dart
Normal file
179
lib/features/reviews/widgets/review_form.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
// lib/features/reviews/widgets/review_form.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/storage/token_storage.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 = e.toString(); });
|
||||
}
|
||||
}
|
||||
|
||||
@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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user