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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 17:17:36 +05:30
parent fe8af7cfe6
commit 632754415d
19 changed files with 2346 additions and 183 deletions

View File

@@ -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