added streak container

This commit is contained in:
George Powell
2026-02-26 00:51:48 -05:00
parent f9f3f3de12
commit a5cf248e29
9 changed files with 167 additions and 56 deletions

View File

@@ -0,0 +1,41 @@
import { fetchRandomVerse } from '../src/lib/server/bible-api';
import { generateShareText } from '../src/lib/utils/share';
import { bibleBooks } from '../src/lib/types/bible';
const NUM_VERSES = 10;
for (let i = 0; i < NUM_VERSES; i++) {
const verse = await fetchRandomVerse();
// Build a fake "solved in N guesses" scenario with some wrong guesses first
const correctBook = bibleBooks.find((b) => b.id === verse.bookId)!;
const wrongBook = bibleBooks.find((b) => b.id !== verse.bookId)!;
const guessCount = Math.floor(Math.random() * 5) + 1;
const guesses = [
...Array(guessCount - 1).fill(null).map(() => ({
book: wrongBook,
testamentMatch: wrongBook.testament === correctBook.testament,
sectionMatch: wrongBook.section === correctBook.section,
adjacent: Math.abs(wrongBook.order - correctBook.order) === 1,
})),
{ book: correctBook, testamentMatch: true, sectionMatch: true, adjacent: false },
];
const fakeStreak = Math.random() > 0.5 ? Math.floor(Math.random() * 14) + 2 : 0;
const shareText = generateShareText({
guesses,
correctBookId: verse.bookId,
dailyVerseDate: new Date().toISOString().slice(0, 10),
chapterCorrect: guessCount === 1 && Math.random() > 0.5,
isLoggedIn: Math.random() > 0.5,
streak: fakeStreak > 0 ? fakeStreak : undefined,
origin: 'https://bibdle.com',
verseText: verse.verseText,
});
console.log(`\n── Verse ${i + 1}: ${verse.reference} ──`);
console.log(`RAW: ${verse.verseText}`);
console.log('─'.repeat(40));
console.log(shareText);
}

View File

@@ -0,0 +1,10 @@
import { fetchRandomVerse } from '../src/lib/server/bible-api';
import { getVerseSnippet } from '../src/lib/utils/share';
const NUM_VERSES = 10;
for (let i = 0; i < NUM_VERSES; i++) {
const verse = await fetchRandomVerse();
console.log(getVerseSnippet(verse.verseText));
}

View File

