/** * Gamification Queries - Drizzle ORM */ import { db } from "@/db"; import { trophies, users, issues } from "@/db/schema"; import { eq, desc, and, count, not, sql } from "drizzle-orm"; import { streakCache, calendarCache } from "@/lib/cache"; // ============================================================================= // Badges / Trophies // ============================================================================= export async function getUserBadges(username: string) { // Case-insensitive lookup - GitHub usernames are case-insensitive const user = await db.select().from(users).where(sql`LOWER(${users.username}) = LOWER(${username})`).limit(1); if (!user[0]) return []; return db.select() .from(trophies) .where(eq(trophies.userId, user[0].id)) .orderBy(desc(trophies.awardedAt)); } // ============================================================================= // Streak // ============================================================================= export async function getUserStreak(username: string) { const cacheKey = `streak:${username}`; // Check cache first const cached = streakCache.get(cacheKey); if (cached) { return cached; } const user = await db.select().from(users).where(sql`LOWER(${users.username}) = LOWER(${username})`).limit(1); if (!user[0]) { const emptyResult = { current_streak: 0, longest_streak: 0, is_active: false, total_contribution_days: 0 }; streakCache.set(cacheKey, emptyResult); return emptyResult; } // Get all activity dates (from issues created by this user) const activity = await db.select({ createdAt: issues.createdAt }) .from(issues) .where(eq(issues.authorName, username)) .orderBy(desc(issues.createdAt)); if (activity.length === 0) { const emptyResult = { current_streak: 0, longest_streak: 0, is_active: false, total_contribution_days: 0 }; streakCache.set(cacheKey, emptyResult); return emptyResult; } // Get unique dates (YYYY-MM-DD format) const activityDates = [...new Set( activity .map(a => a.createdAt?.substring(0, 10)) .filter(Boolean) )].sort().reverse(); // Most recent first if (activityDates.length === 0) { const emptyResult = { current_streak: 0, longest_streak: 0, is_active: false, total_contribution_days: 0 }; streakCache.set(cacheKey, emptyResult); return emptyResult; } const today = new Date().toISOString().substring(0, 10); const yesterday = new Date(Date.now() - 86400000).toISOString().substring(0, 10); // Calculate current streak let currentStreak = 0; let isActive = false; // Check if contributed today or yesterday if (activityDates[0] === today) { isActive = true; currentStreak = 1; } else if (activityDates[0] === yesterday) { isActive = false; // Need to contribute today to keep streak currentStreak = 1; } else { // Streak is broken const brokenResult = { current_streak: 0, longest_streak: calculateLongestStreak(activityDates), is_active: false, total_contribution_days: activityDates.length }; streakCache.set(cacheKey, brokenResult); return brokenResult; } // Count consecutive days for (let i = 1; i < activityDates.length; i++) { const current = new Date(activityDates[i - 1]); const prev = new Date(activityDates[i]); const diffDays = Math.floor((current.getTime() - prev.getTime()) / 86400000); if (diffDays === 1) { currentStreak++; } else { break; } } const longestStreak = calculateLongestStreak(activityDates); const result = { current_streak: currentStreak, longest_streak: Math.max(currentStreak, longestStreak), is_active: isActive, total_contribution_days: activityDates.length }; // Cache the result streakCache.set(cacheKey, result); return result; } // Helper function to calculate longest streak function calculateLongestStreak(sortedDates: string[]): number { if (sortedDates.length === 0) return 0; // Sort dates ascending for this calculation const dates = [...sortedDates].sort(); let longest = 1; let current = 1; for (let i = 1; i < dates.length; i++) { const today = new Date(dates[i]); const yesterday = new Date(dates[i - 1]); const diffDays = Math.floor((today.getTime() - yesterday.getTime()) / 86400000); if (diffDays === 1) { current++; longest = Math.max(longest, current); } else if (diffDays > 1) { current = 1; } // If diffDays === 0, same day, skip } return longest; } // ============================================================================= // Clean Calendar Data // ============================================================================= export async function getUserCalendar(username: string, days: number = 365) { // Case-insensitive lookup const user = await db.select().from(users).where(sql`LOWER(${users.username}) = LOWER(${username})`).limit(1); if (!user[0]) return []; // Calculate date range const endDate = new Date(); const startDate = new Date(Date.now() - days * 86400000); const startDateStr = startDate.toISOString().substring(0, 10); // Aggregate issues created by day const activity = await db.select({ date: sql`substr(${issues.createdAt}, 1, 10)`, contributions: count() }) .from(issues) .where(eq(issues.authorName, username)) .groupBy(sql`substr(${issues.createdAt}, 1, 10)`) .orderBy(sql`substr(${issues.createdAt}, 1, 10)`); // Convert to heatmap format return activity.map(a => ({ date: a.date, contributions: Number(a.contributions) })); }