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:
104
lib/features/reviews/services/review_service.dart
Normal file
104
lib/features/reviews/services/review_service.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// lib/features/reviews/services/review_service.dart
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../models/review_models.dart';
|
||||
|
||||
class ReviewService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// Fetch paginated reviews + stats for an event.
|
||||
Future<ReviewListResponse> getReviews(int eventId, {int page = 1, int pageSize = 10}) async {
|
||||
try {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.reviewList,
|
||||
body: {'event_id': eventId, 'page': page, 'page_size': pageSize},
|
||||
requiresAuth: true,
|
||||
);
|
||||
|
||||
// Parse interactions map: { "review_id": { "helpful": bool, "flag": bool } }
|
||||
final rawInteractions = res['interactions'] as Map<String, dynamic>? ?? {};
|
||||
final interactionsMap = <int, Map<String, bool>>{};
|
||||
rawInteractions.forEach((key, value) {
|
||||
final id = int.tryParse(key);
|
||||
if (id != null && value is Map) {
|
||||
interactionsMap[id] = {
|
||||
'helpful': value['helpful'] == true,
|
||||
'flag': value['flag'] == true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Parse reviews
|
||||
final rawReviews = res['reviews'] as List? ?? [];
|
||||
final reviews = rawReviews.map((r) {
|
||||
final review = Map<String, dynamic>.from(r as Map);
|
||||
return ReviewModel.fromJson(review, interactions: interactionsMap[review['id']]);
|
||||
}).toList();
|
||||
|
||||
// Parse stats
|
||||
final stats = ReviewStatsModel.fromJson(res);
|
||||
|
||||
// Parse user's own review
|
||||
ReviewModel? userReview;
|
||||
if (res['user_review'] != null && res['user_review'] is Map) {
|
||||
final ur = Map<String, dynamic>.from(res['user_review'] as Map);
|
||||
userReview = ReviewModel.fromJson(ur, interactions: interactionsMap[ur['id']]);
|
||||
}
|
||||
|
||||
return ReviewListResponse(
|
||||
reviews: reviews,
|
||||
stats: stats,
|
||||
userReview: userReview,
|
||||
total: (res['total'] as num?)?.toInt() ?? reviews.length,
|
||||
page: (res['page'] as num?)?.toInt() ?? page,
|
||||
pageSize: (res['page_size'] as num?)?.toInt() ?? pageSize,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load reviews: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit or update a review.
|
||||
Future<void> submitReview(int eventId, int rating, String? comment) async {
|
||||
try {
|
||||
await _api.post(
|
||||
ApiEndpoints.reviewSubmit,
|
||||
body: {
|
||||
'event_id': eventId,
|
||||
'rating': rating,
|
||||
if (comment != null && comment.trim().isNotEmpty) 'comment': comment.trim(),
|
||||
},
|
||||
requiresAuth: true,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to submit review: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle helpful vote on a review. Returns new helpful count.
|
||||
Future<int> markHelpful(int reviewId) async {
|
||||
try {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.reviewHelpful,
|
||||
body: {'review_id': reviewId},
|
||||
requiresAuth: true,
|
||||
);
|
||||
return (res['helpful_count'] as num?)?.toInt() ?? 0;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to mark review as helpful: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Flag a review for moderation.
|
||||
Future<void> flagReview(int reviewId) async {
|
||||
try {
|
||||
await _api.post(
|
||||
ApiEndpoints.reviewFlag,
|
||||
body: {'review_id': reviewId},
|
||||
requiresAuth: true,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to flag review: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user