From b24df66b31921685cd714221ace64bd2b3b7a03c Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sat, 4 Apr 2026 19:31:25 +0530 Subject: [PATCH] feat: rewrite contribute tab to match web app (app.eventifyplus.com/contribute) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete UI rewrite of contribute_screen.dart: - 3 tabs (My Events, Submit Event, Reward Shop) replacing old 4-tab layout (Contribute, Leaderboard, Achievements, Shop) - Compact stats bar: tier pill + liquid EP + RP + share button - Horizontal tier roadmap showing Bronze→Diamond progression - Animated tab glider with elastic curve - Submit form matching web: Event Name, Category, District, Date+Time, Description (with EP hint), Location Coordinates (manual lat/lng OR Google Maps URL extraction), Media Upload (5 images, 2 EP each) - My Events tab with status badges (Approved/Pending/Rejected) - Reward Shop "Coming Soon" with ghost teaser cards - Color palette matching web: #0F45CF primary, #ea580c RP orange - File reduced from 2681 to 1093 lines (59% smaller) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/screens/contribute_screen.dart | 3832 ++++++++-------------------- 1 file changed, 1122 insertions(+), 2710 deletions(-) diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index 3391d31..fe8ed14 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -1,30 +1,24 @@ // lib/screens/contribute_screen.dart -// Contributor Module v2 — matches PRD v3 / TechDocs v2 / Web version. -// 4 tabs: Contribute · Leaderboard · Achievements · Shop +// Contributor Module v3 — matches web at app.eventifyplus.com/contribute +// 3 tabs: My Events · Submit Event · Reward Shop import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; -import '../core/utils/error_utils.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -import '../core/app_decoration.dart'; +import '../core/utils/error_utils.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import '../widgets/bouncing_loader.dart'; -import '../widgets/glass_card.dart'; -import '../widgets/landscape_section_header.dart'; -import '../widgets/tier_avatar_ring.dart'; -import '../features/share/share_rank_card.dart'; -import 'contributor_profile_screen.dart'; import '../core/analytics/posthog_service.dart'; // ───────────────────────────────────────────────────────────────────────────── -// Tier colour map +// Constants // ───────────────────────────────────────────────────────────────────────────── const _tierColors = { ContributorTier.BRONZE: Color(0xFFCD7F32), @@ -34,29 +28,20 @@ const _tierColors = { 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, - // ACH-002: icons for expanded badge set (badges 02, 06–11) - 'trending_up': Icons.trending_up, - 'rocket_launch': Icons.rocket_launch_outlined, - 'event_hunter': Icons.search_outlined, - 'location_on': Icons.location_on_outlined, - 'diamond': Icons.diamond_outlined, - 'workspace_premium': Icons.workspace_premium_outlined, +const _tierIcons = { + ContributorTier.BRONZE: Icons.shield_outlined, + ContributorTier.SILVER: Icons.shield_outlined, + ContributorTier.GOLD: Icons.workspace_premium_outlined, + ContributorTier.PLATINUM: Icons.diamond_outlined, + ContributorTier.DIAMOND: Icons.diamond_outlined, }; -// District list for the contribution form +const _tierThresholds = [0, 100, 500, 1500, 5000]; + const _districts = [ 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', - 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', - 'Other', + 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other', ]; const _categories = [ @@ -64,32 +49,42 @@ const _categories = [ 'Dance', 'Film', 'Business', 'Health', 'Education', 'Other', ]; +// Design tokens matching web +const _blue = Color(0xFF0F45CF); +const _darkText = Color(0xFF1E293B); +const _subText = Color(0xFF94A3B8); +const _border = Color(0xFFE2E8F0); +const _lightBlueBg = Color(0xFFEFF6FF); +const _rpOrange = Color(0xFFEA580C); +const _greenBg = Color(0xFFD1FAE5); +const _greenText = Color(0xFF065F46); +const _yellowBg = Color(0xFFFEF3C7); +const _yellowText = Color(0xFF92400E); +const _redBg = Color(0xFFFECDD3); +const _redText = Color(0xFF9F1239); +const _pageBg = Color(0xFFF8FAFC); + // ───────────────────────────────────────────────────────────────────────────── // 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; + int _activeTab = 1; // default to Submit Event - // ── 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(); + // Form + final _formKey = GlobalKey(); + final _titleCtl = TextEditingController(); + final _descriptionCtl = TextEditingController(); + final _latCtl = TextEditingController(); + final _lngCtl = TextEditingController(); + final _mapsLinkCtl = TextEditingController(); DateTime? _selectedDate; TimeOfDay? _selectedTime; @@ -97,6 +92,10 @@ class _ContributeScreenState extends State String _selectedDistrict = _districts.first; List _images = []; bool _submitting = false; + bool _showSuccess = false; + bool _useManualCoords = true; + String? _coordMessage; + bool _coordSuccess = false; final _picker = ImagePicker(); @@ -112,290 +111,111 @@ class _ContributeScreenState extends State @override void dispose() { _titleCtl.dispose(); - _locationCtl.dispose(); - _organizerCtl.dispose(); _descriptionCtl.dispose(); - _ticketPriceCtl.dispose(); - _contactCtl.dispose(); - _websiteCtl.dispose(); + _latCtl.dispose(); + _lngCtl.dispose(); + _mapsLinkCtl.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, _) { + if (provider.isLoading && provider.profile == null) { + return const Scaffold( + backgroundColor: _pageBg, + body: Center(child: BouncingLoader(color: _blue)), + ); + } return Scaffold( - backgroundColor: const Color(0xFFF5F7FB), - body: isDesktop - ? _buildDesktopLayout(context, provider) - : Column( - children: [ - _buildHeader(context, provider), - Expanded(child: _buildTabBody(context, provider)), - ], - ), + backgroundColor: _pageBg, + body: SafeArea( + child: Column( + children: [ + _buildStatsBar(provider), + _buildTierRoadmap(provider), + _buildTabBar(), + Expanded(child: _buildTabContent(provider)), + ], + ), + ), ); }, ); } // ═══════════════════════════════════════════════════════════════════════════ - // DESKTOP LAYOUT — matches web at mvnew.eventifyplus.com/contribute + // 1. COMPACT STATS BAR // ═══════════════════════════════════════════════════════════════════════════ - - 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) { + Widget _buildStatsBar(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; + final tierColor = _tierColors[tier] ?? const Color(0xFFCD7F32); + final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined; - 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: 16), - - // GAM-002: 3-card EP stat row - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - _epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)), - const SizedBox(width: 8), - // GAM-003 + GAM-004: Liquid EP card with cycle countdown and progress - Expanded( - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: const Color(0xFF3B82F6).withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.3)), - ), - child: Column( - children: [ - const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 20), - const SizedBox(height: 4), - Text( - '${profile?.currentEp ?? 0}', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16), - ), - const SizedBox(height: 2), - const Text('Liquid EP', style: TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center), - if (provider.currentUserStats?.rewardCycleDays != null) ...[ - const SizedBox(height: 4), - Text( - 'Converts in ${provider.currentUserStats!.rewardCycleDays}d', - style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Builder( - builder: (_) { - final days = provider.currentUserStats?.rewardCycleDays ?? 30; - final elapsed = (30 - days).clamp(0, 30); - final ratio = elapsed / 30; - return LinearProgressIndicator( - value: ratio, - minHeight: 4, - backgroundColor: Colors.white12, - valueColor: AlwaysStoppedAnimation( - ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6), - ), - ); - }, - ), - ), - ], - ], - ), - ), - ), - const SizedBox(width: 8), - _epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)), - ], - ), - ), - - const SizedBox(height: 16), - - // GAM-005: Tier roadmap - _buildTierRoadmap(lifetimeEp, tier), - - 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, + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( children: [ - LandscapeSectionHeader(title: title, subtitle: subtitle), - Expanded( - child: RepaintBoundary( - child: _buildDesktopTabBody(context, provider), + // Tier pill + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _blue, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(tierIcon, color: tierColor, size: 16), + const SizedBox(width: 6), + Text( + tierLabel(tier), + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13), + ), + ], + ), + ), + const SizedBox(width: 12), + + // Liquid EP + Icon(Icons.bolt, color: _blue, size: 18), + const SizedBox(width: 4), + Text( + '${profile?.currentEp ?? 0}', + style: const TextStyle(color: _darkText, fontWeight: FontWeight.w700, fontSize: 15), + ), + const SizedBox(width: 4), + const Text('EP', style: TextStyle(color: _subText, fontSize: 12)), + + const SizedBox(width: 16), + + // RP + Icon(Icons.card_giftcard, color: _rpOrange, size: 18), + const SizedBox(width: 4), + Text( + '${profile?.currentRp ?? 0}', + style: TextStyle(color: _rpOrange, fontWeight: FontWeight.w700, fontSize: 15), + ), + const SizedBox(width: 4), + const Text('RP', style: TextStyle(color: _subText, fontSize: 12)), + + const Spacer(), + + // Share button + GestureDetector( + onTap: () => _shareRank(provider), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.share_outlined, color: _subText, size: 18), ), ), ], @@ -403,2592 +223,1184 @@ class _ContributeScreenState extends State ); } - 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(); - } + void _shareRank(GamificationProvider provider) { + final profile = provider.profile; + if (profile == null) return; + final text = "I'm a ${tierLabel(profile.tier)} contributor on Eventify with ${profile.lifetimeEp} EP! Join the community."; + Share.share(text); } // ═══════════════════════════════════════════════════════════════════════════ - // DESKTOP — Contribute Tab + // 2. TIER ROADMAP // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildTierRoadmap(GamificationProvider provider) { + final currentTier = provider.profile?.tier ?? ContributorTier.BRONZE; + final tiers = ContributorTier.values; - 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)), - ), - ), - ), - ], + return Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _border), ), - const SizedBox(height: 20), + child: Row( + children: List.generate(tiers.length * 2 - 1, (i) { + if (i.isOdd) { + // Connector line + final tierIdx = i ~/ 2; + final reached = currentTier.index > tierIdx; + return Expanded( + child: Container( + height: 2, + color: reached ? _blue : const Color(0xFFE2E8F0), + ), + ); + } + final tierIdx = i ~/ 2; + final tier = tiers[tierIdx]; + final isActive = tier == currentTier; + final reached = currentTier.index >= tierIdx; + final tierColor = _tierColors[tier]!; + final thresholdLabel = _tierThresholds[tierIdx] >= 1000 + ? '${(_tierThresholds[tierIdx] / 1000).toStringAsFixed(_tierThresholds[tierIdx] % 1000 == 0 ? 0 : 1)}K' + : '${_tierThresholds[tierIdx]}'; - // ── 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), - ], + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isActive ? _lightBlueBg : (reached ? _blue.withValues(alpha: 0.08) : const Color(0xFFF8FAFC)), + shape: BoxShape.circle, + border: Border.all( + color: isActive ? _blue : (reached ? _blue.withValues(alpha: 0.3) : _border), + width: isActive ? 2 : 1, + ), + ), + child: Icon( + _tierIcons[tier] ?? Icons.shield_outlined, + size: 16, + color: reached ? _blue : _subText, + ), + ), + const SizedBox(height: 4), + Text( + tierLabel(tier), + style: TextStyle( + fontSize: 9, + fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, + color: isActive ? _blue : _subText, + ), + ), + Text( + '$thresholdLabel EP', + style: TextStyle(fontSize: 8, color: isActive ? _blue : const Color(0xFFCBD5E1)), + ), + ], + ); + }), + ), + ), ); } - 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), + // ═══════════════════════════════════════════════════════════════════════════ + // 3. TAB BAR WITH ANIMATED GLIDER + // ═══════════════════════════════════════════════════════════════════════════ + static const _tabLabels = ['My Events', 'Submit Event', 'Reward Shop']; + static const _tabIcons = [Icons.list_alt_rounded, Icons.add_circle_outline, Icons.card_giftcard_outlined]; - // Row 2: Date + Location - Row( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildTabBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 52, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _border), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = constraints.maxWidth / 3; + return Stack( 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), - ), + // Animated glider + AnimatedPositioned( + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutBack, + left: tabWidth * _activeTab + 4, + top: 4, + child: Container( + width: tabWidth - 8, + height: 44, + decoration: BoxDecoration( + color: _blue, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow(color: _blue.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + ), + ), + // Tab buttons + Row( + children: List.generate(3, (i) { + final isActive = i == _activeTab; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _activeTab = i), + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 52, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, 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), + Icon( + _tabIcons[i], + size: 18, + color: isActive ? Colors.white : const Color(0xFF64748B), + ), + const SizedBox(width: 6), + Text( + _tabLabels[i], + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isActive ? Colors.white : const Color(0xFF64748B), + ), ), - 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)); - }, + // ═══════════════════════════════════════════════════════════════════════════ + // 4. TAB CONTENT + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildTabContent(GamificationProvider provider) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Container( - height: 150, decoration: BoxDecoration( - border: Border.all(color: const Color(0xFFD1D5DB), style: BorderStyle.solid), - borderRadius: BorderRadius.circular(12), - color: const Color(0xFFFAFBFC), + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + border: Border.all(color: _border), ), - child: Center( + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: _activeTab == 0 + ? _buildMyEventsTab(provider) + : _activeTab == 1 + ? _buildSubmitEventTab(provider) + : _buildRewardShopTab(provider), + ), + ), + ), + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TAB 0: MY EVENTS + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildMyEventsTab(GamificationProvider provider) { + final submissions = provider.submissions; + + if (submissions.isEmpty) { + return Center( + key: const ValueKey('empty'), + child: Padding( + padding: const EdgeInsets.all(32), 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), + width: 64, + height: 64, decoration: BoxDecoration( - color: const Color(0xFFFEF3C7), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFFF59E0B)), + color: _lightBlueBg, + shape: BoxShape.circle, ), - 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)), - ], + child: const Icon(Icons.star_outline_rounded, color: _blue, size: 32), + ), + const SizedBox(height: 16), + const Text( + 'No events submitted yet', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: _darkText), + ), + const SizedBox(height: 8), + const Text( + 'Head over to the Submit tab to earn your first EP!', + style: TextStyle(fontSize: 13, color: _subText), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => setState(() => _activeTab = 1), + style: ElevatedButton.styleFrom( + backgroundColor: _blue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), + child: const Text('Start Contributing', style: TextStyle(fontWeight: FontWeight.w600)), ), ], ), ), - 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(), - ), - ], + ); + } + + return ListView.separated( + key: const ValueKey('list'), + padding: const EdgeInsets.all(16), + itemCount: submissions.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (ctx, i) => _buildSubmissionCard(submissions[i]), ); } - Widget _buildDesktopShopCard(ShopItem item, int currentRp, GamificationProvider provider) { - final canRedeem = currentRp >= item.rpCost && item.stockQuantity > 0; + Widget _buildSubmissionCard(SubmissionModel sub) { + Color statusBg, statusFg; + String statusLabel; + switch (sub.status.toUpperCase()) { + case 'APPROVED': + statusBg = _greenBg; statusFg = _greenText; statusLabel = 'Approved'; + break; + case 'REJECTED': + statusBg = _redBg; statusFg = _redText; statusLabel = 'Rejected'; + break; + default: + statusBg = _yellowBg; statusFg = _yellowText; statusLabel = 'Pending'; + } + return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE5E7EB)), + border: Border.all(color: _border), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))], ), 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)), - ], + Expanded( + child: Text( + sub.eventName, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _darkText), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - 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: BouncingLoader())); - } - - 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), + const SizedBox(width: 8), + if (sub.category.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( - color: isActive ? _primary : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: isActive ? _primary : const Color(0xFFD1D5DB)), + color: _lightBlueBg, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + sub.category, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: _blue), ), - 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), + Row( + children: [ + Icon(Icons.calendar_today_outlined, size: 14, color: _subText), + const SizedBox(width: 4), + Text( + DateFormat('d MMM yyyy').format(sub.createdAt), + style: const TextStyle(fontSize: 12, color: _subText), ), - ), - ], - ), - 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( + if (sub.district != null && sub.district!.isNotEmpty) ...[ + const SizedBox(width: 12), + Icon(Icons.location_on_outlined, size: 14, color: _subText), + const SizedBox(width: 4), + Text(sub.district!, style: const TextStyle(fontSize: 12, color: _subText)), + ], + const Spacer(), + // Status badge + Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: _primary, - borderRadius: BorderRadius.circular(12), + color: statusBg, + borderRadius: BorderRadius.circular(8), ), - child: Text(tierLabel(entry.tier), - style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600)), + child: Text(statusLabel, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: statusFg)), ), - ), - ), - 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: BouncingLoader())); - } - - 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), - // ACH-002: colors for expanded badge set - 'trending_up': const Color(0xFF0EA5E9), - 'rocket_launch': const Color(0xFFEC4899), - 'event_hunter': const Color(0xFF64748B), - 'location_on': const Color(0xFF22C55E), - 'diamond': const Color(0xFF06B6D4), - 'workspace_premium': const Color(0xFFE879F9), - }; - 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), - // ACH-002: backgrounds for expanded badge set - 'trending_up': const Color(0xFFE0F2FE), - 'rocket_launch': const Color(0xFFFCE7F3), - 'event_hunter': const Color(0xFFF1F5F9), - 'location_on': const Color(0xFFDCFCE7), - 'diamond': const Color(0xFFCFFAFE), - 'workspace_premium': const Color(0xFFFAE8FF), - }; - - 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(height: 6), + Row( + children: [ + const Text('EP Earned: ', style: TextStyle(fontSize: 12, color: _subText)), + Text( + sub.status.toUpperCase() == 'APPROVED' ? '${sub.epAwarded}' : '-', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: sub.status.toUpperCase() == 'APPROVED' ? _blue : _subText, ), - 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 + // TAB 1: SUBMIT EVENT // ═══════════════════════════════════════════════════════════════════════════ - Widget _buildContributeTab(BuildContext context, GamificationProvider provider) { - final theme = Theme.of(context); - final bottomInset = MediaQuery.of(context).padding.bottom; + Widget _buildSubmitEventTab(GamificationProvider provider) { + if (_showSuccess) return _buildSuccessState(); + return SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: EdgeInsets.fromLTRB(16, 20, 16, 32 + bottomInset), + key: const ValueKey('submit'), + padding: const EdgeInsets.all(20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Submit an Event', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), + const Text( + 'Submit a New Event', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText), + ), 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 Text( + 'Provide accurate details to maximize your evaluated EP points.', + style: TextStyle(fontSize: 13, color: _subText), ), const SizedBox(height: 20), - _formCard([ - _formField(_titleCtl, 'Event Name *', Icons.event, required: true), - _divider(), - _categoryDropdown(), - _divider(), - _districtDropdown(), - ]), + // Event Name + _inputLabel('Event Name', required: true), + const SizedBox(height: 6), + _textField(_titleCtl, 'e.g. Cochin Carnival 2026', + validator: (v) => (v == null || v.trim().isEmpty) ? 'Event name is required' : null), + const SizedBox(height: 16), - 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), - ), - ), - - // CTR-001/002: Your Submissions list - if (provider.submissions.isNotEmpty) ...[ - const SizedBox(height: 28), - Text('Your Submissions', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), - const SizedBox(height: 12), - ...provider.submissions.map((sub) => _buildSubmissionCard(sub, theme)), - ], - ], - ), - ), - ); - } - - Widget _buildSubmissionCard(SubmissionModel sub, ThemeData theme) { - return Container( - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: theme.dividerColor.withOpacity(0.2)), - ), - child: Row( - children: [ - // Thumbnail or placeholder - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(8), - ), - child: sub.images.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network(sub.images.first, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.event, color: Colors.grey)), - ) - : const Icon(Icons.event, color: Colors.grey), - ), - const SizedBox(width: 12), - // Info - Expanded( - child: Column( + // Category + District row + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(sub.eventName, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis), - const SizedBox(height: 2), - Text('${sub.category} · ${sub.district}', style: TextStyle(color: Colors.grey[500], fontSize: 11)), - ], - ), - ), - const SizedBox(width: 8), - // Status chip + EP badge - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - statusChip(sub.status), - if (sub.epAwarded > 0) ...[ - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFFDBEAFE), - borderRadius: BorderRadius.circular(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _inputLabel('Category', required: true), + const SizedBox(height: 6), + _dropdown(_selectedCategory, _categories, (v) => setState(() => _selectedCategory = v!)), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _inputLabel('District', required: true), + const SizedBox(height: 6), + _dropdown(_selectedDistrict, _districts, (v) => setState(() => _selectedDistrict = v!)), + ], ), - child: Text('+${sub.epAwarded} EP', style: const TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w700, fontSize: 11)), ), ], - ], - ), - ], - ), - ); - } + ), + const SizedBox(height: 16), - 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( + // Date & Time row + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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), - ), - ), - ), + _inputLabel('Date', required: true), + const SizedBox(height: 6), + _datePicker(), ], - ); - }, - ), - ) - else - _addImageButton(full: true), - ], - ), - ); - } + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _inputLabel('Time'), + const SizedBox(height: 6), + _timePicker(), + ], + ), + ), + ], + ), + const SizedBox(height: 16), - 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)), - ], + // Description + _inputLabel('Description', required: true, hint: '(Required for higher EP)'), + const SizedBox(height: 6), + _textField(_descriptionCtl, 'Include agenda, ticket details, organizer contact, etc...', + maxLines: 4, + validator: (v) => (v == null || v.trim().isEmpty) ? 'Description is required' : null), + const SizedBox(height: 16), + + // Location Coordinates + _inputLabel('Location Coordinates'), + const SizedBox(height: 6), + _buildCoordinateInput(), + const SizedBox(height: 16), + + // Media Upload + _buildMediaUpload(), + const SizedBox(height: 24), + + // Submit button + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _submitting ? null : () => _submitForm(provider), + style: ElevatedButton.styleFrom( + backgroundColor: _blue, + foregroundColor: Colors.white, + disabledBackgroundColor: _blue.withValues(alpha: 0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: _submitting + ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Submit for Review', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700)), + SizedBox(width: 8), + Icon(Icons.arrow_forward_rounded, size: 18), + ], + ), + ), + ), + const SizedBox(height: 24), ], ), ), ); } - 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!, - ), + // ── Form helpers ────────────────────────────────────────────────────────── + + Widget _inputLabel(String text, {bool required = false, String? hint}) { + return Row( + children: [ + Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _darkText)), + if (required) const Text(' *', style: TextStyle(color: Color(0xFFEF4444), fontSize: 13)), + if (hint != null) ...[ + const SizedBox(width: 4), + Text(hint, style: const TextStyle(fontSize: 11, color: _subText)), + ], + ], ); - 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!, + Widget _textField(TextEditingController ctl, String placeholder, { + int maxLines = 1, + String? Function(String?)? validator, + TextInputType? keyboardType, + List? inputFormatters, + }) { + return TextFormField( + controller: ctl, + maxLines: maxLines, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + validator: validator, + style: const TextStyle(fontSize: 14, color: _darkText), + decoration: InputDecoration( + hintText: placeholder, + hintStyle: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 14), + filled: true, + fillColor: const Color(0xFFF8FAFC), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)), + errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFEF4444))), ), ); - if (picked != null) setState(() => _selectedTime = picked); + } + + Widget _dropdown(String value, List items, ValueChanged onChanged) { + return DropdownButtonFormField( + value: value, + items: items.map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 14)))).toList(), + onChanged: onChanged, + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFFF8FAFC), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)), + ), + dropdownColor: Colors.white, + style: const TextStyle(fontSize: 14, color: _darkText), + icon: const Icon(Icons.keyboard_arrow_down_rounded, color: _subText), + ); + } + + Widget _datePicker() { + return GestureDetector( + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime(2030), + ); + if (picked != null) setState(() => _selectedDate = picked); + }, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _border), + ), + child: Row( + children: [ + Expanded( + child: Text( + _selectedDate != null ? DateFormat('d MMM yyyy').format(_selectedDate!) : 'Select date', + style: TextStyle( + fontSize: 14, + color: _selectedDate != null ? _darkText : const Color(0xFFCBD5E1), + ), + ), + ), + const Icon(Icons.calendar_today_outlined, color: _subText, size: 18), + ], + ), + ), + ); + } + + Widget _timePicker() { + return GestureDetector( + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? TimeOfDay.now(), + ); + if (picked != null) setState(() => _selectedTime = picked); + }, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _border), + ), + child: Row( + children: [ + Expanded( + child: Text( + _selectedTime != null ? _selectedTime!.format(context) : 'Select time', + style: TextStyle( + fontSize: 14, + color: _selectedTime != null ? _darkText : const Color(0xFFCBD5E1), + ), + ), + ), + const Icon(Icons.access_time_outlined, color: _subText, size: 18), + ], + ), + ), + ); + } + + // ── Coordinate input (Manual / Google Maps Link toggle) ────────────────── + + Widget _buildCoordinateInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Toggle tabs + Row( + children: [ + _coordToggle('Manual', _useManualCoords, () => setState(() => _useManualCoords = true)), + const SizedBox(width: 8), + _coordToggle('Google Maps Link', !_useManualCoords, () => setState(() => _useManualCoords = false)), + ], + ), + const SizedBox(height: 10), + + if (_useManualCoords) ...[ + Row( + children: [ + Expanded( + child: _textField(_latCtl, 'Latitude (e.g. 9.93123)', + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]), + ), + const SizedBox(width: 12), + Expanded( + child: _textField(_lngCtl, 'Longitude (e.g. 76.26730)', + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]), + ), + ], + ), + ] else ...[ + Row( + children: [ + Expanded( + child: _textField(_mapsLinkCtl, 'Paste Google Maps URL here'), + ), + const SizedBox(width: 8), + SizedBox( + height: 48, + child: ElevatedButton( + onPressed: _extractCoordinates, + style: ElevatedButton.styleFrom( + backgroundColor: _blue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16), + ), + child: const Text('Extract', style: TextStyle(fontWeight: FontWeight.w600)), + ), + ), + ], + ), + if (_coordMessage != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon( + _coordSuccess ? Icons.check_circle : Icons.error_outline, + size: 16, + color: _coordSuccess ? const Color(0xFF22C55E) : const Color(0xFFEF4444), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + _coordMessage!, + style: TextStyle( + fontSize: 12, + color: _coordSuccess ? const Color(0xFF22C55E) : const Color(0xFFEF4444), + ), + ), + ), + ], + ), + ], + ], + ], + ); + } + + Widget _coordToggle(String label, bool active, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: active ? _blue : const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: active ? _blue : _border), + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: active ? Colors.white : _subText, + ), + ), + ), + ); + } + + void _extractCoordinates() { + final url = _mapsLinkCtl.text.trim(); + if (url.isEmpty) { + setState(() { _coordMessage = 'Please paste a Google Maps URL'; _coordSuccess = false; }); + return; + } + + double? lat, lng; + + // Pattern 1: @lat,lng + final atMatch = RegExp(r'@(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url); + if (atMatch != null) { + lat = double.tryParse(atMatch.group(1)!); + lng = double.tryParse(atMatch.group(2)!); + } + + // Pattern 2: 3dlat!4dlng + if (lat == null) { + final dMatch = RegExp(r'3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)').firstMatch(url); + if (dMatch != null) { + lat = double.tryParse(dMatch.group(1)!); + lng = double.tryParse(dMatch.group(2)!); + } + } + + // Pattern 3: q=lat,lng + if (lat == null) { + final qMatch = RegExp(r'[?&]q=(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url); + if (qMatch != null) { + lat = double.tryParse(qMatch.group(1)!); + lng = double.tryParse(qMatch.group(2)!); + } + } + + // Pattern 4: ll=lat,lng + if (lat == null) { + final llMatch = RegExp(r'[?&]ll=(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url); + if (llMatch != null) { + lat = double.tryParse(llMatch.group(1)!); + lng = double.tryParse(llMatch.group(2)!); + } + } + + if (lat != null && lng != null) { + _latCtl.text = lat.toStringAsFixed(6); + _lngCtl.text = lng.toStringAsFixed(6); + setState(() { + _coordMessage = 'Coordinates extracted: $lat, $lng'; + _coordSuccess = true; + }); + } else { + setState(() { + _coordMessage = 'Could not extract coordinates from this URL'; + _coordSuccess = false; + }); + } + } + + // ── Media upload ────────────────────────────────────────────────────────── + + Widget _buildMediaUpload() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _inputLabel('Photos'), + const Spacer(), + Text('${_images.length}/5', style: const TextStyle(fontSize: 12, color: _subText)), + ], + ), + const SizedBox(height: 4), + const Text( + '2 EP per image, max 5 EP', + style: TextStyle(fontSize: 11, color: _subText), + ), + const SizedBox(height: 8), + + // Upload area + if (_images.length < 5) + GestureDetector( + onTap: _pickImages, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFCBD5E1), width: 2, strokeAlign: BorderSide.strokeAlignInside), + ), + child: Column( + children: [ + Icon(Icons.cloud_upload_outlined, color: _blue, size: 28), + const SizedBox(height: 6), + const Text('Tap to add photos', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _darkText)), + const SizedBox(height: 2), + const Text('JPEG, PNG, WebP', style: TextStyle(fontSize: 11, color: _subText)), + ], + ), + ), + ), + + // Thumbnail gallery + if (_images.isNotEmpty) ...[ + const SizedBox(height: 10), + SizedBox( + height: 80, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _images.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (ctx, i) => _buildImageThumb(i), + ), + ), + ], + ], + ); + } + + Widget _buildImageThumb(int index) { + final img = _images[index]; + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _border), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: kIsWeb + ? const Center(child: Icon(Icons.image, color: _subText, size: 28)) + : Image.file(File(img.path), fit: BoxFit.cover), + ), + ), + Positioned( + top: -6, + right: -6, + child: GestureDetector( + onTap: () => setState(() => _images.removeAt(index)), + child: Container( + width: 22, + height: 22, + decoration: const BoxDecoration( + color: Color(0xFFEF4444), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, color: Colors.white, size: 14), + ), + ), + ), + ], + ); } Future _pickImages() async { try { - final List picked = await _picker.pickMultiImage(imageQuality: 80); + final picked = await _picker.pickMultiImage(imageQuality: 80); if (picked.isNotEmpty) { setState(() { - _images = [..._images, ...picked].take(5).toList(); + final remaining = 5 - _images.length; + _images.addAll(picked.take(remaining)); }); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not pick images: ${userFriendlyError(e)}')), + ); } } } + // ── Success state ───────────────────────────────────────────────────────── + + Widget _buildSuccessState() { + return Center( + key: const ValueKey('success'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 500), + curve: Curves.elasticOut, + builder: (_, v, child) => Transform.scale(scale: v, child: child), + child: Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: _greenBg, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_rounded, color: Color(0xFF22C55E), size: 44), + ), + ), + const SizedBox(height: 20), + const Text( + 'Event Submitted!', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText), + ), + const SizedBox(height: 8), + const Text( + 'Thank you for contributing. It is now pending\nadmin verification. You can earn up to 10 EP!', + style: TextStyle(fontSize: 13, color: _subText), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + // ── Submit handler ──────────────────────────────────────────────────────── + 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'))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select a date'), backgroundColor: Color(0xFFEF4444)), + ); return; } setState(() => _submitting = true); + try { - await provider.submitContribution({ + final data = { '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(), + }; + + // Add coordinates if provided + final lat = double.tryParse(_latCtl.text.trim()); + final lng = double.tryParse(_lngCtl.text.trim()); + if (lat != null && lng != null) { + data['location_lat'] = lat; + data['location_lng'] = lng; + } + + await provider.submitContribution(data); + + PostHogService.instance.capture('event_contributed', properties: { + 'category': _selectedCategory, + 'district': _selectedDistrict, + 'has_images': _images.isNotEmpty, + 'image_count': _images.length, + 'has_coordinates': lat != null && lng != null, }); + + // Show success, then reset + setState(() { _submitting = false; _showSuccess = true; }); + + await Future.delayed(const Duration(seconds: 2)); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('✅ Submitted for verification! You\'ll earn EP once approved.'), - backgroundColor: Color(0xFF16A34A), - ), - ); _clearForm(); + setState(() { _showSuccess = false; _activeTab = 0; }); + provider.loadAll(force: true); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)), backgroundColor: Colors.red)); + setState(() => _submitting = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(userFriendlyError(e)), backgroundColor: const Color(0xFFEF4444)), + ); } - } 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 = []; - }); + _latCtl.clear(); + _lngCtl.clear(); + _mapsLinkCtl.clear(); + _selectedDate = null; + _selectedTime = null; + _selectedCategory = _categories.first; + _selectedDistrict = _districts.first; + _images.clear(); + _coordMessage = null; + _useManualCoords = true; } // ═══════════════════════════════════════════════════════════════════════════ - // TAB 1 — LEADERBOARD (matches web version at mvnew.eventifyplus.com/contribute) + // TAB 2: REWARD SHOP (Coming Soon) // ═══════════════════════════════════════════════════════════════════════════ - - // 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: BouncingLoader()); - } - - 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), - - // LDR-003: Current user stats card at top of leaderboard - if (provider.currentUserStats != null) - Builder(builder: (context) { - final stats = provider.currentUserStats!; - return GlassCard( - margin: const EdgeInsets.fromLTRB(16, 8, 16, 4), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildStatChip('Rank', '#${stats.rank}', Icons.leaderboard), - Container(width: 1, height: 32, color: Colors.white12), - _buildStatChip('EP', '${stats.points}', Icons.bolt), - Container(width: 1, height: 32, color: Colors.white12), - _buildStatChip('Cycle', '${stats.rewardCycleDays}d', Icons.timelapse), - ], - ), - ); - }), - - 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+) with stagger animation - SliverList( - delegate: SliverChildBuilderDelegate( - (ctx, i) { - final entry = entries.length > 3 ? entries[i + 3] : entries[i]; - return AnimationConfiguration.staggeredList( - position: i, - duration: const Duration(milliseconds: 375), - child: SlideAnimation( - verticalOffset: 40.0, - child: FadeInAnimation(child: _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: [ - // GAM-006: Avatar with tier ring + rank badge overlaid - Stack( - clipBehavior: Clip.none, - children: [ - // TierAvatarRing — tier-coloured glow ring - TierAvatarRing( - username: e.username, - tier: tierLabel(e.tier), - size: avatarSize, - imageUrl: e.avatarUrl, - ), - // 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 GestureDetector( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ContributorProfileScreen( - contributorId: entry.username, - contributorName: entry.username, - ), - ), - ), - child: 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), - ), - ), - ), - // GAM-006: TierAvatarRing replaces plain avatar circle - TierAvatarRing( - username: entry.username, - tier: tierLabel(entry.tier), - size: 36, - imageUrl: entry.avatarUrl, - ), - 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: () { - final gam = context.read(); - showDialog( - context: context, - builder: (_) => Dialog( - backgroundColor: Colors.transparent, - child: ShareRankCard( - username: me.username, - tier: tierLabel(me.tier), - rank: me.rank, - ep: me.lifetimeEp, - rewardPoints: gam.profile?.currentRp ?? 0, - ), - ), - ); - }, - 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)), - ], - ), - ), - ), - ], - ), - ); - } - - // LDR-003: Stat chip helper for current-user leaderboard card - Widget _buildStatChip(String label, String value, IconData icon) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 14, color: const Color(0xFF94A3B8)), - const SizedBox(height: 2), - Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)), - Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF64748B))), - ], - ); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TAB 2 — ACHIEVEMENTS - // ═══════════════════════════════════════════════════════════════════════════ - Widget _buildAchievementsTab(BuildContext context, GamificationProvider provider) { - final theme = Theme.of(context); - - if (provider.isLoading && provider.achievements.isEmpty) { - return const Center(child: BouncingLoader()); - } - - final badges = provider.achievements; - final bottomInset = MediaQuery.of(context).padding.bottom; + Widget _buildRewardShopTab(GamificationProvider provider) { + final rp = provider.profile?.currentRp ?? 0; return SingleChildScrollView( - padding: EdgeInsets.fromLTRB(16, 16, 16, 32 + bottomInset), + key: const ValueKey('shop'), + padding: const EdgeInsets.all(20), 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), - ), - ], - ), - ); - } + const SizedBox(height: 20), - 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: [ + // RP balance Container( - width: 52, - height: 52, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - shape: BoxShape.circle, - color: badge.isUnlocked ? _primary.withOpacity(0.12) : Colors.grey[100], + color: const Color(0xFFFFF7ED), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _rpOrange.withValues(alpha: 0.3)), ), - 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: BouncingLoader()); - } - - 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( + child: Row( + mainAxisSize: MainAxisSize.min, 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)), + Icon(Icons.card_giftcard, color: _rpOrange, size: 20), + const SizedBox(width: 8), + Text( + '$rp RP', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: _rpOrange), ), - 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(width: 6), + const Text('available', style: TextStyle(fontSize: 12, color: _subText)), ], ), - 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)), ), + + const SizedBox(height: 32), + + // Pulsing icon + TweenAnimationBuilder( + tween: Tween(begin: 0.95, end: 1.05), + duration: const Duration(seconds: 2), + curve: Curves.easeInOut, + builder: (_, v, child) => Transform.scale(scale: v, child: child), + onEnd: () {}, // AnimationBuilder loops via repeat — handled below + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [_blue.withValues(alpha: 0.15), _blue.withValues(alpha: 0.05)], + ), + shape: BoxShape.circle, + ), + child: const Icon(Icons.card_giftcard_rounded, color: _blue, size: 36), + ), + ), + + const SizedBox(height: 20), + + // Badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: _lightBlueBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _blue.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration(color: _blue, shape: BoxShape.circle), + ), + const SizedBox(width: 8), + const Text('Coming Soon', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: _blue)), + ], + ), + ), + + const SizedBox(height: 16), + + const Text( + "We're Stocking the Shelves", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Text( + 'Redeem your RP for event vouchers, VIP passes, exclusive merch, and more. Keep contributing to build your balance!', + style: TextStyle(fontSize: 13, color: _subText, height: 1.5), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 28), + + // Ghost teaser cards + ..._buildGhostCards(), + + const SizedBox(height: 20), ], ), ); + } - if (confirmed != true || !mounted) return; + List _buildGhostCards() { + const items = [ + {'name': 'Event Voucher', 'rp': '500', 'icon': Icons.confirmation_number_outlined}, + {'name': 'VIP Access Pass', 'rp': '1,200', 'icon': Icons.verified_outlined}, + {'name': 'Exclusive Merch', 'rp': '2,000', 'icon': Icons.shopping_bag_outlined}, + ]; - 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)), + return items.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Opacity( + opacity: 0.45, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _border), ), - ], - ), - ); - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)), backgroundColor: Colors.red)); - } - } - } - - // ───────────────────────────────────────────────────────────────────────── - // GAM-002: EP stat card helper (used in left panel + profile) - // ───────────────────────────────────────────────────────────────────────── - Widget _epStatCard(String label, String value, IconData icon, Color color) { - return Expanded( - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Column( - children: [ - Icon(icon, color: color, size: 20), - const SizedBox(height: 4), - Text(value, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16)), - const SizedBox(height: 2), - Text(label, style: const TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center), - ], - ), - ), - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // GAM-005: Horizontal tier roadmap (Bronze → Diamond) - // ───────────────────────────────────────────────────────────────────────── - Widget _buildTierRoadmap(int lifetimeEp, ContributorTier currentTier) { - const tiers = ContributorTier.values; // BRONZE, SILVER, GOLD, PLATINUM, DIAMOND - const thresholds = [0, 100, 500, 1500, 5000]; - final overallProgress = (lifetimeEp / 5000).clamp(0.0, 1.0); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.06), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Tier Roadmap', style: TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.w600)), - const SizedBox(height: 10), - Row( - children: List.generate(tiers.length, (i) { - final reached = currentTier.index >= i; - final color = _tierColors[tiers[i]] ?? Colors.grey; - return Expanded( + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: _lightBlueBg, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(item['icon'] as IconData, color: _blue, size: 22), + ), + const SizedBox(width: 14), + Expanded( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: reached ? color : Colors.white24, - border: Border.all(color: reached ? color : Colors.white30, width: 2), - ), - ), - const SizedBox(height: 4), Text( - tierLabel(tiers[i]), - style: TextStyle(color: reached ? Colors.white : Colors.white38, fontSize: 9, fontWeight: FontWeight.w600), - textAlign: TextAlign.center, + item['name'] as String, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: _darkText), ), + const SizedBox(height: 2), Text( - '${thresholds[i]}', - style: TextStyle(color: reached ? Colors.white54 : Colors.white24, fontSize: 8), + '${item['rp']} RP', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _rpOrange), ), ], ), - ); - }), + ), + const Icon(Icons.lock_outline_rounded, color: _subText, size: 20), + ], ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: overallProgress, - minHeight: 4, - backgroundColor: Colors.white12, - valueColor: AlwaysStoppedAnimation(_tierColors[currentTier] ?? Colors.white), - ), - ), - ], + ), ), - ), - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // CTR-001: Status chip for submissions - // ───────────────────────────────────────────────────────────────────────── - static Widget statusChip(String status) { - Color bg; - Color fg; - switch (status.toUpperCase()) { - case 'APPROVED': - bg = const Color(0xFFDCFCE7); - fg = const Color(0xFF16A34A); - break; - case 'REJECTED': - bg = const Color(0xFFFEE2E2); - fg = const Color(0xFFDC2626); - break; - default: // PENDING - bg = const Color(0xFFFEF9C3); - fg = const Color(0xFFCA8A04); - } - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - status.toUpperCase(), - style: TextStyle(color: fg, fontWeight: FontWeight.w700, fontSize: 11), - ), - ); + ); + }).toList(); } }