2014 lines
76 KiB
Dart
2014 lines
76 KiB
Dart
// lib/screens/contribute_screen.dart
|
|
// Contributor Module v3 — matches web at app.eventifyplus.com/contribute
|
|
// 3 tabs: My Events · Submit Event · Reward Shop
|
|
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
import '../core/utils/error_utils.dart';
|
|
import '../features/gamification/models/gamification_models.dart';
|
|
import '../features/gamification/providers/gamification_provider.dart';
|
|
import '../widgets/bouncing_loader.dart';
|
|
import '../core/analytics/posthog_service.dart';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Constants
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
const _tierColors = <ContributorTier, Color>{
|
|
ContributorTier.BRONZE: Color(0xFFCD7F32),
|
|
ContributorTier.SILVER: Color(0xFFA8A9AD),
|
|
ContributorTier.GOLD: Color(0xFFFFD700),
|
|
ContributorTier.PLATINUM: Color(0xFFE5E4E2),
|
|
ContributorTier.DIAMOND: Color(0xFF67E8F9),
|
|
};
|
|
|
|
const _tierIcons = <ContributorTier, IconData>{
|
|
ContributorTier.BRONZE: Icons.shield_outlined,
|
|
ContributorTier.SILVER: Icons.shield_outlined,
|
|
ContributorTier.GOLD: Icons.workspace_premium_outlined,
|
|
ContributorTier.PLATINUM: Icons.diamond_outlined,
|
|
ContributorTier.DIAMOND: Icons.diamond_outlined,
|
|
};
|
|
|
|
const _tierThresholds = [0, 100, 500, 1500, 5000];
|
|
|
|
const _districts = [
|
|
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha',
|
|
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad',
|
|
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other',
|
|
];
|
|
|
|
const _categories_fallback = [
|
|
'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community',
|
|
'Dance', 'Film', 'Business', 'Health', 'Education', 'Other',
|
|
];
|
|
|
|
// Design tokens matching web
|
|
const _blue = Color(0xFF0F45CF);
|
|
const _darkText = Color(0xFF1E293B);
|
|
const _subText = Color(0xFF94A3B8);
|
|
const _border = Color(0xFFE2E8F0);
|
|
const _lightBlueBg = Color(0xFFEFF6FF);
|
|
const _rpOrange = Color(0xFFEA580C);
|
|
const _greenBg = Color(0xFFD1FAE5);
|
|
const _greenText = Color(0xFF065F46);
|
|
const _yellowBg = Color(0xFFFEF3C7);
|
|
const _yellowText = Color(0xFF92400E);
|
|
const _redBg = Color(0xFFFECDD3);
|
|
const _redText = Color(0xFF9F1239);
|
|
const _pageBg = Color(0xFFF8FAFC);
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// ContributeScreen
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
class ContributeScreen extends StatefulWidget {
|
|
const ContributeScreen({Key? key}) : super(key: key);
|
|
@override
|
|
State<ContributeScreen> createState() => _ContributeScreenState();
|
|
}
|
|
|
|
class _ContributeScreenState extends State<ContributeScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
int _activeTab = 1; // default to Submit Event
|
|
|
|
// Form
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _titleCtl = TextEditingController();
|
|
final _descriptionCtl = TextEditingController();
|
|
final _latCtl = TextEditingController();
|
|
final _lngCtl = TextEditingController();
|
|
final _mapsLinkCtl = TextEditingController();
|
|
|
|
DateTime? _selectedDate;
|
|
TimeOfDay? _selectedTime;
|
|
String _selectedCategory = 'Music';
|
|
String _selectedDistrict = _districts.first;
|
|
List<XFile> _images = [];
|
|
bool _submitting = false;
|
|
bool _showSuccess = false;
|
|
bool _useManualCoords = true;
|
|
String? _coordMessage;
|
|
bool _coordSuccess = false;
|
|
|
|
final _picker = ImagePicker();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
PostHogService.instance.screen('Contribute');
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final p = context.read<GamificationProvider>();
|
|
p.loadAll();
|
|
p.loadLeaderboard(); // independent — always fires regardless of loadAll TTL
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleCtl.dispose();
|
|
_descriptionCtl.dispose();
|
|
_latCtl.dispose();
|
|
_lngCtl.dispose();
|
|
_mapsLinkCtl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 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: _blue,
|
|
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(
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 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.isLeaderboardLoading && 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: [
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
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
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildStatsBar(GamificationProvider provider) {
|
|
final profile = provider.profile;
|
|
final tier = profile?.tier ?? ContributorTier.BRONZE;
|
|
final tierColor = _tierColors[tier] ?? const Color(0xFFCD7F32);
|
|
final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
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: 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)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _shareRank(GamificationProvider provider) {
|
|
final profile = provider.profile;
|
|
if (profile == null) return;
|
|
final text = "I'm a ${tierLabel(profile.tier)} contributor on Eventify with ${profile.lifetimeEp} EP! Join the community.";
|
|
Share.share(text);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 2. TIER ROADMAP
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildTierRoadmap(GamificationProvider provider) {
|
|
final currentTier = provider.profile?.tier ?? ContributorTier.BRONZE;
|
|
final tiers = ContributorTier.values;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: Row(
|
|
children: List.generate(tiers.length * 2 - 1, (i) {
|
|
if (i.isOdd) {
|
|
// Connector line
|
|
final tierIdx = i ~/ 2;
|
|
final reached = currentTier.index > tierIdx;
|
|
return Expanded(
|
|
child: Container(
|
|
height: 2,
|
|
color: reached ? _blue : const Color(0xFFE2E8F0),
|
|
),
|
|
);
|
|
}
|
|
final tierIdx = i ~/ 2;
|
|
final tier = tiers[tierIdx];
|
|
final isActive = tier == currentTier;
|
|
final reached = currentTier.index >= tierIdx;
|
|
final tierColor = _tierColors[tier]!;
|
|
final thresholdLabel = _tierThresholds[tierIdx] >= 1000
|
|
? '${(_tierThresholds[tierIdx] / 1000).toStringAsFixed(_tierThresholds[tierIdx] % 1000 == 0 ? 0 : 1)}K'
|
|
: '${_tierThresholds[tierIdx]}';
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: isActive ? _lightBlueBg : (reached ? _blue.withValues(alpha: 0.08) : const Color(0xFFF8FAFC)),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isActive ? _blue : (reached ? _blue.withValues(alpha: 0.3) : _border),
|
|
width: isActive ? 2 : 1,
|
|
),
|
|
),
|
|
child: Icon(
|
|
_tierIcons[tier] ?? Icons.shield_outlined,
|
|
size: 16,
|
|
color: reached ? _blue : _subText,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
tierLabel(tier),
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
|
|
color: isActive ? _blue : _subText,
|
|
),
|
|
),
|
|
Text(
|
|
'$thresholdLabel EP',
|
|
style: TextStyle(fontSize: 8, color: isActive ? _blue : const Color(0xFFCBD5E1)),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 3. TAB BAR WITH ANIMATED GLIDER
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
static const _tabLabels = ['My Events', 'Submit Event', 'Reward Shop'];
|
|
static const _tabIcons = [Icons.list_alt_rounded, Icons.add_circle_outline, Icons.card_giftcard_outlined];
|
|
|
|
Widget _buildTabBar() {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Container(
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final tabWidth = constraints.maxWidth / 3;
|
|
return Stack(
|
|
children: [
|
|
// Animated glider
|
|
AnimatedPositioned(
|
|
duration: const Duration(milliseconds: 350),
|
|
curve: Curves.easeOutBack,
|
|
left: tabWidth * _activeTab + 4,
|
|
top: 4,
|
|
child: Container(
|
|
width: tabWidth > 8 ? tabWidth - 8 : 0,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: _blue,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(color: _blue.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Tab buttons
|
|
Row(
|
|
children: List.generate(3, (i) {
|
|
final isActive = i == _activeTab;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _activeTab = i),
|
|
behavior: HitTestBehavior.opaque,
|
|
child: SizedBox(
|
|
height: 52,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_tabIcons[i],
|
|
size: 16, // Slightly smaller icon
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 4. TAB CONTENT
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildTabContent(GamificationProvider provider) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 250),
|
|
child: _activeTab == 0
|
|
? _buildMyEventsTab(provider)
|
|
: _activeTab == 1
|
|
? _buildSubmitEventTab(provider)
|
|
: _buildRewardShopTab(provider),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// TAB 0: MY EVENTS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildMyEventsTab(GamificationProvider provider) {
|
|
final submissions = provider.submissions;
|
|
|
|
if (submissions.isEmpty) {
|
|
return Center(
|
|
key: const ValueKey('empty'),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 64,
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: _lightBlueBg,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.star_outline_rounded, color: _blue, size: 32),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'No events submitted yet',
|
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: _darkText),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Head over to the Submit tab to earn your first EP!',
|
|
style: TextStyle(fontSize: 13, color: _subText),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 20),
|
|
ElevatedButton(
|
|
onPressed: () => setState(() => _activeTab = 1),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _blue,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
),
|
|
child: const Text('Start Contributing', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
key: const ValueKey('list'),
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: submissions.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
itemBuilder: (ctx, i) => _buildSubmissionCard(submissions[i]),
|
|
);
|
|
}
|
|
|
|
Widget _buildSubmissionCard(SubmissionModel sub) {
|
|
Color statusBg, statusFg;
|
|
String statusLabel;
|
|
switch (sub.status.toUpperCase()) {
|
|
case 'APPROVED':
|
|
statusBg = _greenBg; statusFg = _greenText; statusLabel = 'Approved';
|
|
break;
|
|
case 'REJECTED':
|
|
statusBg = _redBg; statusFg = _redText; statusLabel = 'Rejected';
|
|
break;
|
|
default:
|
|
statusBg = _yellowBg; statusFg = _yellowText; statusLabel = 'Pending';
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _border),
|
|
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2))],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
sub.eventName,
|
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _darkText),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (sub.category.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: _lightBlueBg,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
sub.category,
|
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: _blue),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.calendar_today_outlined, size: 14, color: _subText),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
DateFormat('d MMM yyyy').format(sub.createdAt),
|
|
style: const TextStyle(fontSize: 12, color: _subText),
|
|
),
|
|
if (sub.district != null && sub.district!.isNotEmpty) ...[
|
|
const SizedBox(width: 12),
|
|
Icon(Icons.location_on_outlined, size: 14, color: _subText),
|
|
const SizedBox(width: 4),
|
|
Text(sub.district!, style: const TextStyle(fontSize: 12, color: _subText)),
|
|
],
|
|
const Spacer(),
|
|
// Status badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: statusBg,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(statusLabel, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: statusFg)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
const Text('EP Earned: ', style: TextStyle(fontSize: 12, color: _subText)),
|
|
Text(
|
|
sub.status.toUpperCase() == 'APPROVED' ? '${sub.epAwarded}' : '-',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: sub.status.toUpperCase() == 'APPROVED' ? _blue : _subText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// TAB 1: SUBMIT EVENT
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildSubmitEventTab(GamificationProvider provider) {
|
|
if (_showSuccess) return _buildSuccessState();
|
|
|
|
return SingleChildScrollView(
|
|
key: const ValueKey('submit'),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Submit a New Event',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText),
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'Provide accurate details to maximize your evaluated EP points.',
|
|
style: TextStyle(fontSize: 13, color: _subText),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Event Name
|
|
_inputLabel('Event Name', required: true),
|
|
const SizedBox(height: 6),
|
|
_textField(_titleCtl, 'e.g. Cochin Carnival 2026',
|
|
validator: (v) => (v == null || v.trim().isEmpty) ? 'Event name is required' : null),
|
|
const SizedBox(height: 16),
|
|
|
|
// Category + District row
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_inputLabel('Category', required: true),
|
|
const SizedBox(height: 6),
|
|
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!)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_inputLabel('District', required: true),
|
|
const SizedBox(height: 6),
|
|
_dropdown(_selectedDistrict, _districts, (v) => setState(() => _selectedDistrict = v!)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Date & Time row
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_inputLabel('Date', required: true),
|
|
const SizedBox(height: 6),
|
|
_datePicker(),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_inputLabel('Time'),
|
|
const SizedBox(height: 6),
|
|
_timePicker(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Description
|
|
_inputLabel('Description', required: true, hint: '(Required for higher EP)'),
|
|
const SizedBox(height: 6),
|
|
_textField(_descriptionCtl, 'Include agenda, ticket details, organizer contact, etc...',
|
|
maxLines: 4,
|
|
validator: (v) => (v == null || v.trim().isEmpty) ? 'Description is required' : null),
|
|
const SizedBox(height: 16),
|
|
|
|
// Location Coordinates
|
|
_inputLabel('Location Coordinates'),
|
|
const SizedBox(height: 6),
|
|
_buildCoordinateInput(),
|
|
const SizedBox(height: 16),
|
|
|
|
// Media Upload
|
|
_buildMediaUpload(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Submit button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 48,
|
|
child: ElevatedButton(
|
|
onPressed: _submitting ? null : () => _submitForm(provider),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _blue,
|
|
foregroundColor: Colors.white,
|
|
disabledBackgroundColor: _blue.withValues(alpha: 0.5),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
elevation: 0,
|
|
),
|
|
child: _submitting
|
|
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('Submit for Review', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700)),
|
|
SizedBox(width: 8),
|
|
Icon(Icons.arrow_forward_rounded, size: 18),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Form helpers ──────────────────────────────────────────────────────────
|
|
|
|
Widget _inputLabel(String text, {bool required = false, String? hint}) {
|
|
return Row(
|
|
children: [
|
|
Text(text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _darkText)),
|
|
if (required) const Text(' *', style: TextStyle(color: Color(0xFFEF4444), fontSize: 13)),
|
|
if (hint != null) ...[
|
|
const SizedBox(width: 4),
|
|
Text(hint, style: const TextStyle(fontSize: 11, color: _subText)),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _textField(TextEditingController ctl, String placeholder, {
|
|
int maxLines = 1,
|
|
String? Function(String?)? validator,
|
|
TextInputType? keyboardType,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
}) {
|
|
return TextFormField(
|
|
controller: ctl,
|
|
maxLines: maxLines,
|
|
keyboardType: keyboardType,
|
|
inputFormatters: inputFormatters,
|
|
validator: validator,
|
|
style: const TextStyle(fontSize: 14, color: _darkText),
|
|
decoration: InputDecoration(
|
|
hintText: placeholder,
|
|
hintStyle: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 14),
|
|
filled: true,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)),
|
|
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFEF4444))),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _dropdown(String value, List<String> items, ValueChanged<String?> onChanged) {
|
|
return DropdownButtonFormField<String>(
|
|
value: value,
|
|
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,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _border)),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)),
|
|
),
|
|
dropdownColor: Colors.white,
|
|
style: const TextStyle(fontSize: 14, color: _darkText),
|
|
icon: const Icon(Icons.keyboard_arrow_down_rounded, color: _subText),
|
|
);
|
|
}
|
|
|
|
Widget _datePicker() {
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _selectedDate ?? DateTime.now(),
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime(2030),
|
|
);
|
|
if (picked != null) setState(() => _selectedDate = picked);
|
|
},
|
|
child: Container(
|
|
height: 48,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_selectedDate != null ? DateFormat('d MMM yyyy').format(_selectedDate!) : 'Select date',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: _selectedDate != null ? _darkText : const Color(0xFFCBD5E1),
|
|
),
|
|
),
|
|
),
|
|
const Icon(Icons.calendar_today_outlined, color: _subText, size: 18),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _timePicker() {
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
final picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: _selectedTime ?? TimeOfDay.now(),
|
|
);
|
|
if (picked != null) setState(() => _selectedTime = picked);
|
|
},
|
|
child: Container(
|
|
height: 48,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_selectedTime != null ? _selectedTime!.format(context) : 'Select time',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: _selectedTime != null ? _darkText : const Color(0xFFCBD5E1),
|
|
),
|
|
),
|
|
),
|
|
const Icon(Icons.access_time_outlined, color: _subText, size: 18),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Coordinate input (Manual / Google Maps Link toggle) ──────────────────
|
|
|
|
Widget _buildCoordinateInput() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Toggle tabs
|
|
Row(
|
|
children: [
|
|
_coordToggle('Manual', _useManualCoords, () => setState(() => _useManualCoords = true)),
|
|
const SizedBox(width: 8),
|
|
_coordToggle('Google Maps Link', !_useManualCoords, () => setState(() => _useManualCoords = false)),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
if (_useManualCoords) ...[
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _textField(_latCtl, 'Latitude (e.g. 9.93123)',
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _textField(_lngCtl, 'Longitude (e.g. 76.26730)',
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]),
|
|
),
|
|
],
|
|
),
|
|
] else ...[
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _textField(_mapsLinkCtl, 'Paste Google Maps URL here'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
height: 48,
|
|
child: ElevatedButton(
|
|
onPressed: _extractCoordinates,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _blue,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
),
|
|
child: const Text('Extract', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (_coordMessage != null) ...[
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
_coordSuccess ? Icons.check_circle : Icons.error_outline,
|
|
size: 16,
|
|
color: _coordSuccess ? const Color(0xFF22C55E) : const Color(0xFFEF4444),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: Text(
|
|
_coordMessage!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: _coordSuccess ? const Color(0xFF22C55E) : const Color(0xFFEF4444),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _coordToggle(String label, bool active, VoidCallback onTap) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: active ? _blue : const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: active ? _blue : _border),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: active ? Colors.white : _subText,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _extractCoordinates() {
|
|
final url = _mapsLinkCtl.text.trim();
|
|
if (url.isEmpty) {
|
|
setState(() { _coordMessage = 'Please paste a Google Maps URL'; _coordSuccess = false; });
|
|
return;
|
|
}
|
|
|
|
double? lat, lng;
|
|
|
|
// Pattern 1: @lat,lng
|
|
final atMatch = RegExp(r'@(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url);
|
|
if (atMatch != null) {
|
|
lat = double.tryParse(atMatch.group(1)!);
|
|
lng = double.tryParse(atMatch.group(2)!);
|
|
}
|
|
|
|
// Pattern 2: 3dlat!4dlng
|
|
if (lat == null) {
|
|
final dMatch = RegExp(r'3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)').firstMatch(url);
|
|
if (dMatch != null) {
|
|
lat = double.tryParse(dMatch.group(1)!);
|
|
lng = double.tryParse(dMatch.group(2)!);
|
|
}
|
|
}
|
|
|
|
// Pattern 3: q=lat,lng
|
|
if (lat == null) {
|
|
final qMatch = RegExp(r'[?&]q=(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url);
|
|
if (qMatch != null) {
|
|
lat = double.tryParse(qMatch.group(1)!);
|
|
lng = double.tryParse(qMatch.group(2)!);
|
|
}
|
|
}
|
|
|
|
// Pattern 4: ll=lat,lng
|
|
if (lat == null) {
|
|
final llMatch = RegExp(r'[?&]ll=(-?\d+\.?\d*),(-?\d+\.?\d*)').firstMatch(url);
|
|
if (llMatch != null) {
|
|
lat = double.tryParse(llMatch.group(1)!);
|
|
lng = double.tryParse(llMatch.group(2)!);
|
|
}
|
|
}
|
|
|
|
if (lat != null && lng != null) {
|
|
_latCtl.text = lat.toStringAsFixed(6);
|
|
_lngCtl.text = lng.toStringAsFixed(6);
|
|
setState(() {
|
|
_coordMessage = 'Coordinates extracted: $lat, $lng';
|
|
_coordSuccess = true;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_coordMessage = 'Could not extract coordinates from this URL';
|
|
_coordSuccess = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Media upload ──────────────────────────────────────────────────────────
|
|
|
|
Widget _buildMediaUpload() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_inputLabel('Photos'),
|
|
const Spacer(),
|
|
Text('${_images.length}/5', style: const TextStyle(fontSize: 12, color: _subText)),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'2 EP per image, max 5 EP',
|
|
style: TextStyle(fontSize: 11, color: _subText),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// Upload area
|
|
if (_images.length < 5)
|
|
GestureDetector(
|
|
onTap: _pickImages,
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFCBD5E1), width: 2, strokeAlign: BorderSide.strokeAlignInside),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.cloud_upload_outlined, color: _blue, size: 28),
|
|
const SizedBox(height: 6),
|
|
const Text('Tap to add photos', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _darkText)),
|
|
const SizedBox(height: 2),
|
|
const Text('JPEG, PNG, WebP', style: TextStyle(fontSize: 11, color: _subText)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Thumbnail gallery
|
|
if (_images.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
SizedBox(
|
|
height: 80,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _images.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
itemBuilder: (ctx, i) => _buildImageThumb(i),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildImageThumb(int index) {
|
|
final img = _images[index];
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Container(
|
|
width: 72,
|
|
height: 72,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: kIsWeb
|
|
? const Center(child: Icon(Icons.image, color: _subText, size: 28))
|
|
: Image.file(File(img.path), fit: BoxFit.cover),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: -6,
|
|
right: -6,
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _images.removeAt(index)),
|
|
child: Container(
|
|
width: 22,
|
|
height: 22,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFEF4444),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.close, color: Colors.white, size: 14),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _pickImages() async {
|
|
try {
|
|
final picked = await _picker.pickMultiImage(imageQuality: 80);
|
|
if (picked.isNotEmpty) {
|
|
setState(() {
|
|
final remaining = 5 - _images.length;
|
|
_images.addAll(picked.take(remaining));
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Could not pick images: ${userFriendlyError(e)}')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Success state ─────────────────────────────────────────────────────────
|
|
|
|
Widget _buildSuccessState() {
|
|
return Center(
|
|
key: const ValueKey('success'),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0, end: 1),
|
|
duration: const Duration(milliseconds: 500),
|
|
curve: Curves.elasticOut,
|
|
builder: (_, v, child) => Transform.scale(scale: v, child: child),
|
|
child: Container(
|
|
width: 80,
|
|
height: 80,
|
|
decoration: const BoxDecoration(
|
|
color: _greenBg,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.check_rounded, color: Color(0xFF22C55E), size: 44),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
'Event Submitted!',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Thank you for contributing. It is now pending\nadmin verification. You can earn up to 10 EP!',
|
|
style: TextStyle(fontSize: 13, color: _subText),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Submit handler ────────────────────────────────────────────────────────
|
|
|
|
Future<void> _submitForm(GamificationProvider provider) async {
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
if (_selectedDate == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Please select a date'), backgroundColor: Color(0xFFEF4444)),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => _submitting = true);
|
|
|
|
try {
|
|
final data = <String, dynamic>{
|
|
'title': _titleCtl.text.trim(),
|
|
'category': _selectedCategory,
|
|
'district': _selectedDistrict,
|
|
'date': _selectedDate!.toIso8601String(),
|
|
'time': _selectedTime?.format(context),
|
|
'description': _descriptionCtl.text.trim(),
|
|
'images': _images.map((f) => f.path).toList(),
|
|
};
|
|
|
|
// Add coordinates if provided
|
|
final lat = double.tryParse(_latCtl.text.trim());
|
|
final lng = double.tryParse(_lngCtl.text.trim());
|
|
if (lat != null && lng != null) {
|
|
data['location_lat'] = lat;
|
|
data['location_lng'] = lng;
|
|
}
|
|
|
|
await provider.submitContribution(data);
|
|
|
|
PostHogService.instance.capture('event_contributed', properties: {
|
|
'category': _selectedCategory,
|
|
'district': _selectedDistrict,
|
|
'has_images': _images.isNotEmpty,
|
|
'image_count': _images.length,
|
|
'has_coordinates': lat != null && lng != null,
|
|
});
|
|
|
|
// Show success, then reset
|
|
setState(() { _submitting = false; _showSuccess = true; });
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
if (mounted) {
|
|
_clearForm();
|
|
setState(() { _showSuccess = false; _activeTab = 0; });
|
|
provider.loadAll(force: true);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() => _submitting = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(userFriendlyError(e)), backgroundColor: const Color(0xFFEF4444)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _clearForm() {
|
|
_titleCtl.clear();
|
|
_descriptionCtl.clear();
|
|
_latCtl.clear();
|
|
_lngCtl.clear();
|
|
_mapsLinkCtl.clear();
|
|
_selectedDate = null;
|
|
_selectedTime = null;
|
|
_selectedCategory = _categories_fallback.first;
|
|
_selectedDistrict = _districts.first;
|
|
_images.clear();
|
|
_coordMessage = null;
|
|
_useManualCoords = true;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// TAB 2: REWARD SHOP (Coming Soon)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
Widget _buildRewardShopTab(GamificationProvider provider) {
|
|
final rp = provider.profile?.currentRp ?? 0;
|
|
|
|
return SingleChildScrollView(
|
|
key: const ValueKey('shop'),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 20),
|
|
|
|
// RP balance
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFF7ED),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _rpOrange.withValues(alpha: 0.3)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.card_giftcard, color: _rpOrange, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'$rp RP',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: _rpOrange),
|
|
),
|
|
const SizedBox(width: 6),
|
|
const Text('available', style: TextStyle(fontSize: 12, color: _subText)),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Pulsing icon
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.95, end: 1.05),
|
|
duration: const Duration(seconds: 2),
|
|
curve: Curves.easeInOut,
|
|
builder: (_, v, child) => Transform.scale(scale: v, child: child),
|
|
onEnd: () {}, // AnimationBuilder loops via repeat — handled below
|
|
child: Container(
|
|
width: 80,
|
|
height: 80,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [_blue.withValues(alpha: 0.15), _blue.withValues(alpha: 0.05)],
|
|
),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.card_giftcard_rounded, color: _blue, size: 36),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _lightBlueBg,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: _blue.withValues(alpha: 0.2)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(color: _blue, shape: BoxShape.circle),
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text('Coming Soon', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: _blue)),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
const Text(
|
|
"We're Stocking the Shelves",
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: _darkText),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Redeem your RP for event vouchers, VIP passes, exclusive merch, and more. Keep contributing to build your balance!',
|
|
style: TextStyle(fontSize: 13, color: _subText, height: 1.5),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
// Ghost teaser cards
|
|
..._buildGhostCards(),
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildGhostCards() {
|
|
const items = [
|
|
{'name': 'Event Voucher', 'rp': '500', 'icon': Icons.confirmation_number_outlined},
|
|
{'name': 'VIP Access Pass', 'rp': '1,200', 'icon': Icons.verified_outlined},
|
|
{'name': 'Exclusive Merch', 'rp': '2,000', 'icon': Icons.shopping_bag_outlined},
|
|
];
|
|
|
|
return items.map((item) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Opacity(
|
|
opacity: 0.45,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: _border),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: _lightBlueBg,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(item['icon'] as IconData, color: _blue, size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item['name'] as String,
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: _darkText),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'${item['rp']} RP',
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _rpOrange),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.lock_outline_rounded, color: _subText, size: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
}
|