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>
114 lines
3.3 KiB
Dart
114 lines
3.3 KiB
Dart
// lib/features/reviews/models/review_models.dart
|
|
|
|
class ReviewModel {
|
|
final int id;
|
|
final int eventId;
|
|
final String username;
|
|
final int rating;
|
|
final String? comment;
|
|
final String status;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
final bool isVerified;
|
|
final int helpfulCount;
|
|
final int flagCount;
|
|
final bool userMarkedHelpful;
|
|
final bool userFlagged;
|
|
|
|
ReviewModel({
|
|
required this.id,
|
|
required this.eventId,
|
|
required this.username,
|
|
required this.rating,
|
|
this.comment,
|
|
this.status = 'PUBLISHED',
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
this.isVerified = false,
|
|
this.helpfulCount = 0,
|
|
this.flagCount = 0,
|
|
this.userMarkedHelpful = false,
|
|
this.userFlagged = false,
|
|
});
|
|
|
|
factory ReviewModel.fromJson(Map<String, dynamic> j, {Map<String, bool>? interactions}) {
|
|
return ReviewModel(
|
|
id: j['id'] as int,
|
|
eventId: j['event_id'] as int,
|
|
username: (j['username'] ?? j['display_name'] ?? 'Anonymous') as String,
|
|
rating: j['rating'] as int,
|
|
comment: j['comment'] as String?,
|
|
status: (j['status'] ?? 'PUBLISHED') as String,
|
|
createdAt: DateTime.tryParse(j['created_at'] ?? '') ?? DateTime.now(),
|
|
updatedAt: DateTime.tryParse(j['updated_at'] ?? '') ?? DateTime.now(),
|
|
isVerified: j['is_verified'] == true,
|
|
helpfulCount: (j['helpful_count'] ?? 0) as int,
|
|
flagCount: (j['flag_count'] ?? 0) as int,
|
|
userMarkedHelpful: interactions?['helpful'] ?? false,
|
|
userFlagged: interactions?['flag'] ?? false,
|
|
);
|
|
}
|
|
|
|
ReviewModel copyWith({int? helpfulCount, bool? userMarkedHelpful, bool? userFlagged}) {
|
|
return ReviewModel(
|
|
id: id, eventId: eventId, username: username, rating: rating,
|
|
comment: comment, status: status, createdAt: createdAt, updatedAt: updatedAt,
|
|
isVerified: isVerified,
|
|
helpfulCount: helpfulCount ?? this.helpfulCount,
|
|
flagCount: flagCount,
|
|
userMarkedHelpful: userMarkedHelpful ?? this.userMarkedHelpful,
|
|
userFlagged: userFlagged ?? this.userFlagged,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ReviewStatsModel {
|
|
final double averageRating;
|
|
final int reviewCount;
|
|
final Map<int, int> distribution;
|
|
|
|
ReviewStatsModel({
|
|
required this.averageRating,
|
|
required this.reviewCount,
|
|
required this.distribution,
|
|
});
|
|
|
|
factory ReviewStatsModel.fromJson(Map<String, dynamic> j) {
|
|
final dist = <int, int>{1: 0, 2: 0, 3: 0, 4: 0, 5: 0};
|
|
final rawDist = j['distribution'];
|
|
if (rawDist is Map) {
|
|
rawDist.forEach((k, v) {
|
|
final key = int.tryParse(k.toString());
|
|
if (key != null && key >= 1 && key <= 5) dist[key] = (v as num).toInt();
|
|
});
|
|
} else if (rawDist is List) {
|
|
for (int i = 0; i < rawDist.length && i < 5; i++) {
|
|
dist[i + 1] = (rawDist[i] as num).toInt();
|
|
}
|
|
}
|
|
return ReviewStatsModel(
|
|
averageRating: (j['average_rating'] as num?)?.toDouble() ?? 0.0,
|
|
reviewCount: (j['review_count'] as num?)?.toInt() ?? 0,
|
|
distribution: dist,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ReviewListResponse {
|
|
final List<ReviewModel> reviews;
|
|
final ReviewStatsModel stats;
|
|
final ReviewModel? userReview;
|
|
final int total;
|
|
final int page;
|
|
final int pageSize;
|
|
|
|
ReviewListResponse({
|
|
required this.reviews,
|
|
required this.stats,
|
|
this.userReview,
|
|
required this.total,
|
|
required this.page,
|
|
required this.pageSize,
|
|
});
|
|
}
|