feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
This commit is contained in:
@@ -1,7 +1,97 @@
|
||||
// lib/features/reviews/widgets/review_summary.dart
|
||||
import 'dart:math' show pi;
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/review_models.dart';
|
||||
import 'star_display.dart';
|
||||
|
||||
class _RatingRingPainter extends CustomPainter {
|
||||
final double rating;
|
||||
|
||||
const _RatingRingPainter({required this.rating});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.width / 2 - 6;
|
||||
|
||||
// Background track
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi,
|
||||
false,
|
||||
Paint()
|
||||
..color = Colors.white12
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 7
|
||||
..strokeCap = StrokeCap.round,
|
||||
);
|
||||
|
||||
// Filled arc
|
||||
if (rating > 0) {
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
(rating.clamp(0.0, 5.0) / 5.0) * 2 * pi,
|
||||
false,
|
||||
Paint()
|
||||
..color = const Color(0xFFFBBF24)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 7
|
||||
..strokeCap = StrokeCap.round,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_RatingRingPainter old) => old.rating != rating;
|
||||
}
|
||||
|
||||
class _RatingRingWidget extends StatelessWidget {
|
||||
final double rating;
|
||||
final int reviewCount;
|
||||
|
||||
const _RatingRingWidget({required this.rating, required this.reviewCount});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 84,
|
||||
height: 84,
|
||||
child: CustomPaint(
|
||||
painter: _RatingRingPainter(rating: rating),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
rating.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'/5',
|
||||
style: TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$reviewCount ${reviewCount == 1 ? 'review' : 'reviews'}',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewSummary extends StatelessWidget {
|
||||
final ReviewStatsModel stats;
|
||||
@@ -24,27 +114,10 @@ class ReviewSummary extends StatelessWidget {
|
||||
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)),
|
||||
),
|
||||
],
|
||||
// Left: circular rating ring
|
||||
_RatingRingWidget(
|
||||
rating: stats.averageRating,
|
||||
reviewCount: stats.reviewCount,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
// Right: distribution bars
|
||||
|
||||
Reference in New Issue
Block a user