// lib/screens/contribute_screen.dart import 'dart:io'; import '../core/app_decoration.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class ContributeScreen extends StatefulWidget { const ContributeScreen({Key? key}) : super(key: key); @override State createState() => _ContributeScreenState(); } class _ContributeScreenState extends State with SingleTickerProviderStateMixin { // Primary accent used for buttons / active tab (kept as a single constant) static const Color _primary = Color(0xFF0B63D6); // single corner radius to use everywhere static const double _cornerRadius = 18.0; // Form controllers final TextEditingController _titleCtl = TextEditingController(); final TextEditingController _locationCtl = TextEditingController(); final TextEditingController _organizerCtl = TextEditingController(); final TextEditingController _descriptionCtl = TextEditingController(); DateTime? _selectedDate; String _selectedCategory = 'Music'; // Image pickers final ImagePicker _picker = ImagePicker(); XFile? _coverImageFile; XFile? _thumbImageFile; bool _submitting = false; // Tab state: 0 = Contribute, 1 = Leaderboard, 2 = Achievements int _activeTab = 0; // Example progress value (0..1) double _progress = 0.45; // A few category options final List _categories = ['Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community']; @override void dispose() { _titleCtl.dispose(); _locationCtl.dispose(); _organizerCtl.dispose(); _descriptionCtl.dispose(); super.dispose(); } Future _pickCoverImage() async { try { final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85); if (picked != null) setState(() => _coverImageFile = picked); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); } } Future _pickThumbnailImage() async { try { final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85); if (picked != null) setState(() => _thumbImageFile = picked); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); } } Future _pickDate() async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: _selectedDate ?? now, firstDate: DateTime(now.year - 2), lastDate: DateTime(now.year + 3), builder: (context, child) { return Theme( data: Theme.of(context).copyWith(colorScheme: ColorScheme.light(primary: _primary)), child: child!, ); }, ); if (picked != null) setState(() => _selectedDate = picked); } Future _submit() async { if (_titleCtl.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter an event title'))); return; } setState(() => _submitting = true); // simulate work await Future.delayed(const Duration(milliseconds: 800)); setState(() => _submitting = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Submitted for verification (demo)'))); _clearForm(); } } void _clearForm() { _titleCtl.clear(); _locationCtl.clear(); _organizerCtl.clear(); _descriptionCtl.clear(); _selectedDate = null; _selectedCategory = _categories.first; _coverImageFile = null; _thumbImageFile = null; setState(() {}); } // ---------- UI Builders ---------- Widget _buildHeader(BuildContext context) { final theme = Theme.of(context); // header uses AppDecoration.blueGradient for background (project-specific) return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 32, 20, 24), decoration: AppDecoration.blueGradient.copyWith( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(_cornerRadius), bottomRight: Radius.circular(_cornerRadius), ), // subtle shadow only boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4)), ], ), child: SafeArea( bottom: false, child: Column( children: [ // increased spacing to create a breathable layout const SizedBox(height: 6), // Title & subtitle (centered) — smaller title weight, clearer hierarchy Text( 'Contributor Dashboard', textAlign: TextAlign.center, style: theme.textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 20, ), ), const SizedBox(height: 12), // more space between title & subtitle Text( 'Track your impact, earn rewards, and climb the ranks!', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.92), fontSize: 13, height: 1.35), ), const SizedBox(height: 20), // more space before tabs // Pill-style segmented tabs (animated active) — slimmer / minimal _buildSegmentedTabs(context), const SizedBox(height: 18), // comfortable spacing before contributor card // Contributor level card — lighter, soft border, thinner progress Container( width: double.infinity, padding: const EdgeInsets.all(14), margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.08), // slightly lighter than before borderRadius: BorderRadius.circular(_cornerRadius - 2), border: Border.all(color: Colors.white.withOpacity(0.09)), // soft border ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Contributor Level', style: theme.textTheme.titleSmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w700)), const SizedBox(height: 8), Text('Start earning rewards by contributing!', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70)), const SizedBox(height: 12), Row( children: [ Expanded( // animated progress using TweenAnimationBuilder child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: _progress), duration: const Duration(milliseconds: 700), builder: (context, value, _) => ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: value, minHeight: 6, // thinner progress bar valueColor: const AlwaysStoppedAnimation(Colors.white), backgroundColor: Colors.white24, ), ), ), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.white.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('30 pts', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)), Text('Next: Enthusiast (50 pts)', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70)), ], ), ], ), ), ], ), ), ); } /// Bouncy spring curve matching web CSS: cubic-bezier(0.37, 1.95, 0.66, 0.56) static const Curve _bouncyCurve = Cubic(0.37, 1.95, 0.66, 0.56); /// Tab icons for each tab static const List _tabIcons = [ Icons.edit_outlined, Icons.emoji_events_outlined, Icons.workspace_premium_outlined, ]; Widget _buildSegmentedTabs(BuildContext context) { final tabs = ['Contribute', 'Leaderboard', 'Achievements']; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 10), child: LayoutBuilder( builder: (context, constraints) { final containerWidth = constraints.maxWidth; // 6px padding on each side of the container const double containerPadding = 6.0; final innerWidth = containerWidth - (containerPadding * 2); final tabWidth = innerWidth / tabs.length; return ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( height: 57, decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: Stack( children: [ // ── Sliding glider ── AnimatedPositioned( duration: const Duration(milliseconds: 500), curve: _bouncyCurve, left: containerPadding + (_activeTab * tabWidth), top: containerPadding, width: tabWidth, height: 57 - (containerPadding * 2), child: AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.10), blurRadius: 15, offset: const Offset(0, 4), ), BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 3, offset: const Offset(0, 1), ), ], ), ), ), // ── Tab labels ── Padding( padding: const EdgeInsets.all(containerPadding), child: Row( children: List.generate(tabs.length, (i) { final active = i == _activeTab; return Expanded( child: GestureDetector( onTap: () => setState(() => _activeTab = i), behavior: HitTestBehavior.opaque, child: SizedBox( height: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Animated icon: only shows for active tab AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: active ? Padding( padding: const EdgeInsets.only(right: 6), child: Icon( _tabIcons[i], size: 15, color: _primary, ), ) : const SizedBox.shrink(), ), Flexible( child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, style: TextStyle( color: active ? _primary : Colors.white.withOpacity(0.7), fontWeight: FontWeight.w600, fontSize: 14, fontFamily: Theme.of(context).textTheme.bodyMedium?.fontFamily, ), child: Text( tabs[i], textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), ), ), ], ), ), ), ); }), ), ), ], ), ), ); }, ), ); } Widget _buildForm(BuildContext ctx) { final theme = Theme.of(ctx); return Container( width: double.infinity, margin: const EdgeInsets.only(top: 18), padding: const EdgeInsets.fromLTRB(18, 20, 18, 28), decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(_cornerRadius), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 12, offset: const Offset(0, 6))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // sheet title Center( child: Column( children: [ Text('Contribute an Event', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text('Share local events. Earn points for every verified submission!', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), ], ), ), const SizedBox(height: 18), // small helper button Center( child: OutlinedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Want to edit an existing event? (demo)'))); }, style: OutlinedButton.styleFrom( side: BorderSide(color: Colors.grey.shade300), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), ), child: const Text('Want to edit an existing event?'), ), ), const SizedBox(height: 18), // Event Title Text('Event Title', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _titleCtl, hint: 'e.g. Local Food Festival'), const SizedBox(height: 14), // Category Text('Category', style: theme.textTheme.labelLarge), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedCategory, isExpanded: true, items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(), onChanged: (v) => setState(() => _selectedCategory = v ?? _selectedCategory), icon: const Icon(Icons.keyboard_arrow_down), ), ), ), const SizedBox(height: 14), // Date Text('Date', style: theme.textTheme.labelLarge), const SizedBox(height: 8), GestureDetector( onTap: _pickDate, child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 12), alignment: Alignment.centerLeft, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Text(_selectedDate == null ? 'Select date' : '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}', style: theme.textTheme.bodyMedium), ), ), const SizedBox(height: 14), // Location Text('Location', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _locationCtl, hint: 'e.g. City Park, Calicut'), const SizedBox(height: 14), // Organizer Text('Organizer Name', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _organizerCtl, hint: 'Individual or Organization Name'), const SizedBox(height: 14), // Description Text('Description', style: theme.textTheme.labelLarge), const SizedBox(height: 8), TextField( controller: _descriptionCtl, minLines: 4, maxLines: 6, decoration: InputDecoration( hintText: 'Tell us more about the event...', filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), ), const SizedBox(height: 18), // Event Images header Text('Event Images', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 12), // Cover image Text('Cover Image', style: theme.textTheme.bodySmall), const SizedBox(height: 8), GestureDetector( onTap: _pickCoverImage, child: _imagePickerPlaceholder(file: _coverImageFile, label: 'Cover Image'), ), const SizedBox(height: 12), // Thumbnail image Text('Thumbnail', style: theme.textTheme.bodySmall), const SizedBox(height: 8), GestureDetector( onTap: _pickThumbnailImage, child: _imagePickerPlaceholder(file: _thumbImageFile, label: 'Thumbnail'), ), const SizedBox(height: 22), // Submit button SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: _submitting ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: _primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)), ), child: _submitting ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.2)) : const Text('Submit for Verification', style: TextStyle(fontWeight: FontWeight.w600)), ), ), ], ), ); } Widget _roundedTextField({required TextEditingController controller, required String hint}) { final theme = Theme.of(context); return TextField( controller: controller, decoration: InputDecoration( hintText: hint, filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), ); } Widget _imagePickerPlaceholder({XFile? file, required String label}) { final theme = Theme.of(context); if (file == null) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.image, size: 28, color: theme.hintColor), const SizedBox(height: 8), Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor)), ], ), ), ); } // show picked image (file or network depending on platform) if (kIsWeb || file.path.startsWith('http')) { return ClipRRect( borderRadius: BorderRadius.circular(_cornerRadius - 8), child: Image.network(file.path, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); }), ); } else { final f = File(file.path); if (!f.existsSync()) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); } return ClipRRect( borderRadius: BorderRadius.circular(_cornerRadius - 8), child: Image.file(f, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); }), ); } } // ── Leaderboard state ── int _leaderboardTimeFilter = 0; // 0 = All Time, 1 = This Month int _leaderboardDistrictFilter = 0; // index into _districts static const List _districts = [ 'Overall Kerala', 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', ]; // Demo leaderboard data static const List> _leaderboardData = [ {'name': 'Annette Black', 'points': 4628, 'level': 'Legend', 'events': 156}, {'name': 'Jerome Bell', 'points': 4518, 'level': 'Legend', 'events': 152}, {'name': 'Theresa Webb', 'points': 4368, 'level': 'Legend', 'events': 148}, {'name': 'Courtney Henry', 'points': 4279, 'level': 'Legend', 'events': 149}, {'name': 'Cameron Williamson', 'points': 4150, 'level': 'Legend', 'events': 144}, {'name': 'Brooklyn Simmons', 'points': 4033, 'level': 'Legend', 'events': 139}, {'name': 'Leslie Alexander', 'points': 3914, 'level': 'Champion', 'events': 134}, {'name': 'Jenny Wilson', 'points': 3783, 'level': 'Champion', 'events': 132}, ]; // Demo achievements data static const List> _achievementsData = [ {'name': 'Newcomer', 'subtitle': 'First Event Posted', 'icon': Icons.star_outline, 'color': 0xFFDBEAFE, 'iconColor': 0xFF3B82F6, 'unlocked': true}, {'name': 'Contributor', 'subtitle': '10th Event Posted within a month', 'icon': Icons.workspace_premium, 'color': 0xFFFEF9C3, 'iconColor': 0xFFEAB308, 'unlocked': true}, {'name': 'On Fire!', 'subtitle': '3 Day Streak of logging in', 'icon': Icons.local_fire_department_outlined, 'color': 0xFFFFEDD5, 'iconColor': 0xFFF97316, 'unlocked': true, 'progress': 0.67}, {'name': 'Verified', 'subtitle': 'Identity Verified successfully', 'icon': Icons.verified_outlined, 'color': 0xFFDCFCE7, 'iconColor': 0xFF22C55E, 'unlocked': true}, {'name': 'Quality', 'subtitle': '5 Star Event Rating received', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false}, {'name': 'Community', 'subtitle': 'Referred 5 Friends to the platform', 'icon': Icons.people_outline, 'color': 0xFFE0E7FF, 'iconColor': 0xFF6366F1, 'unlocked': true, 'progress': 0.40}, {'name': 'Expert', 'subtitle': 'Level 10 Reached in 3 months', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false}, {'name': 'Precision', 'subtitle': '100% Data Accuracy on all events', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false}, ]; Widget _buildLeaderboard(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, margin: const EdgeInsets.only(top: 18), padding: const EdgeInsets.fromLTRB(0, 16, 0, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Time filter: All Time / This Month ── Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _buildTimeToggle(theme), ], ), ), const SizedBox(height: 12), // ── District filter chips (horizontal scroll) ── SizedBox( height: 38, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _districts.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, i) { final active = i == _leaderboardDistrictFilter; return GestureDetector( onTap: () => setState(() => _leaderboardDistrictFilter = i), child: AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: active ? _primary : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: active ? _primary : Colors.grey.shade300), ), child: Text( _districts[i], style: TextStyle( color: active ? Colors.white : Colors.grey.shade600, fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ); }, ), ), const SizedBox(height: 24), // ── Podium (top 3) ── _buildPodium(theme), const SizedBox(height: 24), // ── Leaderboard table (rank 4+) ── Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // Header Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ SizedBox(width: 32, child: Text('RANK', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))), const SizedBox(width: 8), Expanded(child: Text('USER', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))), SizedBox(width: 60, child: Text('POINTS', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))), const SizedBox(width: 8), SizedBox(width: 68, child: Text('LEVEL', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500), textAlign: TextAlign.center)), const SizedBox(width: 8), SizedBox(width: 32, child: Text('EVENTS', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Colors.grey.shade500), textAlign: TextAlign.center)), ], ), ), // Rows (rank 4+) ...List.generate( _leaderboardData.length - 3, (i) => _buildLeaderboardRow(theme, i + 3), ), ], ), ), ], ), ); } Widget _buildTimeToggle(ThemeData theme) { final labels = ['All Time', 'This Month']; return Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(24), ), child: Row( mainAxisSize: MainAxisSize.min, children: List.generate(labels.length, (i) { final active = i == _leaderboardTimeFilter; return GestureDetector( onTap: () => setState(() => _leaderboardTimeFilter = i), child: AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: active ? _primary : Colors.transparent, borderRadius: BorderRadius.circular(20), ), child: Text( labels[i], style: TextStyle( color: active ? Colors.white : Colors.grey.shade600, fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ); }), ), ); } Widget _buildPodium(ThemeData theme) { if (_leaderboardData.length < 3) return const SizedBox.shrink(); final first = _leaderboardData[0]; // #1 final second = _leaderboardData[1]; // #2 final third = _leaderboardData[2]; // #3 // Podium colors const goldColor = Color(0xFFFBBF24); const silverColor = Color(0xFFD1D5DB); const bronzeColor = Color(0xFFF97316); Widget podiumSlot(Map user, int rank, Color pillarColor, double pillarHeight, Color badgeColor) { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ // Avatar with rank badge Stack( clipBehavior: Clip.none, children: [ CircleAvatar( radius: rank == 1 ? 32 : 26, backgroundColor: badgeColor.withOpacity(0.2), child: Icon(Icons.person, size: rank == 1 ? 32 : 26, color: badgeColor), ), Positioned( right: -2, bottom: -2, child: Container( width: 20, height: 20, decoration: BoxDecoration( color: badgeColor, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), alignment: Alignment.center, child: Text('$rank', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800)), ), ), ], ), const SizedBox(height: 6), Text( user['name'] as String, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '${_formatNumber(user['points'] as int)} pts', style: TextStyle(color: _primary, fontWeight: FontWeight.w600, fontSize: 12), ), const SizedBox(height: 6), // Pillar Container( width: 80, height: pillarHeight, decoration: BoxDecoration( color: pillarColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(10)), ), ), ], ); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // #2 – left Expanded(child: podiumSlot(second, 2, silverColor, 70, Colors.grey.shade500)), const SizedBox(width: 8), // #1 – center (tallest) Expanded(child: podiumSlot(first, 1, goldColor, 100, goldColor)), const SizedBox(width: 8), // #3 – right Expanded(child: podiumSlot(third, 3, bronzeColor, 55, bronzeColor)), ], ), ); } Widget _buildLeaderboardRow(ThemeData theme, int index) { final user = _leaderboardData[index]; final rank = index + 1; final level = user['level'] as String; Color levelColor; Color levelBg; switch (level) { case 'Legend': levelColor = const Color(0xFF16A34A); levelBg = const Color(0xFFDCFCE7); break; case 'Champion': levelColor = const Color(0xFF9333EA); levelBg = const Color(0xFFF3E8FF); break; default: levelColor = Colors.grey; levelBg = Colors.grey.shade100; } return Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Colors.grey.shade100)), ), child: Row( children: [ SizedBox(width: 32, child: Text('$rank', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: Colors.grey.shade700))), const SizedBox(width: 8), CircleAvatar( radius: 18, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 20, color: Colors.grey.shade500), ), const SizedBox(width: 10), Expanded( child: Text( user['name'] as String, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), overflow: TextOverflow.ellipsis, ), ), SizedBox( width: 60, child: Text( '${_formatNumber(user['points'] as int)} pts', style: TextStyle(color: _primary, fontWeight: FontWeight.w600, fontSize: 12), ), ), const SizedBox(width: 8), Container( width: 68, padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( color: levelBg, borderRadius: BorderRadius.circular(12), ), alignment: Alignment.center, child: Text(level, style: TextStyle(color: levelColor, fontWeight: FontWeight.w600, fontSize: 11)), ), const SizedBox(width: 8), SizedBox( width: 32, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.calendar_today, size: 10, color: Colors.grey.shade400), const SizedBox(width: 2), Text('${user['events']}', style: TextStyle(fontSize: 11, color: Colors.grey.shade600)), ], ), ), ], ), ); } String _formatNumber(int n) { if (n >= 1000) { return '${(n / 1000).toStringAsFixed(n % 1000 == 0 ? 0 : 0)},${(n % 1000).toString().padLeft(3, '0')}'; } return '$n'; } // ── Achievements Tab ── Widget _buildAchievements(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, margin: const EdgeInsets.only(top: 18), padding: const EdgeInsets.fromLTRB(16, 20, 16, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Your Badges', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), // Badge grid GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.05, ), itemCount: _achievementsData.length, itemBuilder: (context, i) => _buildBadgeCard(theme, _achievementsData[i]), ), ], ), ); } Widget _buildBadgeCard(ThemeData theme, Map badge) { final isUnlocked = badge['unlocked'] as bool; final progress = badge['progress'] as double?; final badgeColor = Color(badge['color'] as int); final iconColor = Color(badge['iconColor'] as int); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade100), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 8, offset: const Offset(0, 2)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Icon circle Container( width: 44, height: 44, decoration: BoxDecoration( color: badgeColor, shape: BoxShape.circle, ), child: Icon( badge['icon'] as IconData, color: iconColor, size: 22, ), ), const Spacer(), // Name + lock indicator Row( children: [ Expanded( child: Text( badge['name'] as String, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14, color: isUnlocked ? Colors.black87 : Colors.grey.shade400, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (!isUnlocked) Icon(Icons.lock_outline, size: 14, color: Colors.grey.shade400), ], ), const SizedBox(height: 4), Text( badge['subtitle'] as String, style: TextStyle( fontSize: 11, color: isUnlocked ? Colors.grey.shade600 : Colors.grey.shade400, height: 1.3, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), // Progress bar if applicable if (progress != null) ...[ const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: TweenAnimationBuilder( tween: Tween(begin: 0, end: progress), duration: const Duration(milliseconds: 800), builder: (_, val, __) => LinearProgressIndicator( value: val, minHeight: 5, valueColor: AlwaysStoppedAnimation(_primary), backgroundColor: Colors.grey.shade200, ), ), ), const SizedBox(height: 4), Align( alignment: Alignment.centerRight, child: Text( '${(progress * 100).toInt()}%', style: TextStyle(fontSize: 11, color: Colors.grey.shade500, fontWeight: FontWeight.w600), ), ), ], ], ), ); } @override Widget build(BuildContext context) { // Switch content based on active tab Widget tabContent; switch (_activeTab) { case 1: tabContent = _buildLeaderboard(context); break; case 2: tabContent = _buildAchievements(context); break; default: tabContent = Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: _buildForm(context), ); } return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( bottom: false, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ _buildHeader(context), AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: KeyedSubtree( key: ValueKey(_activeTab), child: tabContent, ), ), const SizedBox(height: 36), ], ), ), ), ); } }