feat: Phase 2 — 11 high-priority gaps implemented across home, auth, gamification, profile, and event detail
Phase 2 gaps completed: - HOME-001: Hero slider pause-on-touch (GestureDetector wraps PageView) - HOME-003: Calendar bottom sheet with TableCalendar (replaces custom dialog) - AUTH-004: District dropdown on signup (14 Kerala districts) - EVT-003: Mobile sticky "Book Now" bar + desktop CTA wired to CheckoutScreen - ACH-001: Real achievements from dashboard API with fallback defaults - GAM-002: 3-card EP row (Lifetime EP / Liquid EP / Reward Points) - GAM-005: Horizontal tier roadmap Bronze→Silver→Gold→Platinum→Diamond - CTR-001: Submission status chips (PENDING/APPROVED/REJECTED) - CTR-002: +EP badge on approved submissions - PROF-003: Gamification cards on profile screen with Consumer<GamificationProvider> - UX-001: Shimmer skeleton loaders (shimmer ^3.0.0) replacing CircularProgressIndicator Already complete (verified, no changes needed): - HOME-002: Category shelves already built in _buildTypeSection() - LDR-002: Podium visualization already built (_buildPodium / _buildDesktopPodium) - BOOK-004: UPI handled natively by Razorpay SDK Deferred: LOC-001/002 (needs Django haversine endpoint) Skipped: AUTH-002 (OTP needs SMS provider decision) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -245,6 +245,27 @@ class _ContributeScreenState extends State<ContributeScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// GAM-002: 3-card EP stat row
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
_epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)),
|
||||
const SizedBox(width: 8),
|
||||
_epStatCard('Liquid EP', '${profile?.currentEp ?? 0}', Icons.bolt, const Color(0xFF3B82F6)),
|
||||
const SizedBox(width: 8),
|
||||
_epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// GAM-005: Tier roadmap
|
||||
_buildTierRoadmap(lifetimeEp, tier),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Vertical tab navigation
|
||||
@@ -1587,12 +1608,82 @@ class _ContributeScreenState extends State<ContributeScreen>
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[500], fontSize: 11),
|
||||
),
|
||||
),
|
||||
|
||||
// CTR-001/002: Your Submissions list
|
||||
if (provider.submissions.isNotEmpty) ...[
|
||||
const SizedBox(height: 28),
|
||||
Text('Your Submissions', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 12),
|
||||
...provider.submissions.map((sub) => _buildSubmissionCard(sub, theme)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmissionCard(SubmissionModel sub, ThemeData theme) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: theme.dividerColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Thumbnail or placeholder
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: sub.images.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(sub.images.first, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.event, color: Colors.grey)),
|
||||
)
|
||||
: const Icon(Icons.event, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(sub.eventName, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 2),
|
||||
Text('${sub.category} · ${sub.district}', style: TextStyle(color: Colors.grey[500], fontSize: 11)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Status chip + EP badge
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
statusChip(sub.status),
|
||||
if (sub.epAwarded > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDBEAFE),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text('+${sub.epAwarded} EP', style: const TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w700, fontSize: 11)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _formCard(List<Widget> children) {
|
||||
return RepaintBoundary(
|
||||
child: Container(
|
||||
@@ -2657,4 +2748,129 @@ class _ContributeScreenState extends State<ContributeScreen>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GAM-002: EP stat card helper (used in left panel + profile)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget _epStatCard(String label, String value, IconData icon, Color color) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label, style: const TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GAM-005: Horizontal tier roadmap (Bronze → Diamond)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget _buildTierRoadmap(int lifetimeEp, ContributorTier currentTier) {
|
||||
const tiers = ContributorTier.values; // BRONZE, SILVER, GOLD, PLATINUM, DIAMOND
|
||||
const thresholds = [0, 100, 500, 1500, 5000];
|
||||
final overallProgress = (lifetimeEp / 5000).clamp(0.0, 1.0);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Tier Roadmap', style: TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: List.generate(tiers.length, (i) {
|
||||
final reached = currentTier.index >= i;
|
||||
final color = _tierColors[tiers[i]] ?? Colors.grey;
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: reached ? color : Colors.white24,
|
||||
border: Border.all(color: reached ? color : Colors.white30, width: 2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tierLabel(tiers[i]),
|
||||
style: TextStyle(color: reached ? Colors.white : Colors.white38, fontSize: 9, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
'${thresholds[i]}',
|
||||
style: TextStyle(color: reached ? Colors.white54 : Colors.white24, fontSize: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: overallProgress,
|
||||
minHeight: 4,
|
||||
backgroundColor: Colors.white12,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(_tierColors[currentTier] ?? Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// CTR-001: Status chip for submissions
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static Widget statusChip(String status) {
|
||||
Color bg;
|
||||
Color fg;
|
||||
switch (status.toUpperCase()) {
|
||||
case 'APPROVED':
|
||||
bg = const Color(0xFFDCFCE7);
|
||||
fg = const Color(0xFF16A34A);
|
||||
break;
|
||||
case 'REJECTED':
|
||||
bg = const Color(0xFFFEE2E2);
|
||||
fg = const Color(0xFFDC2626);
|
||||
break;
|
||||
default: // PENDING
|
||||
bg = const Color(0xFFFEF9C3);
|
||||
fg = const Color(0xFFCA8A04);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status.toUpperCase(),
|
||||
style: TextStyle(color: fg, fontWeight: FontWeight.w700, fontSize: 11),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user