Files
Eventify-frontend/lib/features/reviews/widgets/review_form.dart
Sicherhaven d2b49d4eb5 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

180 lines
7.0 KiB
Dart

// 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),
),
),
),
],
),
),
);
}
}