mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
Added streak percentage
This commit is contained in:
26
scripts/seed-fake-completions.sh
Executable file
26
scripts/seed-fake-completions.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/zsh
|
||||
|
||||
# Seed the database with 10 fake completions with random anonymous_ids
|
||||
# Useful for testing streak percentile and stats features
|
||||
|
||||
DB_PATH="dev.db"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
NOW=$(date +%s)
|
||||
|
||||
echo "Seeding 10 fake completions for date: $TODAY"
|
||||
|
||||
for i in {1..50}; do
|
||||
ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
||||
ANON_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
||||
GUESS_COUNT=$(( (RANDOM % 6) + 1 )) # 1–6 guesses
|
||||
|
||||
sqlite3 "$DB_PATH" "
|
||||
INSERT OR IGNORE INTO daily_completions (id, anonymous_id, date, guess_count, completed_at)
|
||||
VALUES ('$ID', '$ANON_ID', '$TODAY', $GUESS_COUNT, $NOW);
|
||||
"
|
||||
|
||||
echo " [$i] anon=$ANON_ID guesses=$GUESS_COUNT"
|
||||
done
|
||||
|
||||
TOTAL=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM daily_completions WHERE date = '$TODAY';")
|
||||
echo "✓ Done. Total completions for $TODAY: $TOTAL"
|
||||
@@ -90,7 +90,7 @@
|
||||
return chapterCounts[bookId] || 1;
|
||||
}
|
||||
|
||||
// Generate 6 random chapter options including the correct one
|
||||
// Generate 4 random chapter options including the correct one
|
||||
function generateChapterOptions(
|
||||
correctChapter: number,
|
||||
totalChapters: number,
|
||||
@@ -98,14 +98,14 @@
|
||||
const options = new Set<number>();
|
||||
options.add(correctChapter);
|
||||
|
||||
if (totalChapters >= 6) {
|
||||
while (options.size < 6) {
|
||||
if (totalChapters >= 4) {
|
||||
while (options.size < 4) {
|
||||
const randomChapter =
|
||||
Math.floor(Math.random() * totalChapters) + 1;
|
||||
options.add(randomChapter);
|
||||
}
|
||||
} else {
|
||||
while (options.size < 6) {
|
||||
while (options.size < 4) {
|
||||
const randomChapter = Math.floor(Math.random() * 10) + 1;
|
||||
options.add(randomChapter);
|
||||
}
|
||||
@@ -167,18 +167,18 @@
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="w-full p-6 sm:p-8 bg-linear-to-br from-yellow-100/80 to-amber-200/80 text-gray-800 shadow-md"
|
||||
class="w-full p-3 sm:p-4 bg-linear-to-br from-yellow-100/80 to-amber-200/80 text-gray-800 shadow-md"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="text-xl sm:text-2xl font-bold mb-2">Bonus Challenge</p>
|
||||
<p class="text-sm sm:text-base opacity-80 mb-6">
|
||||
Guess the chapter for an even higher grade
|
||||
<p class="font-bold mb-3 text-lg sm:text-xl">
|
||||
Bonus Challenge
|
||||
<span class="text-base sm:text-lg opacity-60 font-normal"
|
||||
>— guess the chapter for an even higher grade</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4 justify-center mx-auto mb-6"
|
||||
>
|
||||
{#each chapterOptions as chapter}
|
||||
<div class="grid grid-cols-4 gap-2 justify-center mx-auto mb-3">
|
||||
{#each chapterOptions as chapter (chapter)}
|
||||
<button
|
||||
onclick={() => handleChapterSelect(chapter)}
|
||||
disabled={hasAnswered}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
onChapterGuessCompleted,
|
||||
shareText,
|
||||
streak = 0,
|
||||
streakPercentile = null,
|
||||
}: {
|
||||
statsData: StatsData | null;
|
||||
correctBookId: string;
|
||||
@@ -44,6 +45,7 @@
|
||||
onChapterGuessCompleted: () => void;
|
||||
shareText: string;
|
||||
streak?: number;
|
||||
streakPercentile?: number | null;
|
||||
} = $props();
|
||||
|
||||
let bookName = $derived(getBookById(correctBookId)?.name ?? "");
|
||||
@@ -66,9 +68,9 @@
|
||||
if (guessCount === 1) {
|
||||
const n = Math.random();
|
||||
if (n < 0.99) {
|
||||
return "🌟 First try! 🌟";
|
||||
return "First try!";
|
||||
} else {
|
||||
return "🗣️ Axios! 🗣️";
|
||||
return "Axios!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +109,20 @@
|
||||
</p>
|
||||
|
||||
{#if streak > 1}
|
||||
<p class="big-text text-orange-500! text-sm! mt-4">
|
||||
🔥 {streak} day streak!
|
||||
<p class="big-text text-orange-500! text-lg! my-4">
|
||||
🔥 {streak} days in a row!
|
||||
</p>
|
||||
{#if streak >= 7}
|
||||
<p class="font-black text-lg font-triodion">
|
||||
Thank you for making Bibdle part of your daily routine!
|
||||
</p>
|
||||
{/if}
|
||||
{#if streakPercentile !== null}
|
||||
<p class="text-sm mt-4 text-gray-700 font-triodion">
|
||||
Only {streakPercentile}% of players have a streak of {streak}
|
||||
or greater.
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -5,3 +5,11 @@ export async function fetchStreak(anonymousId: string, localDate: string): Promi
|
||||
const data = await res.json();
|
||||
return typeof data.streak === 'number' ? data.streak : 0;
|
||||
}
|
||||
|
||||
export async function fetchStreakPercentile(streak: number, localDate: string): Promise<number | null> {
|
||||
const params = new URLSearchParams({ streak: String(streak), localDate });
|
||||
const res = await fetch(`/api/streak-percentile?${params}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return typeof data.percentile === 'number' ? data.percentile : null;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
shareResult,
|
||||
copyToClipboard as clipboardCopy,
|
||||
} from "$lib/utils/share";
|
||||
import { fetchStreak } from "$lib/utils/streak";
|
||||
import { fetchStreak, fetchStreakPercentile } from "$lib/utils/streak";
|
||||
import {
|
||||
submitCompletion,
|
||||
fetchExistingStats,
|
||||
@@ -41,6 +41,7 @@
|
||||
let showWinScreen = $state(false);
|
||||
let statsData = $state<StatsData | null>(null);
|
||||
let streak = $state(0);
|
||||
let streakPercentile = $state<number | null>(null);
|
||||
|
||||
const persistence = createGamePersistence(
|
||||
() => dailyVerse.date,
|
||||
@@ -217,6 +218,11 @@
|
||||
const localDate = new Date().toLocaleDateString("en-CA");
|
||||
fetchStreak(persistence.anonymousId, localDate).then((result) => {
|
||||
streak = result;
|
||||
if (result >= 2) {
|
||||
fetchStreakPercentile(result, localDate).then((p) => {
|
||||
streakPercentile = p;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,6 +314,7 @@
|
||||
onChapterGuessCompleted={persistence.onChapterGuessCompleted}
|
||||
shareText={getShareText()}
|
||||
{streak}
|
||||
{streakPercentile}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
72
src/routes/api/streak-percentile/+server.ts
Normal file
72
src/routes/api/streak-percentile/+server.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
const streaks: number[] = [];
|
||||
for (const [, dates] of byUser) {
|
||||
// dates are already desc-sorted
|
||||
const dateSet = new Set(dates);
|
||||
let streak = 0;
|
||||
let cursor = new Date(`${localDate}T00:00:00`);
|
||||
|
||||
while (true) {
|
||||
const dateStr = cursor.toLocaleDateString('en-CA');
|
||||
if (!dateSet.has(dateStr)) break;
|
||||
streak++;
|
||||
cursor.setDate(cursor.getDate() - 1);
|
||||
}
|
||||
|
||||
streaks.push(streak);
|
||||
}
|
||||
|
||||
// Only count users who have an active streak (streak >= 1)
|
||||
const activeStreaks = streaks.filter((s) => s >= 1);
|
||||
|
||||
if (activeStreaks.length === 0) {
|
||||
return json({ percentile: 100 });
|
||||
}
|
||||
|
||||
// Percentage of active-streak users who have a streak >= targetStreak
|
||||
const atOrAbove = activeStreaks.filter((s) => s >= targetStreak).length;
|
||||
const raw = (atOrAbove / activeStreaks.length) * 100;
|
||||
const percentile = raw < 1 ? Math.round(raw * 100) / 100 : Math.round(raw);
|
||||
|
||||
return json({ percentile });
|
||||
};
|
||||
Reference in New Issue
Block a user