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

@@ -24,10 +24,19 @@ class GamificationProvider extends ChangeNotifier {
bool isLoading = false;
String? error;
// TTL guard — prevents redundant API calls from multiple screens
DateTime? _lastLoadTime;
static const _loadTtl = Duration(minutes: 2);
// ---------------------------------------------------------------------------
// Load everything at once (called when ContributeScreen is mounted)
// Load everything at once (called when ContributeScreen or ProfileScreen mounts)
// ---------------------------------------------------------------------------
Future<void> loadAll() async {
Future<void> loadAll({bool force = false}) async {
// Skip if recently loaded (within 2 minutes) unless forced
if (!force && _lastLoadTime != null && DateTime.now().difference(_lastLoadTime!) < _loadTtl) {
return;
}
isLoading = true;
error = null;
notifyListeners();
@@ -50,7 +59,13 @@ class GamificationProvider extends ChangeNotifier {
totalParticipants = lbResponse.totalParticipants;
shopItems = results[2] as List<ShopItem>;
achievements = results[3] as List<AchievementBadge>;
// Prefer achievements from dashboard API; fall back to getAchievements()
final dashAchievements = dashboard.achievements;
final fetchedAchievements = results[3] as List<AchievementBadge>;
achievements = dashAchievements.isNotEmpty ? dashAchievements : fetchedAchievements;
_lastLoadTime = DateTime.now();
} catch (e) {
error = userFriendlyError(e);
} finally {