project-standalo-todo-super/app/lib/badges.ts

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);
}