Update default location to Thrissur and remove Whitefield, Bengaluru

This commit is contained in:
Rishad7594
2026-04-07 20:49:40 +05:30
parent 685c6755d8
commit 7bc396bdde
11 changed files with 944 additions and 284 deletions

View File

@@ -121,26 +121,56 @@ class _ContributeScreenState extends State<ContributeScreen>
// ─────────────────────────────────────────────────────────────────────────
// Build
// ─────────────────────────────────────────────────────────────────────────
int _mainTab = 0; // 0: Contribute, 1: Leaderboard, 2: Achievements
@override
@override
Widget build(BuildContext context) {
return Consumer<GamificationProvider>(
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<ContributeScreen>
);
}
// ═══════════════════════════════════════════════════════════════════════════
// 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<ContributeScreen>
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<ContributeScreen>
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<ContributeScreen>
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<ContributeScreen>
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<ContributeScreen>
Widget _dropdown(String value, List<String> items, ValueChanged<String?> onChanged) {
return DropdownButtonFormField<String>(
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,

View File

@@ -93,7 +93,7 @@ class _HomeScreenState extends State<HomeScreen> 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<HomeScreen> 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<HomeScreen> 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<HomeScreen> 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<int>(
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<HomeScreen> 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<HomeScreen> 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(

View File

@@ -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<LearnMoreScreen> createState() => _LearnMoreScreenState();
@@ -301,7 +302,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> 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<LearnMoreScreen> 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<LearnMoreScreen> 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),

View File

@@ -33,7 +33,7 @@ class _LoginScreenState extends State<LoginScreen> {
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<LoginScreen> {
}
Future<void> _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<LoginScreen> {
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!),
),
),
),

View File

@@ -57,6 +57,7 @@ class _SearchScreenState extends State<SearchScreen> {
List<_LocationItem> _searchResults = [];
bool _showSearchResults = false;
bool _loadingLocation = false;
bool _isSearching = false;
@override
void initState() {
@@ -124,14 +125,48 @@ class _SearchScreenState extends State<SearchScreen> {
Navigator.of(context).pop(result);
}
void _selectAndClose(String location) {
Future<void> _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 = <String>[];
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<void> _useCurrentLocation() async {
@@ -263,6 +298,7 @@ class _SearchScreenState extends State<SearchScreen> {
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<SearchScreen> {
},
),
),
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: () {