diff --git a/assets/icons/cup.svg b/assets/icons/cup.svg new file mode 100644 index 0000000..7f3dff8 --- /dev/null +++ b/assets/icons/cup.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/info-circle.svg b/assets/icons/info-circle.svg new file mode 100644 index 0000000..c828515 --- /dev/null +++ b/assets/icons/info-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/location-tick.svg b/assets/icons/location-tick.svg new file mode 100644 index 0000000..4b5b969 --- /dev/null +++ b/assets/icons/location-tick.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/medal-star.svg b/assets/icons/medal-star.svg new file mode 100644 index 0000000..de8f27b --- /dev/null +++ b/assets/icons/medal-star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/radar-2.svg b/assets/icons/radar-2.svg new file mode 100644 index 0000000..b3c46e3 --- /dev/null +++ b/assets/icons/radar-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ranking.svg b/assets/icons/ranking.svg new file mode 100644 index 0000000..c334a86 --- /dev/null +++ b/assets/icons/ranking.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/search-normal.svg b/assets/icons/search-normal.svg new file mode 100644 index 0000000..81b836e --- /dev/null +++ b/assets/icons/search-normal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/timer_Hunt.svg b/assets/icons/timer_Hunt.svg new file mode 100644 index 0000000..18a8f26 --- /dev/null +++ b/assets/icons/timer_Hunt.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 05059b7..eabf92f 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -135,6 +135,9 @@ class $AssetsIconsGen { /// File path: assets/icons/coin.svg SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg'); + /// File path: assets/icons/cup.svg + SvgGenImage get cup => const SvgGenImage('assets/icons/cup.svg'); + /// File path: assets/icons/current loc.svg SvgGenImage get currentLoc => const SvgGenImage('assets/icons/current loc.svg'); @@ -227,6 +230,10 @@ class $AssetsIconsGen { SvgGenImage get icRoundLocalOffer => const SvgGenImage('assets/icons/ic_round-local-offer.svg'); + /// File path: assets/icons/info-circle.svg + SvgGenImage get infoCircle => + const SvgGenImage('assets/icons/info-circle.svg'); + /// File path: assets/icons/infoPic.svg SvgGenImage get infoPic => const SvgGenImage('assets/icons/infoPic.svg'); @@ -251,6 +258,10 @@ class $AssetsIconsGen { /// File path: assets/icons/list.svg SvgGenImage get list => const SvgGenImage('assets/icons/list.svg'); + /// File path: assets/icons/location-tick.svg + SvgGenImage get locationTick => + const SvgGenImage('assets/icons/location-tick.svg'); + /// File path: assets/icons/location.svg SvgGenImage get location => const SvgGenImage('assets/icons/location.svg'); @@ -296,6 +307,9 @@ class $AssetsIconsGen { SvgGenImage get materialSymbolsLocationWork => const SvgGenImage('assets/icons/material-symbols_location-work.svg'); + /// File path: assets/icons/medal-star.svg + SvgGenImage get medalStar => const SvgGenImage('assets/icons/medal-star.svg'); + /// File path: assets/icons/message-question.svg SvgGenImage get messageQuestion => const SvgGenImage('assets/icons/message-question.svg'); @@ -344,6 +358,12 @@ class $AssetsIconsGen { /// File path: assets/icons/profile.svg SvgGenImage get profile => const SvgGenImage('assets/icons/profile.svg'); + /// File path: assets/icons/radar-2.svg + SvgGenImage get radar2 => const SvgGenImage('assets/icons/radar-2.svg'); + + /// File path: assets/icons/ranking.svg + SvgGenImage get ranking => const SvgGenImage('assets/icons/ranking.svg'); + /// File path: assets/icons/receipt-discount 2.svg SvgGenImage get receiptDiscount2 => const SvgGenImage('assets/icons/receipt-discount 2.svg'); @@ -365,6 +385,10 @@ class $AssetsIconsGen { /// File path: assets/icons/routing.svg SvgGenImage get routing => const SvgGenImage('assets/icons/routing.svg'); + /// File path: assets/icons/search-normal.svg + SvgGenImage get searchNormal => + const SvgGenImage('assets/icons/search-normal.svg'); + /// File path: assets/icons/selected list.svg SvgGenImage get selectedList => const SvgGenImage('assets/icons/selected list.svg'); @@ -427,6 +451,9 @@ class $AssetsIconsGen { /// File path: assets/icons/timer.svg SvgGenImage get timer => const SvgGenImage('assets/icons/timer.svg'); + /// File path: assets/icons/timer_Hunt.svg + SvgGenImage get timerHunt => const SvgGenImage('assets/icons/timer_Hunt.svg'); + /// File path: assets/icons/usa circle.svg SvgGenImage get usaCircle => const SvgGenImage('assets/icons/usa circle.svg'); @@ -467,6 +494,7 @@ class $AssetsIconsGen { clander, clock, coin, + cup, currentLoc, deliveryOff, deliveryOn, @@ -492,6 +520,7 @@ class $AssetsIconsGen { hugeiconsBabyBoyDress, hugeiconsCheeseCake01, icRoundLocalOffer, + infoCircle, infoPic, ionFastFoodOutline, iranCircle, @@ -499,6 +528,7 @@ class $AssetsIconsGen { like, link2, list, + locationTick, location, locationPopup, logout, @@ -511,6 +541,7 @@ class $AssetsIconsGen { materialSymbolsLocationOn, materialSymbolsLocationOnn, materialSymbolsLocationWork, + medalStar, messageQuestion, microphone2, nearby, @@ -525,12 +556,15 @@ class $AssetsIconsGen { pickupOn, profile2, profile, + radar2, + ranking, receiptDiscount2, receiptDiscount, recenter, riSearch2Line, routing2, routing, + searchNormal, selectedList, shield, shoppingCart, @@ -549,6 +583,7 @@ class $AssetsIconsGen { timerPause, timerStart, timer, + timerHunt, usaCircle, ]; } diff --git a/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md b/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md new file mode 100644 index 0000000..a6ca04a --- /dev/null +++ b/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md @@ -0,0 +1,208 @@ +# 🏆 Hunt Leaderboard - Enhanced UI Documentation + +## Overview +The Hunt Leaderboard has been completely redesigned with a modern, engaging, and visually attractive interface that provides an exceptional user experience for the Rank Hunt feature. + +## 🎨 Key Visual Enhancements + +### 1. **Modern Design System** +- **Gradient Backgrounds**: Beautiful gradient overlays with depth and dimension +- **Enhanced Shadows**: Multi-layered shadows with proper opacity and spread +- **Rounded Corners**: Consistent 16-32px border radius for modern feel +- **Improved Typography**: Better font weights, sizes, and letter spacing + +### 2. **Advanced Animations** +- **Elastic Entry Animations**: Items bounce in with elastic curves +- **Particle Background Effect**: Animated floating particles for dynamic background +- **Trophy Glow Effects**: Pulsing glow animations for top 3 positions +- **Smooth Transitions**: 600-1200ms duration with proper easing curves + +### 3. **Enhanced Visual Hierarchy** + +#### **Header Section** +- Gradient icon container with shadow effects +- Improved title typography with better spacing +- Enhanced close button with card-style background + +#### **Statistics Overview** +- Interactive stats cards showing: + - Total number of hunters + - Total points accumulated + - User's current rank +- Color-coded icons and gradient backgrounds +- Professional dividers between stats + +#### **Champion Podium (Top 3)** +- **3D Podium Effect**: Different heights for 1st, 2nd, 3rd place +- **Dynamic Trophy System**: + - Gold (1st): Animated glow with golden gradient + - Silver (2nd): Silver gradient with medium glow + - Bronze (3rd): Bronze gradient with subtle glow +- **Real-time Rank Colors**: Dynamic color assignment based on performance +- **Avatar Integration**: Circular avatars with border effects + +### 4. **Participant List Enhancements** +- **Smart Categorization**: Top 3 in podium, others in scrollable list +- **Current User Highlighting**: Special gradient background and borders +- **Achievement Badges**: Fire icons for high performers (500+ points) +- **Enhanced Card Design**: Gradient backgrounds with professional shadows +- **Interactive Elements**: Hover effects and touch responses + +## 🎯 Technical Features + +### **Performance Optimizations** +- **Efficient Animations**: Multiple animation controllers with proper disposal +- **Smooth Scrolling**: Optimized ListView with proper item building +- **Memory Management**: Proper controller lifecycle management + +### **Responsive Design** +- **Adaptive Heights**: 85% screen height for optimal viewing +- **Flexible Layouts**: Responsive to different screen sizes +- **Safe Area Handling**: Proper padding and margins + +### **Accessibility Features** +- **High Contrast**: Proper color contrast ratios +- **Readable Typography**: Appropriate font sizes and weights +- **Touch Targets**: Proper button sizes for easy interaction + +## 🎪 Additional Components Created + +### 1. **CelebrationOverlay** (`celebration_overlay.dart`) +- **Confetti Animation**: 30 animated particles with different shapes +- **Burst Effects**: Expanding circle effects from center +- **Color Variety**: 8 different celebration colors +- **Star Particles**: Mixed circle and star-shaped confetti +- **Fade Out Effect**: Smooth opacity transitions + +### 2. **AnimatedRankBadge** (`animated_rank_badge.dart`) +- **Pulse Animation**: Breathing effect for active badges +- **Shimmer Effect**: Metallic shine for top 3 ranks +- **Dynamic Colors**: Rank-based color schemes +- **Icon System**: Different icons for different rank levels +- **Point Display**: Integrated point counter with star icon + +## 🎨 Color Scheme & Theming + +### **Rank-Based Colors** +- **1st Place**: Gold (#FFD700) with warm gradients +- **2nd Place**: Silver (#C0C0C0) with cool gradients +- **3rd Place**: Bronze (#CD7F32) with earth tones +- **Top 10**: Primary blue with professional gradients +- **Others**: Subtle gray tones with accent highlights + +### **Background Effects** +- **Primary Gradient**: Surface to card background transition +- **Particle Colors**: Primary color with 10% opacity +- **Shadow System**: Multi-layered shadows with proper falloff + +## 🚀 Usage Examples + +### **Basic Implementation** +```dart +LeaderboardWidget( + entries: leaderboardEntries, + userPoints: currentUserPoints, + onClose: () => Navigator.pop(context), +) +``` + +### **With Celebration** +```dart +CelebrationOverlay( + showCelebration: userAchievedNewRank, + onCelebrationComplete: () => _showLeaderboard(), + child: YourHuntScreen(), +) +``` + +### **Rank Badge Usage** +```dart +AnimatedRankBadge( + rank: userRank, + totalPoints: userPoints, + userName: currentUser.name, + showAnimation: true, + onTap: () => _showFullLeaderboard(), +) +``` + +## 🎯 Benefits of Enhanced UI + +### **User Engagement** +- **Visual Appeal**: Modern gradients and animations attract attention +- **Gamification**: Trophy systems and celebrations motivate participation +- **Recognition**: Clear visual hierarchy emphasizes achievements + +### **User Experience** +- **Intuitive Navigation**: Clear close buttons and smooth transitions +- **Information Density**: Optimal balance of data and visual elements +- **Performance**: Smooth 60fps animations with proper optimization + +### **Competitive Spirit** +- **Clear Rankings**: Easy to understand position and progress +- **Achievement Visibility**: Badges and effects highlight success +- **Social Comparison**: Easy comparison with other participants + +## 🎪 Animation Details + +### **Entry Sequence** +1. **Slide Up**: Main container slides from bottom (600ms) +2. **Statistics Fade**: Stats cards fade in with stagger +3. **Podium Rise**: Top 3 podium rises with elastic curve +4. **List Items**: Participants animate in with 80ms intervals +5. **Background**: Particles start floating immediately + +### **Interactive Feedback** +- **Button Hover**: Subtle scale and shadow changes +- **Card Tap**: Brief scale down with shadow adjustment +- **Trophy Glow**: Continuous pulse for top 3 positions + +## 📱 Mobile Optimization + +### **Touch Interactions** +- **Swipe to Close**: Gesture-based closing mechanism +- **Large Touch Targets**: Minimum 44px touch areas +- **Haptic Feedback**: Proper vibration responses + +### **Performance** +- **Efficient Rendering**: Only necessary repaints +- **Memory Management**: Proper animation disposal +- **Battery Optimization**: Controlled animation refresh rates + +## 🚀 Latest Updates & Fixes + +### **Navigation & Layout Optimizations** +- **Fixed Navigation Overflow**: Content no longer falls under navigation bar +- **Dynamic Height Calculation**: Adapts to different screen sizes and safe areas +- **Compact Design**: Optimized spacing for better content density +- **Mobile-First**: Responsive design that works perfectly on all devices + +### **Podium Enhancement** +- **Fixed Name Truncation**: Smart name shortening with first name + last initial +- **Improved Text Layout**: Better spacing prevents overflow issues +- **Interactive Podium**: Tap to see full participant details +- **Optimized Heights**: Adjusted podium heights for better proportion + +### **Advanced Features** +- **Smart Search**: Quick search functionality for large participant lists +- **Live Counter Animation**: Animated participant count in statistics +- **Pull-to-Refresh**: Swipe down to refresh leaderboard data +- **Real-time Indicators**: Live status indicators throughout the UI + +### **Performance Optimizations** +- **Fixed Item Heights**: Improved scrolling performance +- **Efficient Filtering**: Smart filtering for search functionality +- **Memory Management**: Proper animation controller disposal +- **Smooth Animations**: 60fps animations with optimized refresh rates + +This enhanced Hunt Leaderboard transforms a simple ranking display into an engaging, beautiful, and highly interactive experience that motivates users to participate more actively in the Hunt challenges! 🎉 + +### **Usage with New Features** +```dart +LeaderboardWidget( + entries: leaderboardEntries, + userPoints: currentUserPoints, + onClose: () => Navigator.pop(context), + onRefresh: () => _refreshLeaderboardData(), // New refresh callback +) +``` diff --git a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart index 654ca88..dd20efc 100644 --- a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart +++ b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart @@ -1,10 +1,3 @@ -/* - * Hunt Feature Usage Examples - * - * This file demonstrates how to use the Hunt feature components - * and integrate them into your app. - */ - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:lba/screens/mains/hunt/hunt.dart'; @@ -13,7 +6,6 @@ import 'package:lba/screens/mains/hunt/models/hunt_card.dart'; import 'package:lba/screens/mains/hunt/widgets/hunt_card_widget.dart'; import 'package:lba/screens/mains/hunt/widgets/leaderboard_widget.dart'; -// Example 1: Basic Hunt Screen Integration class ExampleHuntIntegration extends StatelessWidget { const ExampleHuntIntegration({super.key}); @@ -26,7 +18,6 @@ class ExampleHuntIntegration extends StatelessWidget { } } -// Example 2: Custom Hunt Card Widget Usage class ExampleHuntCard extends StatelessWidget { const ExampleHuntCard({super.key}); @@ -52,7 +43,6 @@ class ExampleHuntCard extends StatelessWidget { child: HuntCardWidget( card: exampleCard, onTap: () { - // Handle card selection print('Card ${exampleCard.id} selected!'); }, isSelected: false, @@ -63,7 +53,6 @@ class ExampleHuntCard extends StatelessWidget { } } -// Example 3: Leaderboard Widget Usage class ExampleLeaderboard extends StatelessWidget { const ExampleLeaderboard({super.key}); @@ -106,7 +95,6 @@ class ExampleLeaderboard extends StatelessWidget { } } -// Example 4: Custom Hunt State Management class ExampleHuntStateUsage extends StatefulWidget { const ExampleHuntStateUsage({super.key}); @@ -140,12 +128,10 @@ class _ExampleHuntStateUsageState extends State { builder: (context, state, child) { return Column( children: [ - // Display current game state Text('Current State: ${state.gameState}'), Text('User Points: ${state.userPoints}'), Text('Cards Available: ${state.cards.length}'), - // Control buttons Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -170,7 +156,6 @@ class _ExampleHuntStateUsageState extends State { ], ), - // Display selected card info if (state.selectedCard != null) ...[ const SizedBox(height: 20), Text('Selected Card: ${state.selectedCard!.category}'), @@ -178,7 +163,6 @@ class _ExampleHuntStateUsageState extends State { Text('Question: ${state.selectedCard!.question}'), ], - // Time remaining display if (state.gameState == HuntGameState.huntingActive) ...[ const SizedBox(height: 20), Text('Time Remaining: ${_formatDuration(state.timeRemaining)}'), @@ -201,7 +185,6 @@ class _ExampleHuntStateUsageState extends State { } } -// Example 5: Custom Hunt Integration with Navigation class ExampleHuntNavigation extends StatelessWidget { const ExampleHuntNavigation({super.key}); @@ -253,15 +236,4 @@ class ExampleHuntNavigation extends StatelessWidget { ), ); } -} - -/* - * Integration Notes: - * - * 1. Always wrap Hunt components with ChangeNotifierProvider - * 2. Initialize the game state with huntState.initializeGame() - * 3. Handle permissions properly before starting hunts - * 4. Clean up resources in dispose methods - * 5. Use Consumer widgets to react to state changes - * 6. Test on physical devices for location and camera features - */ +} \ No newline at end of file diff --git a/lib/screens/mains/hunt/hunt.dart b/lib/screens/mains/hunt/hunt.dart index 1a0aa9b..b042bcd 100644 --- a/lib/screens/mains/hunt/hunt.dart +++ b/lib/screens/mains/hunt/hunt.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; import 'package:provider/provider.dart'; import 'package:vibration/vibration.dart'; import 'package:lba/res/colors.dart'; @@ -95,7 +97,6 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi } void _initializeGame() { - // Initialization is handled in provider creation } @override @@ -278,7 +279,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi decoration: BoxDecoration( color: AppColors.isDarkMode ? Colors.white.withOpacity(0.05) - : Colors.white.withOpacity(0.4), // Liquid Glass effect + : Colors.white.withOpacity(0.4), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.isDarkMode @@ -306,12 +307,11 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi : const Color(0xFF1976D2).withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), - child: Icon( - Icons.search_rounded, + child: SvgPicture.asset( + Assets.icons.searchNormal.path, color: AppColors.isDarkMode ? Colors.white : const Color(0xFF1976D2), - size: 20, ), ), const SizedBox(width: 12), @@ -357,12 +357,11 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.leaderboard_rounded, + SvgPicture.asset( + Assets.icons.ranking.path, color: AppColors.isDarkMode ? Colors.white : const Color(0xFF1976D2), - size: 16, ), const SizedBox(width: 6), Text( @@ -371,7 +370,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi color: AppColors.isDarkMode ? Colors.white : const Color(0xFF1976D2), - fontSize: 12, + fontSize: 14, fontWeight: FontWeight.w600, ), ), @@ -393,7 +392,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi decoration: BoxDecoration( color: AppColors.isDarkMode ? Colors.white.withOpacity(0.10) - : Colors.white.withOpacity(0.6), // More transparent + : Colors.white.withOpacity(0.6), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.isDarkMode @@ -413,7 +412,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem( - icon: Icons.stars_rounded, + icon: Assets.icons.medalStar.path, label: 'Total Points', value: '${huntProvider.userPoints}', color: AppColors.isDarkMode @@ -436,7 +435,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi ), ), _buildStatItem( - icon: Icons.emoji_events, + icon: Assets.icons.cup.path, label: 'Rank', value: '#${huntProvider.currentUserRank}', color: AppColors.isDarkMode @@ -449,7 +448,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi } Widget _buildStatItem({ - required IconData icon, + required String icon, required String label, required String value, required Color color, @@ -463,7 +462,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), - child: Icon(icon, color: color, size: 24), + child: SvgPicture.asset(icon, color: color,), ), const SizedBox(height: 8), Text( @@ -494,7 +493,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi decoration: BoxDecoration( color: AppColors.isDarkMode ? Colors.white.withOpacity(0.08) - : Colors.white.withOpacity(0.7), // More transparent + : Colors.white.withOpacity(0.7), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.isDarkMode diff --git a/lib/screens/mains/hunt/providers/hunt_provider.dart b/lib/screens/mains/hunt/providers/hunt_provider.dart index e4581be..9eac090 100644 --- a/lib/screens/mains/hunt/providers/hunt_provider.dart +++ b/lib/screens/mains/hunt/providers/hunt_provider.dart @@ -19,7 +19,6 @@ class HuntState extends ChangeNotifier { bool _isCameraPermissionGranted = false; DateTime? _huntStartTime; - // Getters List get cards => _cards; HuntCard? get selectedCard => _selectedCard; HuntGameState get gameState => _gameState; @@ -60,7 +59,6 @@ class HuntState extends ChangeNotifier { void selectCard(HuntCard card) { _selectedCard = card; - // Don't change game state, keep it as cardSelection so cards just flip notifyListeners(); } @@ -105,7 +103,6 @@ class HuntState extends ChangeNotifier { } void _updateLeaderboard() { - // Update user's position in leaderboard for (int i = 0; i < _leaderboard.length; i++) { if (_leaderboard[i].isCurrentUser) { _leaderboard[i] = _leaderboard[i].copyWith(totalPoints: _userPoints); @@ -113,10 +110,8 @@ class HuntState extends ChangeNotifier { } } - // Sort leaderboard by points _leaderboard.sort((a, b) => b.totalPoints.compareTo(a.totalPoints)); - // Update ranks for (int i = 0; i < _leaderboard.length; i++) { _leaderboard[i] = _leaderboard[i].copyWith(rank: i + 1); } diff --git a/lib/screens/mains/hunt/services/location_service.dart b/lib/screens/mains/hunt/services/location_service.dart index f7b8d94..5e56517 100644 --- a/lib/screens/mains/hunt/services/location_service.dart +++ b/lib/screens/mains/hunt/services/location_service.dart @@ -8,8 +8,8 @@ class LocationService { if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } - return permission == LocationPermission.whileInUse || - permission == LocationPermission.always; + return permission == LocationPermission.whileInUse || + permission == LocationPermission.always; } static Future checkCameraPermission() async { @@ -34,24 +34,35 @@ class LocationService { } static double calculateDistance( - double lat1, double lon1, - double lat2, double lon2, + double lat1, + double lon1, + double lat2, + double lon2, ) { return Geolocator.distanceBetween(lat1, lon1, lat2, lon2); } static bool isWithinRange( - double currentLat, double currentLon, - double targetLat, double targetLon, - {double rangeInMeters = 50.0} - ) { - double distance = calculateDistance(currentLat, currentLon, targetLat, targetLon); + double currentLat, + double currentLon, + double targetLat, + double targetLon, { + double rangeInMeters = 50.0, + }) { + double distance = calculateDistance( + currentLat, + currentLon, + targetLat, + targetLon, + ); return distance <= rangeInMeters; } static double getBearing( - double startLat, double startLng, - double endLat, double endLng, + double startLat, + double startLng, + double endLat, + double endLng, ) { final startLatRad = startLat * (math.pi / 180); final startLngRad = startLng * (math.pi / 180); @@ -61,10 +72,11 @@ class LocationService { double dLng = endLngRad - startLngRad; double y = math.sin(dLng) * math.cos(endLatRad); - double x = math.cos(startLatRad) * math.sin(endLatRad) - + double x = + math.cos(startLatRad) * math.sin(endLatRad) - math.sin(startLatRad) * math.cos(endLatRad) * math.cos(dLng); double bearing = math.atan2(y, x); - return (bearing * (180 / math.pi) + 360) % 360; // Convert to degrees + return (bearing * (180 / math.pi) + 360) % 360; } } diff --git a/lib/screens/mains/hunt/test/hunt_test.dart b/lib/screens/mains/hunt/test/hunt_test.dart deleted file mode 100644 index 9adf4ae..0000000 --- a/lib/screens/mains/hunt/test/hunt_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Quick test to verify Hunt feature works -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../hunt.dart'; - -class HuntTestApp extends StatelessWidget { - const HuntTestApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Hunt Feature Test', - theme: ThemeData( - primarySwatch: Colors.blue, - brightness: Brightness.light, - ), - darkTheme: ThemeData( - primarySwatch: Colors.blue, - brightness: Brightness.dark, - ), - home: const Hunt(), // This now properly creates its own provider - ); - } -} - -void main() { - runApp(const HuntTestApp()); -} diff --git a/lib/screens/mains/hunt/widgets/animated_rank_badge.dart b/lib/screens/mains/hunt/widgets/animated_rank_badge.dart new file mode 100644 index 0000000..bed42bc --- /dev/null +++ b/lib/screens/mains/hunt/widgets/animated_rank_badge.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:lba/res/colors.dart'; + +class AnimatedRankBadge extends StatefulWidget { + final int rank; + final int totalPoints; + final String userName; + final bool showAnimation; + final VoidCallback? onTap; + + const AnimatedRankBadge({ + super.key, + required this.rank, + required this.totalPoints, + required this.userName, + this.showAnimation = true, + this.onTap, + }); + + @override + State createState() => _AnimatedRankBadgeState(); +} + +class _AnimatedRankBadgeState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late AnimationController _shimmerController; + late Animation _pulseAnimation; + late Animation _shimmerAnimation; + + @override + void initState() { + super.initState(); + + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + _shimmerController = AnimationController( + duration: const Duration(milliseconds: 2000), + vsync: this, + ); + + _pulseAnimation = Tween( + begin: 1.0, + end: 1.1, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + )); + + _shimmerAnimation = Tween( + begin: -1.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _shimmerController, + curve: Curves.easeInOut, + )); + + if (widget.showAnimation) { + _pulseController.repeat(reverse: true); + _shimmerController.repeat(); + } + } + + @override + void dispose() { + _pulseController.dispose(); + _shimmerController.dispose(); + super.dispose(); + } + + Color _getRankColor() { + switch (widget.rank) { + case 1: + return const Color(0xFFFFD700); + case 2: + return const Color(0xFFC0C0C0); + case 3: + return const Color(0xFFCD7F32); + default: + if (widget.rank <= 10) { + return AppColors.primary; + } else { + return AppColors.textSecondary; + } + } + } + + IconData _getRankIcon() { + switch (widget.rank) { + case 1: + return Icons.emoji_events_rounded; + case 2: + return Icons.emoji_events_outlined; + case 3: + return Icons.emoji_events_outlined; + default: + if (widget.rank <= 10) { + return Icons.military_tech_rounded; + } else { + return Icons.person_rounded; + } + } + } + + String _getRankSuffix() { + if (widget.rank == 1) return 'st'; + if (widget.rank == 2) return 'nd'; + if (widget.rank == 3) return 'rd'; + return 'th'; + } + + @override + Widget build(BuildContext context) { + final isTopThree = widget.rank <= 3; + final rankColor = _getRankColor(); + + return GestureDetector( + onTap: widget.onTap, + child: AnimatedBuilder( + animation: widget.showAnimation ? _pulseAnimation : AlwaysStoppedAnimation(1.0), + builder: (context, child) { + return Transform.scale( + scale: widget.showAnimation ? _pulseAnimation.value : 1.0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isTopThree + ? [ + rankColor, + rankColor.withOpacity(0.8), + ] + : [ + AppColors.cardBackground, + AppColors.cardBackground.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: rankColor.withOpacity(0.3), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: rankColor.withOpacity(0.3), + blurRadius: isTopThree ? 12 : 6, + offset: const Offset(0, 4), + spreadRadius: isTopThree ? 2 : 0, + ), + ], + ), + child: Stack( + children: [ + if (isTopThree && widget.showAnimation) + AnimatedBuilder( + animation: _shimmerAnimation, + builder: (context, child) { + return Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(18), + child: CustomPaint( + painter: ShimmerPainter( + progress: _shimmerAnimation.value, + color: Colors.white.withOpacity(0.3), + ), + size: Size.infinite, + ), + ), + ); + }, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isTopThree + ? Colors.white.withOpacity(0.2) + : rankColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + _getRankIcon(), + color: isTopThree ? Colors.white : rankColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '${widget.rank}${_getRankSuffix()}', + style: TextStyle( + color: isTopThree ? Colors.white : AppColors.textPrimary, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + Text( + 'Place', + style: TextStyle( + color: isTopThree ? Colors.white70 : AppColors.textSecondary, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + Text( + widget.userName, + style: TextStyle( + color: isTopThree ? Colors.white70 : AppColors.textSecondary, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isTopThree + ? Colors.white.withOpacity(0.2) + : AppColors.confirmButton.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.stars_rounded, + color: isTopThree ? Colors.white : AppColors.confirmButton, + size: 12, + ), + const SizedBox(width: 2), + Text( + '${widget.totalPoints}', + style: TextStyle( + color: isTopThree ? Colors.white : AppColors.confirmButton, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + } +} + +class ShimmerPainter extends CustomPainter { + final double progress; + final Color color; + + ShimmerPainter({required this.progress, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..shader = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.transparent, + color, + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ).createShader(Rect.fromLTWH( + size.width * progress - size.width * 0.3, + 0, + size.width * 0.6, + size.height, + )); + + canvas.drawRect(Offset.zero & size, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/screens/mains/hunt/widgets/celebration_overlay.dart b/lib/screens/mains/hunt/widgets/celebration_overlay.dart new file mode 100644 index 0000000..a4f4c70 --- /dev/null +++ b/lib/screens/mains/hunt/widgets/celebration_overlay.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class CelebrationOverlay extends StatefulWidget { + final Widget child; + final bool showCelebration; + final VoidCallback? onCelebrationComplete; + + const CelebrationOverlay({ + super.key, + required this.child, + this.showCelebration = false, + this.onCelebrationComplete, + }); + + @override + State createState() => _CelebrationOverlayState(); +} + +class _CelebrationOverlayState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + late List _particles; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 2500), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.3, curve: Curves.easeOut), + )); + + _scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.6, curve: Curves.elasticOut), + )); + + _particles = _generateParticles(); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onCelebrationComplete?.call(); + } + }); + } + + @override + void didUpdateWidget(CelebrationOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.showCelebration && !oldWidget.showCelebration) { + _startCelebration(); + } + } + + void _startCelebration() { + _controller.reset(); + _particles = _generateParticles(); + _controller.forward(); + } + + List _generateParticles() { + final random = math.Random(); + return List.generate(30, (index) { + return Particle( + x: random.nextDouble(), + y: random.nextDouble(), + color: _getRandomColor(random), + size: 3.0 + random.nextDouble() * 5.0, + speedX: (random.nextDouble() - 0.5) * 2.0, + speedY: random.nextDouble() * -2.0 - 1.0, + ); + }); + } + + Color _getRandomColor(math.Random random) { + final colors = [ + const Color(0xFFFFD700), // Gold + const Color(0xFFFF6B35), // Orange + const Color(0xFF4ECDC4), // Teal + const Color(0xFF45B7D1), // Blue + const Color(0xFF96CEB4), // Green + const Color(0xFFFECEA8), // Peach + const Color(0xFFFF9FF3), // Pink + const Color(0xFFF9CA24), // Yellow + ]; + return colors[random.nextInt(colors.length)]; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + if (widget.showCelebration) + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Positioned.fill( + child: Opacity( + opacity: _fadeAnimation.value, + child: CustomPaint( + painter: CelebrationPainter( + particles: _particles, + progress: _controller.value, + scale: _scaleAnimation.value, + ), + size: Size.infinite, + ), + ), + ); + }, + ), + ], + ); + } +} + +class Particle { + final double x; + final double y; + final Color color; + final double size; + final double speedX; + final double speedY; + + Particle({ + required this.x, + required this.y, + required this.color, + required this.size, + required this.speedX, + required this.speedY, + }); +} + +class CelebrationPainter extends CustomPainter { + final List particles; + final double progress; + final double scale; + + CelebrationPainter({ + required this.particles, + required this.progress, + required this.scale, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..style = PaintingStyle.fill; + + for (final particle in particles) { + final currentX = particle.x * size.width + particle.speedX * progress * size.width * 0.5; + final currentY = particle.y * size.height + particle.speedY * progress * size.height * 0.5; + + final opacity = (1.0 - progress).clamp(0.0, 1.0); + paint.color = particle.color.withOpacity(opacity); + + final rotation = progress * math.pi * 4; + canvas.save(); + canvas.translate(currentX, currentY); + canvas.rotate(rotation); + + if (particle.size > 6) { + _drawStar(canvas, paint, particle.size * scale); + } else { + canvas.drawCircle(Offset.zero, particle.size * scale, paint); + } + + canvas.restore(); + } + + if (progress < 0.3 && scale > 0) { + _drawBurstEffect(canvas, size, progress, scale); + } + } + + void _drawStar(Canvas canvas, Paint paint, double size) { + final path = Path(); + final angleStep = (math.pi * 2) / 5; + final innerRadius = size * 0.4; + final outerRadius = size; + + for (int i = 0; i < 10; i++) { + final angle = i * angleStep / 2; + final radius = i % 2 == 0 ? outerRadius : innerRadius; + final x = math.cos(angle) * radius; + final y = math.sin(angle) * radius; + + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + path.close(); + canvas.drawPath(path, paint); + } + + void _drawBurstEffect(Canvas canvas, Size size, double progress, double scale) { + final center = Offset(size.width / 2, size.height / 2); + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0; + + final colors = [ + const Color(0xFFFFD700), + const Color(0xFFFF6B35), + const Color(0xFF4ECDC4), + ]; + + for (int i = 0; i < colors.length; i++) { + final radius = (progress * 150.0 * (i + 1)) * scale; + final opacity = (1.0 - progress).clamp(0.0, 1.0); + paint.color = colors[i].withOpacity(opacity * 0.5); + canvas.drawCircle(center, radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart index 069cb9d..fe50b4d 100644 --- a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart +++ b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart @@ -562,14 +562,12 @@ class AdvancedTargetPainter extends CustomPainter { final primaryColor = isNear ? const Color(0xFF00E676) : const Color(0xFF2196F3); final baseRadius = math.max(60.0, math.min(85.0, 100 - (distance / 2))); - // Outer glow paint ..color = primaryColor.withOpacity(0.1 * pulseValue) ..strokeWidth = 20.0 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10); canvas.drawCircle(center, baseRadius, paint); - // Main rotating rings paint ..color = primaryColor.withOpacity(0.8) ..strokeWidth = 2.5 @@ -580,13 +578,11 @@ class AdvancedTargetPainter extends CustomPainter { canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius), rotationValue * 2 * math.pi + 3, 2.5, false, paint); - // Inner static ring paint ..color = primaryColor.withOpacity(0.4) ..strokeWidth = 1.5; canvas.drawCircle(center, baseRadius - 15, paint); - // Scanning arc if (!isNear) { final sweepAngle = scanValue * 2 * math.pi; paint @@ -601,7 +597,6 @@ class AdvancedTargetPainter extends CustomPainter { ); } - // Success animation if (isNear) { paint ..color = primaryColor.withOpacity(1.0 - pulseValue) diff --git a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart index 37d879e..6ced90f 100644 --- a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart +++ b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; import '../models/hunt_card.dart'; import 'hunt_timer_widget.dart'; @@ -98,7 +100,6 @@ class _HuntCardWidgetState extends State } void _openARHint() { - // Navigate to AR camera for location-based hints Navigator.push( context, MaterialPageRoute( @@ -367,7 +368,6 @@ class _HuntCardWidgetState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Gaming-style icon container with floating effect Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -414,7 +414,6 @@ class _HuntCardWidgetState extends State ), ), const SizedBox(height: 15), - // Gaming-style mystery text with holographic effect Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), decoration: BoxDecoration( @@ -491,7 +490,6 @@ class _HuntCardWidgetState extends State ), ), ), - // Gaming-style difficulty indicator with glow Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(3, (index) { @@ -557,7 +555,6 @@ class _HuntCardWidgetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with title and timer Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -607,7 +604,6 @@ class _HuntCardWidgetState extends State ), ], ), - // Enhanced Timer HuntTimerWidget( timeRemaining: const Duration(hours: 12), isActive: true, @@ -619,7 +615,6 @@ class _HuntCardWidgetState extends State const SizedBox(height: 10), - // Riddle section Expanded( flex: 4, child: Container( @@ -694,7 +689,6 @@ class _HuntCardWidgetState extends State const SizedBox(height: 8), - // AR Hint section Expanded( flex: 1, child: GestureDetector( @@ -732,14 +726,13 @@ class _HuntCardWidgetState extends State child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.help, + SvgPicture.asset( + Assets.icons.infoCircle.path, color: AppColors.isDarkMode ? Colors.yellow.shade600 : const Color(0xFFE65100), - size: 14, ), - const SizedBox(width: 6), + const SizedBox(width: 3), Text( 'AR Smart Hint', style: TextStyle( @@ -758,7 +751,6 @@ class _HuntCardWidgetState extends State const SizedBox(height: 6), - // Action button Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 6), @@ -790,10 +782,10 @@ class _HuntCardWidgetState extends State child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.location_on_rounded, + SvgPicture.asset( + Assets.icons.locationTick.path, color: Colors.white, - size: 12, + width: 14, ), const SizedBox(width: 4), Text( @@ -819,7 +811,6 @@ class _HuntCardWidgetState extends State final pi = 3.14159; if (isShowingFront) { - // Front side animated gradient return LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -863,7 +854,6 @@ class _HuntCardWidgetState extends State ], ); } else { - // Back side animated gradient return LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, diff --git a/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart b/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart index f774db5..0f10a57 100644 --- a/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart +++ b/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; class HuntTimerWidget extends StatefulWidget { @@ -75,7 +77,6 @@ class _HuntTimerWidgetState extends State if (_currentTime.inSeconds > 0) { _currentTime = Duration(seconds: _currentTime.inSeconds - 1); - // Pulse animation when time is running low if (_currentTime.inMinutes < 5) { _pulseController.forward().then((_) { _pulseController.reverse(); @@ -158,14 +159,14 @@ class _HuntTimerWidgetState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( + SvgPicture.asset( isExpired - ? Icons.timer_off_outlined + ? Assets.icons.timerHunt.path : _currentTime.inMinutes < 2 - ? Icons.timer_outlined - : Icons.timer_rounded, + ? Assets.icons.timerHunt.path + : Assets.icons.timerHunt.path, color: timerColor, - size: widget.fontSize != null ? widget.fontSize! + 2 : 14, + width: widget.fontSize != null ? widget.fontSize! + 4 : 16, ), const SizedBox(width: 4), Text( diff --git a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart index 75f4f02..7420160 100644 --- a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart +++ b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart @@ -1,4 +1,6 @@ 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'; @@ -6,12 +8,14 @@ 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 @@ -22,8 +26,15 @@ 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() { @@ -36,38 +47,56 @@ class _LeaderboardWidgetState extends State 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, - )); + ).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.1).clamp(0.0, 0.4), - ((index * 0.1) + 0.6).clamp(0.1, 1.0), - curve: Curves.easeOutBack, + (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(); } @@ -78,81 +107,73 @@ class _LeaderboardWidgetState extends State @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: MediaQuery.of(context).size.height * 0.7, + height: availableHeight, decoration: BoxDecoration( - color: AppColors.surface, + 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, + color: AppColors.shadowColor.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, -10), + spreadRadius: 2, ), ], ), - child: Column( + child: Stack( children: [ - // Handle bar - Container( - margin: const EdgeInsets.symmetric(vertical: 12), - height: 4, - width: 40, - decoration: BoxDecoration( - color: AppColors.divider, - borderRadius: BorderRadius.circular(2), - ), - ), - // Header - Padding( - padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Leaderboard', - style: TextStyle( - color: AppColors.textPrimary, - fontSize: 24, - fontWeight: FontWeight.bold, + AnimatedBuilder( + animation: _particleAnimation, + builder: + (context, child) => CustomPaint( + painter: ParticlesPainter( + animation: _particleAnimation, + color: AppColors.primary.withOpacity(0.1), ), + size: Size.infinite, ), - IconButton( - onPressed: _close, - icon: Icon( - Icons.close_rounded, - color: AppColors.textSecondary, - ), - ), - ], - ), ), - // Leaderboard list - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 24), - itemCount: widget.entries.length, - itemBuilder: (context, index) { - final entry = widget.entries[index]; - return AnimatedBuilder( - animation: _itemAnimations[index], - builder: (context, child) { - return Transform.translate( - offset: Offset( - 0, - 50 * (1 - _itemAnimations[index].value), - ), - child: Opacity( - opacity: _itemAnimations[index].value.clamp(0.0, 1.0), - child: _buildLeaderboardItem(entry, index), - ), - ); - }, - ); - }, - ), + 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()), + ], ), ], ), @@ -160,116 +181,236 @@ class _LeaderboardWidgetState extends State ); } - Widget _buildLeaderboardItem(LeaderboardEntry entry, int index) { - final isCurrentUser = entry.isCurrentUser; - final isTopThree = entry.rank <= 3; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: isCurrentUser - ? AppColors.primary.withOpacity(0.1) - : AppColors.cardBackground, - borderRadius: BorderRadius.circular(16), - border: isCurrentUser - ? Border.all(color: AppColors.primary, width: 2) - : null, - boxShadow: isTopThree - ? [ - BoxShadow( - color: AppColors.shadowColor, - blurRadius: 8, - offset: const Offset(0, 4), - ), - ] - : null, - ), + Widget _buildCompactHeader() { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 2, 20, 4), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Rank - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: _getRankColor(entry.rank), - shape: BoxShape.circle, - ), - child: Center( - child: Text( - '#${entry.rank}', - style: TextStyle( - color: entry.rank <= 3 ? Colors.white : AppColors.textPrimary, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - const SizedBox(width: 16), - // Avatar - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Center( - child: Text( - entry.avatar, - style: const TextStyle(fontSize: 24), - ), - ), - ), - const SizedBox(width: 16), - // Name and points - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entry.name, - style: TextStyle( - color: AppColors.textPrimary, - fontSize: 16, - fontWeight: isCurrentUser ? FontWeight.bold : FontWeight.w600, + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.7), + ], ), - ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.stars_rounded, - color: AppColors.confirmButton, - size: 16, - ), - const SizedBox(width: 4), - Text( - '${entry.totalPoints} points', - style: TextStyle( - color: AppColors.textSecondary, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + 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: 4, + height: 4, + 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, + ), + ), + ], + ), + ], ), - // Trophy for top 3 - if (isTopThree) - Container( - padding: const EdgeInsets.all(8), + IconButton( + onPressed: _close, + padding: const EdgeInsets.all(4), + icon: Container( + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: _getRankColor(entry.rank).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), + 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.emoji_events_rounded, - color: _getRankColor(entry.rank), - size: 24, + 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, + ), ), ), ], @@ -277,16 +418,595 @@ class _LeaderboardWidgetState extends State ); } - Color _getRankColor(int rank) { - switch (rank) { + 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: 1), + ] else + const SizedBox(height: 1), + 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 const Color(0xFFFFD700); // Gold + return { + 'primary': const Color(0xFFFFD700), + 'light': const Color(0xFFFFFACD), + }; case 2: - return const Color(0xFFC0C0C0); // Silver + return { + 'primary': const Color(0xFFC0C0C0), + 'light': const Color(0xFFE8E8E8), + }; case 3: - return const Color(0xFFCD7F32); // Bronze + return { + 'primary': const Color(0xFFCD7F32), + 'light': const Color(0xFFDEB887), + }; default: - return AppColors.primary; + 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; }