- Wrap Top Events skeleton Row in SingleChildScrollView to fix 225px RenderFlex overflow when 3x 200px skeletons exceed container width - Fix gamification service using POST for GET endpoints: dashboard, leaderboard, and shop/items all use router.get() on the Node.js server - CORS: add http://localhost:8080 to CORS_ALLOWED_ORIGINS (applied live to eventify-django container + local settings.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
8.6 KiB
Dart
195 lines
8.6 KiB
Dart
// lib/features/gamification/services/gamification_service.dart
|
|
//
|
|
// Real API service for the Contributor / Gamification module.
|
|
// Calls the Node.js gamification server at app.eventifyplus.com.
|
|
|
|
import '../../../core/api/api_client.dart';
|
|
import '../../../core/api/api_endpoints.dart';
|
|
import '../../../core/storage/token_storage.dart';
|
|
import '../models/gamification_models.dart';
|
|
|
|
class GamificationService {
|
|
final ApiClient _api = ApiClient();
|
|
|
|
/// Helper: get current user's email for API calls.
|
|
Future<String> _getUserEmail() async {
|
|
final email = await TokenStorage.getUsername();
|
|
return email ?? '';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dashboard (profile + submissions)
|
|
// GET /v1/gamification/dashboard?user_id={email}
|
|
// ---------------------------------------------------------------------------
|
|
Future<DashboardResponse> getDashboard() async {
|
|
final email = await _getUserEmail();
|
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$email';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
|
final rawSubs = res['submissions'] as List? ?? [];
|
|
final rawAchievements = res['achievements'] as List? ?? [];
|
|
|
|
final submissions = rawSubs
|
|
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
|
.toList();
|
|
|
|
final achievements = rawAchievements
|
|
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
|
.toList();
|
|
|
|
return DashboardResponse(
|
|
profile: UserGamificationProfile.fromJson(profileJson),
|
|
submissions: submissions,
|
|
achievements: achievements,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public contributor profile (any user by userId / email)
|
|
// GET /v1/gamification/dashboard?user_id={userId}
|
|
// ---------------------------------------------------------------------------
|
|
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$userId';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
|
final rawSubs = res['submissions'] as List? ?? [];
|
|
final rawAchievements = res['achievements'] as List? ?? [];
|
|
|
|
final submissions = rawSubs
|
|
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
|
.toList();
|
|
|
|
final achievements = rawAchievements
|
|
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
|
.toList();
|
|
|
|
return DashboardResponse(
|
|
profile: UserGamificationProfile.fromJson(profileJson),
|
|
submissions: submissions,
|
|
achievements: achievements,
|
|
);
|
|
}
|
|
|
|
/// Convenience — returns just the profile (backward-compatible with provider).
|
|
Future<UserGamificationProfile> getProfile() async {
|
|
final dashboard = await getDashboard();
|
|
return dashboard.profile;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Leaderboard
|
|
// GET /v1/gamification/leaderboard?period=all|month&district=X&user_id=Y&limit=50
|
|
// ---------------------------------------------------------------------------
|
|
Future<LeaderboardResponse> getLeaderboard({
|
|
required String district,
|
|
required String timePeriod,
|
|
}) async {
|
|
final email = await _getUserEmail();
|
|
|
|
// Map Flutter filter values to API params
|
|
final period = timePeriod == 'this_month' ? 'month' : 'all';
|
|
|
|
final params = <String, String>{
|
|
'period': period,
|
|
'user_id': email,
|
|
'limit': '50',
|
|
};
|
|
if (district != 'Overall Kerala') {
|
|
params['district'] = district;
|
|
}
|
|
|
|
final query = Uri(queryParameters: params).query;
|
|
final url = '${ApiEndpoints.leaderboard}?$query';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final rawList = res['leaderboard'] as List? ?? [];
|
|
final entries = rawList
|
|
.map((e) => LeaderboardEntry.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
.toList();
|
|
|
|
CurrentUserStats? currentUser;
|
|
if (res['currentUser'] != null && res['currentUser'] is Map) {
|
|
currentUser = CurrentUserStats.fromJson(
|
|
Map<String, dynamic>.from(res['currentUser'] as Map),
|
|
);
|
|
}
|
|
|
|
return LeaderboardResponse(
|
|
entries: entries,
|
|
currentUser: currentUser,
|
|
totalParticipants: (res['totalParticipants'] as num?)?.toInt() ?? entries.length,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shop Items
|
|
// GET /v1/shop/items
|
|
// ---------------------------------------------------------------------------
|
|
Future<List<ShopItem>> getShopItems() async {
|
|
final res = await _api.get(ApiEndpoints.shopItems, requiresAuth: false);
|
|
final rawItems = res['items'] as List? ?? [];
|
|
return rawItems
|
|
.map((e) => ShopItem.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
.toList();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Redeem Item
|
|
// POST /v1/shop/redeem body: { user_id, item_id }
|
|
// ---------------------------------------------------------------------------
|
|
Future<RedemptionRecord> redeemItem(String itemId) async {
|
|
final email = await _getUserEmail();
|
|
final res = await _api.post(
|
|
ApiEndpoints.shopRedeem,
|
|
body: {'user_id': email, 'item_id': itemId},
|
|
requiresAuth: false,
|
|
);
|
|
final voucher = res['voucher'] as Map<String, dynamic>? ?? res;
|
|
return RedemptionRecord.fromJson(Map<String, dynamic>.from(voucher));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Submit Contribution
|
|
// POST /v1/gamification/submit-event body: event data
|
|
// ---------------------------------------------------------------------------
|
|
Future<void> submitContribution(Map<String, dynamic> data) async {
|
|
final email = await _getUserEmail();
|
|
final body = <String, dynamic>{'user_id': email, ...data};
|
|
await _api.post(
|
|
ApiEndpoints.contributeSubmit,
|
|
body: body,
|
|
requiresAuth: false,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Achievements — sourced from dashboard API `achievements` array.
|
|
// Falls back to default badges if API doesn't return achievements yet.
|
|
// ---------------------------------------------------------------------------
|
|
Future<List<AchievementBadge>> getAchievements() async {
|
|
try {
|
|
final dashboard = await getDashboard();
|
|
if (dashboard.achievements.isNotEmpty) return dashboard.achievements;
|
|
} catch (_) {
|
|
// Fall through to defaults
|
|
}
|
|
return _defaultBadges;
|
|
}
|
|
|
|
static const _defaultBadges = [
|
|
AchievementBadge(id: 'badge-01', title: 'First Step', description: 'Submit your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Submit 5 events.', iconName: 'trending_up', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Submit 15 events.', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Reach top 10 on leaderboard.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Upload 10 event images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first contributors.', iconName: 'rocket_launch', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-07', title: 'Community Star', description: 'Get 10 events approved.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-08', title: 'Event Hunter', description: 'Submit 25 events.', iconName: 'event_hunter', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-09', title: 'District Champion', description: 'Rank #1 in your district.', iconName: 'location_on', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-10', title: 'Platinum Achiever', description: 'Earn 1500 EP.', iconName: 'diamond', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-11', title: 'Diamond Legend', description: 'Earn 5000 EP.', iconName: 'workspace_premium', isUnlocked: false, progress: 0.0),
|
|
];
|
|
}
|