@@ -50,9 +50,9 @@
}); });
</script> </script>
<div class="w-full"> <div class="w-full flex flex-col flex-1">
<div <div
class="flex flex-col items-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm w-full" class="flex flex-col items-center justify-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm w-full flex-1"
> >
{#if newVerseReady} {#if newVerseReady}
<p <p

View File

@@ -0,0 +1,29 @@
<script lang="ts">
let {
streak,
streakPercentile = null,
}: {
streak: number;
streakPercentile?: number | null;
} = $props();
</script>
<div
class="flex flex-col items-center justify-center bg-white/50 backdrop-blur-sm px-4 py-4 rounded-2xl border border-white/50 shadow-sm flex-1 text-center"
>
<p
class="text-4xl font-triodion font-black text-orange-500 leading-none tabular-nums"
>
{streak}
</p>
<p
class="text-xs uppercase tracking-[0.2em] text-gray-500 font-bold mt-1 leading-tight"
>
day{streak === 1 ? "" : "s"}<br />in a row
</p>
{#if streakPercentile !== null && streakPercentile <= 50}
<p class="text-xs text-gray-700 font-semibold mt-2">
Top {streakPercentile}%
</p>
{/if}
</div>

View File

@@ -80,19 +80,6 @@
> >
{displayReference} {displayReference}
</p> </p>
<div
transition:fade={{ duration: 300 }}
class="flex justify-center mt-3"
>
<button
onclick={copyVerse}
data-umami-event="Copy Verse"
class="flex items-center gap-1.5 px-3 py-1.5 text-xs big-text text-gray-600 bg-white/50 hover:bg-white/70 border border-gray-300 rounded-lg transition-colors cursor-pointer"
>
{copied ? "✅" : "📋"}
{copied ? "Copied!" : "Copy verse to clipboard"}
</button>
</div>
{/if} {/if}
</div> </div>
</Container> </Container>

View File

@@ -3,6 +3,7 @@
import { getBookById, toOrdinal } from "$lib/utils/game"; import { getBookById, toOrdinal } from "$lib/utils/game";
import Container from "./Container.svelte"; import Container from "./Container.svelte";
import CountdownTimer from "./CountdownTimer.svelte"; import CountdownTimer from "./CountdownTimer.svelte";
import StreakCounter from "./StreakCounter.svelte";
import ChapterGuess from "./ChapterGuess.svelte"; import ChapterGuess from "./ChapterGuess.svelte";
interface StatsData { interface StatsData {
@@ -53,6 +54,7 @@
); );
let copySuccess = $state(false); let copySuccess = $state(false);
let bubbleCopied = $state(false); let bubbleCopied = $state(false);
let copyTracked = $state(false);
// List of congratulations messages with weights // List of congratulations messages with weights
const congratulationsMessages: WeightedMessage[] = [ const congratulationsMessages: WeightedMessage[] = [
@@ -97,37 +99,25 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<Container <Container
class="w-full px-2 sm:px-4 py-6 sm:py-8 bg-linear-to-r from-green-400/10 to-green-600/30 text-gray-800 shadow-2xl text-center fade-in" class="w-full px-4 sm:px-6 py-6 sm:py-8 bg-linear-to-r from-green-400/10 to-green-600/30 text-gray-800 shadow-2xl text-center fade-in"
> >
<div class="flex flex-col gap-3">
<p class="text-2xl sm:text-3xl md:text-4xl leading-tight"> <p class="text-2xl sm:text-3xl md:text-4xl leading-tight">
{congratulationsMessage} The verse is from<br /> {congratulationsMessage} The verse is from<br />
<span class="font-black text-3xl md:text-4xl">{bookName}</span>. <span class="font-black font-triodion text-3xl md:text-4xl"
>{bookName}</span
>.
</p> </p>
<p class="text-lg sm:text-xl md:text-2xl mt-2"> <p class="text-lg sm:text-xl md:text-2xl">
You guessed correctly after {guessCount} You guessed correctly after {guessCount}
{guessCount === 1 ? "guess" : "guesses"}. {guessCount === 1 ? "guess" : "guesses"}.
</p> </p>
{#if streak > 1}
<div class="flex flex-col gap-4 my-4">
<p class="big-text text-orange-500! text-lg!">
🔥 {streak} days in a row!
</p>
{#if streak >= 7} {#if streak >= 7}
<p class="font-black text-lg font-triodion"> <p class="text-gray-700! big-text">
Thank you for making Bibdle part of your daily routine! Thank you for making Bibdle part of your daily routine!
</p> </p>
{/if} {/if}
{#if streakPercentile !== null}
<p class="text-sm text-gray-700 font-triodion">
{streakPercentile <= 50
? "Only "
: ""}{streakPercentile}% of players have a streak of {streak}
or greater.
</p>
{/if}
</div> </div>
{/if}
</Container> </Container>
<!-- S++ Bonus Challenge for first try --> <!-- S++ Bonus Challenge for first try -->
@@ -139,7 +129,16 @@
/> />
{/if} {/if}
<div class="flex flex-row gap-3 items-stretch w-full">
<div class="flex-[2] min-w-0 flex flex-col">
<CountdownTimer /> <CountdownTimer />
</div>
{#if streak > 0}
<div class="flex-[1] min-w-0 flex flex-col">
<StreakCounter {streak} {streakPercentile} />
</div>
{/if}
</div>
<!-- Statistics Display --> <!-- Statistics Display -->
{#if statsData} {#if statsData}
@@ -225,7 +224,12 @@
(window as any).rybbit?.event("Share"); (window as any).rybbit?.event("Share");
handleShare(); handleShare();
} else { } else {
(window as any).rybbit?.event("Copy to Clipboard"); if (!copyTracked) {
(window as any).rybbit?.event(
"Copy to Clipboard",
);
copyTracked = true;
}
copyToClipboard(); copyToClipboard();
copySuccess = true; copySuccess = true;
setTimeout(() => { setTimeout(() => {
@@ -251,7 +255,10 @@
aria-label="Copy to clipboard" aria-label="Copy to clipboard"
data-umami-event="Copy to Clipboard" data-umami-event="Copy to Clipboard"
onclick={() => { onclick={() => {
if (!copyTracked) {
(window as any).rybbit?.event("Copy to Clipboard"); (window as any).rybbit?.event("Copy to Clipboard");
copyTracked = true;
}
copyToClipboard(); copyToClipboard();
bubbleCopied = true; bubbleCopied = true;
setTimeout(() => { setTimeout(() => {

View File

@@ -1,5 +1,4 @@
import { integer, sqliteTable, text, index, unique } from 'drizzle-orm/sqlite-core'; import { integer, sqliteTable, text, index, unique } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const user = sqliteTable('user', { export const user = sqliteTable('user', {
id: text('id').primaryKey(), id: text('id').primaryKey(),

View File

@@ -1,5 +1,43 @@
import type { SHA512_256 } from '@oslojs/crypto/sha2';
import type { Guess } from './game'; import type { Guess } from './game';
export function getVerseSnippet(verseText: string): string {
const words = verseText.trim().split(/\s+/);
const slice = words.slice(0, 25);
const text = slice.join(' ');
// Returns character index immediately after the Nth word (1-indexed)
function posAfterWord(n: number): number {
let pos = 0;
for (let w = 0; w < Math.min(n, slice.length); w++) {
if (w > 0) pos++; // space between words
pos += slice[w].length;
}
return pos;
}
const start = posAfterWord(10);
const end = posAfterWord(25);
// Find first punctuation mark between words 10 and 25
const range = text.substring(start, end);
const match = range.match(/[,;:.!?—–-]/);
function withClosedQuotes(snippet: string): string {
const opens = (snippet.match(/\u201C/g) ?? []).length;
const closes = (snippet.match(/\u201D/g) ?? []).length;
const closeQuote = opens > closes ? '\u201D' : '';
return `\u201C${snippet}...${closeQuote}\u201D`;
}
if (match && match.index !== undefined) {
const cutPos = start + match.index;
return withClosedQuotes(text.substring(0, cutPos).trimEnd());
}
return withClosedQuotes(text);
}
export function generateShareText(params: { export function generateShareText(params: {
guesses: Guess[]; guesses: Guess[];
correctBookId: string; correctBookId: string;
@@ -8,8 +46,9 @@ export function generateShareText(params: {
isLoggedIn: boolean; isLoggedIn: boolean;
streak?: number; streak?: number;
origin: string; origin: string;
verseText: string;
}): string { }): string {
const { guesses, correctBookId, dailyVerseDate, chapterCorrect, isLoggedIn, streak, origin } = params; const { guesses, correctBookId, dailyVerseDate, chapterCorrect, isLoggedIn, streak, origin, verseText } = params;
const emojis = guesses const emojis = guesses
.slice() .slice()
@@ -35,17 +74,15 @@ export function generateShareText(params: {
const bookEmoji = isLoggedIn ? "📜" : "📖"; const bookEmoji = isLoggedIn ? "📜" : "📖";
const guessWord = guesses.length === 1 ? "guess" : "guesses"; const guessWord = guesses.length === 1 ? "guess" : "guesses";
const streakPart = streak !== undefined && streak > 1 ? `, ${streak} days 🔥` : ""; const streakPart = streak !== undefined && streak > 1 ? ` (${streak} days🔥)` : "";
const chapterStar = guesses.length === 1 && chapterCorrect ? " ⭐" : "";
const lines = [ const lines = [
`${bookEmoji} Bibdle | ${formattedDate} ${bookEmoji}`, `${bookEmoji} Bibdle | ${formattedDate} ${bookEmoji}`,
`${guesses.length} ${guessWord}${streakPart}`, getVerseSnippet(verseText),
]; `${emojis}${chapterStar} ${guesses.length} ${guessWord}${streakPart}`,
lines.push(
`${emojis}${guesses.length === 1 && chapterCorrect ? " ⭐" : ""}`,
origin, origin,
); ];
return lines.join("\n"); return lines.join("\n");
} }

View File

@@ -240,6 +240,7 @@
isLoggedIn: !!user, isLoggedIn: !!user,
streak, streak,
origin: window.location.origin, origin: window.location.origin,
verseText: dailyVerse.verseText,
}); });
} }