// lib/screens/contribute_screen.dart // 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 '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/utils/error_utils.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; import '../widgets/bouncing_loader.dart'; import '../core/analytics/posthog_service.dart'; // ───────────────────────────────────────────────────────────────────────────── // Constants // ───────────────────────────────────────────────────────────────────────────── const _tierColors = { ContributorTier.BRONZE: Color(0xFFCD7F32), ContributorTier.SILVER: Color(0xFFA8A9AD), ContributorTier.GOLD: Color(0xFFFFD700), ContributorTier.PLATINUM: Color(0xFFE5E4E2), ContributorTier.DIAMOND: Color(0xFF67E8F9), }; 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, }; const _tierThresholds = [0, 100, 500, 1500, 5000]; const _districts = [ 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other', ]; const _categories = [ 'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community', 'Dance', 'Film', 'Business', 'Health', 'Education', 'Other', ]; // 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 { int _activeTab = 1; // default to Submit Event // Form final _formKey = GlobalKey(); final _titleCtl = TextEditingController(); final _descriptionCtl = TextEditingController(); final _latCtl = TextEditingController(); final _lngCtl = TextEditingController(); final _mapsLinkCtl = TextEditingController(); DateTime? _selectedDate; TimeOfDay? _selectedTime; String _selectedCategory = _categories.first; String _selectedDistrict = _districts.first; List _images = []; bool _submitting = false; bool _showSuccess = false; bool _useManualCoords = true; String? _coordMessage; bool _coordSuccess = false; final _picker = ImagePicker(); @override void initState() { super.initState(); PostHogService.instance.screen('Contribute'); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadAll(); }); } @override void dispose() { _titleCtl.dispose(); _descriptionCtl.dispose(); _latCtl.dispose(); _lngCtl.dispose(); _mapsLinkCtl.dispose(); super.dispose(); } // ───────────────────────────────────────────────────────────────────────── // Build // ───────────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { 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: _pageBg, body: SafeArea( child: Column( children: [ _buildStatsBar(provider), _buildTierRoadmap(provider), _buildTabBar(), Expanded(child: _buildTabContent(provider)), ], ), ), ); }, ); } // ═══════════════════════════════════════════════════════════════════════════ // 1. COMPACT STATS BAR // ═══════════════════════════════════════════════════════════════════════════ Widget _buildStatsBar(GamificationProvider provider) { final profile = provider.profile; final tier = profile?.tier ?? ContributorTier.BRONZE; final tierColor = _tierColors[tier] ?? const Color(0xFFCD7F32); final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined; return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Row( children: [ // 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), ), ), ], ), ); } 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); } // ═══════════════════════════════════════════════════════════════════════════ // 2. TIER ROADMAP // ═══════════════════════════════════════════════════════════════════════════ Widget _buildTierRoadmap(GamificationProvider provider) { final currentTier = provider.profile?.tier ?? ContributorTier.BRONZE; final tiers = ContributorTier.values; 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), ), 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]}'; 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)), ), ], ); }), ), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // 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]; 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: [ // 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.center, children: [ 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), ), ), ], ), ), ), ); }), ), ], ); }, ), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // 4. TAB CONTENT // ═══════════════════════════════════════════════════════════════════════════ Widget _buildTabContent(GamificationProvider provider) { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), border: Border.all(color: _border), ), 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: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: _lightBlueBg, shape: BoxShape.circle, ), 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)), ), ], ), ), ); } 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 _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(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), 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: [ Row( children: [ Expanded( child: Text( sub.eventName, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _darkText), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), if (sub.category.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: _lightBlueBg, borderRadius: BorderRadius.circular(8), ), child: Text( sub.category, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: _blue), ), ), ], ), 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), ), 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: statusBg, borderRadius: BorderRadius.circular(8), ), child: Text(statusLabel, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: statusFg)), ), ], ), 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, ), ), ], ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // TAB 1: SUBMIT EVENT // ═══════════════════════════════════════════════════════════════════════════ Widget _buildSubmitEventTab(GamificationProvider provider) { if (_showSuccess) return _buildSuccessState(); return SingleChildScrollView( key: const ValueKey('submit'), padding: const EdgeInsets.all(20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Submit a New Event', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText), ), const SizedBox(height: 4), const Text( 'Provide accurate details to maximize your evaluated EP points.', style: TextStyle(fontSize: 13, color: _subText), ), const SizedBox(height: 20), // 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), // Category + District row Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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!)), ], ), ), ], ), const SizedBox(height: 16), // Date & Time row Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _inputLabel('Date', required: true), const SizedBox(height: 6), _datePicker(), ], ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _inputLabel('Time'), const SizedBox(height: 6), _timePicker(), ], ), ), ], ), const SizedBox(height: 16), // 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), ], ), ), ); } // ── 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)), ], ], ); } 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))), ), ); } 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 picked = await _picker.pickMultiImage(imageQuality: 80); if (picked.isNotEmpty) { setState(() { final remaining = 5 - _images.length; _images.addAll(picked.take(remaining)); }); } } catch (e) { if (mounted) { 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'), backgroundColor: Color(0xFFEF4444)), ); return; } setState(() => _submitting = true); try { final data = { 'title': _titleCtl.text.trim(), 'category': _selectedCategory, 'district': _selectedDistrict, 'date': _selectedDate!.toIso8601String(), 'time': _selectedTime?.format(context), 'description': _descriptionCtl.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) { _clearForm(); setState(() { _showSuccess = false; _activeTab = 0; }); provider.loadAll(force: true); } } catch (e) { if (mounted) { setState(() => _submitting = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(userFriendlyError(e)), backgroundColor: const Color(0xFFEF4444)), ); } } } void _clearForm() { _titleCtl.clear(); _descriptionCtl.clear(); _latCtl.clear(); _lngCtl.clear(); _mapsLinkCtl.clear(); _selectedDate = null; _selectedTime = null; _selectedCategory = _categories.first; _selectedDistrict = _districts.first; _images.clear(); _coordMessage = null; _useManualCoords = true; } // ═══════════════════════════════════════════════════════════════════════════ // TAB 2: REWARD SHOP (Coming Soon) // ═══════════════════════════════════════════════════════════════════════════ Widget _buildRewardShopTab(GamificationProvider provider) { final rp = provider.profile?.currentRp ?? 0; return SingleChildScrollView( key: const ValueKey('shop'), padding: const EdgeInsets.all(20), child: Column( children: [ const SizedBox(height: 20), // RP balance Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFFFF7ED), borderRadius: BorderRadius.circular(12), border: Border.all(color: _rpOrange.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ 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: 6), const Text('available', style: TextStyle(fontSize: 12, color: _subText)), ], ), ), 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), ], ), ); } 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}, ]; 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), ), 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: [ Text( item['name'] as String, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: _darkText), ), const SizedBox(height: 2), Text( '${item['rp']} RP', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _rpOrange), ), ], ), ), const Icon(Icons.lock_outline_rounded, color: _subText, size: 20), ], ), ), ), ); }).toList(); } }