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>
99 lines
3.6 KiB
Dart
99 lines
3.6 KiB
Dart
// lib/features/reviews/widgets/review_summary.dart
|
|
import 'package:flutter/material.dart';
|
|
import '../models/review_models.dart';
|
|
import 'star_display.dart';
|
|
|
|
class ReviewSummary extends StatelessWidget {
|
|
final ReviewStatsModel stats;
|
|
|
|
const ReviewSummary({Key? key, required this.stats}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final maxCount = stats.distribution.values.fold<int>(0, (a, b) => a > b ? a : b);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 12, offset: const Offset(0, 4)),
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// Left: average rating + stars + count
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
stats.averageRating.toStringAsFixed(1),
|
|
style: const TextStyle(
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF1E293B),
|
|
height: 1.1,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
StarDisplay(rating: stats.averageRating, size: 18),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${stats.reviewCount} review${stats.reviewCount == 1 ? '' : 's'}',
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 24),
|
|
// Right: distribution bars
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: List.generate(5, (i) {
|
|
final star = 5 - i;
|
|
final count = stats.distribution[star] ?? 0;
|
|
final fraction = maxCount > 0 ? count / maxCount : 0.0;
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 18,
|
|
child: Text('$star', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF64748B))),
|
|
),
|
|
const Icon(Icons.star_rounded, size: 12, color: Color(0xFFFBBF24)),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
height: 8,
|
|
child: LinearProgressIndicator(
|
|
value: fraction,
|
|
backgroundColor: const Color(0xFFF1F5F9),
|
|
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFFFBBF24)),
|
|
minHeight: 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
width: 24,
|
|
child: Text('$count', style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|