// lib/screens/leaderboard_screen.dart // Dedicated Leaderboard screen for the Eventify Contributor Module. import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; // ───────────────────────────────────────────────────────────────────────────── // Design tokens // ───────────────────────────────────────────────────────────────────────────── const _blue = Color(0xFF0F45CF); const _darkText = Color(0xFF1E293B); const _subText = Color(0xFF94A3B8); const _border = Color(0xFFE2E8F0); const _lightBlueBg = Color(0xFFEFF6FF); const _green = Color(0xFF10B981); const _tierColors = { ContributorTier.BRONZE: Color(0xFFCD7F32), ContributorTier.SILVER: Color(0xFFC0C0C0), ContributorTier.GOLD: Color(0xFFFFD700), ContributorTier.PLATINUM: Color(0xFFE5E4E2), ContributorTier.DIAMOND: Color(0xFFB9F2FF), }; const _districts = [ 'Overall Kerala', 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', ]; class LeaderboardScreen extends StatefulWidget { const LeaderboardScreen({super.key}); @override State createState() => _LeaderboardScreenState(); } class _LeaderboardScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); if (provider.leaderboard.isEmpty) { provider.loadLeaderboard(); } }); } @override Widget build(BuildContext context) { return Consumer( builder: (context, provider, _) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( bottom: false, child: Column( children: [ _buildAppBar(context), Expanded( child: SingleChildScrollView( child: Column( children: [ // 1. User stat cards if (provider.currentUserStats != null) _buildStatCards(provider.currentUserStats!), // 2. Time period toggle const SizedBox(height: 16), _buildPeriodToggle(provider), // 3. District chips const SizedBox(height: 16), _buildDistrictChips(provider), const SizedBox(height: 20), // 4–5. Content area _buildContent(provider), const SizedBox(height: 100), ], ), ), ), ], ), ), ); }, ); } // ═══════════════════════════════════════════════════════════════════════════ // APP BAR // ═══════════════════════════════════════════════════════════════════════════ Widget _buildAppBar(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: _border.withValues(alpha: 0.5))), ), child: Row( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), child: const Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: _darkText), ), const SizedBox(width: 12), const Expanded( child: Text( 'Leaderboard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _darkText), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: _lightBlueBg, borderRadius: BorderRadius.circular(12), ), child: Consumer( builder: (_, p, __) => Text( '${p.totalParticipants} contributors', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: _blue), ), ), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // STAT CARDS (Your Rank / Total Points / Reward Cycle) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildStatCards(CurrentUserStats stats) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( children: [ _statCard( icon: Icons.emoji_events_rounded, iconColor: const Color(0xFFEAB308), label: 'Your Rank', value: stats.rank > 0 ? '#${stats.rank}' : '--', ), const SizedBox(width: 10), _statCard( icon: Icons.bolt_rounded, iconColor: _blue, label: 'Total Points', value: stats.points > 0 ? '${stats.points}' : '--', ), const SizedBox(width: 10), _statCard( icon: Icons.timer_outlined, iconColor: _green, label: 'Reward Cycle', value: stats.rewardCycleDays > 0 ? '${stats.rewardCycleDays} Days' : '--', ), ], ), ); } Widget _statCard({ required IconData icon, required Color iconColor, required String label, required String value, }) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: _border), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), ], ), child: Column( children: [ Icon(icon, size: 22, color: iconColor), const SizedBox(height: 6), Text( value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _darkText), ), const SizedBox(height: 2), Text( label, style: const TextStyle(fontSize: 11, color: _subText, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), ], ), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // PERIOD TOGGLE (All Time / This Month) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildPeriodToggle(GamificationProvider provider) { final current = provider.leaderboardTimePeriod; return Center( child: Container( height: 44, width: 280, margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(22), ), child: Stack( children: [ AnimatedAlign( duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, alignment: current == 'all_time' ? Alignment.centerLeft : Alignment.centerRight, child: Container( width: 140, height: 36, margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: _blue, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow(color: _blue.withValues(alpha: 0.3), blurRadius: 6, offset: const Offset(0, 2)), ], ), ), ), Row( children: [ _periodButton('All Time', 'all_time', current, provider), _periodButton('This Month', 'this_month', current, provider), ], ), ], ), ), ); } Widget _periodButton(String label, String value, String current, GamificationProvider provider) { final isActive = current == value; return Expanded( child: GestureDetector( onTap: () => provider.setTimePeriod(value), behavior: HitTestBehavior.opaque, child: Center( child: Text( label, style: TextStyle( color: isActive ? Colors.white : _subText, fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // DISTRICT CHIPS // ═══════════════════════════════════════════════════════════════════════════ Widget _buildDistrictChips(GamificationProvider provider) { final selected = provider.leaderboardDistrict; return SizedBox( height: 36, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _districts.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, index) { final d = _districts[index]; final isSelected = selected == d; return GestureDetector( onTap: () => provider.setDistrict(d), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: isSelected ? _blue : Colors.white, borderRadius: BorderRadius.circular(18), border: Border.all(color: isSelected ? _blue : _border), ), alignment: Alignment.center, child: Text( d, style: TextStyle( color: isSelected ? Colors.white : _darkText, fontSize: 13, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), ); }, ), ); } // ═══════════════════════════════════════════════════════════════════════════ // CONTENT AREA (loading / empty / podium + list) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildContent(GamificationProvider provider) { final leaderboard = provider.leaderboard; final isLoading = provider.isLoading || provider.isLeaderboardLoading; // Loading shimmer if (isLoading) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: Column( children: List.generate(5, (i) => _shimmerRow(i)), ), ); } // Empty state if (leaderboard.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 80), child: Center( child: Column( children: [ Icon(Icons.emoji_events_outlined, size: 56, color: Colors.grey.shade300), const SizedBox(height: 16), Text( 'No contributors yet', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey.shade400), ), const SizedBox(height: 6), Text( 'Be the first to contribute events!', style: TextStyle(fontSize: 13, color: Colors.grey.shade400), ), ], ), ), ); } // Has data final period = provider.leaderboardTimePeriod; final hasPodium = leaderboard.length >= 3; return Column( children: [ // Podium (top 3) if (hasPodium) _buildPodium(leaderboard.sublist(0, 3), period), // Remaining entries (rank 4+, or all if < 3) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: hasPodium ? leaderboard.length - 3 : leaderboard.length, separatorBuilder: (_, __) => Divider(height: 1, color: _border.withValues(alpha: 0.5)), itemBuilder: (_, index) { final entry = hasPodium ? leaderboard[index + 3] : leaderboard[index]; return _buildListRow(entry, period); }, ), ), ], ); } // ═══════════════════════════════════════════════════════════════════════════ // SHIMMER SKELETON // ═══════════════════════════════════════════════════════════════════════════ Widget _shimmerRow(int index) { return Padding( padding: EdgeInsets.only(bottom: index < 4 ? 12 : 0), child: Row( children: [ _shimmerBox(24, 24, radius: 4), const SizedBox(width: 12), _shimmerBox(40, 40, radius: 20), const SizedBox(width: 12), Expanded(child: _shimmerBox(16, double.infinity)), const SizedBox(width: 12), _shimmerBox(16, 50), ], ), ); } Widget _shimmerBox(double height, double width, {double radius = 6}) { return Container( height: height, width: width, decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(radius), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // PODIUM (Rank 1–3) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildPodium(List top3, String period) { // Display order: [rank 2, rank 1, rank 3] final first = top3[0]; final second = top3[1]; final third = top3[2]; return Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Rank 2 (left) Expanded(child: _podiumColumn(second, period, height: 100, medal: '🥈')), const SizedBox(width: 8), // Rank 1 (center, tallest) Expanded(child: _podiumColumn(first, period, height: 130, medal: '🥇')), const SizedBox(width: 8), // Rank 3 (right) Expanded(child: _podiumColumn(third, period, height: 80, medal: '🥉')), ], ), ); } Widget _podiumColumn(LeaderboardEntry entry, String period, {required double height, required String medal}) { final isMonthly = period == 'this_month'; final displayPts = isMonthly ? entry.monthlyPoints : entry.lifetimeEp; final tierColor = _tierColors[entry.tier] ?? _tierColors[ContributorTier.BRONZE]!; return Column( mainAxisSize: MainAxisSize.min, children: [ // Medal Text(medal, style: const TextStyle(fontSize: 24)), const SizedBox(height: 4), // Avatar Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: tierColor, width: 2.5), boxShadow: [ BoxShadow(color: tierColor.withValues(alpha: 0.3), blurRadius: 8), ], ), child: CircleAvatar( radius: height == 130 ? 28 : 22, backgroundColor: _lightBlueBg, backgroundImage: entry.avatarUrl != null ? NetworkImage(entry.avatarUrl!) : null, child: entry.avatarUrl == null ? Icon(Icons.person_rounded, color: _blue, size: height == 130 ? 28 : 22) : null, ), ), const SizedBox(height: 8), // Name Text( entry.username, style: TextStyle( fontSize: height == 130 ? 13 : 12, fontWeight: FontWeight.w600, color: _darkText, ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), const SizedBox(height: 2), // Tier badge _tierBadge(entry.tier, small: height != 130), const SizedBox(height: 6), // Podium block Container( height: height, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ _blue.withValues(alpha: 0.08), _blue.withValues(alpha: 0.03), ], ), borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), border: Border.all(color: _blue.withValues(alpha: 0.1)), ), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '#${entry.rank}', style: TextStyle( fontSize: height == 130 ? 22 : 18, fontWeight: FontWeight.w800, color: _blue, ), ), const SizedBox(height: 4), Text( '$displayPts EP', style: TextStyle( fontSize: height == 130 ? 14 : 12, fontWeight: FontWeight.w600, color: _green, ), ), ], ), ), ], ); } // ═══════════════════════════════════════════════════════════════════════════ // LIST ROW (Rank 4+) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildListRow(LeaderboardEntry entry, String period) { final isMonthly = period == 'this_month'; final displayPts = isMonthly ? entry.monthlyPoints : entry.lifetimeEp; final ptsLabel = isMonthly ? 'mo.' : 'EP'; final isMe = entry.isCurrentUser; return Container( color: isMe ? _lightBlueBg : Colors.transparent, padding: EdgeInsets.symmetric(vertical: 12, horizontal: isMe ? 12 : 0), child: Row( children: [ // Rank SizedBox( width: 32, child: Text( '${entry.rank}', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: entry.rank <= 3 ? _blue : _subText, ), ), ), const SizedBox(width: 8), // Avatar CircleAvatar( radius: 18, backgroundColor: _lightBlueBg, backgroundImage: entry.avatarUrl != null ? NetworkImage(entry.avatarUrl!) : null, child: entry.avatarUrl == null ? const Icon(Icons.person_outline, color: _blue, size: 18) : null, ), const SizedBox(width: 10), // Name + district Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.username, style: TextStyle( fontSize: 14, fontWeight: isMe ? FontWeight.w600 : FontWeight.normal, color: _darkText, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (entry.district != null) Text( entry.district!, style: const TextStyle(fontSize: 11, color: _subText), ), ], ), ), // Tier badge _tierBadge(entry.tier, small: true), const SizedBox(width: 10), // Points Text( '$displayPts $ptsLabel', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _green, ), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // TIER BADGE // ═══════════════════════════════════════════════════════════════════════════ Widget _tierBadge(ContributorTier tier, {bool small = false}) { final color = _tierColors[tier] ?? _tierColors[ContributorTier.BRONZE]!; final label = tierLabel(tier); return Container( padding: EdgeInsets.symmetric(horizontal: small ? 6 : 8, vertical: small ? 2 : 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withValues(alpha: 0.4), width: 0.5), ), child: Text( label, style: TextStyle( fontSize: small ? 10 : 11, fontWeight: FontWeight.w600, color: tier == ContributorTier.PLATINUM || tier == ContributorTier.SILVER ? const Color(0xFF64748B) : color.computeLuminance() > 0.5 ? const Color(0xFF64748B) : color, ), ), ); } }