P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
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.post(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.post(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.post(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.post(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),
|
|
];
|
|
}
|