mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { db } from '$lib/server/db';
|
|
import { dailyCompletions } from '$lib/server/db/schema';
|
|
import { desc } from 'drizzle-orm';
|
|
|
|
export const GET: RequestHandler = async ({ url }) => {
|
|
const streakParam = url.searchParams.get('streak');
|
|
const localDate = url.searchParams.get('localDate');
|
|
|
|
if (!streakParam || !localDate) {
|
|
error(400, 'Missing streak or localDate');
|
|
}
|
|
|
|
const targetStreak = parseInt(streakParam, 10);
|
|
if (isNaN(targetStreak) || targetStreak < 1) {
|
|
error(400, 'Invalid streak');
|
|
}
|
|
|
|
// Fetch all completions ordered by anonymous_id and date desc
|
|
// so we can walk each user's history to compute their current streak.
|
|
const rows = await db
|
|
.select({
|
|
anonymousId: dailyCompletions.anonymousId,
|
|
date: dailyCompletions.date,
|
|
})
|
|
.from(dailyCompletions)
|
|
.orderBy(desc(dailyCompletions.date));
|
|
|
|
// Group dates by user
|
|
const byUser = new Map<string, string[]>();
|
|
for (const row of rows) {
|
|
const list = byUser.get(row.anonymousId);
|
|
if (list) {
|
|
list.push(row.date);
|
|
} else {
|
|
byUser.set(row.anonymousId, [row.date]);
|
|
}
|
|
}
|
|
|
|
// Calculate the current streak for each user.
|
|
// Start from today; if the user hasn't played today yet, try yesterday so
|
|
// that streaks aren't zeroed out mid-day before the player has had a chance
|
|
// to complete today's puzzle.
|
|
const yesterday = new Date(`${localDate}T00:00:00`);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const yesterdayStr = yesterday.toLocaleDateString('en-CA');
|
|
|
|
const thirtyDaysAgo = new Date(`${localDate}T00:00:00`);
|
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
const thirtyDaysAgoStr = thirtyDaysAgo.toLocaleDateString('en-CA');
|
|
|
|
// For each user, compute their current streak and whether they've played
|
|
// within the last 30 days. "Eligible players" = active streak OR recent play.
|
|
const userStats: { streak: number; isEligible: boolean }[] = [];
|
|
for (const [, dates] of byUser) {
|
|
// dates are already desc-sorted
|
|
const dateSet = new Set(dates);
|
|
|
|
// Pick the most recent anchor: today if played, otherwise yesterday
|
|
const anchor = dateSet.has(localDate) ? localDate : yesterdayStr;
|
|
|
|
let streak = 0;
|
|
let cursor = new Date(`${anchor}T00:00:00`);
|
|
|
|
while (true) {
|
|
const dateStr = cursor.toLocaleDateString('en-CA');
|
|
if (!dateSet.has(dateStr)) break;
|
|
streak++;
|
|
cursor.setDate(cursor.getDate() - 1);
|
|
}
|
|
|
|
const hasRecentPlay = dates.some((d) => d >= thirtyDaysAgoStr);
|
|
userStats.push({ streak, isEligible: streak >= 1 || hasRecentPlay });
|
|
}
|
|
|
|
const eligiblePlayers = userStats.filter((u) => u.isEligible);
|
|
|
|
if (eligiblePlayers.length === 0) {
|
|
console.log('[streak-percentile] No eligible players found, returning 100th percentile');
|
|
return json({ percentile: 100 });
|
|
}
|
|
|
|
// Percentage of eligible players who have a streak >= targetStreak
|
|
const atOrAbove = eligiblePlayers.filter((u) => u.streak >= targetStreak).length;
|
|
const raw = (atOrAbove / eligiblePlayers.length) * 100;
|
|
const percentile = raw < 1 ? Math.round(raw * 100) / 100 : Math.round(raw);
|
|
|
|
console.log('[streak-percentile]', {
|
|
localDate,
|
|
targetStreak,
|
|
totalUsers: byUser.size,
|
|
totalRows: rows.length,
|
|
eligiblePlayers: eligiblePlayers.length,
|
|
activeStreaks: userStats.filter((u) => u.streak >= 1).length,
|
|
recentPlayers: userStats.filter((u) => u.isEligible).length,
|
|
atOrAbove,
|
|
raw,
|
|
percentile,
|
|
});
|
|
|
|
return json({ percentile });
|
|
};
|