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:
2026-04-04 16:51:30 +05:30
parent 8955febd00
commit e365361451
12 changed files with 715 additions and 250 deletions

View File

@@ -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),
),
);
}
}