fix: leaderboard empty on first open — decouple from loadAll()
- Add isLeaderboardLoading flag separate from isLoading - Add loadLeaderboard() method that fires independently of loadAll TTL - Remove leaderboard from loadAll() Future.wait (failures in dashboard/shop no longer silently zero-out leaderboard data) - setDistrict / setTimePeriod now use isLeaderboardLoading - contribute_screen calls loadLeaderboard() alongside loadAll() on mount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month'
|
String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month'
|
||||||
|
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
bool isLeaderboardLoading = false;
|
||||||
String? error;
|
String? error;
|
||||||
|
|
||||||
// TTL guard — prevents redundant API calls from multiple screens
|
// TTL guard — prevents redundant API calls from multiple screens
|
||||||
@@ -44,7 +45,6 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
_service.getDashboard(),
|
_service.getDashboard(),
|
||||||
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod),
|
|
||||||
_service.getShopItems(),
|
_service.getShopItems(),
|
||||||
_service.getAchievements(),
|
_service.getAchievements(),
|
||||||
]);
|
]);
|
||||||
@@ -53,17 +53,12 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
profile = dashboard.profile;
|
profile = dashboard.profile;
|
||||||
submissions = dashboard.submissions;
|
submissions = dashboard.submissions;
|
||||||
|
|
||||||
final lbResponse = results[1] as LeaderboardResponse;
|
shopItems = results[1] as List<ShopItem>;
|
||||||
leaderboard = lbResponse.entries;
|
|
||||||
currentUserStats = lbResponse.currentUser;
|
|
||||||
totalParticipants = lbResponse.totalParticipants;
|
|
||||||
|
|
||||||
shopItems = results[2] as List<ShopItem>;
|
|
||||||
|
|
||||||
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
||||||
final dashAchievements = dashboard.achievements;
|
final dashAchievements = dashboard.achievements;
|
||||||
final fetchedAchievements = results[3] as List<AchievementBadge>;
|
final fetchedAchievements = results[2] as List<AchievementBadge>;
|
||||||
|
|
||||||
if (dashAchievements.isNotEmpty) {
|
if (dashAchievements.isNotEmpty) {
|
||||||
achievements = dashAchievements;
|
achievements = dashAchievements;
|
||||||
} else if (fetchedAchievements.isNotEmpty) {
|
} else if (fetchedAchievements.isNotEmpty) {
|
||||||
@@ -80,12 +75,35 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Load leaderboard independently (decoupled from loadAll)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
Future<void> loadLeaderboard() async {
|
||||||
|
isLeaderboardLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
final response = await _service.getLeaderboard(
|
||||||
|
district: leaderboardDistrict,
|
||||||
|
timePeriod: leaderboardTimePeriod,
|
||||||
|
);
|
||||||
|
leaderboard = response.entries;
|
||||||
|
currentUserStats = response.currentUser;
|
||||||
|
totalParticipants = response.totalParticipants;
|
||||||
|
} catch (e) {
|
||||||
|
error = userFriendlyError(e);
|
||||||
|
} finally {
|
||||||
|
isLeaderboardLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Change district filter
|
// Change district filter
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Future<void> setDistrict(String district) async {
|
Future<void> setDistrict(String district) async {
|
||||||
if (leaderboardDistrict == district) return;
|
if (leaderboardDistrict == district) return;
|
||||||
leaderboardDistrict = district;
|
leaderboardDistrict = district;
|
||||||
|
isLeaderboardLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
|
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
|
||||||
@@ -94,6 +112,8 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
totalParticipants = response.totalParticipants;
|
totalParticipants = response.totalParticipants;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = userFriendlyError(e);
|
error = userFriendlyError(e);
|
||||||
|
} finally {
|
||||||
|
isLeaderboardLoading = false;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -104,6 +124,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
Future<void> setTimePeriod(String period) async {
|
Future<void> setTimePeriod(String period) async {
|
||||||
if (leaderboardTimePeriod == period) return;
|
if (leaderboardTimePeriod == period) return;
|
||||||
leaderboardTimePeriod = period;
|
leaderboardTimePeriod = period;
|
||||||
|
isLeaderboardLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
|
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
|
||||||
@@ -112,6 +133,8 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
totalParticipants = response.totalParticipants;
|
totalParticipants = response.totalParticipants;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = userFriendlyError(e);
|
error = userFriendlyError(e);
|
||||||
|
} finally {
|
||||||
|
isLeaderboardLoading = false;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
super.initState();
|
super.initState();
|
||||||
PostHogService.instance.screen('Contribute');
|
PostHogService.instance.screen('Contribute');
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<GamificationProvider>().loadAll();
|
final p = context.read<GamificationProvider>();
|
||||||
|
p.loadAll();
|
||||||
|
p.loadLeaderboard(); // independent — always fires regardless of loadAll TTL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +276,7 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Leaderboard List
|
// Leaderboard List
|
||||||
if (provider.isLoading && leaderboard.isEmpty)
|
if (provider.isLeaderboardLoading && leaderboard.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 60),
|
padding: EdgeInsets.symmetric(vertical: 60),
|
||||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
|||||||
Reference in New Issue
Block a user