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 29e326b8fc
commit fe8af7cfe6
12 changed files with 715 additions and 250 deletions

View File

@@ -7,8 +7,12 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:provider/provider.dart';
import '../features/events/services/events_service.dart';
import '../features/events/models/event_models.dart';
import '../features/gamification/providers/gamification_provider.dart';
import '../features/gamification/models/gamification_models.dart';
import '../widgets/skeleton_loader.dart';
import 'learn_more_screen.dart';
import 'settings_screen.dart';
import '../core/app_decoration.dart';
@@ -73,6 +77,11 @@ class _ProfileScreenState extends State<ProfileScreen>
_loadProfile();
_startAnimations();
// Load gamification data for profile EP cards
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.read<GamificationProvider>().loadAll();
});
}
@override
@@ -1588,6 +1597,9 @@ class _ProfileScreenState extends State<ProfileScreen>
),
),
// PROF-003: Gamification stat cards
SliverToBoxAdapter(child: _buildGamificationCards(theme)),
// ── Ongoing Events ──
if (_ongoingEvents.isNotEmpty) ...[
SliverToBoxAdapter(child: sectionTitle('Ongoing Events')),
@@ -1657,4 +1669,54 @@ class _ProfileScreenState extends State<ProfileScreen>
),
);
}
// ─────────────────────────────────────────────────────────────────────────
// PROF-003: Gamification 3-card row (Lifetime EP / Liquid EP / RP)
// ─────────────────────────────────────────────────────────────────────────
Widget _buildGamificationCards(ThemeData theme) {
return Consumer<GamificationProvider>(
builder: (context, gp, _) {
if (gp.isLoading && gp.profile == null) {
return const ProfileStatsSkeleton();
}
if (gp.profile == null) return const SizedBox.shrink();
final p = gp.profile!;
return Padding(
padding: const EdgeInsets.fromLTRB(18, 8, 18, 8),
child: Row(
children: [
_gamStatCard('Lifetime EP', '${p.lifetimeEp}', Icons.star, const Color(0xFFF59E0B), theme),
const SizedBox(width: 10),
_gamStatCard('Liquid EP', '${p.currentEp}', Icons.bolt, const Color(0xFF3B82F6), theme),
const SizedBox(width: 10),
_gamStatCard('Reward Pts', '${p.currentRp}', Icons.redeem, const Color(0xFF10B981), theme),
],
),
);
},
);
}
Widget _gamStatCard(String label, String value, IconData icon, Color color, ThemeData theme) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 22),
const SizedBox(height: 4),
Text(value, style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16, color: theme.textTheme.bodyLarge?.color)),
const SizedBox(height: 2),
Text(label, style: TextStyle(color: theme.hintColor, fontSize: 10), textAlign: TextAlign.center),
],
),
),
);
}
}