diff --git a/lib/features/gamification/providers/gamification_provider.dart b/lib/features/gamification/providers/gamification_provider.dart index a5498aa..74eeaf4 100644 --- a/lib/features/gamification/providers/gamification_provider.dart +++ b/lib/features/gamification/providers/gamification_provider.dart @@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart'; import '../../../core/utils/error_utils.dart'; import '../models/gamification_models.dart'; import '../services/gamification_service.dart'; +import '../../events/services/events_service.dart'; +import '../../events/models/event_models.dart'; class GamificationProvider extends ChangeNotifier { final GamificationService _service = GamificationService(); @@ -16,6 +18,7 @@ class GamificationProvider extends ChangeNotifier { List submissions = []; CurrentUserStats? currentUserStats; int totalParticipants = 0; + List eventCategories = []; // Leaderboard filters — matches web version String leaderboardDistrict = 'Overall Kerala'; @@ -44,26 +47,53 @@ class GamificationProvider extends ChangeNotifier { try { final results = await Future.wait([ - _service.getDashboard(), - _service.getShopItems(), - _service.getAchievements(), + _service.getDashboard().catchError((e) { + debugPrint('Dashboard error: $e'); + return const DashboardResponse(profile: UserGamificationProfile(userId: '', lifetimeEp: 0, currentEp: 0, currentRp: 0, tier: ContributorTier.BRONZE)); + }), + _service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod).catchError((e) { + debugPrint('Leaderboard error: $e'); + return const LeaderboardResponse(entries: []); + }), + _service.getShopItems().catchError((e) { + debugPrint('Shop error: $e'); + return []; + }), + _service.getAchievements().catchError((e) { + debugPrint('Achievements error: $e'); + return []; + }), + EventsService().getEventTypes().catchError((e) { + debugPrint('EventTypes error: $e'); + return []; + }), ]); final dashboard = results[0] as DashboardResponse; profile = dashboard.profile; submissions = dashboard.submissions; - shopItems = results[1] as List; + final lbResponse = results[1] as LeaderboardResponse; + leaderboard = _filterAndReRank(lbResponse.entries, leaderboardDistrict, leaderboardTimePeriod); + currentUserStats = lbResponse.currentUser; + totalParticipants = lbResponse.totalParticipants; + + shopItems = results[2] as List; // Prefer achievements from dashboard API; fall back to fetched or existing defaults final dashAchievements = dashboard.achievements; - final fetchedAchievements = results[2] as List; + final fetchedAchievements = results[3] as List; if (dashAchievements.isNotEmpty) { achievements = dashAchievements; } else if (fetchedAchievements.isNotEmpty) { achievements = fetchedAchievements; } + + final eventTypes = results[4] as List; + if (eventTypes.isNotEmpty) { + eventCategories = eventTypes.map((e) => e.name).toList(); + } // Otherwise, keep current defaults _lastLoadTime = DateTime.now(); @@ -107,7 +137,7 @@ class GamificationProvider extends ChangeNotifier { notifyListeners(); try { final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod); - leaderboard = response.entries; + leaderboard = _filterAndReRank(response.entries, district, leaderboardTimePeriod); currentUserStats = response.currentUser; totalParticipants = response.totalParticipants; } catch (e) { @@ -128,7 +158,7 @@ class GamificationProvider extends ChangeNotifier { notifyListeners(); try { final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period); - leaderboard = response.entries; + leaderboard = _filterAndReRank(response.entries, leaderboardDistrict, period); currentUserStats = response.currentUser; totalParticipants = response.totalParticipants; } catch (e) { @@ -182,4 +212,41 @@ class GamificationProvider extends ChangeNotifier { Future submitContribution(Map data) async { await _service.submitContribution(data); } + + // --------------------------------------------------------------------------- + // Helper: Filter by district and re-rank results locally. + // This is a fallback in case the backend returns a global list for a district-specific query. + // --------------------------------------------------------------------------- + List _filterAndReRank(List entries, String district, String period) { + if (entries.isEmpty) return []; + + List result = entries; + if (district != 'Overall Kerala') { + // Case-insensitive filtering to be robust + result = entries.where((e) => e.district?.toLowerCase() == district.toLowerCase()).toList(); + } + + // Sort based on period + if (period == 'this_month') { + result.sort((a, b) => b.monthlyPoints.compareTo(a.monthlyPoints)); + } else { + result.sort((a, b) => b.lifetimeEp.compareTo(a.lifetimeEp)); + } + + // Assign new ranks based on local sort order + return List.generate(result.length, (i) { + final e = result[i]; + return LeaderboardEntry( + rank: i + 1, + username: e.username, + avatarUrl: e.avatarUrl, + lifetimeEp: e.lifetimeEp, + monthlyPoints: e.monthlyPoints, + tier: e.tier, + eventsCount: e.eventsCount, + isCurrentUser: e.isCurrentUser, + district: e.district, + ); + }); + } } diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index f03410e..4e4b348 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -44,7 +44,7 @@ const _districts = [ 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other', ]; -const _categories = [ +const _categories_fallback = [ 'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community', 'Dance', 'Film', 'Business', 'Health', 'Education', 'Other', ]; @@ -88,7 +88,7 @@ class _ContributeScreenState extends State DateTime? _selectedDate; TimeOfDay? _selectedTime; - String _selectedCategory = _categories.first; + String _selectedCategory = 'Music'; String _selectedDistrict = _districts.first; List _images = []; bool _submitting = false; @@ -135,6 +135,13 @@ class _ContributeScreenState extends State body: Center(child: BouncingLoader(color: Colors.white)), ); } + // Sync _selectedCategory with provider data if it's missing from current list + if (provider.eventCategories.isNotEmpty && !provider.eventCategories.contains(_selectedCategory)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() => _selectedCategory = provider.eventCategories.first); + }); + } + return Scaffold( backgroundColor: Colors.white, // Changed from _blue body: SafeArea( @@ -287,9 +294,36 @@ class _ContributeScreenState extends State child: Center( child: Column( children: [ - Icon(Icons.emoji_events_outlined, size: 48, color: Colors.grey.shade300), - const SizedBox(height: 12), - Text('No rankings available for this area.', style: TextStyle(color: Colors.grey.shade400)), + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.emoji_events_rounded, size: 72, color: Colors.amber), + ), + const SizedBox(height: 24), + const Text( + 'No Contributor Yet', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + color: _darkText, + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'No contributors in $currentDistrict yet. Be the first to join the ranks!', + textAlign: TextAlign.center, + style: const TextStyle( + color: _subText, + fontSize: 15, + height: 1.5, + ), + ), + ), ], ), ), @@ -1181,7 +1215,13 @@ class _ContributeScreenState extends State children: [ _inputLabel('Category', required: true), const SizedBox(height: 6), - _dropdown(_selectedCategory, _categories, (v) => setState(() => _selectedCategory = v!)), + if (provider.eventCategories.isEmpty) + const SizedBox( + height: 48, + child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), + ) + else + _dropdown(_selectedCategory, provider.eventCategories, (v) => setState(() => _selectedCategory = v!)), ], ), ), @@ -1799,7 +1839,7 @@ class _ContributeScreenState extends State _mapsLinkCtl.clear(); _selectedDate = null; _selectedTime = null; - _selectedCategory = _categories.first; + _selectedCategory = _categories_fallback.first; _selectedDistrict = _districts.first; _images.clear(); _coordMessage = null; diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 0158954..179e446 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -225,7 +225,7 @@ class _LearnMoreScreenState extends State with SingleTickerProv Future _shareEvent() async { final title = _event?.title ?? _event?.name ?? 'Check out this event'; final url = - 'https://uat.eventifyplus.com/events/${widget.eventId}'; + 'https://app.eventifyplus.com/event/${widget.eventId}'; await Share.share('$title\n$url', subject: title); } diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 1138fdf..aa78c74 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -68,8 +68,6 @@ class _ProfileScreenState extends State } List _ongoingEvents = []; - List _upcomingEvents = []; - List _pastEvents = []; bool _loadingEvents = true; @@ -202,12 +200,7 @@ class _ProfileScreenState extends State } Future _loadEventsForProfile([SharedPreferences? prefs]) async { - setState(() { - _loadingEvents = true; _ongoingEvents = []; - _upcomingEvents = []; - _pastEvents = []; - }); prefs ??= await SharedPreferences.getInstance(); final pincode = prefs.getString('pincode') ?? 'all'; @@ -218,8 +211,6 @@ class _ProfileScreenState extends State final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final ongoing = []; - final upcoming = []; - final past = []; DateTime? tryParseDate(String? s) { if (s == null) return null; @@ -235,35 +226,21 @@ class _ProfileScreenState extends State final parsedEnd = tryParseDate(e.endDate); if (parsedStart == null) { - upcoming.add(e); + // treat as ongoing or handle differently } else if (parsedStart.isAtSameMomentAs(today) || (parsedStart.isBefore(today) && parsedEnd != null && !parsedEnd.isBefore(today))) { ongoing.add(e); } else if (parsedStart.isBefore(today)) { - past.add(e); - } else { - upcoming.add(e); + // ignore past } } - upcoming.sort((a, b) { - final da = tryParseDate(a.startDate) ?? DateTime(9999); - final db = tryParseDate(b.startDate) ?? DateTime(9999); - return da.compareTo(db); - }); - past.sort((a, b) { - final da = tryParseDate(a.startDate) ?? DateTime(0); - final db = tryParseDate(b.startDate) ?? DateTime(0); - return db.compareTo(da); - }); if (mounted) { setState(() { _ongoingEvents = ongoing; - _upcomingEvents = upcoming; - _pastEvents = past; }); } } catch (e) { @@ -787,8 +764,516 @@ class _ProfileScreenState extends State // NEW UI WIDGETS — matching web profile layout // ═══════════════════════════════════════════════ + // ═══════════════════════════════════════════════ + // MODERN UI WIDGETS + // ═══════════════════════════════════════════════ + + Widget _buildModernHeader(BuildContext context, ThemeData theme) { + return Consumer( + builder: (context, gam, _) { + final stats = gam.currentUserStats; + final p = gam.profile; + final tier = stats?.level ?? _userTier ?? 'Bronze'; + + return Container( + width: double.infinity, + padding: const EdgeInsets.only(bottom: 30), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF2563EB), Color(0xFF3B82F6)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(40), + bottomRight: Radius.circular(40), + ), + ), + child: Column( + children: [ + // Top Bar + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + const Spacer(), + GestureDetector( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.settings, color: Colors.white), + ), + ), + ], + ), + ), + ), + + // Profile Avatar + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withOpacity(0.5), width: 2), + ), + child: _buildProfileAvatar(size: 100), + ), + const SizedBox(height: 16), + + // Username + Text( + _username, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 12), + + // Eventify ID Badge + if (_eventifyId != null) + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: _eventifyId!)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('ID copied to clipboard')), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.copy, size: 14, color: Colors.white70), + const SizedBox(width: 8), + Text( + _eventifyId!, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Location (District) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), + const SizedBox(width: 4), + Text( + _district ?? 'No district selected', + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400), + ), + ], + ), + if (_districtNextChange.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Next change: $_districtNextChange', + style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 12), + ), + ), + const SizedBox(height: 20), + + // Tier Badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFD1A9), // Matching "Bronze" color from screenshot + borderRadius: BorderRadius.circular(30), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield_outlined, size: 18, color: Color(0xFF92400E)), + const SizedBox(width: 8), + Text( + tier.toUpperCase(), + style: const TextStyle( + color: Color(0xFF92400E), + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + _buildHeaderButton( + label: 'Edit Profile', + icon: Icons.person_outline, + onTap: _openEditDialog, + ), + const SizedBox(height: 12), + _buildHeaderButton( + label: 'Share Rank', + icon: Icons.share_outlined, + onTap: () { + showDialog( + context: context, + builder: (_) => Dialog( + backgroundColor: Colors.transparent, + child: ShareRankCard( + username: _username, + tier: stats?.level ?? _userTier ?? '', + rank: stats?.rank ?? 0, + ep: p?.currentEp ?? 0, + rewardPoints: p?.currentRp ?? 0, + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildHeaderButton({required String label, required IconData icon, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.3)), + color: Colors.white.withValues(alpha: 0.05), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Colors.white, size: 20), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + + Widget _buildModernStatCards(GamificationProvider gam, ThemeData theme) { + final p = gam.profile; + if (p == null) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20), + child: Column( + children: [ + _buildStatRowCard('Lifetime EP', '${p.lifetimeEp}', Icons.bolt, const Color(0xFFEFF6FF), const Color(0xFF3B82F6)), + const SizedBox(height: 12), + _buildStatRowCard('Liquid EP', '${p.currentEp}', Icons.hexagon_outlined, const Color(0xFFF0FDF4), const Color(0xFF22C55E)), + const SizedBox(height: 12), + _buildStatRowCard('Reward Points', '${p.currentRp}', Icons.card_giftcard, const Color(0xFFFFFBEB), const Color(0xFFF59E0B)), + ], + ), + ); + } + + Widget _buildStatRowCard(String label, String value, IconData icon, Color bgColor, Color iconColor) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.shade100), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(14), + ), + child: Icon(icon, color: iconColor, size: 24), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)), + ), + Text( + label, + style: TextStyle(fontSize: 14, color: Colors.grey.shade500, fontWeight: FontWeight.w500), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTierProgressCard(GamificationProvider gam, ThemeData theme) { + final ep = gam.profile?.lifetimeEp ?? 0; + final currentTier = tierFromEp(ep); + final nextThreshold = nextTierThreshold(currentTier); + final startEp = tierStartEp(currentTier); + + double progress = 0.0; + String nextInfo = ''; + if (nextThreshold != null) { + final needed = nextThreshold - ep; + final range = nextThreshold - startEp; + progress = ((ep - startEp) / range).clamp(0.0, 1.0); + nextInfo = '$needed EP to ${tierLabel(ContributorTier.values[currentTier.index + 1]).toUpperCase()}'; + } else { + progress = 1.0; + nextInfo = 'MAX TIER REACHED'; + } + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 18), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.grey.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + tierLabel(currentTier).toUpperCase(), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)), + ), + Text( + nextInfo, + style: TextStyle(fontSize: 13, color: Colors.grey.shade500, fontWeight: FontWeight.w600), + ), + ], + ), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: progress, + minHeight: 10, + backgroundColor: const Color(0xFFF3F4F6), + valueColor: const AlwaysStoppedAnimation(Color(0xFF2563EB)), + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: Text( + '$ep EP total', + style: TextStyle(fontSize: 13, color: Colors.grey.shade400, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ); + } + + Widget _buildContributedEventsSection(GamificationProvider gam, ThemeData theme) { + final submissions = gam.submissions; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 32, 20, 16), + child: Row( + children: [ + const Text( + 'Contributed Events', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)), + ), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${submissions.length}', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF6B7280)), + ), + ), + ], + ), + ), + if (submissions.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Text('No contributions yet. Start contributing to earn EP!'), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 18), + itemCount: submissions.length, + itemBuilder: (ctx, i) => _buildSubmissionCard(submissions[i], theme), + ), + const SizedBox(height: 100), + ], + ); + } + + Widget _buildSubmissionCard(SubmissionModel sub, ThemeData theme) { + Color statusColor; + Color statusBgColor; + String statusLabel = sub.status.toUpperCase(); + + switch (sub.status.toUpperCase()) { + case 'APPROVED': + statusColor = const Color(0xFF059669); + statusBgColor = const Color(0xFFD1FAE5); + break; + case 'REJECTED': + statusColor = const Color(0xFFDC2626); + statusBgColor = const Color(0xFFFEE2E2); + break; + default: // PENDING + statusColor = const Color(0xFFD97706); + statusBgColor = const Color(0xFFFEF3C7); + statusLabel = 'PENDING'; + } + + final dateStr = '${sub.createdAt.day} ${_getMonth(sub.createdAt.month)} ${sub.createdAt.year}'; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.shade100), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.01), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + sub.eventName, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: statusBgColor, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Icon(Icons.access_time_filled, size: 14, color: statusColor), + const SizedBox(width: 4), + Text( + statusLabel, + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: statusColor), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.calendar_today_outlined, size: 14, color: Colors.grey.shade400), + const SizedBox(width: 6), + Text(dateStr, style: TextStyle(color: Colors.grey.shade500, fontSize: 13)), + const SizedBox(width: 16), + Icon(Icons.location_on_outlined, size: 14, color: Colors.grey.shade400), + const SizedBox(width: 6), + Expanded( + child: Text( + sub.district ?? 'Unknown', + style: TextStyle(color: Colors.grey.shade500, fontSize: 13), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (sub.status.toUpperCase() == 'APPROVED' && sub.epAwarded > 0) + Text( + '+${sub.epAwarded} EP', + style: const TextStyle(color: Color(0xFF059669), fontWeight: FontWeight.w800, fontSize: 14), + ), + ], + ), + if (sub.category.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + sub.category, + style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280), fontWeight: FontWeight.w500), + ), + ), + ], + ], + ), + ); + } + + String _getMonth(int m) { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return months[m - 1]; + } + // ───────── Gradient Header ───────── + Widget _buildGradientHeader(BuildContext context, double height) { return Container( width: double.infinity, @@ -1224,43 +1709,6 @@ class _ProfileScreenState extends State _ongoingEvents.map((e) => _eventListTileFromModel(e)).toList()), const SizedBox(height: 24), ], - - // Upcoming Events - sectionHeading('Upcoming Events'), - const SizedBox(height: 12), - if (_loadingEvents) - const SizedBox.shrink() - else if (_upcomingEvents.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text('No upcoming events', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.hintColor)), - ) - else - Column( - children: _upcomingEvents - .map((e) => _eventListTileFromModel(e)) - .toList()), - const SizedBox(height: 24), - - // Past Events - sectionHeading('Past Events'), - const SizedBox(height: 12), - if (_loadingEvents) - const SizedBox.shrink() - else if (_pastEvents.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text('No past events', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.hintColor)), - ) - else - Column( - children: _pastEvents - .map((e) => _eventListTileFromModel(e, faded: true)) - .toList()), ], ), ); @@ -1438,7 +1886,7 @@ class _ProfileScreenState extends State return SafeArea( child: DefaultTabController( - length: 3, + length: 1, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -1461,8 +1909,6 @@ class _ProfileScreenState extends State labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), tabs: const [ Tab(text: 'Ongoing'), - Tab(text: 'Upcoming'), - Tab(text: 'Past'), ], ), ), @@ -1471,8 +1917,6 @@ class _ProfileScreenState extends State child: TabBarView( children: [ _eventList(_ongoingEvents), - _eventList(_upcomingEvents), - _eventList(_pastEvents, faded: true), ], ), ), @@ -1515,24 +1959,6 @@ class _ProfileScreenState extends State faded: false, ), - // Upcoming Events - _buildDesktopEventSection( - context, - title: 'Upcoming Events', - events: _upcomingEvents, - faded: false, - emptyMessage: 'No upcoming events', - ), - - // Past Events - _buildDesktopEventSection( - context, - title: 'Past Events', - events: _pastEvents, - faded: true, - emptyMessage: 'No past events', - ), - const SizedBox(height: 32), ], ), @@ -1801,19 +2227,8 @@ class _ProfileScreenState extends State @override Widget build(BuildContext context) { final theme = Theme.of(context); - const double headerHeight = 200.0; - const double cardTopOffset = 130.0; final width = MediaQuery.of(context).size.width; - Widget sectionTitle(String text) => Padding( - padding: const EdgeInsets.fromLTRB(18, 16, 18, 12), - child: Text( - text, - style: theme.textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w600, fontSize: 18), - ), - ); - // ── DESKTOP / LANDSCAPE layout ───────────────────────────────────────── if (width >= AppConstants.desktopBreakpoint) { return _buildDesktopLayout(context, theme); @@ -1821,97 +2236,27 @@ class _ProfileScreenState extends State // ── MOBILE layout ───────────────────────────────────────────────────── return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - // CustomScrollView: only visible event cards are built — no full-tree Column renders - // SafeArea(bottom: false) — bottom is handled by home screen's floating nav buffer - body: SafeArea( - bottom: false, - child: CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - // Header gradient + Profile card overlap (same visual as before) - SliverToBoxAdapter( - child: Stack( - children: [ - _buildGradientHeader(context, headerHeight), - Padding( - padding: EdgeInsets.only(top: cardTopOffset), - child: _buildProfileCard(context), - ), - ], - ), - ), - - // PROF-003: Gamification stat cards - SliverToBoxAdapter(child: _buildGamificationCards(theme)), - - // ── Ongoing Events ── - if (_ongoingEvents.isNotEmpty) ...[ - SliverToBoxAdapter(child: sectionTitle('Ongoing Events')), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 18), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (ctx, i) => _eventListTileFromModel(_ongoingEvents[i]), - childCount: _ongoingEvents.length, - ), + backgroundColor: const Color(0xFFF9FAFB), // Softer light bg + body: Consumer( + builder: (context, gam, _) { + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: _buildModernHeader(context, theme), ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 24)), - ], - - // ── Upcoming Events ── - SliverToBoxAdapter(child: sectionTitle('Upcoming Events')), - if (_loadingEvents) - const SliverToBoxAdapter(child: SizedBox.shrink()) - else if (_upcomingEvents.isEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), - child: Text('No upcoming events', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.hintColor)), + SliverToBoxAdapter( + child: _buildModernStatCards(gam, theme), ), - ) - else - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 18), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (ctx, i) => _eventListTileFromModel(_upcomingEvents[i]), - childCount: _upcomingEvents.length, - ), + SliverToBoxAdapter( + child: _buildTierProgressCard(gam, theme), ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 24)), - - // ── Past Events ── - SliverToBoxAdapter(child: sectionTitle('Past Events')), - if (_loadingEvents) - const SliverToBoxAdapter(child: SizedBox.shrink()) - else if (_pastEvents.isEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), - child: Text('No past events', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.hintColor)), + SliverToBoxAdapter( + child: _buildContributedEventsSection(gam, theme), ), - ) - else - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 18), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (ctx, i) => _eventListTileFromModel(_pastEvents[i], faded: true), - childCount: _pastEvents.length, - ), - ), - ), - - const SliverToBoxAdapter(child: SizedBox(height: 32)), - ], - ), + ], + ); + }, ), ); }