feat: implement leaderboard and achievements tabs in contribute screen
- Add Leaderboard tab with top 3 podium, time/district filters, and ranking table - Add Achievements tab with badge grid (locked/unlocked with progress bars) - Implement AnimatedSwitcher for smooth tab content transitions - Add demo data for leaderboard users and achievement badges - Responsive layout for mobile and desktop views Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -595,9 +595,505 @@ class _ContributeScreenState extends State<ContributeScreen> with SingleTickerPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Leaderboard state ──
|
||||||
|
int _leaderboardTimeFilter = 0; // 0 = All Time, 1 = This Month
|
||||||
|
int _leaderboardDistrictFilter = 0; // index into _districts
|
||||||
|
|
||||||
|
static const List<String> _districts = [
|
||||||
|
'Overall Kerala',
|
||||||
|
'Thiruvananthapuram',
|
||||||
|
'Kollam',
|
||||||
|
'Pathanamthitta',
|
||||||
|
'Alappuzha',
|
||||||
|
'Kottayam',
|
||||||
|
'Idukki',
|
||||||
|
'Ernakulam',
|
||||||
|
'Thrissur',
|
||||||
|
'Palakkad',
|
||||||
|
'Malappuram',
|
||||||
|
'Kozhikode',
|
||||||
|
'Wayanad',
|
||||||
|
'Kannur',
|
||||||
|
'Kasaragod',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Demo leaderboard data
|
||||||
|
static const List<Map<String, dynamic>> _leaderboardData = [
|
||||||
|
{'name': 'Annette Black', 'points': 4628, 'level': 'Legend', 'events': 156},
|
||||||
|
{'name': 'Jerome Bell', 'points': 4518, 'level': 'Legend', 'events': 152},
|
||||||
|
{'name': 'Theresa Webb', 'points': 4368, 'level': 'Legend', 'events': 148},
|
||||||
|
{'name': 'Courtney Henry', 'points': 4279, 'level': 'Legend', 'events': 149},
|
||||||
|
{'name': 'Cameron Williamson', 'points': 4150, 'level': 'Legend', 'events': 144},
|
||||||
|
{'name': 'Brooklyn Simmons', 'points': 4033, 'level': 'Legend', 'events': 139},
|
||||||
|
{'name': 'Leslie Alexander', 'points': 3914, 'level': 'Champion', 'events': 134},
|
||||||
|
{'name': 'Jenny Wilson', 'points': 3783, 'level': 'Champion', 'events': 132},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Demo achievements data
|
||||||
|
static const List<Map<String, dynamic>> _achievementsData = [
|
||||||
|
{'name': 'Newcomer', 'subtitle': 'First Event Posted', 'icon': Icons.star_outline, 'color': 0xFFDBEAFE, 'iconColor': 0xFF3B82F6, 'unlocked': true},
|
||||||
|
{'name': 'Contributor', 'subtitle': '10th Event Posted within a month', 'icon': Icons.workspace_premium, 'color': 0xFFFEF9C3, 'iconColor': 0xFFEAB308, 'unlocked': true},
|
||||||
|
{'name': 'On Fire!', 'subtitle': '3 Day Streak of logging in', 'icon': Icons.local_fire_department_outlined, 'color': 0xFFFFEDD5, 'iconColor': 0xFFF97316, 'unlocked': true, 'progress': 0.67},
|
||||||
|
{'name': 'Verified', 'subtitle': 'Identity Verified successfully', 'icon': Icons.verified_outlined, 'color': 0xFFDCFCE7, 'iconColor': 0xFF22C55E, 'unlocked': true},
|
||||||
|
{'name': 'Quality', 'subtitle': '5 Star Event Rating received', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false},
|
||||||
|
{'name': 'Community', 'subtitle': 'Referred 5 Friends to the platform', 'icon': Icons.people_outline, 'color': 0xFFE0E7FF, 'iconColor': 0xFF6366F1, 'unlocked': true, 'progress': 0.40},
|
||||||
|
{'name': 'Expert', 'subtitle': 'Level 10 Reached in 3 months', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false},
|
||||||
|
{'name': 'Precision', 'subtitle': '100% Data Accuracy on all events', 'icon': Icons.lock_outline, 'color': 0xFFF1F5F9, 'iconColor': 0xFF94A3B8, 'unlocked': false},
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLeaderboard(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.only(top: 18),
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 16, 0, 28),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// ── Time filter: All Time / This Month ──
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
_buildTimeToggle(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// ── District filter chips (horizontal scroll) ──
|
||||||
|
SizedBox(
|
||||||
|
height: 38,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: _districts.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final active = i == _leaderboardDistrictFilter;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _leaderboardDistrictFilter = i),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? _primary : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: active ? _primary : Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_districts[i],
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? Colors.white : Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ── Podium (top 3) ──
|
||||||
|
_buildPodium(theme),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ── Leaderboard table (rank 4+) ──
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 32, child: Text('RANK', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Text('USER', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))),
|
||||||
|
SizedBox(width: 60, child: Text('POINTS', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(width: 68, child: Text('LEVEL', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey.shade500), textAlign: TextAlign.center)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(width: 32, child: Text('EVENTS', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Colors.grey.shade500), textAlign: TextAlign.center)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Rows (rank 4+)
|
||||||
|
...List.generate(
|
||||||
|
_leaderboardData.length - 3,
|
||||||
|
(i) => _buildLeaderboardRow(theme, i + 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeToggle(ThemeData theme) {
|
||||||
|
final labels = ['All Time', 'This Month'];
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: List.generate(labels.length, (i) {
|
||||||
|
final active = i == _leaderboardTimeFilter;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _leaderboardTimeFilter = i),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? _primary : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
labels[i],
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? Colors.white : Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPodium(ThemeData theme) {
|
||||||
|
if (_leaderboardData.length < 3) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final first = _leaderboardData[0]; // #1
|
||||||
|
final second = _leaderboardData[1]; // #2
|
||||||
|
final third = _leaderboardData[2]; // #3
|
||||||
|
|
||||||
|
// Podium colors
|
||||||
|
const goldColor = Color(0xFFFBBF24);
|
||||||
|
const silverColor = Color(0xFFD1D5DB);
|
||||||
|
const bronzeColor = Color(0xFFF97316);
|
||||||
|
|
||||||
|
Widget podiumSlot(Map<String, dynamic> user, int rank, Color pillarColor, double pillarHeight, Color badgeColor) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Avatar with rank badge
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: rank == 1 ? 32 : 26,
|
||||||
|
backgroundColor: badgeColor.withOpacity(0.2),
|
||||||
|
child: Icon(Icons.person, size: rank == 1 ? 32 : 26, color: badgeColor),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
child: Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: badgeColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text('$rank', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
user['name'] as String,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_formatNumber(user['points'] as int)} pts',
|
||||||
|
style: TextStyle(color: _primary, fontWeight: FontWeight.w600, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// Pillar
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: pillarHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: pillarColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// #2 – left
|
||||||
|
Expanded(child: podiumSlot(second, 2, silverColor, 70, Colors.grey.shade500)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// #1 – center (tallest)
|
||||||
|
Expanded(child: podiumSlot(first, 1, goldColor, 100, goldColor)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// #3 – right
|
||||||
|
Expanded(child: podiumSlot(third, 3, bronzeColor, 55, bronzeColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLeaderboardRow(ThemeData theme, int index) {
|
||||||
|
final user = _leaderboardData[index];
|
||||||
|
final rank = index + 1;
|
||||||
|
final level = user['level'] as String;
|
||||||
|
|
||||||
|
Color levelColor;
|
||||||
|
Color levelBg;
|
||||||
|
switch (level) {
|
||||||
|
case 'Legend':
|
||||||
|
levelColor = const Color(0xFF16A34A);
|
||||||
|
levelBg = const Color(0xFFDCFCE7);
|
||||||
|
break;
|
||||||
|
case 'Champion':
|
||||||
|
levelColor = const Color(0xFF9333EA);
|
||||||
|
levelBg = const Color(0xFFF3E8FF);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
levelColor = Colors.grey;
|
||||||
|
levelBg = Colors.grey.shade100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.grey.shade100)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 32, child: Text('$rank', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: Colors.grey.shade700))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 18,
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
child: Icon(Icons.person, size: 20, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
user['name'] as String,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 60,
|
||||||
|
child: Text(
|
||||||
|
'${_formatNumber(user['points'] as int)} pts',
|
||||||
|
style: TextStyle(color: _primary, fontWeight: FontWeight.w600, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 68,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: levelBg,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(level, style: TextStyle(color: levelColor, fontWeight: FontWeight.w600, fontSize: 11)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 32,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today, size: 10, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text('${user['events']}', style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatNumber(int n) {
|
||||||
|
if (n >= 1000) {
|
||||||
|
return '${(n / 1000).toStringAsFixed(n % 1000 == 0 ? 0 : 0)},${(n % 1000).toString().padLeft(3, '0')}';
|
||||||
|
}
|
||||||
|
return '$n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Achievements Tab ──
|
||||||
|
|
||||||
|
Widget _buildAchievements(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.only(top: 18),
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 28),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Your Badges',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Badge grid
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.05,
|
||||||
|
),
|
||||||
|
itemCount: _achievementsData.length,
|
||||||
|
itemBuilder: (context, i) => _buildBadgeCard(theme, _achievementsData[i]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBadgeCard(ThemeData theme, Map<String, dynamic> badge) {
|
||||||
|
final isUnlocked = badge['unlocked'] as bool;
|
||||||
|
final progress = badge['progress'] as double?;
|
||||||
|
final badgeColor = Color(badge['color'] as int);
|
||||||
|
final iconColor = Color(badge['iconColor'] as int);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey.shade100),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 8, offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Icon circle
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: badgeColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
badge['icon'] as IconData,
|
||||||
|
color: iconColor,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// Name + lock indicator
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
badge['name'] as String,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 14,
|
||||||
|
color: isUnlocked ? Colors.black87 : Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isUnlocked)
|
||||||
|
Icon(Icons.lock_outline, size: 14, color: Colors.grey.shade400),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
badge['subtitle'] as String,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: isUnlocked ? Colors.grey.shade600 : Colors.grey.shade400,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Progress bar if applicable
|
||||||
|
if (progress != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: progress),
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
builder: (_, val, __) => LinearProgressIndicator(
|
||||||
|
value: val,
|
||||||
|
minHeight: 5,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(_primary),
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
'${(progress * 100).toInt()}%',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade500, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// The whole screen is scrollable — header is part of the normal scroll (not floating).
|
// Switch content based on active tab
|
||||||
|
Widget tabContent;
|
||||||
|
switch (_activeTab) {
|
||||||
|
case 1:
|
||||||
|
tabContent = _buildLeaderboard(context);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
tabContent = _buildAchievements(context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tabContent = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: _buildForm(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -607,9 +1103,12 @@ class _ContributeScreenState extends State<ContributeScreen> with SingleTickerPr
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(context),
|
_buildHeader(context),
|
||||||
Padding(
|
AnimatedSwitcher(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: _buildForm(context),
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey<int>(_activeTab),
|
||||||
|
child: tabContent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user