import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; import '../models/hunt_card.dart'; class LeaderboardWidget extends StatefulWidget { final List entries; final int userPoints; final VoidCallback onClose; final VoidCallback? onRefresh; const LeaderboardWidget({ super.key, required this.entries, required this.userPoints, required this.onClose, this.onRefresh, }); @override State createState() => _LeaderboardWidgetState(); } class _LeaderboardWidgetState extends State with TickerProviderStateMixin { late AnimationController _slideController; late AnimationController _itemController; late AnimationController _particleController; late AnimationController _trophyController; late Animation _slideAnimation; late List> _itemAnimations; late Animation _particleAnimation; late Animation _trophyGlowAnimation; String _searchQuery = ''; bool _showSearch = false; @override void initState() { super.initState(); _slideController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); _itemController = AnimationController( duration: const Duration(milliseconds: 1200), vsync: this, ); _particleController = AnimationController( duration: const Duration(milliseconds: 3000), vsync: this, ); _trophyController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); _slideAnimation = Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate( CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), ); _itemAnimations = List.generate( widget.entries.length, (index) => Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _itemController, curve: Interval( (index * 0.08).clamp(0.0, 0.3), ((index * 0.08) + 0.7).clamp(0.1, 1.0), curve: Curves.elasticOut, ), ), ), ); _particleAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _particleController, curve: Curves.linear), ); _trophyGlowAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _trophyController, curve: Curves.easeInOut), ); _slideController.forward(); _itemController.forward(); _particleController.repeat(); _trophyController.repeat(reverse: true); } @override void dispose() { _slideController.dispose(); _itemController.dispose(); _particleController.dispose(); _trophyController.dispose(); super.dispose(); } Future _close() async { await _slideController.reverse(); widget.onClose(); } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final statusBarHeight = MediaQuery.of(context).padding.top; final bottomPadding = MediaQuery.of(context).padding.bottom; final availableHeight = screenHeight - statusBarHeight - bottomPadding - 100; return SlideTransition( position: _slideAnimation, child: Container( height: availableHeight, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppColors.surface, AppColors.cardBackground.withOpacity(0.8), AppColors.surface, ], stops: const [0.0, 0.5, 1.0], ), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( color: AppColors.shadowColor.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, -10), spreadRadius: 2, ), ], ), child: Stack( children: [ AnimatedBuilder( animation: _particleAnimation, builder: (context, child) => CustomPaint( painter: ParticlesPainter( animation: _particleAnimation, color: AppColors.primary.withOpacity(0.1), ), size: Size.infinite, ), ), Column( children: [ // Handle bar Container( margin: const EdgeInsets.symmetric(vertical: 4), height: 3, width: 32, decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.3), AppColors.primary, AppColors.primary.withOpacity(0.3), ], ), borderRadius: BorderRadius.circular(2), ), ), _buildCompactHeader(), if (widget.entries.length > 10) _buildQuickSearch(), if (widget.entries.length >= 3) _buildPodium(), Expanded(child: _buildParticipantsList()), ], ), ], ), ), ); } Widget _buildCompactHeader() { return Padding( padding: const EdgeInsets.fromLTRB(20, 2, 20, 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary, AppColors.primary.withOpacity(0.7), ], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: SvgPicture.asset( Assets.icons.cup.path, color: Colors.white, ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Hunt Leaderboard', style: TextStyle( color: AppColors.textPrimary, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: -0.3, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.confirmButton.withOpacity(0.2), AppColors.confirmButton.withOpacity(0.1), ], ), borderRadius: BorderRadius.circular(8), border: Border.all( color: AppColors.confirmButton.withOpacity(0.3), width: 0.5, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: AppColors.confirmButton, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( '${widget.entries.length}', style: TextStyle( color: AppColors.confirmButton, fontSize: 10, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), Text( 'Live ranking updates', style: TextStyle( color: AppColors.textSecondary, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ], ), IconButton( onPressed: _close, padding: const EdgeInsets.all(4), icon: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: AppColors.shadowColor.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Icon( Icons.close_rounded, color: AppColors.textSecondary, size: 20, ), ), ), ], ), ); } String _formatNumber(int number) { if (number >= 1000) { return '${(number / 1000).toStringAsFixed(1)}k'; } return number.toString(); } String _getShortName(String fullName) { if (fullName.length <= 10) return fullName; final parts = fullName.split(' '); if (parts.length > 1) { final firstName = parts[0]; final lastInitial = parts[1].isNotEmpty ? parts[1][0] : ''; if (firstName.length <= 8) { return '$firstName $lastInitial.'; } return '${firstName.substring(0, 8)}...'; } return '${fullName.substring(0, 8)}...'; } Widget _buildQuickSearch() { return AnimatedContainer( duration: const Duration(milliseconds: 300), height: _showSearch ? 60 : 40, margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.primary.withOpacity(0.1), width: 1), ), child: Column( children: [ Row( children: [ Icon(Icons.search_rounded, color: AppColors.primary, size: 20), const SizedBox(width: 8), Expanded( child: _showSearch ? TextField( onChanged: (value) => setState(() => _searchQuery = value), style: TextStyle( color: AppColors.textPrimary, fontSize: 14, ), decoration: InputDecoration( hintText: 'Search hunters...', hintStyle: TextStyle( color: AppColors.textSecondary, fontSize: 14, ), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), ) : GestureDetector( onTap: () => setState(() => _showSearch = true), child: Text( 'Search in ${widget.entries.length} hunters', style: TextStyle( color: AppColors.textSecondary, fontSize: 14, ), ), ), ), if (_showSearch) IconButton( onPressed: () => setState(() { _showSearch = false; _searchQuery = ''; }), icon: Icon( Icons.close_rounded, color: AppColors.textSecondary, size: 18, ), padding: const EdgeInsets.all(4), constraints: const BoxConstraints( minWidth: 24, minHeight: 24, ), ), ], ), if (_showSearch && _searchQuery.isNotEmpty) Expanded( child: Text( '${_getFilteredEntries().length} results found', style: TextStyle( color: AppColors.primary, fontSize: 11, fontWeight: FontWeight.w500, ), ), ), ], ), ); } List _getFilteredEntries() { if (_searchQuery.isEmpty) return widget.entries; return widget.entries .where( (entry) => entry.name.toLowerCase().contains(_searchQuery.toLowerCase()), ) .toList(); } Widget _buildListView(List remainingParticipants) { return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: remainingParticipants.length, itemExtent: 70, itemBuilder: (context, index) { final entry = remainingParticipants[index]; final actualIndex = index + 3; return AnimatedBuilder( animation: _itemAnimations.length > actualIndex ? _itemAnimations[actualIndex] : AlwaysStoppedAnimation(1.0), builder: (context, child) { final animation = _itemAnimations.length > actualIndex ? _itemAnimations[actualIndex] : AlwaysStoppedAnimation(1.0); return Transform.translate( offset: Offset(0, 20 * (1 - animation.value)), child: Opacity( opacity: animation.value.clamp(0.0, 1.0), child: _buildCompactLeaderboardItem(entry, actualIndex + 1), ), ); }, ); }, ); } Widget _buildPodium() { final topThree = widget.entries.take(3).toList(); return Container( margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), height: 150, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (topThree.length > 1) _buildPodiumPlace(topThree[1], 2, 105), _buildPodiumPlace(topThree[0], 1, 130), if (topThree.length > 2) _buildPodiumPlace(topThree[2], 3, 85), ], ), ); } Widget _buildPodiumPlace(LeaderboardEntry entry, int place, double height) { final colors = _getPodiumColors(place); return AnimatedBuilder( animation: _trophyGlowAnimation, builder: (context, child) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( padding: const EdgeInsets.all(4), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: colors['primary'], shape: BoxShape.circle, ), child: Text( '#$place', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 12), Expanded( child: Row( children: [ Expanded( child: Text( entry.name, style: TextStyle( color: AppColors.textPrimary, fontWeight: FontWeight.bold, fontSize: 15, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( '${entry.totalPoints}', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: Text( entry.avatar, style: const TextStyle(fontSize: 18), ), ), ], ), ), backgroundColor: AppColors.surface, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 8, margin: const EdgeInsets.all(16), duration: const Duration(seconds: 2), ), ); }, child: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ if (place == 1) ...[ Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ colors['primary']!.withOpacity(0.8), colors['primary']!, ], ), boxShadow: [ BoxShadow( color: colors['primary']!.withOpacity( 0.3 + (_trophyGlowAnimation.value * 0.2), ), blurRadius: 10 + (_trophyGlowAnimation.value * 5), spreadRadius: 1, ), ], ), child: Icon( Icons.emoji_events_rounded, color: Colors.white, size: 12, ), ), const SizedBox(height: 3), ] else const SizedBox(height: 3), Container( width: 70, height: height, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [colors['light']!, colors['primary']!], ), borderRadius: const BorderRadius.vertical( top: Radius.circular(10), ), boxShadow: [ BoxShadow( color: colors['primary']!.withOpacity(0.25), blurRadius: 6, offset: const Offset(0, 3), ), ], ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 1, vertical: 1, ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 24, height: 24, child: Center( child: Text( entry.avatar, style: const TextStyle(fontSize: 10), ), ), ), SizedBox( height: place == 1 ? 14 : 12, child: Center( child: Text( _getShortName(entry.name), style: TextStyle( color: place == 1 ? Colors.black : Colors.white, fontSize: place == 1 ? 12 : 10, fontWeight: place == 1 ? FontWeight.w900 : FontWeight.bold, height: 1.0, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), Text( _formatNumber(entry.totalPoints), style: TextStyle( color: place == 1 ? Colors.black : Colors.white, fontSize: place == 1 ? 13 : 11, fontWeight: place == 1 ? FontWeight.w900 : FontWeight.bold, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.3), borderRadius: BorderRadius.circular(6), ), child: Text( '#$place', style: TextStyle( color: place == 1 ? Colors.black : Colors.white, fontSize: place == 1 ? 10 : 9, fontWeight: place == 1 ? FontWeight.w900 : FontWeight.bold, ), ), ), ], ), ), ), ], ), ); }, ); } Map _getPodiumColors(int place) { switch (place) { case 1: return { 'primary': const Color(0xFFFFD700), 'light': const Color(0xFFFFFACD), }; case 2: return { 'primary': const Color(0xFFC0C0C0), 'light': const Color(0xFFE8E8E8), }; case 3: return { 'primary': const Color(0xFFCD7F32), 'light': const Color(0xFFDEB887), }; default: return { 'primary': AppColors.primary, 'light': AppColors.primary.withOpacity(0.3), }; } } Widget _buildParticipantsList() { final filteredEntries = _getFilteredEntries(); final remainingParticipants = filteredEntries.length > 3 ? filteredEntries.skip(3).toList() : []; if (_searchQuery.isNotEmpty) { final allFiltered = filteredEntries.toList(); if (allFiltered.isEmpty) { return Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search_off_rounded, color: AppColors.textSecondary, size: 48, ), const SizedBox(height: 16), Text( 'No hunters found', style: TextStyle( color: AppColors.textSecondary, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( 'Try a different search term', style: TextStyle( color: AppColors.textSecondary, fontSize: 12, ), ), ], ), ), ); } remainingParticipants.clear(); remainingParticipants.addAll(allFiltered); } if (remainingParticipants.isEmpty) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _searchQuery.isNotEmpty ? 'Search Results' : 'Other Participants', style: TextStyle( color: AppColors.textPrimary, fontSize: 16, fontWeight: FontWeight.bold, ), ), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Text( '${widget.entries.length} hunters', style: TextStyle( color: AppColors.primary, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ), ], ), ), Expanded( child: widget.onRefresh != null ? RefreshIndicator( onRefresh: () async { widget.onRefresh?.call(); await Future.delayed(const Duration(milliseconds: 500)); }, color: AppColors.primary, backgroundColor: AppColors.surface, child: _buildListView(remainingParticipants), ) : _buildListView(remainingParticipants), ), SizedBox(height: MediaQuery.of(context).padding.bottom + 20), ], ); } Widget _buildCompactLeaderboardItem(LeaderboardEntry entry, int index) { final isCurrentUser = entry.isCurrentUser; return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( gradient: isCurrentUser ? LinearGradient( colors: [ AppColors.primary.withOpacity(0.08), AppColors.primary.withOpacity(0.04), ], ) : null, color: isCurrentUser ? null : AppColors.cardBackground, borderRadius: BorderRadius.circular(14), border: isCurrentUser ? Border.all( color: AppColors.primary.withOpacity(0.2), width: 1, ) : null, boxShadow: [ BoxShadow( color: isCurrentUser ? AppColors.primary.withOpacity(0.08) : AppColors.shadowColor.withOpacity(0.03), blurRadius: isCurrentUser ? 8 : 3, offset: const Offset(0, 1), ), ], ), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppColors.cardBackground, shape: BoxShape.circle, border: Border.all( color: AppColors.divider.withOpacity(0.3), width: 0.8, ), ), child: Center( child: Text( '#$index', style: TextStyle( color: AppColors.textPrimary, fontWeight: FontWeight.bold, fontSize: 12, ), ), ), ), const SizedBox(width: 12), Container( width: 36, height: 36, decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.15), AppColors.primary.withOpacity(0.08), ], ), shape: BoxShape.circle, border: Border.all( color: AppColors.primary.withOpacity(0.2), width: 0.8, ), ), child: Center( child: Text(entry.avatar, style: const TextStyle(fontSize: 18)), ), ), const SizedBox(width: 12), Expanded( child: Row( children: [ Expanded( child: Text( entry.name.length > 12 ? '${entry.name.substring(0, 12)}...' : entry.name, style: TextStyle( color: AppColors.textPrimary, fontSize: 14, fontWeight: isCurrentUser ? FontWeight.bold : FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (isCurrentUser) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 1, ), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(8), ), child: const Text( 'You', style: TextStyle( color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 8), ], Text( '${_formatNumber(entry.totalPoints)}', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), if (entry.totalPoints > 500) Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.confirmButton.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( Icons.local_fire_department_rounded, color: AppColors.confirmButton, size: 14, ), ), ], ), ); } } class ParticlesPainter extends CustomPainter { final Animation animation; final Color color; ParticlesPainter({required this.animation, required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; final particleCount = 20; for (int i = 0; i < particleCount; i++) { final progress = (animation.value + (i / particleCount)) % 1.0; final x = (i * 37.0) % size.width; final y = size.height * progress; final radius = (2.0 + (i % 3)) * (1.0 - progress); paint.color = color.withOpacity((1.0 - progress) * 0.3); canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }