mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
added streak container
This commit is contained in:
41
scripts/test-share-text.ts
Normal file
41
scripts/test-share-text.ts
Normal 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);
|
||||||
|
}
|
||||||
10
scripts/test-verse-snippets.ts
Normal file
10
scripts/test-verse-snippets.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
29
src/lib/components/StreakCounter.svelte
Normal file
29
src/lib/components/StreakCounter.svelte
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
<p class="text-2xl sm:text-3xl md:text-4xl leading-tight">
|
<div class="flex flex-col gap-3">
|
||||||
{congratulationsMessage} The verse is from<br />
|
<p class="text-2xl sm:text-3xl md:text-4xl leading-tight">
|
||||||
<span class="font-black text-3xl md:text-4xl">{bookName}</span>.
|
{congratulationsMessage} The verse is from<br />
|
||||||
</p>
|
<span class="font-black font-triodion text-3xl md:text-4xl"
|
||||||
<p class="text-lg sm:text-xl md:text-2xl mt-2">
|
>{bookName}</span
|
||||||
You guessed correctly after {guessCount}
|
>.
|
||||||
{guessCount === 1 ? "guess" : "guesses"}.
|
</p>
|
||||||
</p>
|
<p class="text-lg sm:text-xl md:text-2xl">
|
||||||
|
You guessed correctly after {guessCount}
|
||||||
{#if streak > 1}
|
{guessCount === 1 ? "guess" : "guesses"}.
|
||||||
<div class="flex flex-col gap-4 my-4">
|
</p>
|
||||||
<p class="big-text text-orange-500! text-lg!">
|
{#if streak >= 7}
|
||||||
🔥 {streak} days in a row!
|
<p class="text-gray-700! big-text">
|
||||||
|
Thank you for making Bibdle part of your daily routine!
|
||||||
</p>
|
</p>
|
||||||
{#if streak >= 7}
|
{/if}
|
||||||
<p class="font-black text-lg font-triodion">
|
</div>
|
||||||
Thank you for making Bibdle part of your daily routine!
|
|
||||||
</p>
|
|
||||||
{/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>
|
|
||||||
{/if}
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<!-- S++ Bonus Challenge for first try -->
|
<!-- S++ Bonus Challenge for first try -->
|
||||||
@@ -139,7 +129,16 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<CountdownTimer />
|
<div class="flex flex-row gap-3 items-stretch w-full">
|
||||||
|
<div class="flex-[2] min-w-0 flex flex-col">
|
||||||
|
<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={() => {
|
||||||
(window as any).rybbit?.event("Copy to Clipboard");
|
if (!copyTracked) {
|
||||||
|
(window as any).rybbit?.event("Copy to Clipboard");
|
||||||
|
copyTracked = true;
|
||||||
|
}
|
||||||
copyToClipboard();
|
copyToClipboard();
|
||||||
bubbleCopied = true;
|
bubbleCopied = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,7 @@
|
|||||||
isLoggedIn: !!user,
|
isLoggedIn: !!user,
|
||||||
streak,
|
streak,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
|
verseText: dailyVerse.verseText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user