// lib/screens/contribute_screen.dart // Contributor Module v2 — matches PRD v3 / TechDocs v2 / Web version. // 4 tabs: Contribute · Leaderboard · Achievements · Shop import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import '../core/app_decoration.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; import '../widgets/landscape_section_header.dart'; // ───────────────────────────────────────────────────────────────────────────── // Tier colour map // ───────────────────────────────────────────────────────────────────────────── const _tierColors = { ContributorTier.BRONZE: Color(0xFFCD7F32), ContributorTier.SILVER: Color(0xFFA8A9AD), ContributorTier.GOLD: Color(0xFFFFD700), ContributorTier.PLATINUM: Color(0xFFE5E4E2), ContributorTier.DIAMOND: Color(0xFF67E8F9), }; // Icon map for achievement badges const _badgeIcons = { 'edit': Icons.edit_outlined, 'star': Icons.star_outline, 'emoji_events': Icons.emoji_events_outlined, 'leaderboard': Icons.leaderboard_outlined, 'photo_library': Icons.photo_library_outlined, 'verified': Icons.verified_outlined, }; // District list for the contribution form const _districts = [ 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other', ]; const _categories = [ 'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community', 'Dance', 'Film', 'Business', 'Health', 'Education', 'Other', ]; // ───────────────────────────────────────────────────────────────────────────── // ContributeScreen // ───────────────────────────────────────────────────────────────────────────── class ContributeScreen extends StatefulWidget { const ContributeScreen({Key? key}) : super(key: key); @override State createState() => _ContributeScreenState(); } class _ContributeScreenState extends State with SingleTickerProviderStateMixin { static const Color _primary = Color(0xFF0B63D6); static const double _cornerRadius = 18.0; int _activeTab = 0; // ── Contribution form state ────────────────────────────────────────────── final _formKey = GlobalKey(); final _titleCtl = TextEditingController(); final _locationCtl = TextEditingController(); final _organizerCtl = TextEditingController(); final _descriptionCtl = TextEditingController(); final _ticketPriceCtl = TextEditingController(); final _contactCtl = TextEditingController(); final _websiteCtl = TextEditingController(); DateTime? _selectedDate; TimeOfDay? _selectedTime; String _selectedCategory = _categories.first; String _selectedDistrict = _districts.first; List _images = []; bool _submitting = false; final _picker = ImagePicker(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadAll(); }); } @override void dispose() { _titleCtl.dispose(); _locationCtl.dispose(); _organizerCtl.dispose(); _descriptionCtl.dispose(); _ticketPriceCtl.dispose(); _contactCtl.dispose(); _websiteCtl.dispose(); super.dispose(); } // ───────────────────────────────────────────────────────────────────────── // Build // ───────────────────────────────────────────────────────────────────────── // Desktop sub-nav state: 0=Submit Event, 1=My Events, 2=Reward Shop int _desktopSubNav = 0; @override Widget build(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width >= 820; return Consumer( builder: (context, provider, _) { return Scaffold( backgroundColor: const Color(0xFFF5F7FB), body: isDesktop ? _buildDesktopLayout(context, provider) : Column( children: [ _buildHeader(context, provider), Expanded(child: _buildTabBody(context, provider)), ], ), ); }, ); } // ═══════════════════════════════════════════════════════════════════════════ // DESKTOP LAYOUT — matches web at mvnew.eventifyplus.com/contribute // ═══════════════════════════════════════════════════════════════════════════ static const _desktopTabs = ['Contribute', 'Leaderboard', 'Achievements']; static const _desktopTabIcons = [Icons.edit_note, null, null]; Widget _buildDesktopLayout(BuildContext context, GamificationProvider provider) { return Row( children: [ Flexible( flex: 2, child: RepaintBoundary( child: Container( decoration: AppDecoration.blueGradient, child: _buildContributeLeftPanel(context, provider), ), ), ), Flexible( flex: 3, child: RepaintBoundary( child: _buildContributeRightPanel(context, provider), ), ), ], ); } // ── Landscape left panel: contributor info + vertical nav ─────────────── Widget _buildContributeLeftPanel(BuildContext context, GamificationProvider provider) { final profile = provider.profile; final tier = profile?.tier ?? ContributorTier.BRONZE; final lifetimeEp = profile?.lifetimeEp ?? 0; const thresholds = [0, 100, 500, 1500, 5000]; final tierIdx = tier.index; final nextThresh = tierIdx < 4 ? thresholds[tierIdx + 1] : thresholds[4]; final prevThresh = thresholds[tierIdx]; final progress = tierIdx >= 4 ? 1.0 : (lifetimeEp - prevThresh) / (nextThresh - prevThresh); final tierColor = _tierColors[tier] ?? Colors.white; return SafeArea( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 24), // Title const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: Text( 'Contributor\nDashboard', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w800, height: 1.2), ), ), const SizedBox(height: 6), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: Text( 'Track your impact & earn rewards', style: TextStyle(color: Colors.white70, fontSize: 13), ), ), const SizedBox(height: 24), // Contributor Level badge Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(14), border: Border.all(color: tierColor.withOpacity(0.5)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: tierColor.withOpacity(0.2), borderRadius: BorderRadius.circular(20), border: Border.all(color: tierColor.withOpacity(0.6)), ), child: Text(tierLabel(tier), style: TextStyle(color: tierColor, fontWeight: FontWeight.w700, fontSize: 12)), ), const Spacer(), Text('$lifetimeEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14)), ]), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: progress.clamp(0.0, 1.0), minHeight: 6, backgroundColor: Colors.white24, valueColor: AlwaysStoppedAnimation(tierColor), ), ), if (tierIdx < 4) ...[ const SizedBox(height: 6), Text( 'Next: ${tierLabel(ContributorTier.values[tierIdx + 1])} at ${thresholds[tierIdx + 1]} pts', style: const TextStyle(color: Colors.white54, fontSize: 11), ), ], ], ), ), ), const SizedBox(height: 24), // Vertical tab navigation ...List.generate(_desktopTabs.length, (i) { final isActive = _activeTab == i; final icons = [Icons.edit_note, Icons.leaderboard_outlined, Icons.emoji_events_outlined]; return GestureDetector( onTap: () => setState(() => _activeTab = i), child: AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.fromLTRB(16, 0, 16, 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isActive ? Colors.white : Colors.white.withOpacity(0.08), borderRadius: BorderRadius.circular(12), ), child: Row(children: [ Icon(icons[i], size: 20, color: isActive ? _primary : Colors.white70), const SizedBox(width: 12), Text( _desktopTabs[i], style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: isActive ? _primary : Colors.white, ), ), const Spacer(), if (isActive) Icon(Icons.chevron_right, size: 18, color: _primary), ]), ), ); }), const SizedBox(height: 24), ], ), ), ); } // ── Landscape right panel: active tab content ──────────────────────────── Widget _buildContributeRightPanel(BuildContext context, GamificationProvider provider) { String title; String subtitle; switch (_activeTab) { case 1: title = 'Leaderboard'; subtitle = 'Top contributors this month'; break; case 2: title = 'Achievements'; subtitle = 'Your earned badges'; break; default: title = 'Submit Event'; subtitle = 'Share events with the community'; } return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ LandscapeSectionHeader(title: title, subtitle: subtitle), Expanded( child: RepaintBoundary( child: _buildDesktopTabBody(context, provider), ), ), ], ), ); } Widget _buildDesktopTabBody(BuildContext context, GamificationProvider provider) { switch (_activeTab) { case 0: return _buildDesktopContributeTab(context, provider); case 1: return _buildDesktopLeaderboardTab(context, provider); case 2: return _buildDesktopAchievementsTab(context, provider); default: return const SizedBox(); } } // ═══════════════════════════════════════════════════════════════════════════ // DESKTOP — Contribute Tab // ═══════════════════════════════════════════════════════════════════════════ Widget _buildDesktopContributeTab(BuildContext context, GamificationProvider provider) { final profile = provider.profile; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Sub-navigation buttons ── Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _desktopSubNav = 1), icon: const Icon(Icons.list_alt, size: 18), label: const Text('My Events'), style: OutlinedButton.styleFrom( foregroundColor: _desktopSubNav == 1 ? Colors.white : const Color(0xFF374151), backgroundColor: _desktopSubNav == 1 ? _primary : Colors.white, side: BorderSide(color: _desktopSubNav == 1 ? _primary : const Color(0xFFD1D5DB)), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _desktopSubNav = 0), icon: const Icon(Icons.add, size: 18), label: const Text('Submit Event'), style: OutlinedButton.styleFrom( foregroundColor: _desktopSubNav == 0 ? Colors.white : const Color(0xFF374151), backgroundColor: _desktopSubNav == 0 ? _primary : Colors.white, side: BorderSide(color: _desktopSubNav == 0 ? _primary : const Color(0xFFD1D5DB)), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: () => setState(() => _desktopSubNav = 2), icon: const Icon(Icons.shopping_bag_outlined, size: 18), label: const Text('Reward Shop'), style: ElevatedButton.styleFrom( foregroundColor: _desktopSubNav == 2 ? Colors.white : Colors.white, backgroundColor: _desktopSubNav == 2 ? const Color(0xFF1D4ED8) : _primary, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), const SizedBox(height: 20), // ── Sub-nav content ── if (_desktopSubNav == 0) _buildDesktopSubmitForm(context, provider), if (_desktopSubNav == 1) _buildDesktopMyEvents(), if (_desktopSubNav == 2) _buildDesktopRewardShop(provider), const SizedBox(height: 24), // ── Tier progress bar with milestones ── _buildDesktopTierBar(profile), ], ); } Widget _buildDesktopSubmitForm(BuildContext context, GamificationProvider provider) { return Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Row 1: Event Title + Category Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Event Title', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), TextFormField( controller: _titleCtl, decoration: InputDecoration( hintText: 'e.g. Local Food Festival', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), validator: (v) => (v == null || v.isEmpty) ? 'Required' : null, ), ], ), ), const SizedBox(width: 20), Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Category', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), DropdownButtonFormField( value: _selectedCategory, items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(), onChanged: (v) => setState(() => _selectedCategory = v!), decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), ), ], ), ), ], ), const SizedBox(height: 20), // Row 2: Date + Location Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Date', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), GestureDetector( onTap: () async { final d = await showDatePicker( context: context, initialDate: _selectedDate ?? DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (d != null) setState(() => _selectedDate = d); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( border: Border.all(color: const Color(0xFFD1D5DB)), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _selectedDate != null ? '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}' : 'dd/mm/yyyy', style: TextStyle( color: _selectedDate != null ? const Color(0xFF111827) : const Color(0xFF9CA3AF), fontSize: 14), ), const Icon(Icons.calendar_today, size: 18, color: Color(0xFF9CA3AF)), ], ), ), ), ], ), ), const SizedBox(width: 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Location', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), TextFormField( controller: _locationCtl, decoration: InputDecoration( hintText: 'e.g. City Park, Calicut', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), ), ], ), ), ], ), const SizedBox(height: 20), // Organizer Name const Text('Organizer Name', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), TextFormField( controller: _organizerCtl, decoration: InputDecoration( hintText: 'Individual or Organization Name', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), ), const SizedBox(height: 20), // Description const Text('Description', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(height: 6), TextFormField( controller: _descriptionCtl, maxLines: 4, decoration: InputDecoration( hintText: 'Tell us more about the event...', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFD1D5DB))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), ), const SizedBox(height: 20), // Event Images const Text('Event Images', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)), const SizedBox(height: 12), Row( children: [ Expanded(child: _buildDesktopImageUpload('Cover Image', Icons.image_outlined)), const SizedBox(width: 20), Expanded(child: _buildDesktopImageUpload('Thumbnail', Icons.crop_original)), ], ), const SizedBox(height: 28), // Submit button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _submitting ? null : () => _submitForm(provider), style: ElevatedButton.styleFrom( backgroundColor: _primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 18), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), child: _submitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Text('Submit for Verification'), ), ), ], ), ), ); } Widget _buildDesktopImageUpload(String label, IconData icon) { return GestureDetector( onTap: () async { final picked = await _picker.pickImage(source: ImageSource.gallery); if (picked != null) setState(() => _images.add(picked)); }, child: Container( height: 150, decoration: BoxDecoration( border: Border.all(color: const Color(0xFFD1D5DB), style: BorderStyle.solid), borderRadius: BorderRadius.circular(12), color: const Color(0xFFFAFBFC), ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 36, color: const Color(0xFF9CA3AF)), const SizedBox(height: 8), Text(label, style: const TextStyle(color: Color(0xFF6B7280), fontSize: 13)), ], ), ), ), ); } Widget _buildDesktopMyEvents() { return Container( padding: const EdgeInsets.all(40), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Center( child: Column( children: [ Icon(Icons.event_note, size: 48, color: const Color(0xFF9CA3AF)), const SizedBox(height: 12), const Text('No submitted events yet', style: TextStyle(color: Color(0xFF6B7280), fontSize: 15)), const SizedBox(height: 4), const Text('Events you submit will appear here.', style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 13)), ], ), ), ); } Widget _buildDesktopRewardShop(GamificationProvider provider) { final profile = provider.profile; final currentRp = profile?.currentRp ?? 0; final items = provider.shopItems; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Row( children: [ const Icon(Icons.shopping_bag_outlined, color: Color(0xFF0B63D6), size: 24), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('Reward Shop', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18)), SizedBox(height: 2), Text('Spend your hard-earned RP on exclusive vouchers and perks.', style: TextStyle(color: Color(0xFF6B7280), fontSize: 13)), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFFEF3C7), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFF59E0B)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text('YOUR BALANCE ', style: TextStyle(color: Color(0xFF92400E), fontSize: 11, fontWeight: FontWeight.w600)), Text('$currentRp RP', style: const TextStyle(color: Color(0xFFDC2626), fontSize: 16, fontWeight: FontWeight.w800)), ], ), ), ], ), ), const SizedBox(height: 16), // Items grid or empty if (items.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(60), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB), style: BorderStyle.solid), ), child: Column( children: [ Icon(Icons.shopping_bag_outlined, size: 48, color: const Color(0xFFD1D5DB)), const SizedBox(height: 12), const Text('No items available', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Color(0xFF374151))), const SizedBox(height: 4), const Text('Check back soon for new rewards!', style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 13)), ], ), ) else GridView.count( crossAxisCount: 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.3, children: items.map((item) => _buildDesktopShopCard(item, currentRp, provider)).toList(), ), ], ); } Widget _buildDesktopShopCard(ShopItem item, int currentRp, GamificationProvider provider) { final canRedeem = currentRp >= item.rpCost && item.stockQuantity > 0; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.name, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14)), const SizedBox(height: 4), Text(item.description, style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('${item.rpCost} RP', style: const TextStyle(color: Color(0xFFDC2626), fontWeight: FontWeight.w700)), ElevatedButton( onPressed: canRedeem ? () async { final code = await provider.redeemItem(item.id); if (mounted) { showDialog(context: context, builder: (_) => AlertDialog( title: const Text('Redeemed!'), content: Text('Voucher code: $code'), actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], )); } } : null, style: ElevatedButton.styleFrom( backgroundColor: _primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Redeem'), ), ], ), ], ), ); } Widget _buildDesktopTierBar(UserGamificationProfile? profile) { final lifetimeEp = profile?.lifetimeEp ?? 0; final currentEp = profile?.currentEp ?? 0; final currentRp = profile?.currentRp ?? 0; final tier = profile?.tier ?? ContributorTier.BRONZE; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Column( children: [ // Top row: tier chip + stats + share Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( color: _primary, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.diamond_outlined, size: 16, color: Colors.white), const SizedBox(width: 6), Text(tierLabel(tier).toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12, letterSpacing: 0.5)), ], ), ), const SizedBox(width: 16), Icon(Icons.bolt, size: 18, color: const Color(0xFF6B7280)), const SizedBox(width: 4), Text('$currentEp', style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14)), const Text(' Liquid EP', style: TextStyle(color: Color(0xFF6B7280), fontSize: 13)), const SizedBox(width: 20), Icon(Icons.card_giftcard, size: 18, color: const Color(0xFF6B7280)), const SizedBox(width: 4), Text('$currentRp', style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14)), const Text(' RP', style: TextStyle(color: Color(0xFF6B7280), fontSize: 13)), const Spacer(), OutlinedButton.icon( onPressed: () { Share.share( 'I\'m a ${tierLabel(tier)} contributor on @EventifyPlus with $lifetimeEp EP! 🏆 ' 'Discover & contribute to events near you at eventifyplus.com', subject: 'My Eventify.Plus Contributor Rank', ); }, icon: const Icon(Icons.share_outlined, size: 16), label: const Text('Share Rank'), style: OutlinedButton.styleFrom( foregroundColor: _primary, side: const BorderSide(color: Color(0xFFD1D5DB)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), ), ), ], ), const SizedBox(height: 16), // Tier milestones Row( children: [ _tierMilestone('Bronze', '0 EP', tier.index >= 0), Expanded(child: Container(height: 2, color: tier.index >= 1 ? _primary : const Color(0xFFE5E7EB))), _tierMilestone('Silver', '100 EP', tier.index >= 1), Expanded(child: Container(height: 2, color: tier.index >= 2 ? _primary : const Color(0xFFE5E7EB))), _tierMilestone('Gold', '500 EP', tier.index >= 2), Expanded(child: Container(height: 2, color: tier.index >= 3 ? _primary : const Color(0xFFE5E7EB))), _tierMilestone('Platinum', '1.5K EP', tier.index >= 3), Expanded(child: Container(height: 2, color: tier.index >= 4 ? _primary : const Color(0xFFE5E7EB))), _tierMilestone('Diamond', '5K EP', tier.index >= 4), ], ), ], ), ); } Widget _tierMilestone(String label, String ep, bool active) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text(label, style: TextStyle( fontSize: 12, fontWeight: active ? FontWeight.w700 : FontWeight.w500, color: active ? const Color(0xFF111827) : const Color(0xFF9CA3AF))), Text(ep, style: TextStyle( fontSize: 10, color: active ? const Color(0xFF6B7280) : const Color(0xFFD1D5DB))), ], ); } // ═══════════════════════════════════════════════════════════════════════════ // DESKTOP — Leaderboard Tab // ═══════════════════════════════════════════════════════════════════════════ Widget _buildDesktopLeaderboardTab(BuildContext context, GamificationProvider provider) { if (provider.isLoading && provider.leaderboard.isEmpty) { return const Center(child: Padding(padding: EdgeInsets.all(40), child: CircularProgressIndicator())); } final entries = provider.leaderboard; final matching = entries.where((e) => e.isCurrentUser).toList(); final myEntry = matching.isNotEmpty ? matching.first : null; return Column( children: [ // Filters _buildDesktopLeaderboardFilters(provider), const SizedBox(height: 16), // Podium if (entries.length >= 3) _buildDesktopPodium(entries.take(3).toList()), const SizedBox(height: 16), // Rank table Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ // Headers Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))), ), child: Row( children: const [ SizedBox(width: 50, child: Text('RANK', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), SizedBox(width: 50), Expanded(child: Text('USER', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), SizedBox(width: 100, child: Text('POINTS', textAlign: TextAlign.right, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), SizedBox(width: 20), SizedBox(width: 80, child: Text('LEVEL', textAlign: TextAlign.center, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), SizedBox(width: 20), SizedBox(width: 100, child: Text('EVENTS ADDED', textAlign: TextAlign.right, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), ], ), ), // Rows ...entries.skip(3).map((e) => _buildDesktopRankRow(e)).toList(), ], ), ), // My rank if (myEntry != null) ...[ const SizedBox(height: 16), _buildMyRankCard(myEntry), ], ], ); } Widget _buildDesktopLeaderboardFilters(GamificationProvider provider) { return Column( children: [ // Time toggle — right aligned Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _timePill('All Time', 'all_time', provider), const SizedBox(width: 6), _timePill('This Month', 'this_month', provider), ], ), const SizedBox(height: 10), // District pills SizedBox( height: 42, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _lbDistricts.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) { final d = _lbDistricts[i]; final isActive = provider.leaderboardDistrict == d; return GestureDetector( onTap: () => provider.setDistrict(d), child: Container( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), decoration: BoxDecoration( color: isActive ? _primary : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: isActive ? _primary : const Color(0xFFD1D5DB)), ), child: Text(d, style: TextStyle( color: isActive ? Colors.white : const Color(0xFF374151), fontWeight: FontWeight.w500, fontSize: 13, )), ), ); }, ), ), ], ); } // Reuses _lbDistricts defined in the mobile leaderboard section below Widget _buildDesktopPodium(List top3) { final first = top3[0]; final second = top3[1]; final third = top3[2]; return SizedBox( height: 260, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ // #2 Silver _desktopPodiumUser(second, 2, 120, const Color(0xFFBDBDBD)), const SizedBox(width: 8), // #1 Gold _desktopPodiumUser(first, 1, 160, const Color(0xFFF59E0B)), const SizedBox(width: 8), // #3 Bronze _desktopPodiumUser(third, 3, 100, const Color(0xFF92400E)), ], ), ); } Widget _desktopPodiumUser(LeaderboardEntry entry, int rank, double pillarHeight, Color pillarColor) { final rankColors = {1: const Color(0xFFF59E0B), 2: const Color(0xFF6B7280), 3: const Color(0xFF92400E)}; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ // Avatar with rank badge Stack( clipBehavior: Clip.none, children: [ CircleAvatar( radius: rank == 1 ? 36 : 28, backgroundColor: pillarColor.withOpacity(0.2), child: Text(entry.username[0], style: TextStyle(fontSize: rank == 1 ? 24 : 18, fontWeight: FontWeight.w700, color: pillarColor)), ), Positioned( bottom: -4, right: -4, child: Container( width: 22, height: 22, decoration: BoxDecoration( color: rankColors[rank], shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), child: Center(child: Text('$rank', style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700))), ), ), ], ), const SizedBox(height: 8), Text(entry.username, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), Text(_fmtPts(entry.lifetimeEp), style: TextStyle(color: rankColors[rank], fontWeight: FontWeight.w600, fontSize: 12)), const SizedBox(height: 6), // Pillar Container( width: 140, height: pillarHeight, decoration: BoxDecoration( color: pillarColor.withOpacity(rank == 1 ? 0.85 : 0.6), borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)), ), ), ], ); } Widget _buildDesktopRankRow(LeaderboardEntry entry) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: entry.isCurrentUser ? const Color(0xFFEFF6FF) : Colors.white, border: const Border(bottom: BorderSide(color: Color(0xFFF3F4F6))), ), child: Row( children: [ SizedBox(width: 50, child: Text('${entry.rank}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: Color(0xFF374151)))), CircleAvatar( radius: 18, backgroundColor: const Color(0xFFE5E7EB), child: Text(entry.username[0], style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), ), const SizedBox(width: 12), Expanded(child: Text(entry.username, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14))), SizedBox( width: 100, child: Text(_fmtPts(entry.lifetimeEp), textAlign: TextAlign.right, style: const TextStyle(color: Color(0xFF16A34A), fontWeight: FontWeight.w700, fontSize: 14)), ), const SizedBox(width: 20), SizedBox( width: 80, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: _primary, borderRadius: BorderRadius.circular(12), ), child: Text(tierLabel(entry.tier), style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600)), ), ), ), const SizedBox(width: 20), SizedBox( width: 100, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ const Icon(Icons.calendar_today, size: 14, color: Color(0xFF9CA3AF)), const SizedBox(width: 6), Text('${entry.eventsCount}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), ], ), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // DESKTOP — Achievements Tab // ═══════════════════════════════════════════════════════════════════════════ Widget _buildDesktopAchievementsTab(BuildContext context, GamificationProvider provider) { final badges = provider.achievements; if (provider.isLoading && badges.isEmpty) { return const Center(child: Padding(padding: EdgeInsets.all(40), child: CircularProgressIndicator())); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Your Badges', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 20, color: Color(0xFF111827))), const SizedBox(height: 16), GridView.count( crossAxisCount: 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 16, crossAxisSpacing: 16, childAspectRatio: 1.6, children: badges.map((badge) => _buildDesktopBadgeCard(badge)).toList(), ), ], ); } Widget _buildDesktopBadgeCard(AchievementBadge badge) { final icon = _badgeIcons[badge.iconName] ?? Icons.emoji_events_outlined; final isUnlocked = badge.isUnlocked; // Badge icon colors final iconColors = { 'edit': const Color(0xFF3B82F6), 'star': const Color(0xFFF59E0B), 'emoji_events': const Color(0xFFF97316), 'leaderboard': const Color(0xFF8B5CF6), 'photo_library': const Color(0xFF6B7280), 'verified': const Color(0xFF10B981), }; final bgColors = { 'edit': const Color(0xFFDBEAFE), 'star': const Color(0xFFFEF3C7), 'emoji_events': const Color(0xFFFED7AA), 'leaderboard': const Color(0xFFEDE9FE), 'photo_library': const Color(0xFFF3F4F6), 'verified': const Color(0xFFD1FAE5), }; final iconColor = isUnlocked ? (iconColors[badge.iconName] ?? const Color(0xFF6B7280)) : const Color(0xFF9CA3AF); final bgColor = isUnlocked ? (bgColors[badge.iconName] ?? const Color(0xFFF3F4F6)) : const Color(0xFFF3F4F6); return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: iconColor, size: 22), ), if (!isUnlocked) ...[ const Spacer(), const Icon(Icons.lock_outline, size: 16, color: Color(0xFF9CA3AF)), ], ], ), const SizedBox(height: 12), Text(badge.title, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14, color: isUnlocked ? const Color(0xFF111827) : const Color(0xFF9CA3AF))), const SizedBox(height: 2), Text(badge.description, style: TextStyle( fontSize: 12, color: isUnlocked ? const Color(0xFF6B7280) : const Color(0xFFD1D5DB)), maxLines: 2, overflow: TextOverflow.ellipsis), if (badge.progress < 1.0 && badge.progress > 0) ...[ const Spacer(), Row( children: [ Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: badge.progress, minHeight: 6, backgroundColor: const Color(0xFFE5E7EB), valueColor: const AlwaysStoppedAnimation(Color(0xFF3B82F6)), ), ), ), const SizedBox(width: 8), Text('${(badge.progress * 100).round()}%', style: const TextStyle(fontSize: 11, color: Color(0xFF6B7280), fontWeight: FontWeight.w600)), ], ), ], ], ), ); } // ───────────────────────────────────────────────────────────────────────── // MOBILE Header (unchanged) // ───────────────────────────────────────────────────────────────────────── Widget _buildHeader(BuildContext context, GamificationProvider provider) { final theme = Theme.of(context); final profile = provider.profile; final tier = profile?.tier ?? ContributorTier.BRONZE; final tierColor = _tierColors[tier]!; final currentEp = profile?.currentEp ?? 0; final currentRp = profile?.currentRp ?? 0; final lifetimeEp = profile?.lifetimeEp ?? 0; final nextThresh = nextTierThreshold(tier); final startEp = tierStartEp(tier); final progress = nextThresh == null ? 1.0 : ((lifetimeEp - startEp) / (nextThresh - startEp)).clamp(0.0, 1.0); return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 32, 20, 20), decoration: AppDecoration.blueGradient.copyWith( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(_cornerRadius), bottomRight: Radius.circular(_cornerRadius), ), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4)), ], ), child: SafeArea( bottom: false, child: Column( children: [ const SizedBox(height: 4), Text( 'Contributor Dashboard', textAlign: TextAlign.center, style: theme.textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 20, ), ), const SizedBox(height: 6), Text( 'Track your impact, earn rewards, and climb the ranks!', textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: Colors.white.withOpacity(0.88), fontSize: 12, ), ), const SizedBox(height: 16), // ── Segmented tabs ────────────────────────────────────────────── _buildSegmentedTabs(context), const SizedBox(height: 14), // ── Contributor level card ────────────────────────────────────── if (provider.isLoading && profile == null) const SizedBox( height: 80, child: Center(child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)), ) else Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.10), borderRadius: BorderRadius.circular(_cornerRadius - 2), border: Border.all(color: Colors.white.withOpacity(0.12)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( 'Contributor Level', style: theme.textTheme.titleSmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), // Tier badge Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: tierColor.withOpacity(0.85), borderRadius: BorderRadius.circular(20), ), child: Text( tierLabel(tier), style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, ), ), ), ], ), const SizedBox(height: 10), // EP / RP stats row Row( children: [ _statChip('$currentEp EP', 'This month', Colors.white.withOpacity(0.15)), const SizedBox(width: 8), _statChip('$currentRp RP', 'Redeemable', Colors.white.withOpacity(0.15)), const SizedBox(width: 8), _statChip('$lifetimeEp', 'Lifetime EP', Colors.white.withOpacity(0.15)), ], ), const SizedBox(height: 10), // Progress bar TweenAnimationBuilder( tween: Tween(begin: 0.0, end: progress), duration: const Duration(milliseconds: 800), builder: (_, val, __) => ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: val, minHeight: 6, valueColor: AlwaysStoppedAnimation(tierColor), backgroundColor: Colors.white24, ), ), ), const SizedBox(height: 6), Text( nextThresh != null ? '${nextThresh - lifetimeEp} EP to ${tierLabel(ContributorTier.values[tier.index + 1])}' : '🎉 Maximum tier reached!', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70, fontSize: 11), ), ], ), ), ], ), ), ); } Widget _statChip(String value, String label, Color bg) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(10)), child: Column( children: [ Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)), Text(label, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 10)), ], ), ), ); } // ───────────────────────────────────────────────────────────────────────── // Segmented Tabs (4 tabs) // ───────────────────────────────────────────────────────────────────────── static const Curve _bouncyCurve = Curves.easeInOutCubic; static const List _tabIcons = [ Icons.edit_outlined, Icons.emoji_events_outlined, Icons.workspace_premium_outlined, Icons.storefront_outlined, ]; Widget _buildSegmentedTabs(BuildContext context) { const tabs = ['Contribute', 'Leaderboard', 'Achievements', 'Shop']; return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: LayoutBuilder( builder: (context, constraints) { const double padding = 5.0; final tabWidth = (constraints.maxWidth - padding * 2) / tabs.length; return RepaintBoundary( child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( height: 52, 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: 280), curve: _bouncyCurve, left: padding + _activeTab * tabWidth, top: padding, width: tabWidth, height: 52 - padding * 2, child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.10), blurRadius: 12, offset: const Offset(0, 3)), ], ), ), ), // Labels Padding( padding: const EdgeInsets.all(padding), child: Row( children: List.generate(tabs.length, (i) { final isActive = _activeTab == i; return GestureDetector( onTap: () => setState(() => _activeTab = i), behavior: HitTestBehavior.opaque, child: SizedBox( width: tabWidth, height: 52 - padding * 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _tabIcons[i], size: 15, color: isActive ? _primary : Colors.white.withOpacity(0.8), ), const SizedBox(height: 2), Text( tabs[i], style: TextStyle( fontSize: 10, fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, color: isActive ? _primary : Colors.white.withOpacity(0.85), ), ), ], ), ), ); }), ), ), ], ), ), )); // RepaintBoundary }, ), ); } // ───────────────────────────────────────────────────────────────────────── // Tab Body Router // ───────────────────────────────────────────────────────────────────────── Widget _buildTabBody(BuildContext context, GamificationProvider provider) { switch (_activeTab) { case 0: return _buildContributeTab(context, provider); case 1: return _buildLeaderboardTab(context, provider); case 2: return _buildAchievementsTab(context, provider); case 3: return _buildShopTab(context, provider); default: return const SizedBox(); } } // ═══════════════════════════════════════════════════════════════════════════ // TAB 0 — CONTRIBUTE // ═══════════════════════════════════════════════════════════════════════════ Widget _buildContributeTab(BuildContext context, GamificationProvider provider) { final theme = Theme.of(context); return SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 20, 16, 32), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Submit an Event', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), const SizedBox(height: 4), Text( 'Fill in the details below. Earn up to 10 EP per approved submission.', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[600]), ), const SizedBox(height: 20), _formCard([ _formField(_titleCtl, 'Event Name *', Icons.event, required: true), _divider(), _categoryDropdown(), _divider(), _districtDropdown(), ]), const SizedBox(height: 12), _formCard([ _dateTile(), _divider(), _timeTile(), ]), const SizedBox(height: 12), _formCard([ _formField(_locationCtl, 'Location / Venue', Icons.location_on_outlined), _divider(), _formField(_organizerCtl, 'Organizer Name', Icons.person_outline), _divider(), _formField(_descriptionCtl, 'Description', Icons.notes_outlined, maxLines: 3), ]), const SizedBox(height: 12), _formCard([ _formField( _ticketPriceCtl, 'Ticket Price (₹)', Icons.confirmation_number_outlined, keyboardType: TextInputType.number, hint: 'Leave blank if free', inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), _divider(), _formField(_contactCtl, 'Contact Details', Icons.phone_outlined), _divider(), _formField(_websiteCtl, 'Website / Social Media', Icons.link_outlined), ]), const SizedBox(height: 12), // Image picker _buildImagePickerSection(theme), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 52, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: _primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), elevation: 0, ), onPressed: _submitting ? null : () => _submitForm(provider), child: _submitting ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Submit for Verification', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 15)), ), ), const SizedBox(height: 12), Center( child: Text( 'Your submission will be reviewed by our team.\nApproved events earn EP immediately.', textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[500], fontSize: 11), ), ), ], ), ), ); } Widget _formCard(List children) { return RepaintBoundary( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(children: children), ), ); } Widget _divider() => const Divider(height: 1, indent: 16, endIndent: 16, color: Color(0xFFF0F0F0)); Widget _formField( TextEditingController ctl, String label, IconData icon, { bool required = false, int maxLines = 1, TextInputType keyboardType = TextInputType.text, String? hint, List? inputFormatters, }) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: TextFormField( controller: ctl, maxLines: maxLines, keyboardType: keyboardType, inputFormatters: inputFormatters, decoration: InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon, size: 18, color: Colors.grey[500]), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, labelStyle: TextStyle(color: Colors.grey[600], fontSize: 13), hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13), ), validator: required ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null : null, ), ); } Widget _categoryDropdown() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: DropdownButtonFormField( value: _selectedCategory, decoration: InputDecoration( labelText: 'Category *', prefixIcon: Icon(Icons.category_outlined, size: 18, color: Colors.grey[500]), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, labelStyle: TextStyle(color: Colors.grey[600], fontSize: 13), ), items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c, style: const TextStyle(fontSize: 14)))).toList(), onChanged: (v) => setState(() => _selectedCategory = v!), isExpanded: true, icon: Icon(Icons.keyboard_arrow_down, color: Colors.grey[500]), ), ); } Widget _districtDropdown() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: DropdownButtonFormField( value: _selectedDistrict, decoration: InputDecoration( labelText: 'District *', prefixIcon: Icon(Icons.map_outlined, size: 18, color: Colors.grey[500]), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, labelStyle: TextStyle(color: Colors.grey[600], fontSize: 13), ), items: _districts.map((d) => DropdownMenuItem(value: d, child: Text(d, style: const TextStyle(fontSize: 14)))).toList(), onChanged: (v) => setState(() => _selectedDistrict = v!), isExpanded: true, icon: Icon(Icons.keyboard_arrow_down, color: Colors.grey[500]), ), ); } Widget _dateTile() { return ListTile( leading: Icon(Icons.calendar_today_outlined, size: 18, color: Colors.grey[500]), title: Text( _selectedDate != null ? '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}' : 'Select Date *', style: TextStyle(fontSize: 14, color: _selectedDate != null ? Colors.black87 : Colors.grey[600]), ), onTap: _pickDate, dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ); } Widget _timeTile() { return ListTile( leading: Icon(Icons.access_time_outlined, size: 18, color: Colors.grey[500]), title: Text( _selectedTime != null ? _selectedTime!.format(context) : 'Select Time', style: TextStyle(fontSize: 14, color: _selectedTime != null ? Colors.black87 : Colors.grey[600]), ), onTap: _pickTime, dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ); } Widget _buildImagePickerSection(ThemeData theme) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2))], ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.photo_library_outlined, size: 18, color: Colors.grey[500]), const SizedBox(width: 8), Text('Images (up to 5)', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey[700])), const Spacer(), Text('${_images.length}/5', style: TextStyle(color: Colors.grey[500], fontSize: 12)), ], ), const SizedBox(height: 12), if (_images.isNotEmpty) SizedBox( height: 80, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _images.length + (_images.length < 5 ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) { if (i == _images.length) return _addImageButton(); final img = _images[i]; return Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: kIsWeb ? const SizedBox(width: 80, height: 80, child: Icon(Icons.image)) : Image.file(File(img.path), width: 80, height: 80, fit: BoxFit.cover), ), Positioned( top: 2, right: 2, child: GestureDetector( onTap: () => setState(() => _images.removeAt(i)), child: Container( decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle), padding: const EdgeInsets.all(2), child: const Icon(Icons.close, size: 12, color: Colors.white), ), ), ), ], ); }, ), ) else _addImageButton(full: true), ], ), ); } Widget _addImageButton({bool full = false}) { return GestureDetector( onTap: _pickImages, child: Container( width: full ? double.infinity : 80, height: 80, decoration: BoxDecoration( color: const Color(0xFFF5F7FB), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFD1D5DB), style: BorderStyle.solid), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_photo_alternate_outlined, color: Colors.grey[400], size: full ? 28 : 22), if (full) ...[ const SizedBox(height: 4), Text('Add Photos', style: TextStyle(color: Colors.grey[500], fontSize: 12)), ], ], ), ), ); } Future _pickDate() async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: _selectedDate ?? now, firstDate: DateTime(now.year - 1), lastDate: DateTime(now.year + 3), builder: (ctx, child) => Theme( data: Theme.of(ctx).copyWith(colorScheme: ColorScheme.light(primary: _primary)), child: child!, ), ); if (picked != null) setState(() => _selectedDate = picked); } Future _pickTime() async { final picked = await showTimePicker( context: context, initialTime: _selectedTime ?? TimeOfDay.now(), builder: (ctx, child) => Theme( data: Theme.of(ctx).copyWith(colorScheme: ColorScheme.light(primary: _primary)), child: child!, ), ); if (picked != null) setState(() => _selectedTime = picked); } Future _pickImages() async { try { final List picked = await _picker.pickMultiImage(imageQuality: 80); if (picked.isNotEmpty) { setState(() { _images = [..._images, ...picked].take(5).toList(); }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick images: $e'))); } } } Future _submitForm(GamificationProvider provider) async { if (!(_formKey.currentState?.validate() ?? false)) return; if (_selectedDate == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select a date'))); return; } setState(() => _submitting = true); try { await provider.submitContribution({ 'title': _titleCtl.text.trim(), 'category': _selectedCategory, 'district': _selectedDistrict, 'date': _selectedDate!.toIso8601String(), 'time': _selectedTime?.format(context), 'location': _locationCtl.text.trim(), 'organizer_name': _organizerCtl.text.trim(), 'description': _descriptionCtl.text.trim(), 'ticket_price': _ticketPriceCtl.text.trim(), 'contact': _contactCtl.text.trim(), 'website': _websiteCtl.text.trim(), 'images': _images.map((f) => f.path).toList(), }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('✅ Submitted for verification! You\'ll earn EP once approved.'), backgroundColor: Color(0xFF16A34A), ), ); _clearForm(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red)); } } finally { if (mounted) setState(() => _submitting = false); } } void _clearForm() { _titleCtl.clear(); _locationCtl.clear(); _organizerCtl.clear(); _descriptionCtl.clear(); _ticketPriceCtl.clear(); _contactCtl.clear(); _websiteCtl.clear(); setState(() { _selectedDate = null; _selectedTime = null; _selectedCategory = _categories.first; _selectedDistrict = _districts.first; _images = []; }); } // ═══════════════════════════════════════════════════════════════════════════ // TAB 1 — LEADERBOARD (matches web version at mvnew.eventifyplus.com/contribute) // ═══════════════════════════════════════════════════════════════════════════ // Kerala districts matching the web version (leaderboard filter) static const _lbDistricts = [ 'Overall Kerala', 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', ]; // Format a points number with comma separator + " pts" suffix static String _fmtPts(int ep) { if (ep >= 1000) { final s = ep.toString(); final intPart = s.substring(0, s.length - 3); final fracPart = s.substring(s.length - 3); return '$intPart,$fracPart pts'; } return '$ep pts'; } Widget _buildLeaderboardTab(BuildContext context, GamificationProvider provider) { if (provider.isLoading && provider.leaderboard.isEmpty) { return const Center(child: CircularProgressIndicator()); } final entries = provider.leaderboard; final _matching = entries.where((e) => e.isCurrentUser).toList(); final myEntry = _matching.isNotEmpty ? _matching.first : null; return Column( children: [ // ── Time period toggle (top-right) + district scroll ────────────────── _buildLeaderboardFilters(provider), Expanded( child: Container( color: const Color(0xFFFAFBFC), child: CustomScrollView( slivers: [ // Podium top-3 if (entries.length >= 3) SliverToBoxAdapter(child: _buildPodium(entries.take(3).toList())), // Column headers SliverToBoxAdapter( child: Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( children: [ const SizedBox(width: 36, child: Text('RANK', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), const SizedBox(width: 44), const Expanded(child: Text('USER', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), const SizedBox(width: 72, child: Text('POINTS', textAlign: TextAlign.right, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), const SizedBox(width: 8), const SizedBox(width: 60, child: Text('LEVEL', textAlign: TextAlign.center, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), const SizedBox(width: 8), const SizedBox(width: 36, child: Text('EVENTS', textAlign: TextAlign.right, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Color(0xFF9CA3AF), letterSpacing: 0.5))), ], ), ), ), // Ranked list (rank 4+) SliverList( delegate: SliverChildBuilderDelegate( (ctx, i) { final entry = entries.length > 3 ? entries[i + 3] : entries[i]; return _buildRankRow(entry); }, childCount: entries.length > 3 ? entries.length - 3 : 0, ), ), const SliverToBoxAdapter(child: SizedBox(height: 100)), ], ), ), ), // My rank sticky card with Share if (myEntry != null) SafeArea(top: false, child: _buildMyRankCard(myEntry)), ], ); } Widget _buildLeaderboardFilters(GamificationProvider provider) { return Container( color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Time period toggle — right-aligned Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 4), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _timePill('All Time', 'all_time', provider), const SizedBox(width: 6), _timePill('This Month', 'this_month', provider), ], ), ), // Horizontal scroll of district pills SizedBox( height: 42, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), itemCount: _lbDistricts.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) { final d = _lbDistricts[i]; final isActive = provider.leaderboardDistrict == d; return GestureDetector( onTap: () => provider.setDistrict(d), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: isActive ? _primary : Colors.white, border: Border.all(color: isActive ? _primary : const Color(0xFFD1D5DB)), borderRadius: BorderRadius.circular(999), ), child: Text( d, style: TextStyle( color: isActive ? Colors.white : const Color(0xFF374151), fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, fontSize: 13, ), ), ), ); }, ), ), const Divider(height: 1, color: Color(0xFFE5E7EB)), ], ), ); } Widget _timePill(String label, String key, GamificationProvider provider) { final isActive = provider.leaderboardTimePeriod == key; return GestureDetector( onTap: () => provider.setTimePeriod(key), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: isActive ? _primary : const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(999), ), child: Text( label, style: TextStyle( color: isActive ? Colors.white : const Color(0xFF6B7280), fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, fontSize: 12, ), ), ), ); } Widget _buildPodium(List top3) { // Layout: [#2 left] [#1 centre, tallest] [#3 right] final order = [top3[1], top3[0], top3[2]]; final heights = [90.0, 120.0, 70.0]; // Pillar colors matching the web: silver, gold/yellow, brown final pillarColors = [ const Color(0xFFBDBDBD), // 2nd: silver-grey const Color(0xFFF59E0B), // 1st: gold/amber const Color(0xFF92400E), // 3rd: bronze-brown ]; // Badge colors (overlaid on avatar) final badgeColors = [ const Color(0xFFD97706), // #2: orange const Color(0xFF1D4ED8), // #1: blue const Color(0xFF92400E), // #3: brown ]; final ranks = [2, 1, 3]; return Container( color: const Color(0xFFFAFBFC), padding: const EdgeInsets.fromLTRB(16, 24, 16, 0), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(3, (i) { final e = order[i]; final avatarSize = i == 1 ? 64.0 : 52.0; // #1 is larger return Expanded( child: Column( children: [ // Avatar with rank badge overlaid Stack( clipBehavior: Clip.none, children: [ // Avatar circle Container( width: avatarSize, height: avatarSize, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xFFE0F2FE), border: Border.all(color: pillarColors[i], width: 2.5), ), child: Center( child: Text( e.username.isNotEmpty ? e.username[0].toUpperCase() : '?', style: TextStyle( fontSize: i == 1 ? 24 : 18, fontWeight: FontWeight.w800, color: pillarColors[i], ), ), ), ), // Rank badge — bottom-right corner of avatar Positioned( bottom: -2, right: -2, child: Container( width: 20, height: 20, decoration: BoxDecoration( color: badgeColors[i], shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1.5), ), child: Center( child: Text( '${ranks[i]}', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800), ), ), ), ), ], ), const SizedBox(height: 8), Text( e.username.split(' ').first, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Color(0xFF111827)), textAlign: TextAlign.center, ), const SizedBox(height: 2), Text( _fmtPts(e.lifetimeEp), style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF0F45CF)), textAlign: TextAlign.center, ), const SizedBox(height: 8), // Podium pillar Container( height: heights[i], decoration: BoxDecoration( color: pillarColors[i], borderRadius: const BorderRadius.only( topLeft: Radius.circular(6), topRight: Radius.circular(6), ), ), ), ], ), ); }), ), ); } Widget _buildRankRow(LeaderboardEntry entry) { final tierColor = _tierColors[entry.tier]!; final isMe = entry.isCurrentUser; return Container( decoration: BoxDecoration( color: isMe ? const Color(0xFFEFF6FF) : Colors.white, border: const Border(bottom: BorderSide(color: Color(0xFFE5E7EB), width: 0.8)), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // Rank number SizedBox( width: 36, child: Text( '${entry.rank}', style: TextStyle( fontWeight: FontWeight.w700, fontSize: 16, color: isMe ? _primary : const Color(0xFF111827), ), ), ), // Avatar circle Container( width: 36, height: 36, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xFFE0F2FE), border: Border.all(color: tierColor.withOpacity(0.5), width: 1.5), ), child: Center( child: Text( entry.username.isNotEmpty ? entry.username[0].toUpperCase() : '?', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: tierColor), ), ), ), const SizedBox(width: 8), // Name Expanded( child: Text( entry.username + (isMe ? ' (You)' : ''), style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: isMe ? _primary : const Color(0xFF111827), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Points SizedBox( width: 80, child: Text( _fmtPts(entry.lifetimeEp), textAlign: TextAlign.right, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF0F45CF)), ), ), const SizedBox(width: 8), // Level badge SizedBox( width: 60, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: tierColor.withOpacity(0.12), borderRadius: BorderRadius.circular(999), ), child: Text( tierLabel(entry.tier), style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: tierColor), textAlign: TextAlign.center, ), ), ), ), const SizedBox(width: 8), // Events added SizedBox( width: 36, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ const Icon(Icons.calendar_today_outlined, size: 12, color: Color(0xFF9CA3AF)), const SizedBox(width: 3), Text('${entry.eventsCount}', style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280), fontWeight: FontWeight.w500)), ], ), ), ], ), ); } Widget _buildMyRankCard(LeaderboardEntry me) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 20), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 10, offset: const Offset(0, -4))], ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration(color: _primary, borderRadius: BorderRadius.circular(10)), child: Text('Your Rank: #${me.rank}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)), ), const SizedBox(width: 10), Expanded( child: Text('${_fmtPts(me.lifetimeEp)} · ${tierLabel(me.tier)}', style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12)), ), GestureDetector( onTap: () { Share.share( 'I\'m ranked #${me.rank} on @EventifyPlus with ${me.lifetimeEp} EP! 🏆 ' 'Discover & contribute to events near you at eventifyplus.com', subject: 'My Eventify.Plus Leaderboard Rank', ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration(border: Border.all(color: _primary), borderRadius: BorderRadius.circular(10)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.share_outlined, size: 14, color: _primary), const SizedBox(width: 4), Text('Share', style: TextStyle(color: _primary, fontSize: 12, fontWeight: FontWeight.w600)), ], ), ), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // TAB 2 — ACHIEVEMENTS // ═══════════════════════════════════════════════════════════════════════════ Widget _buildAchievementsTab(BuildContext context, GamificationProvider provider) { final theme = Theme.of(context); if (provider.isLoading && provider.achievements.isEmpty) { return const Center(child: CircularProgressIndicator()); } final badges = provider.achievements; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Your Badges', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), const SizedBox(height: 4), Text( 'Earn badges by contributing events and climbing tiers.', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[600]), ), const SizedBox(height: 16), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: badges.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.95, ), itemBuilder: (_, i) => _buildBadgeCard(badges[i], theme), ), ], ), ); } Widget _buildBadgeCard(AchievementBadge badge, ThemeData theme) { final icon = _badgeIcons[badge.iconName] ?? Icons.star_outline; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2))], border: badge.isUnlocked ? Border.all(color: _primary.withOpacity(0.3)) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( shape: BoxShape.circle, color: badge.isUnlocked ? _primary.withOpacity(0.12) : Colors.grey[100], ), child: Icon( badge.isUnlocked ? icon : Icons.lock_outline, size: 26, color: badge.isUnlocked ? _primary : Colors.grey[400], ), ), const SizedBox(height: 10), Text( badge.title, textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 13, color: badge.isUnlocked ? Colors.black87 : Colors.grey[500], ), ), const SizedBox(height: 4), Text( badge.description, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 10, color: Colors.grey[500]), ), if (!badge.isUnlocked && badge.progress > 0) ...[ const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: badge.progress, minHeight: 4, valueColor: AlwaysStoppedAnimation(_primary.withOpacity(0.6)), backgroundColor: Colors.grey[200]!, ), ), const SizedBox(height: 3), Text('${(badge.progress * 100).round()}%', style: TextStyle(fontSize: 9, color: Colors.grey[500])), ], if (badge.isUnlocked) Padding( padding: const EdgeInsets.only(top: 6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: const Color(0xFFDCFCE7), borderRadius: BorderRadius.circular(8)), child: const Text('Unlocked', style: TextStyle(fontSize: 9, color: Color(0xFF16A34A), fontWeight: FontWeight.w600)), ), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // TAB 3 — SHOP // ═══════════════════════════════════════════════════════════════════════════ Widget _buildShopTab(BuildContext context, GamificationProvider provider) { final theme = Theme.of(context); final rp = provider.profile?.currentRp ?? 0; if (provider.isLoading && provider.shopItems.isEmpty) { return const Center(child: CircularProgressIndicator()); } return Column( children: [ // RP balance banner Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFF0B63D6), Color(0xFF3B82F6)]), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Icon(Icons.monetization_on_outlined, color: Colors.white, size: 16), const SizedBox(width: 6), Text('$rp RP Balance', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14)), ], ), ), const SizedBox(width: 10), Expanded( child: Text( '10 EP = 1 RP • Converted monthly', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[600], fontSize: 11), ), ), ], ), ), Expanded( child: GridView.builder( padding: const EdgeInsets.all(16), itemCount: provider.shopItems.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.78, ), itemBuilder: (_, i) => _buildShopCard(context, provider, provider.shopItems[i]), ), ), ], ); } Widget _buildShopCard(BuildContext context, GamificationProvider provider, ShopItem item) { final rp = provider.profile?.currentRp ?? 0; final canRedeem = rp >= item.rpCost && item.stockQuantity > 0; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2))], ), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Icon area Container( height: 60, decoration: BoxDecoration( color: _primary.withOpacity(0.07), borderRadius: BorderRadius.circular(10), ), child: Center( child: Icon( item.stockQuantity == 0 ? Icons.inventory_2_outlined : Icons.card_giftcard_outlined, size: 30, color: item.stockQuantity == 0 ? Colors.grey[400] : _primary, ), ), ), const SizedBox(height: 10), Text(item.name, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 4), Text(item.description, style: TextStyle(fontSize: 10, color: Colors.grey[500]), maxLines: 2, overflow: TextOverflow.ellipsis), const Spacer(), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFF59E0B).withOpacity(0.12), borderRadius: BorderRadius.circular(8), ), child: Text('${item.rpCost} RP', style: const TextStyle(color: Color(0xFFD97706), fontSize: 11, fontWeight: FontWeight.w700)), ), const SizedBox(width: 4), if (item.stockQuantity == 0) const Text('Out of stock', style: TextStyle(fontSize: 9, color: Colors.redAccent)) else Text('${item.stockQuantity} left', style: TextStyle(fontSize: 9, color: Colors.grey[500])), ], ), const SizedBox(height: 8), SizedBox( width: double.infinity, height: 34, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: canRedeem ? _primary : Colors.grey[200], foregroundColor: canRedeem ? Colors.white : Colors.grey[400], elevation: 0, padding: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), onPressed: canRedeem ? () => _confirmRedeem(context, provider, item) : null, child: Text( item.stockQuantity == 0 ? 'Out of Stock' : canRedeem ? 'Redeem' : 'Need ${item.rpCost - rp} more RP', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600), ), ), ), ], ), ), ); } Future _confirmRedeem(BuildContext context, GamificationProvider provider, ShopItem item) async { final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('Confirm Redemption', style: TextStyle(fontWeight: FontWeight.w700)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.name, style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 6), Text('This will deduct ${item.rpCost} RP from your balance.', style: TextStyle(color: Colors.grey[600], fontSize: 13)), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: _primary), onPressed: () => Navigator.pop(context, true), child: const Text('Confirm', style: TextStyle(color: Colors.white)), ), ], ), ); if (confirmed != true || !mounted) return; try { final voucherCode = await provider.redeemItem(item.id); if (!mounted) return; await showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Row( children: [ Icon(Icons.check_circle, color: Color(0xFF16A34A)), SizedBox(width: 8), Text('Redeemed!', style: TextStyle(fontWeight: FontWeight.w700)), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Your voucher code for ${item.name}:', style: TextStyle(color: Colors.grey[600], fontSize: 13)), const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFD1D5DB)), ), child: Text( voucherCode, style: const TextStyle(fontFamily: 'monospace', fontWeight: FontWeight.w700, fontSize: 16, letterSpacing: 2), textAlign: TextAlign.center, ), ), const SizedBox(height: 8), Text('Save this code — it will not be shown again.', style: TextStyle(color: Colors.grey[500], fontSize: 11)), ], ), actions: [ ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: _primary), onPressed: () { Clipboard.setData(ClipboardData(text: voucherCode)); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Voucher code copied!'), backgroundColor: Color(0xFF16A34A)), ); }, child: const Text('Copy & Close', style: TextStyle(color: Colors.white)), ), ], ), ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Redemption failed: $e'), backgroundColor: Colors.red)); } } } }