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:
2026-04-04 17:17:36 +05:30
parent e365361451
commit e9752c3d61
19 changed files with 2346 additions and 183 deletions

View File

@@ -14,7 +14,11 @@ import 'package:share_plus/share_plus.dart';
import '../core/app_decoration.dart';
import '../features/gamification/models/gamification_models.dart';
import '../features/gamification/providers/gamification_provider.dart';
import '../widgets/glass_card.dart';
import '../widgets/landscape_section_header.dart';
import '../widgets/tier_avatar_ring.dart';
import '../features/share/share_rank_card.dart';
import 'contributor_profile_screen.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Tier colour map
@@ -29,12 +33,19 @@ const _tierColors = <ContributorTier, Color>{
// Icon map for achievement badges
const _badgeIcons = <String, IconData>{
'edit': Icons.edit_outlined,
'star': Icons.star_outline,
'emoji_events': Icons.emoji_events_outlined,
'leaderboard': Icons.leaderboard_outlined,
'photo_library': Icons.photo_library_outlined,
'verified': Icons.verified_outlined,
'edit': Icons.edit_outlined,
'star': Icons.star_outline,
'emoji_events': Icons.emoji_events_outlined,
'leaderboard': Icons.leaderboard_outlined,
'photo_library': Icons.photo_library_outlined,
'verified': Icons.verified_outlined,
// ACH-002: icons for expanded badge set (badges 02, 0611)
'trending_up': Icons.trending_up,
'rocket_launch': Icons.rocket_launch_outlined,
'event_hunter': Icons.search_outlined,
'location_on': Icons.location_on_outlined,
'diamond': Icons.diamond_outlined,
'workspace_premium': Icons.workspace_premium_outlined,
};
// District list for the contribution form
@@ -254,7 +265,56 @@ class _ContributeScreenState extends State<ContributeScreen>
children: [
_epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)),
const SizedBox(width: 8),
_epStatCard('Liquid EP', '${profile?.currentEp ?? 0}', Icons.bolt, const Color(0xFF3B82F6)),
// GAM-003 + GAM-004: Liquid EP card with cycle countdown and progress
Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF3B82F6).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.3)),
),
child: Column(
children: [
const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 20),
const SizedBox(height: 4),
Text(
'${profile?.currentEp ?? 0}',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16),
),
const SizedBox(height: 2),
const Text('Liquid EP', style: TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center),
if (provider.currentUserStats?.rewardCycleDays != null) ...[
const SizedBox(height: 4),
Text(
'Converts in ${provider.currentUserStats!.rewardCycleDays}d',
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Builder(
builder: (_) {
final days = provider.currentUserStats?.rewardCycleDays ?? 30;
final elapsed = (30 - days).clamp(0, 30);
final ratio = elapsed / 30;
return LinearProgressIndicator(
value: ratio,
minHeight: 4,
backgroundColor: Colors.white12,
valueColor: AlwaysStoppedAnimation<Color>(
ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6),
),
);
},
),
),
],
],
),
),
),
const SizedBox(width: 8),
_epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)),
],
@@ -1169,20 +1229,34 @@ class _ContributeScreenState extends State<ContributeScreen>
// Badge icon colors
final iconColors = <String, Color>{
'edit': const Color(0xFF3B82F6),
'star': const Color(0xFFF59E0B),
'emoji_events': const Color(0xFFF97316),
'leaderboard': const Color(0xFF8B5CF6),
'photo_library': const Color(0xFF6B7280),
'verified': const Color(0xFF10B981),
'edit': const Color(0xFF3B82F6),
'star': const Color(0xFFF59E0B),
'emoji_events': const Color(0xFFF97316),
'leaderboard': const Color(0xFF8B5CF6),
'photo_library': const Color(0xFF6B7280),
'verified': const Color(0xFF10B981),
// ACH-002: colors for expanded badge set
'trending_up': const Color(0xFF0EA5E9),
'rocket_launch': const Color(0xFFEC4899),
'event_hunter': const Color(0xFF64748B),
'location_on': const Color(0xFF22C55E),
'diamond': const Color(0xFF06B6D4),
'workspace_premium': const Color(0xFFE879F9),
};
final bgColors = <String, Color>{
'edit': const Color(0xFFDBEAFE),
'star': const Color(0xFFFEF3C7),
'emoji_events': const Color(0xFFFED7AA),
'leaderboard': const Color(0xFFEDE9FE),
'photo_library': const Color(0xFFF3F4F6),
'verified': const Color(0xFFD1FAE5),
'edit': const Color(0xFFDBEAFE),
'star': const Color(0xFFFEF3C7),
'emoji_events': const Color(0xFFFED7AA),
'leaderboard': const Color(0xFFEDE9FE),
'photo_library': const Color(0xFFF3F4F6),
'verified': const Color(0xFFD1FAE5),
// ACH-002: backgrounds for expanded badge set
'trending_up': const Color(0xFFE0F2FE),
'rocket_launch': const Color(0xFFFCE7F3),
'event_hunter': const Color(0xFFF1F5F9),
'location_on': const Color(0xFFDCFCE7),
'diamond': const Color(0xFFCFFAFE),
'workspace_premium': const Color(0xFFFAE8FF),
};
final iconColor = isUnlocked ? (iconColors[badge.iconName] ?? const Color(0xFF6B7280)) : const Color(0xFF9CA3AF);
@@ -1520,9 +1594,10 @@ class _ContributeScreenState extends State<ContributeScreen>
// ═══════════════════════════════════════════════════════════════════════════
Widget _buildContributeTab(BuildContext context, GamificationProvider provider) {
final theme = Theme.of(context);
final bottomInset = MediaQuery.of(context).padding.bottom;
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 20, 16, 32),
padding: EdgeInsets.fromLTRB(16, 20, 16, 32 + bottomInset),
child: Form(
key: _formKey,
child: Column(
@@ -2021,6 +2096,26 @@ class _ContributeScreenState extends State<ContributeScreen>
// ── Time period toggle (top-right) + district scroll ──────────────────
_buildLeaderboardFilters(provider),
// LDR-003: Current user stats card at top of leaderboard
if (provider.currentUserStats != null)
Builder(builder: (context) {
final stats = provider.currentUserStats!;
return GlassCard(
margin: const EdgeInsets.fromLTRB(16, 8, 16, 4),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatChip('Rank', '#${stats.rank}', Icons.leaderboard),
Container(width: 1, height: 32, color: Colors.white12),
_buildStatChip('EP', '${stats.points}', Icons.bolt),
Container(width: 1, height: 32, color: Colors.white12),
_buildStatChip('Cycle', '${stats.rewardCycleDays}d', Icons.timelapse),
],
),
);
}),
Expanded(
child: Container(
color: const Color(0xFFFAFBFC),
@@ -2183,29 +2278,16 @@ class _ContributeScreenState extends State<ContributeScreen>
return Expanded(
child: Column(
children: [
// Avatar with rank badge overlaid
// GAM-006: Avatar with tier ring + rank badge overlaid
Stack(
clipBehavior: Clip.none,
children: [
// Avatar circle
Container(
width: avatarSize,
height: avatarSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFE0F2FE),
border: Border.all(color: pillarColors[i], width: 2.5),
),
child: Center(
child: Text(
e.username.isNotEmpty ? e.username[0].toUpperCase() : '?',
style: TextStyle(
fontSize: i == 1 ? 24 : 18,
fontWeight: FontWeight.w800,
color: pillarColors[i],
),
),
),
// TierAvatarRing — tier-coloured glow ring
TierAvatarRing(
username: e.username,
tier: tierLabel(e.tier),
size: avatarSize,
imageUrl: e.avatarUrl,
),
// Rank badge — bottom-right corner of avatar
Positioned(
@@ -2267,7 +2349,17 @@ class _ContributeScreenState extends State<ContributeScreen>
final tierColor = _tierColors[entry.tier]!;
final isMe = entry.isCurrentUser;
return Container(
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ContributorProfileScreen(
contributorId: entry.username,
contributorName: entry.username,
),
),
),
child: Container(
decoration: BoxDecoration(
color: isMe ? const Color(0xFFEFF6FF) : Colors.white,
border: const Border(bottom: BorderSide(color: Color(0xFFE5E7EB), width: 0.8)),
@@ -2287,21 +2379,12 @@ class _ContributeScreenState extends State<ContributeScreen>
),
),
),
// Avatar circle
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFE0F2FE),
border: Border.all(color: tierColor.withOpacity(0.5), width: 1.5),
),
child: Center(
child: Text(
entry.username.isNotEmpty ? entry.username[0].toUpperCase() : '?',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: tierColor),
),
),
// GAM-006: TierAvatarRing replaces plain avatar circle
TierAvatarRing(
username: entry.username,
tier: tierLabel(entry.tier),
size: 36,
imageUrl: entry.avatarUrl,
),
const SizedBox(width: 8),
// Name
@@ -2360,6 +2443,7 @@ class _ContributeScreenState extends State<ContributeScreen>
),
],
),
),
);
}
@@ -2386,10 +2470,19 @@ class _ContributeScreenState extends State<ContributeScreen>
),
GestureDetector(
onTap: () {
Share.share(
'I\'m ranked #${me.rank} on @EventifyPlus with ${me.lifetimeEp} EP! 🏆 '
'Discover & contribute to events near you at eventifyplus.com',
subject: 'My Eventify.Plus Leaderboard Rank',
final gam = context.read<GamificationProvider>();
showDialog(
context: context,
builder: (_) => Dialog(
backgroundColor: Colors.transparent,
child: ShareRankCard(
username: me.username,
tier: tierLabel(me.tier),
rank: me.rank,
ep: me.lifetimeEp,
rewardPoints: gam.profile?.currentRp ?? 0,
),
),
);
},
child: Container(
@@ -2410,6 +2503,19 @@ class _ContributeScreenState extends State<ContributeScreen>
);
}
// LDR-003: Stat chip helper for current-user leaderboard card
Widget _buildStatChip(String label, String value, IconData icon) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: const Color(0xFF94A3B8)),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)),
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF64748B))),
],
);
}
// ═══════════════════════════════════════════════════════════════════════════
// TAB 2 — ACHIEVEMENTS
// ═══════════════════════════════════════════════════════════════════════════
@@ -2421,9 +2527,10 @@ class _ContributeScreenState extends State<ContributeScreen>
}
final badges = provider.achievements;
final bottomInset = MediaQuery.of(context).padding.bottom;
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
padding: EdgeInsets.fromLTRB(16, 16, 16, 32 + bottomInset),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [