From 7bc396bdde807e72d6a1a744106b382d9fd08b0e Mon Sep 17 00:00:00 2001 From: Rishad7594 Date: Tue, 7 Apr 2026 20:49:40 +0530 Subject: [PATCH] Update default location to Thrissur and remove Whitefield, Bengaluru --- lib/core/api/api_client.dart | 12 +- lib/core/api/api_endpoints.dart | 2 +- .../events/services/events_service.dart | 38 +- .../providers/gamification_provider.dart | 12 +- .../services/gamification_service.dart | 27 +- lib/screens/contribute_screen.dart | 717 ++++++++++++++++-- lib/screens/home_screen.dart | 290 ++++--- lib/screens/learn_more_screen.dart | 15 +- lib/screens/login_screen.dart | 28 +- lib/screens/search_screen.dart | 51 +- pubspec.lock | 36 +- 11 files changed, 944 insertions(+), 284 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 021b7df..a64de9b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -150,8 +150,8 @@ class ApiClient { 'end_date': '2026-04-16', 'start_time': '09:00', 'end_time': '18:00', - 'pincode': '560001', - 'place': 'Bengaluru International Exhibition Centre', + 'pincode': '680001', + 'place': 'Thekkinkadu Maidanam', 'is_bookable': true, 'event_type': 5, 'thumb_img': 'https://picsum.photos/seed/event1/600/400', @@ -160,11 +160,11 @@ class ApiClient { {'is_primary': false, 'image': 'https://picsum.photos/seed/event1b/800/500'}, ], 'important_information': 'Please carry a valid photo ID for entry.', - 'venue_name': 'BIEC Hall 2', + 'venue_name': 'Maidanam Grounds', 'event_status': 'active', - 'latitude': 13.0147, - 'longitude': 77.5636, - 'location_name': 'Bengaluru', + 'latitude': 10.5276, + 'longitude': 76.2144, + 'location_name': 'Thrissur', 'important_info': [ {'title': 'Entry', 'value': 'Free with registration'}, {'title': 'Parking', 'value': 'Available on-site'}, diff --git a/lib/core/api/api_endpoints.dart b/lib/core/api/api_endpoints.dart index 3b46e58..a25d179 100644 --- a/lib/core/api/api_endpoints.dart +++ b/lib/core/api/api_endpoints.dart @@ -58,5 +58,5 @@ class ApiEndpoints { // Notifications static const String notificationList = "$baseUrl/notifications/list/"; static const String notificationMarkRead = "$baseUrl/notifications/mark-read/"; - static const String notificationCount = "$baseUrl/notifications/count/"; + static const String notificationCount = "$baseUrl/notifications/count"; } diff --git a/lib/features/events/services/events_service.dart b/lib/features/events/services/events_service.dart index fe1fb4b..3ad66c4 100644 --- a/lib/features/events/services/events_service.dart +++ b/lib/features/events/services/events_service.dart @@ -109,30 +109,24 @@ class EventsService { } /// Get events by GPS coordinates using haversine distance filtering. - /// Automatically expands radius (10 → 25 → 50 → 100 km) until ≥ 6 events found. - Future> getEventsByLocation(double lat, double lng, {double initialRadiusKm = 10}) async { - const radii = [10.0, 25.0, 50.0, 100.0]; - for (final radius in radii) { - if (radius < initialRadiusKm) continue; - final body = { - 'latitude': lat, - 'longitude': lng, - 'radius_km': radius, - 'page': 1, - 'page_size': 50, - 'per_type': 5, - }; - final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false); - final list = []; - final events = res['events'] ?? res['data'] ?? []; - if (events is List) { - for (final e in events) { - if (e is Map) list.add(EventModel.fromJson(Map.from(e))); - } + Future> getEventsByLocation(double lat, double lng, {double radiusKm = 10.0}) async { + final body = { + 'latitude': lat, + 'longitude': lng, + 'radius_km': radiusKm, + 'page': 1, + 'page_size': 50, + 'per_type': 5, + }; + final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false); + final list = []; + final events = res['events'] ?? res['data'] ?? []; + if (events is List) { + for (final e in events) { + if (e is Map) list.add(EventModel.fromJson(Map.from(e))); } - if (list.length >= 6 || radius >= 100) return list; } - return []; + return list; } /// Events by month and year for calendar (POST to /events/events-by-month-year/) diff --git a/lib/features/gamification/providers/gamification_provider.dart b/lib/features/gamification/providers/gamification_provider.dart index 61c11e3..e0ed173 100644 --- a/lib/features/gamification/providers/gamification_provider.dart +++ b/lib/features/gamification/providers/gamification_provider.dart @@ -12,7 +12,7 @@ class GamificationProvider extends ChangeNotifier { UserGamificationProfile? profile; List leaderboard = []; List shopItems = []; - List achievements = []; + List achievements = GamificationService.defaultBadges; // Initialize with defaults List submissions = []; CurrentUserStats? currentUserStats; int totalParticipants = 0; @@ -60,10 +60,16 @@ class GamificationProvider extends ChangeNotifier { shopItems = results[2] as List; - // Prefer achievements from dashboard API; fall back to getAchievements() + // Prefer achievements from dashboard API; fall back to fetched or existing defaults final dashAchievements = dashboard.achievements; final fetchedAchievements = results[3] as List; - achievements = dashAchievements.isNotEmpty ? dashAchievements : fetchedAchievements; + + if (dashAchievements.isNotEmpty) { + achievements = dashAchievements; + } else if (fetchedAchievements.isNotEmpty) { + achievements = fetchedAchievements; + } + // Otherwise, keep current defaults _lastLoadTime = DateTime.now(); } catch (e) { diff --git a/lib/features/gamification/services/gamification_service.dart b/lib/features/gamification/services/gamification_service.dart index 3f58fd6..b13fdd7 100644 --- a/lib/features/gamification/services/gamification_service.dart +++ b/lib/features/gamification/services/gamification_service.dart @@ -23,7 +23,7 @@ class GamificationService { // --------------------------------------------------------------------------- Future getDashboard() async { final email = await _getUserEmail(); - final url = '${ApiEndpoints.gamificationDashboard}?user_id=$email'; + final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(email)}'; final res = await _api.get(url, requiresAuth: false); final profileJson = res['profile'] as Map? ?? {}; @@ -50,7 +50,7 @@ class GamificationService { // GET /v1/gamification/dashboard?user_id={userId} // --------------------------------------------------------------------------- Future getDashboardForUser(String userId) async { - final url = '${ApiEndpoints.gamificationDashboard}?user_id=$userId'; + final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(userId)}'; final res = await _api.get(url, requiresAuth: false); final profileJson = res['profile'] as Map? ?? {}; @@ -175,20 +175,17 @@ class GamificationService { } catch (_) { // Fall through to defaults } - return _defaultBadges; + return defaultBadges; } - static const _defaultBadges = [ - AchievementBadge(id: 'badge-01', title: 'First Step', description: 'Submit your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Submit 5 events.', iconName: 'trending_up', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Submit 15 events.', iconName: 'emoji_events', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Reach top 10 on leaderboard.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Upload 10 event images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first contributors.', iconName: 'rocket_launch', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-07', title: 'Community Star', description: 'Get 10 events approved.', iconName: 'verified', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-08', title: 'Event Hunter', description: 'Submit 25 events.', iconName: 'event_hunter', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-09', title: 'District Champion', description: 'Rank #1 in your district.', iconName: 'location_on', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-10', title: 'Platinum Achiever', description: 'Earn 1500 EP.', iconName: 'diamond', isUnlocked: false, progress: 0.0), - AchievementBadge(id: 'badge-11', title: 'Diamond Legend', description: 'Earn 5000 EP.', iconName: 'workspace_premium', isUnlocked: false, progress: 0.0), + static const defaultBadges = [ + AchievementBadge(id: 'badge-01', title: 'Newcomer', description: 'First Event Posted', iconName: 'star', isUnlocked: false, progress: 0.0), + AchievementBadge(id: 'badge-02', title: 'Contributor', description: '10th Event Posted within a month', iconName: 'crown', isUnlocked: false, progress: 0.0), + AchievementBadge(id: 'badge-03', title: 'On Fire!', description: '3 Day Streak of logging in', iconName: 'fire', isUnlocked: false, progress: 0.67), + AchievementBadge(id: 'badge-04', title: 'Verified', description: 'Identity Verified successfully', iconName: 'verified', isUnlocked: true, progress: 1.0), + AchievementBadge(id: 'badge-05', title: 'Quality', description: '5 Star Event Rating received', iconName: 'star', isUnlocked: false, progress: 0.0), + AchievementBadge(id: 'badge-06', title: 'Community', description: 'Referred 5 Friends to the platform', iconName: 'community', isUnlocked: false, progress: 0.4), + AchievementBadge(id: 'badge-07', title: 'Expert', description: 'Level 10 Reached in 3 months', iconName: 'expert', isUnlocked: false, progress: 0.0), + AchievementBadge(id: 'badge-08', title: 'Precision', description: '100% Data Accuracy on all events', iconName: 'precision', isUnlocked: false, progress: 0.0), ]; } diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index fe8ed14..973a7b3 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -121,26 +121,56 @@ class _ContributeScreenState extends State // ───────────────────────────────────────────────────────────────────────── // Build // ───────────────────────────────────────────────────────────────────────── + int _mainTab = 0; // 0: Contribute, 1: Leaderboard, 2: Achievements + @override @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)), + backgroundColor: _blue, + body: Center(child: BouncingLoader(color: Colors.white)), ); } return Scaffold( - backgroundColor: _pageBg, + backgroundColor: Colors.white, // Changed from _blue body: SafeArea( - child: Column( - children: [ - _buildStatsBar(provider), - _buildTierRoadmap(provider), - _buildTabBar(), - Expanded(child: _buildTabContent(provider)), - ], + bottom: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(provider), + Transform.translate( + offset: const Offset(0, -24), + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_mainTab == 0) ...[ + _buildStatsBar(provider), + _buildTierRoadmap(provider), + const SizedBox(height: 12), + _buildTabBar(), + _buildTabContent(provider), + ] else if (_mainTab == 1) ...[ + _buildLeaderboardTab(provider), + ] else if (_mainTab == 2) ...[ + _buildAchievementsTab(provider), + ], + const SizedBox(height: 100), + ], + ), + ), + ), + ], + ), ), ), ); @@ -148,6 +178,534 @@ class _ContributeScreenState extends State ); } + // ═══════════════════════════════════════════════════════════════════════════ + // LEADERBOARD TAB + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildLeaderboardTab(GamificationProvider provider) { + final leaderboard = provider.leaderboard; + final currentPeriod = provider.leaderboardTimePeriod; + final currentDistrict = provider.leaderboardDistrict; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + // Time Period Toggle + Center( + child: Container( + height: 48, + width: 300, + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(24), + ), + child: Stack( + children: [ + AnimatedAlign( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + alignment: currentPeriod == 'all_time' ? Alignment.centerLeft : Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + width: 144, + decoration: BoxDecoration( + color: _blue, + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => provider.setTimePeriod('all_time'), + behavior: HitTestBehavior.opaque, + child: Center( + child: Text( + 'All Time', + style: TextStyle( + color: currentPeriod == 'all_time' ? Colors.white : _subText, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => provider.setTimePeriod('this_month'), + behavior: HitTestBehavior.opaque, + child: Center( + child: Text( + 'This Month', + style: TextStyle( + color: currentPeriod == 'this_month' ? Colors.white : _subText, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // District Chips + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + _buildDistrictChip(provider, 'Overall Kerala'), + ..._districts.where((d) => d != 'Other').map((d) => _buildDistrictChip(provider, d)), + ], + ), + ), + + const SizedBox(height: 16), + + // Leaderboard List + if (provider.isLoading && leaderboard.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 60), + child: Center(child: CircularProgressIndicator(strokeWidth: 2)), + ) + else if (leaderboard.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 60), + 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)), + ], + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 20), + itemCount: leaderboard.length, + separatorBuilder: (_, __) => Divider(height: 1, color: _border.withValues(alpha: 0.5)), + itemBuilder: (context, index) { + final entry = leaderboard[index]; + return _buildLeaderboardTile(entry); + }, + ), + const SizedBox(height: 100), // Bottom padding + ], + ); + } + + Widget _buildDistrictChip(GamificationProvider provider, String district) { + final isSelected = provider.leaderboardDistrict == district; + return GestureDetector( + onTap: () => provider.setDistrict(district), + child: Container( + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? _blue : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: isSelected ? _blue : _border), + ), + child: Text( + district, + style: TextStyle( + color: isSelected ? Colors.white : _darkText, + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ); + } + + Widget _buildLeaderboardTile(LeaderboardEntry entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + SizedBox( + width: 32, + child: Text( + '${entry.rank}', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: entry.rank <= 3 ? _blue : _subText, + ), + ), + ), + const SizedBox(width: 8), + CircleAvatar( + radius: 20, + backgroundColor: _lightBlueBg, + backgroundImage: entry.avatarUrl != null ? NetworkImage(entry.avatarUrl!) : null, + child: entry.avatarUrl == null + ? const Icon(Icons.person_outline, color: _blue, size: 20) + : null, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + entry.username, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.normal, color: _darkText), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '${entry.lifetimeEp} pts', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF10B981), // Emerald green + ), + ), + ], + ), + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ACHIEVEMENTS TAB + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildAchievementsTab(GamificationProvider provider) { + final achievements = provider.achievements; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (achievements.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 60), + child: Center( + child: Text('No achievements found.', style: TextStyle(color: _subText)), + ), + ) + else + Column( + children: achievements.map((badge) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildAchievementCard(badge), + ); + }).toList(), + ), + const SizedBox(height: 100), + ], + ), + ); + } + + Widget _buildAchievementCard(AchievementBadge badge) { + final bool isLocked = !badge.isUnlocked; + final Color iconColor = _getAchievementColor(badge.iconName); + final IconData iconData = _getAchievementIcon(badge.iconName); + + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _border.withValues(alpha: 0.8)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.02), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Large Icon Container + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: isLocked ? Colors.grey.shade100 : iconColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + isLocked ? Icons.lock_outline : iconData, + color: isLocked ? Colors.grey.shade400 : iconColor, + size: 32, + ), + ), + const SizedBox(height: 20), + + // Title with Lock Icon if needed + Row( + children: [ + Text( + badge.title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: isLocked ? Colors.grey : _darkText, + ), + ), + if (isLocked) ...[ + const SizedBox(width: 8), + Icon(Icons.lock_outline, size: 18, color: Colors.grey.shade300), + ], + ], + ), + const SizedBox(height: 6), + + // Description + Text( + badge.description, + style: TextStyle( + fontSize: 14, + color: isLocked ? Colors.grey.shade400 : _subText, + height: 1.4, + ), + ), + + // Progress Section + if (!badge.isUnlocked && badge.progress > 0) ...[ + const SizedBox(height: 24), + Stack( + children: [ + Container( + height: 6, + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(3), + ), + ), + FractionallySizedBox( + widthFactor: badge.progress, + child: Container( + height: 6, + decoration: BoxDecoration( + color: _blue, + borderRadius: BorderRadius.circular(3), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: Text( + '${(badge.progress * 100).toInt()}%', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.black26, + ), + ), + ), + ], + ], + ), + ); + } + + Color _getAchievementColor(String iconName) { + switch (iconName.toLowerCase()) { + case 'star': return const Color(0xFF3B82F6); // Blue + case 'crown': return const Color(0xFFF59E0B); // Amber + case 'fire': return const Color(0xFFEF4444); // Red + case 'verified': return const Color(0xFF10B981); // Emerald + case 'community': return const Color(0xFF8B5CF6); // Purple + case 'expert': return const Color(0xFF6366F1); // Indigo + default: return _blue; + } + } + + IconData _getAchievementIcon(String iconName) { + switch (iconName.toLowerCase()) { + case 'star': return Icons.star_rounded; + case 'crown': return Icons.emoji_events_rounded; + case 'fire': return Icons.local_fire_department_rounded; + case 'verified': return Icons.verified_rounded; + case 'community': return Icons.people_alt_rounded; + case 'expert': return Icons.workspace_premium_rounded; + case 'precision': return Icons.gps_fixed_rounded; + default: return Icons.stars_rounded; + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // NEW BLUE HEADER DESIGN + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildHeader(GamificationProvider provider) { + return Container( + width: double.infinity, + color: _blue, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), // Increased bottom padding + child: Column( + children: [ + const Text( + 'Contributor Dashboard', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.normal), + ), + const SizedBox(height: 6), + const Text( + 'Track your impact, earn rewards, and climb\nthe ranks!', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 14, height: 1.4), + ), + const SizedBox(height: 24), + _buildMainTabGlider(), + const SizedBox(height: 20), + _buildContributorLevelCard(provider), + ], + ), + ), + ); + } + + Widget _buildMainTabGlider() { + const labels = ['Contribute', 'Leaderboard', 'Achievements']; + return Container( + height: 48, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = constraints.maxWidth / 3; + return Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + left: tabWidth * _mainTab, + top: 0, + bottom: 0, + child: Container( + width: tabWidth, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2)), + ], + ), + ), + ), + Row( + children: List.generate(3, (i) { + final active = _mainTab == i; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _mainTab = i), + behavior: HitTestBehavior.opaque, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (i == 0) ...[ + Icon(Icons.edit_square, size: 16, color: active ? _blue : Colors.white), + const SizedBox(width: 6), + ], + Text( + labels[i], + style: TextStyle( + color: active ? _blue : Colors.white, + fontWeight: FontWeight.w500, + fontSize: 13, + ), + ), + ], + ), + ), + ), + ); + }), + ), + ], + ); + }, + ), + ); + } + + Widget _buildContributorLevelCard(GamificationProvider provider) { + final profile = provider.profile; + final tier = profile?.tier ?? ContributorTier.BRONZE; + final currentEp = profile?.lifetimeEp ?? 0; + int nextThreshold = _tierThresholds.last; + String nextTierLabel = 'Max'; + for (int i = 0; i < ContributorTier.values.length; i++) { + if (currentEp < _tierThresholds[i]) { + nextThreshold = _tierThresholds[i]; + nextTierLabel = tierLabel(ContributorTier.values[i]); + break; + } + } + double progress = (currentEp / nextThreshold).clamp(0.0, 1.0); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.12), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Contributor Level', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.normal)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.25), borderRadius: BorderRadius.circular(20)), + child: Text(tierLabel(tier), style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)), + ), + ], + ), + const SizedBox(height: 8), + Text('Start earning rewards by\ncontributing!', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14)), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('$currentEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.normal, fontSize: 13)), + Text('Next: $nextTierLabel ($nextThreshold pts)', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12)), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + backgroundColor: Colors.white.withOpacity(0.2), + valueColor: const AlwaysStoppedAnimation(Colors.white), + minHeight: 8, + ), + ), + ], + ), + ); + } + // ═══════════════════════════════════════════════════════════════════════════ // 1. COMPACT STATS BAR // ═══════════════════════════════════════════════════════════════════════════ @@ -158,65 +716,65 @@ class _ContributeScreenState extends State final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined; return Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Row( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: _blue, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(tierIcon, color: Colors.white, size: 14), + const SizedBox(width: 6), + Text( + tierLabel(tier).toUpperCase(), + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.normal, fontSize: 13, letterSpacing: 0.5), + ), + ], ), - ], - ), - ), - 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), - ), + const SizedBox(width: 16), + // Liquid EP + Icon(Icons.bolt_outlined, color: _blue, size: 18), + const SizedBox(width: 4), + Text('${profile?.currentEp ?? 0}', style: const TextStyle(color: _darkText, fontWeight: FontWeight.normal, fontSize: 15)), + const SizedBox(width: 4), + const Text('Liquid EP', style: TextStyle(color: _subText, fontSize: 12)), + const SizedBox(width: 16), + // RP + Icon(Icons.card_giftcard_outlined, color: _rpOrange, size: 18), + const SizedBox(width: 4), + Text('${profile?.currentRp ?? 0}', style: TextStyle(color: _rpOrange, fontWeight: FontWeight.normal, fontSize: 15)), + const SizedBox(width: 4), + const Text('RP', style: TextStyle(color: _subText, fontSize: 12)), + ], + ), + const SizedBox(height: 16), + // Share Rank button + GestureDetector( + onTap: () => _shareRank(provider), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: _border), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.ios_share_outlined, color: _blue, size: 16), + const SizedBox(width: 8), + const Text('Share Rank', style: TextStyle(color: _blue, fontWeight: FontWeight.normal, fontSize: 13)), + ], + ), + ), ), ], ), @@ -337,7 +895,7 @@ class _ContributeScreenState extends State left: tabWidth * _activeTab + 4, top: 4, child: Container( - width: tabWidth - 8, + width: tabWidth > 8 ? tabWidth - 8 : 0, height: 44, decoration: BoxDecoration( color: _blue, @@ -363,16 +921,20 @@ class _ContributeScreenState extends State children: [ Icon( _tabIcons[i], - size: 18, + size: 16, // Slightly smaller icon 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 SizedBox(width: 4), // Tighter spacing + Flexible( + child: Text( + _tabLabels[i], + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontSize: 11, // Smaller font for better fit + fontWeight: FontWeight.w600, + color: isActive ? Colors.white : const Color(0xFF64748B), + ), ), ), ], @@ -470,6 +1032,8 @@ class _ContributeScreenState extends State return ListView.separated( key: const ValueKey('list'), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(16), itemCount: submissions.length, separatorBuilder: (_, __) => const SizedBox(height: 10), @@ -757,7 +1321,8 @@ class _ContributeScreenState extends State 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(), + isExpanded: true, + items: items.map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis))).toList(), onChanged: onChanged, decoration: InputDecoration( filled: true, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 5f6a11d..0dd7682 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -93,7 +93,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM setState(() => _loading = true); final prefs = await SharedPreferences.getInstance(); _username = prefs.getString('display_name') ?? prefs.getString('username') ?? ''; - final storedLocation = prefs.getString('location') ?? 'Whitefield, Bengaluru'; + final storedLocation = prefs.getString('location') ?? 'Thrissur'; // Fix legacy lat,lng strings saved before the reverse-geocoding fix final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation); if (coordMatch != null) { @@ -467,7 +467,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM onTap: () { Navigator.of(context).pop(); if (ev.id != null) { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev))); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-search'))); } }, ); @@ -919,7 +919,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM onTap: () { Navigator.of(context).pop(); if (ev.id != null) { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev))); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-sheet'))); } }, child: Container( @@ -1197,129 +1197,181 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } + /// Returns the image URL for a given event (for blurred bg). + String? _getEventImageUrl(EventModel event) { + if (event.thumbImg != null && event.thumbImg!.isNotEmpty) return event.thumbImg; + if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) return event.images.first.image; + return null; + } + Widget _buildHeroSection() { return SafeArea( bottom: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Top bar: location pill + search button - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: _openLocationSearch, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(25), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), - const SizedBox(width: 6), - Text( - _location.length > 20 ? '${_location.substring(0, 20)}...' : _location, - style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + child: ValueListenableBuilder( + valueListenable: _heroPageNotifier, + builder: (context, currentPage, _) { + final currentImg = _heroEvents.isNotEmpty ? _getEventImageUrl(_heroEvents[currentPage.clamp(0, _heroEvents.length - 1)]) : null; + return Stack( + children: [ + // ── Blurred background image layer ── + if (currentImg != null && currentImg.isNotEmpty) + Positioned.fill( + child: ClipRect( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: CachedNetworkImage( + key: ValueKey(currentImg), + imageUrl: currentImg, + memCacheWidth: 200, + memCacheHeight: 200, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + imageBuilder: (context, imageProvider) => Stack( + fit: StackFit.expand, + children: [ + ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Image( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + Container( + color: Colors.black.withOpacity(0.35), + ), + ], ), - const SizedBox(width: 4), - const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18), - ], + ), ), ), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const NotificationBell(), - const SizedBox(width: 8), - GestureDetector( - onTap: _openEventSearch, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - shape: BoxShape.circle, - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: const Icon(Icons.search, color: Colors.white, size: 24), - ), - ), - ], - ), - ], - ), - ), - const SizedBox(height: 24), - // Featured carousel - _heroEvents.isEmpty - ? _loading - ? const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: SizedBox( - height: 320, - child: _HeroShimmer(), - ), - ) - : const SizedBox( - height: 280, - child: Center( - child: Text('No events available', - style: TextStyle(color: Colors.white70)), - ), - ) - : Column( - children: [ - RepaintBoundary( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanDown: (_) => _autoScrollTimer?.cancel(), - onPanEnd: (_) => _startAutoScroll(delay: const Duration(seconds: 3)), - onPanCancel: () => _startAutoScroll(delay: const Duration(seconds: 3)), - child: SizedBox( - height: 320, - child: PageView.builder( - controller: _heroPageController, - onPageChanged: (page) { - _heroPageNotifier.value = page; - // 8s delay after manual swipe for full read time - _startAutoScroll(delay: const Duration(seconds: 8)); - }, - itemCount: _heroEvents.length, - itemBuilder: (context, index) { - // Scale animation: active card = 1.0, adjacent = 0.94 - return AnimatedBuilder( - animation: _heroPageController, - builder: (context, child) { - double scale = index == _heroPageNotifier.value ? 1.0 : 0.94; - if (_heroPageController.position.haveDimensions) { - scale = (1.0 - - (_heroPageController.page! - index).abs() * 0.06) - .clamp(0.94, 1.0); - } - return Transform.scale(scale: scale, child: child); - }, - child: _buildHeroEventImage(_heroEvents[index]), - ); - }, + // ── Foreground content ── + Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Top bar: location pill + search button + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: _openLocationSearch, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), + const SizedBox(width: 6), + Text( + _location.length > 20 ? '${_location.substring(0, 20)}...' : _location, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 4), + const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18), + ], + ), ), ), - ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const NotificationBell(), + const SizedBox(width: 8), + GestureDetector( + onTap: _openEventSearch, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: const Icon(Icons.search, color: Colors.white, size: 24), + ), + ), + ], + ), + ], ), - const SizedBox(height: 16), - // Pagination dots - _buildCarouselDots(), - ], - ), - const SizedBox(height: 24), - ], + ), + const SizedBox(height: 24), + + // Featured carousel + _heroEvents.isEmpty + ? _loading + ? const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: SizedBox( + height: 320, + child: _HeroShimmer(), + ), + ) + : const SizedBox( + height: 280, + child: Center( + child: Text('No events available', + style: TextStyle(color: Colors.white70)), + ), + ) + : Column( + children: [ + RepaintBoundary( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanDown: (_) => _autoScrollTimer?.cancel(), + onPanEnd: (_) => _startAutoScroll(delay: const Duration(seconds: 3)), + onPanCancel: () => _startAutoScroll(delay: const Duration(seconds: 3)), + child: SizedBox( + height: 320, + child: PageView.builder( + controller: _heroPageController, + onPageChanged: (page) { + _heroPageNotifier.value = page; + // 8s delay after manual swipe for full read time + _startAutoScroll(delay: const Duration(seconds: 8)); + }, + itemCount: _heroEvents.length, + itemBuilder: (context, index) { + // Scale animation: active card = 1.0, adjacent = 0.94 + return AnimatedBuilder( + animation: _heroPageController, + builder: (context, child) { + double scale = index == _heroPageNotifier.value ? 1.0 : 0.94; + if (_heroPageController.position.haveDimensions) { + scale = (1.0 - + (_heroPageController.page! - index).abs() * 0.06) + .clamp(0.94, 1.0); + } + return Transform.scale(scale: scale, child: child); + }, + child: _buildHeroEventImage(_heroEvents[index]), + ); + }, + ), + ), + ), + ), + const SizedBox(height: 16), + // Pagination dots + _buildCarouselDots(), + ], + ), + const SizedBox(height: 24), + ], + ), + ], + ); + }, ), ); } @@ -1390,13 +1442,13 @@ class _HomeScreenState extends State with SingleTickerProviderStateM 'source': 'hero_carousel', }); Navigator.push(context, - MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event))); + MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-carousel'))); } }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Hero( - tag: 'event-hero-${event.id}', + tag: 'event-hero-${event.id}-carousel', child: ClipRRect( borderRadius: BorderRadius.circular(radius), child: Stack( @@ -1815,11 +1867,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM return GestureDetector( onTap: () { if (event.id != null) { - Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event))); + Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-top'))); } }, child: Hero( - tag: 'event-hero-${event.id}', + tag: 'event-hero-${event.id}-top', child: Container( width: 150, decoration: BoxDecoration( diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 8b9c32c..0158954 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -23,7 +23,8 @@ import '../core/analytics/posthog_service.dart'; class LearnMoreScreen extends StatefulWidget { final int eventId; final EventModel? initialEvent; - const LearnMoreScreen({Key? key, required this.eventId, this.initialEvent}) : super(key: key); + final String? heroTag; + const LearnMoreScreen({Key? key, required this.eventId, this.initialEvent, this.heroTag}) : super(key: key); @override State createState() => _LearnMoreScreenState(); @@ -301,7 +302,7 @@ class _LearnMoreScreenState extends State with SingleTickerProv final mediaQuery = MediaQuery.of(context); final screenWidth = mediaQuery.size.width; final screenHeight = mediaQuery.size.height; - final imageHeight = screenHeight * 0.45; + final imageHeight = screenHeight * 0.52; final topPadding = mediaQuery.padding.top; // ── DESKTOP layout ────────────────────────────────────────────────── @@ -891,12 +892,12 @@ class _LearnMoreScreenState extends State with SingleTickerProv // ---- Foreground image with rounded corners ---- if (images.isNotEmpty) Positioned( - top: topPad + 56, // below the icon row + top: topPad + 70, // safely below the icon row left: 20, right: 20, - bottom: 16, + bottom: 40, // clear from the bottom card's -28 overlap child: Hero( - tag: 'event-hero-${widget.eventId}', + tag: widget.heroTag ?? 'event-hero-${widget.eventId}', child: ClipRRect( borderRadius: BorderRadius.circular(20), child: PageView.builder( @@ -926,10 +927,10 @@ class _LearnMoreScreenState extends State with SingleTickerProv // ---- No-image placeholder ---- if (images.isEmpty) Positioned( - top: topPad + 56, + top: topPad + 70, left: 20, right: 20, - bottom: 16, + bottom: 40, child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 706e626..a0af62e 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -33,7 +33,7 @@ class _LoginScreenState extends State { bool _obscurePassword = true; bool _rememberMe = false; - late VideoPlayerController _videoController; + VideoPlayerController? _videoController; bool _videoInitialized = false; // Glassmorphism color palette @@ -53,17 +53,21 @@ class _LoginScreenState extends State { } Future _initVideo() async { - _videoController = VideoPlayerController.asset('assets/login-bg.mp4'); - await _videoController.initialize(); - _videoController.setLooping(true); - _videoController.setVolume(0); - _videoController.play(); - if (mounted) setState(() => _videoInitialized = true); + try { + _videoController = VideoPlayerController.asset('assets/login-bg.mp4'); + await _videoController!.initialize(); + _videoController!.setLooping(true); + _videoController!.setVolume(0); + _videoController!.play(); + if (mounted) setState(() => _videoInitialized = true); + } catch (_) { + // Video asset not available — skip background video + } } @override void dispose() { - _videoController.dispose(); + _videoController?.dispose(); _emailCtrl.dispose(); _passCtrl.dispose(); _emailFocus.dispose(); @@ -240,14 +244,14 @@ class _LoginScreenState extends State { body: Stack( children: [ // Video background - if (_videoInitialized) + if (_videoInitialized && _videoController != null) Positioned.fill( child: FittedBox( fit: BoxFit.cover, child: SizedBox( - width: _videoController.value.size.width, - height: _videoController.value.size.height, - child: VideoPlayer(_videoController), + width: _videoController!.value.size.width, + height: _videoController!.value.size.height, + child: VideoPlayer(_videoController!), ), ), ), diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index f2ed40c..7fa7861 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -57,6 +57,7 @@ class _SearchScreenState extends State { List<_LocationItem> _searchResults = []; bool _showSearchResults = false; bool _loadingLocation = false; + bool _isSearching = false; @override void initState() { @@ -124,14 +125,48 @@ class _SearchScreenState extends State { Navigator.of(context).pop(result); } - void _selectAndClose(String location) { + Future _selectAndClose(String location) async { // Looks up pincode + coordinates from the database for the given city name. final match = _locationDb.cast<_LocationItem?>().firstWhere( - (loc) => loc!.city.toLowerCase() == location.toLowerCase() || - loc.displayTitle.toLowerCase() == location.toLowerCase(), + (loc) => loc != null && (loc.city.toLowerCase() == location.toLowerCase() || + loc.displayTitle.toLowerCase() == location.toLowerCase()), orElse: () => null, ); - _selectWithPincode(location, pincode: match?.pincode, lat: match?.lat, lng: match?.lng); + + if (match != null) { + _selectWithPincode(location, pincode: match.pincode, lat: match.lat, lng: match.lng); + return; + } + + // Fallback: Geocode the location name + setState(() => _isSearching = true); + try { + final placemarksByAddress = await locationFromAddress(location); + if (placemarksByAddress.isNotEmpty) { + final loc = placemarksByAddress.first; + final placemarks = await placemarkFromCoordinates(loc.latitude, loc.longitude); + String label = location; + String? pincode; + if (placemarks.isNotEmpty) { + final p = placemarks.first; + final parts = []; + if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!); + if ((p.subAdministrativeArea ?? '').isNotEmpty && p.subAdministrativeArea != p.locality) parts.add(p.subAdministrativeArea!); + if (parts.isNotEmpty) label = parts.join(', '); + pincode = p.postalCode; + } + if (mounted) { + _selectWithPincode(label, pincode: pincode ?? 'all', lat: loc.latitude, lng: loc.longitude); + } + return; + } + } catch (_) { + // Geocoding failed, proceed with just the text label + } finally { + if (mounted) setState(() => _isSearching = false); + } + + _selectWithPincode(location); } Future _useCurrentLocation() async { @@ -263,6 +298,7 @@ class _SearchScreenState extends State { Expanded( child: TextField( controller: _ctrl, + enabled: !_isSearching, decoration: const InputDecoration( hintText: 'Search city, area or locality', hintStyle: TextStyle(color: Color(0xFF9CA3AF)), @@ -282,7 +318,12 @@ class _SearchScreenState extends State { }, ), ), - if (_ctrl.text.isNotEmpty) + if (_isSearching) + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)), + ) + else if (_ctrl.text.isNotEmpty) IconButton( icon: const Icon(Icons.clear, size: 20), onPressed: () { diff --git a/pubspec.lock b/pubspec.lock index b2e9e15..f31d029 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -588,26 +588,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -873,10 +873,10 @@ packages: dependency: transitive description: name: sqflite_android - sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 url: "https://pub.dev" source: hosted - version: "2.4.2+3" + version: "2.4.2+2" sqflite_common: dependency: transitive description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.6" typed_data: dependency: transitive description: @@ -1089,10 +1089,10 @@ packages: dependency: "direct main" description: name: video_player - sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" url: "https://pub.dev" source: hosted - version: "2.11.1" + version: "2.10.1" video_player_android: dependency: transitive description: @@ -1105,10 +1105,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e + sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 url: "https://pub.dev" source: hosted - version: "2.9.4" + version: "2.8.9" video_player_platform_interface: dependency: transitive description: @@ -1174,5 +1174,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0"