230 lines
5.6 KiB
TypeScript
230 lines
5.6 KiB
TypeScript
/**
|
|
* Badge management and achievement utilities
|
|
*/
|
|
|
|
import {
|
|
Badge,
|
|
UserBadge,
|
|
RequirementType
|
|
} from './types';
|
|
import {
|
|
getAllBadges,
|
|
getUserBadges,
|
|
createUserBadge,
|
|
hasUserEarnedBadge,
|
|
getUserCompletedTasks,
|
|
findBadge
|
|
} from './db/store';
|
|
import { getBalance } from './points';
|
|
|
|
/**
|
|
* Check user's progress towards badge requirements
|
|
*/
|
|
function checkRequirement(
|
|
userId: string,
|
|
requirementType: RequirementType,
|
|
requirementValue: number
|
|
): boolean {
|
|
switch (requirementType) {
|
|
case RequirementType.POINTS_TOTAL: {
|
|
const balance = getBalance(userId);
|
|
return balance >= requirementValue;
|
|
}
|
|
|
|
case RequirementType.TASKS_COMPLETED: {
|
|
const completedTasks = getUserCompletedTasks(userId);
|
|
return completedTasks.length >= requirementValue;
|
|
}
|
|
|
|
case RequirementType.STREAK_DAYS: {
|
|
// For MVP, we'll implement basic streak tracking
|
|
// In production, this would check consecutive daily check-ins
|
|
const completedTasks = getUserCompletedTasks(userId);
|
|
const uniqueDays = new Set(
|
|
completedTasks.map(task =>
|
|
task.completedAt.toISOString().split('T')[0]
|
|
)
|
|
);
|
|
return uniqueDays.size >= requirementValue;
|
|
}
|
|
|
|
case RequirementType.REFERRALS: {
|
|
// This would check referral count from referrals table
|
|
// For MVP, simplified implementation
|
|
return false; // Will be implemented with referral system
|
|
}
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check and award all eligible badges to a user
|
|
*/
|
|
export function checkAndAwardBadges(userId: string): Badge[] {
|
|
const allBadges = getAllBadges();
|
|
const newlyEarnedBadges: Badge[] = [];
|
|
|
|
for (const badge of allBadges) {
|
|
// Skip if user already has this badge
|
|
if (hasUserEarnedBadge(userId, badge.id)) {
|
|
continue;
|
|
}
|
|
|
|
// Check if user meets the requirement
|
|
const meetsRequirement = checkRequirement(
|
|
userId,
|
|
badge.requirementType,
|
|
badge.requirementValue
|
|
);
|
|
|
|
if (meetsRequirement) {
|
|
// Award the badge
|
|
createUserBadge(userId, badge.id);
|
|
newlyEarnedBadges.push(badge);
|
|
}
|
|
}
|
|
|
|
return newlyEarnedBadges;
|
|
}
|
|
|
|
/**
|
|
* Get all badges earned by a user with badge details
|
|
*/
|
|
export function getUserBadgesWithDetails(userId: string): (UserBadge & { badge: Badge })[] {
|
|
const userBadges = getUserBadges(userId);
|
|
|
|
return userBadges.map(ub => {
|
|
const badge = findBadge(ub.badgeId);
|
|
if (!badge) {
|
|
throw new Error(`Badge not found: ${ub.badgeId}`);
|
|
}
|
|
return { ...ub, badge };
|
|
}).sort((a, b) => b.earnedAt.getTime() - a.earnedAt.getTime());
|
|
}
|
|
|
|
/**
|
|
* Get available badges (not yet earned)
|
|
*/
|
|
export function getAvailableBadges(userId: string): Badge[] {
|
|
const allBadges = getAllBadges();
|
|
const earnedBadgeIds = new Set(
|
|
getUserBadges(userId).map(ub => ub.badgeId)
|
|
);
|
|
|
|
return allBadges.filter(badge => !earnedBadgeIds.has(badge.id));
|
|
}
|
|
|
|
/**
|
|
* Get badge progress for a specific badge
|
|
*/
|
|
export function getBadgeProgress(
|
|
userId: string,
|
|
badgeId: string
|
|
): {
|
|
badge: Badge;
|
|
earned: boolean;
|
|
progress: number;
|
|
requirement: number;
|
|
percentage: number;
|
|
} {
|
|
const badge = findBadge(badgeId);
|
|
if (!badge) {
|
|
throw new Error(`Badge not found: ${badgeId}`);
|
|
}
|
|
|
|
const earned = hasUserEarnedBadge(userId, badgeId);
|
|
|
|
let progress = 0;
|
|
const requirement = badge.requirementValue;
|
|
|
|
switch (badge.requirementType) {
|
|
case RequirementType.POINTS_TOTAL:
|
|
progress = getBalance(userId);
|
|
break;
|
|
|
|
case RequirementType.TASKS_COMPLETED:
|
|
progress = getUserCompletedTasks(userId).length;
|
|
break;
|
|
|
|
case RequirementType.STREAK_DAYS: {
|
|
const completedTasks = getUserCompletedTasks(userId);
|
|
const uniqueDays = new Set(
|
|
completedTasks.map(task =>
|
|
task.completedAt.toISOString().split('T')[0]
|
|
)
|
|
);
|
|
progress = uniqueDays.size;
|
|
break;
|
|
}
|
|
|
|
case RequirementType.REFERRALS:
|
|
progress = 0; // Will be implemented with referral system
|
|
break;
|
|
}
|
|
|
|
const percentage = Math.min((progress / requirement) * 100, 100);
|
|
|
|
return {
|
|
badge,
|
|
earned,
|
|
progress,
|
|
requirement,
|
|
percentage
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all badge progress for a user
|
|
*/
|
|
export function getAllBadgeProgress(userId: string): ReturnType<typeof getBadgeProgress>[] {
|
|
const allBadges = getAllBadges();
|
|
return allBadges.map(badge => getBadgeProgress(userId, badge.id));
|
|
}
|
|
|
|
/**
|
|
* Get recently earned badges
|
|
*/
|
|
export function getRecentlyEarnedBadges(
|
|
userId: string,
|
|
limit: number = 5
|
|
): (UserBadge & { badge: Badge })[] {
|
|
const earnedBadges = getUserBadgesWithDetails(userId);
|
|
return earnedBadges.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Get badge statistics for a user
|
|
*/
|
|
export function getBadgeStats(userId: string): {
|
|
totalEarned: number;
|
|
totalAvailable: number;
|
|
completionPercentage: number;
|
|
recentlyEarned: (UserBadge & { badge: Badge })[];
|
|
} {
|
|
const allBadges = getAllBadges();
|
|
const earnedBadges = getUserBadges(userId);
|
|
const recentlyEarned = getRecentlyEarnedBadges(userId, 3);
|
|
|
|
return {
|
|
totalEarned: earnedBadges.length,
|
|
totalAvailable: allBadges.length,
|
|
completionPercentage: (earnedBadges.length / allBadges.length) * 100,
|
|
recentlyEarned
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if badge should be awarded after a specific action
|
|
*/
|
|
export function checkBadgesAfterAction(
|
|
userId: string,
|
|
action: 'task_complete' | 'points_earned' | 'referral' | 'streak'
|
|
): Badge[] {
|
|
// This is a convenience wrapper for checkAndAwardBadges
|
|
// In a real implementation, you might want to optimize by only checking
|
|
// relevant badges based on the action type
|
|
return checkAndAwardBadges(userId);
|
|
}
|