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>
105 lines
3.3 KiB
Dart
105 lines
3.3 KiB
Dart
// 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');
|
|
}
|
|
}
|
|
}
|