mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
Compare commits
5 Commits
9406498cc9
...
stats
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d93ead70c | ||
|
|
4c82aa078b | ||
|
|
2058149207 | ||
|
|
2bd86d37a1 | ||
|
|
33d6fae446 |
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
DATABASE_URL=example.db
|
||||
|
||||
AUTH_SECRET=your-random-secret-here
|
||||
APPLE_ID=com.yourcompany.yourapp.client
|
||||
APPLE_TEAM_ID=your-team-id
|
||||
APPLE_KEY_ID=your-key-id
|
||||
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
||||
your-private-key-here
|
||||
-----END PRIVATE KEY-----"
|
||||
@@ -1,3 +0,0 @@
|
||||
EnglishNKJBible.xml
|
||||
GreekModern1904Bible.xml
|
||||
engwebu_usfx.xml
|
||||
46
CLAUDE.md
46
CLAUDE.md
@@ -4,7 +4,33 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Bibdle is a daily Bible verse guessing game built with SvelteKit 5. Players read a verse and try to guess which book of the Bible it comes from. The game provides feedback hints (Testament match, Section match, Adjacent book) similar to Wordle-style games. Progress is stored locally in the browser and a new verse is generated daily.
|
||||
Bibdle is a daily Bible verse guessing game built with SvelteKit 5. Players read a verse and try to guess which book of the Bible it comes from. The game provides feedback hints (Testament match, Section match, Adjacent book, etc.) similar to Wordle-style games. Progress is stored locally in the browser and a new verse is generated daily.
|
||||
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
(Make sure you use the Svelte agent to execute these commands)
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -19,23 +45,23 @@ Bibdle is a daily Bible verse guessing game built with SvelteKit 5. Players read
|
||||
|
||||
```bash
|
||||
# Start development server
|
||||
npm run dev
|
||||
bun run dev
|
||||
|
||||
# Type checking
|
||||
npm run check
|
||||
npm run check:watch
|
||||
bun run check
|
||||
bun run check:watch
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
bun run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
bun run preview
|
||||
|
||||
# Database operations
|
||||
npm run db:push # Push schema changes to database
|
||||
npm run db:generate # Generate migrations
|
||||
npm run db:migrate # Run migrations
|
||||
npm run db:studio # Open Drizzle Studio GUI
|
||||
bun run db:push # Push schema changes to database
|
||||
bun run db:generate # Generate migrations
|
||||
bun run db:migrate # Run migrations
|
||||
bun run db:studio # Open Drizzle Studio GUI
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bibdle",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "3.0.0alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
71
src/lib/utils/stats.ts
Normal file
71
src/lib/utils/stats.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface UserStats {
|
||||
totalSolves: number;
|
||||
avgGuesses: number;
|
||||
gradeDistribution: {
|
||||
'S++': number;
|
||||
'S+': number;
|
||||
'A+': number;
|
||||
'A': number;
|
||||
'B+': number;
|
||||
'B': number;
|
||||
'C+': number;
|
||||
'C': number;
|
||||
};
|
||||
currentStreak: number;
|
||||
bestStreak: number;
|
||||
recentCompletions: Array<{
|
||||
date: string;
|
||||
guessCount: number;
|
||||
grade: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function getGradeColor(grade: string): string {
|
||||
switch (grade) {
|
||||
case 'S++': return 'text-purple-600 bg-purple-100';
|
||||
case 'S+': return 'text-yellow-600 bg-yellow-100';
|
||||
case 'A+': return 'text-green-600 bg-green-100';
|
||||
case 'A': return 'text-green-500 bg-green-50';
|
||||
case 'B+': return 'text-blue-600 bg-blue-100';
|
||||
case 'B': return 'text-blue-500 bg-blue-50';
|
||||
case 'C+': return 'text-orange-600 bg-orange-100';
|
||||
case 'C': return 'text-red-600 bg-red-100';
|
||||
default: return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr + 'T00:00:00');
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
export function getStreakMessage(currentStreak: number): string {
|
||||
if (currentStreak === 0) {
|
||||
return "Start your streak today!";
|
||||
} else if (currentStreak === 1) {
|
||||
return "Keep it going!";
|
||||
} else if (currentStreak < 7) {
|
||||
return `${currentStreak} days strong!`;
|
||||
} else if (currentStreak < 30) {
|
||||
return `${currentStreak} day streak - amazing!`;
|
||||
} else {
|
||||
return `${currentStreak} days - you're unstoppable!`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPerformanceMessage(avgGuesses: number): string {
|
||||
if (avgGuesses <= 2) {
|
||||
return "Exceptional performance!";
|
||||
} else if (avgGuesses <= 4) {
|
||||
return "Great performance!";
|
||||
} else if (avgGuesses <= 6) {
|
||||
return "Good performance!";
|
||||
} else if (avgGuesses <= 8) {
|
||||
return "Room for improvement!";
|
||||
} else {
|
||||
return "Keep practicing!";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import "./layout.css";
|
||||
import favicon from "$lib/assets/favicon.ico";
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const script = document.createElement('script');
|
||||
script.defer = true;
|
||||
script.src = 'https://umami.snail.city/script.js';
|
||||
script.setAttribute('data-website-id', '5b8c31ad-71cd-4317-940b-6bccea732acc');
|
||||
script.setAttribute('data-domains', 'bibdle.com,www.bibdle.com');
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
});
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<script
|
||||
<!-- <script
|
||||
defer
|
||||
src="https://umami.snail.city/script.js"
|
||||
data-website-id="5b8c31ad-71cd-4317-940b-6bccea732acc"
|
||||
data-domains="bibdle.com,www.bibdle.com"
|
||||
></script>
|
||||
></script> -->
|
||||
</svelte:head>
|
||||
{@render children()}
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
anonymousId = getOrCreateAnonymousId();
|
||||
if ((window as any).umami) {
|
||||
(window as any).umami.identify(anonymousId);
|
||||
}
|
||||
const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`;
|
||||
statsSubmitted = localStorage.getItem(statsKey) === "true";
|
||||
const chapterGuessKey = `bibdle-chapter-guess-${dailyVerse.reference}`;
|
||||
@@ -444,6 +447,14 @@
|
||||
<span class="big-text"
|
||||
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
||||
>
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href="/stats?anonymousId={anonymousId}"
|
||||
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium shadow-md"
|
||||
>
|
||||
📊 View Stats
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
149
src/routes/stats/+page.server.ts
Normal file
149
src/routes/stats/+page.server.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { dailyCompletions, type DailyCompletion } from '$lib/server/db/schema';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const anonymousId = url.searchParams.get('anonymousId');
|
||||
|
||||
if (!anonymousId) {
|
||||
return {
|
||||
stats: null,
|
||||
error: 'No anonymous ID provided'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all completions for this user
|
||||
const completions = await db
|
||||
.select()
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.anonymousId, anonymousId))
|
||||
.orderBy(desc(dailyCompletions.date));
|
||||
|
||||
if (completions.length === 0) {
|
||||
return {
|
||||
stats: {
|
||||
totalSolves: 0,
|
||||
avgGuesses: 0,
|
||||
gradeDistribution: {
|
||||
'S++': 0,
|
||||
'S+': 0,
|
||||
'A+': 0,
|
||||
'A': 0,
|
||||
'B+': 0,
|
||||
'B': 0,
|
||||
'C+': 0,
|
||||
'C': 0
|
||||
},
|
||||
currentStreak: 0,
|
||||
bestStreak: 0,
|
||||
recentCompletions: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate basic stats
|
||||
const totalSolves = completions.length;
|
||||
const totalGuesses = completions.reduce((sum: number, c: DailyCompletion) => sum + c.guessCount, 0);
|
||||
const avgGuesses = Math.round((totalGuesses / totalSolves) * 100) / 100;
|
||||
|
||||
// Calculate grade distribution
|
||||
const gradeDistribution = {
|
||||
'S++': 0, // This will be calculated differently since we don't store chapter correctness
|
||||
'S+': completions.filter((c: DailyCompletion) => c.guessCount === 1).length,
|
||||
'A+': completions.filter((c: DailyCompletion) => c.guessCount === 2).length,
|
||||
'A': completions.filter((c: DailyCompletion) => c.guessCount === 3).length,
|
||||
'B+': completions.filter((c: DailyCompletion) => c.guessCount >= 4 && c.guessCount <= 6).length,
|
||||
'B': completions.filter((c: DailyCompletion) => c.guessCount >= 7 && c.guessCount <= 10).length,
|
||||
'C+': completions.filter((c: DailyCompletion) => c.guessCount >= 11 && c.guessCount <= 15).length,
|
||||
'C': completions.filter((c: DailyCompletion) => c.guessCount > 15).length
|
||||
};
|
||||
|
||||
// Calculate streaks
|
||||
const sortedDates = completions
|
||||
.map((c: DailyCompletion) => c.date)
|
||||
.sort();
|
||||
|
||||
let currentStreak = 0;
|
||||
let bestStreak = 0;
|
||||
let tempStreak = 1;
|
||||
|
||||
if (sortedDates.length > 0) {
|
||||
// Check if current streak is active (includes today or yesterday)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
const lastPlayedDate = sortedDates[sortedDates.length - 1];
|
||||
|
||||
if (lastPlayedDate === today || lastPlayedDate === yesterday) {
|
||||
currentStreak = 1;
|
||||
|
||||
// Count backwards from the most recent date
|
||||
for (let i = sortedDates.length - 2; i >= 0; i--) {
|
||||
const currentDate = new Date(sortedDates[i + 1]);
|
||||
const prevDate = new Date(sortedDates[i]);
|
||||
const daysDiff = Math.floor((currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff === 1) {
|
||||
currentStreak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate best streak
|
||||
bestStreak = 1;
|
||||
for (let i = 1; i < sortedDates.length; i++) {
|
||||
const currentDate = new Date(sortedDates[i]);
|
||||
const prevDate = new Date(sortedDates[i - 1]);
|
||||
const daysDiff = Math.floor((currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff === 1) {
|
||||
tempStreak++;
|
||||
} else {
|
||||
bestStreak = Math.max(bestStreak, tempStreak);
|
||||
tempStreak = 1;
|
||||
}
|
||||
}
|
||||
bestStreak = Math.max(bestStreak, tempStreak);
|
||||
}
|
||||
|
||||
// Get recent completions (last 7 days)
|
||||
const recentCompletions = completions
|
||||
.slice(0, 7)
|
||||
.map((c: DailyCompletion) => ({
|
||||
date: c.date,
|
||||
guessCount: c.guessCount,
|
||||
grade: getGradeFromGuesses(c.guessCount)
|
||||
}));
|
||||
|
||||
return {
|
||||
stats: {
|
||||
totalSolves,
|
||||
avgGuesses,
|
||||
gradeDistribution,
|
||||
currentStreak,
|
||||
bestStreak,
|
||||
recentCompletions
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
return {
|
||||
stats: null,
|
||||
error: 'Failed to fetch stats'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function getGradeFromGuesses(guessCount: number): string {
|
||||
if (guessCount === 1) return "S+";
|
||||
if (guessCount === 2) return "A+";
|
||||
if (guessCount === 3) return "A";
|
||||
if (guessCount >= 4 && guessCount <= 6) return "B+";
|
||||
if (guessCount >= 7 && guessCount <= 10) return "B";
|
||||
if (guessCount >= 11 && guessCount <= 15) return "C+";
|
||||
return "C";
|
||||
}
|
||||
206
src/routes/stats/+page.svelte
Normal file
206
src/routes/stats/+page.svelte
Normal file
@@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
getGradeColor,
|
||||
formatDate,
|
||||
getStreakMessage,
|
||||
getPerformanceMessage,
|
||||
type UserStats
|
||||
} from "$lib/utils/stats";
|
||||
|
||||
interface PageData {
|
||||
stats: UserStats | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
|
||||
function getOrCreateAnonymousId(): string {
|
||||
if (!browser) return "";
|
||||
const key = "bibdle-anonymous-id";
|
||||
let id = localStorage.getItem(key);
|
||||
if (!id) {
|
||||
id = crypto.randomUUID();
|
||||
localStorage.setItem(key, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const anonymousId = getOrCreateAnonymousId();
|
||||
if (!anonymousId) {
|
||||
goto("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// If no anonymousId in URL, redirect with it
|
||||
const url = new URL(window.location.href);
|
||||
if (!url.searchParams.get('anonymousId')) {
|
||||
url.searchParams.set('anonymousId', anonymousId);
|
||||
goto(url.pathname + url.search);
|
||||
return;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function getGradePercentage(count: number, total: number): number {
|
||||
return total > 0 ? Math.round((count / total) * 100) : 0;
|
||||
}
|
||||
|
||||
$inspect(data);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Stats | Bibdle</title>
|
||||
<meta name="description" content="View your Bibdle game statistics and performance" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-gradient-to-br from-amber-50 to-orange-100 p-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-800 mb-2">Your Stats</h1>
|
||||
<p class="text-gray-600">Track your Bibdle performance over time</p>
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
|
||||
>
|
||||
← Back to Game
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-center py-12">
|
||||
<div class="inline-block w-8 h-8 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<p class="mt-4 text-gray-600">Loading your stats...</p>
|
||||
</div>
|
||||
{:else if data.error}
|
||||
<div class="text-center py-12">
|
||||
<div class="bg-red-100 border border-red-300 rounded-lg p-6 max-w-md mx-auto">
|
||||
<p class="text-red-700">{data.error}</p>
|
||||
<a
|
||||
href="/"
|
||||
class="mt-4 inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Return to Game
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !data.stats}
|
||||
<div class="text-center py-12">
|
||||
<div class="bg-yellow-100 border border-yellow-300 rounded-lg p-6 max-w-md mx-auto">
|
||||
<p class="text-yellow-700">No stats available.</p>
|
||||
<a
|
||||
href="/"
|
||||
class="mt-4 inline-block px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors"
|
||||
>
|
||||
Start Playing
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@const stats = data.stats}
|
||||
|
||||
<!-- Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Total Solves -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-amber-600 mb-2">{stats.totalSolves}</div>
|
||||
<div class="text-gray-600">Total Solves</div>
|
||||
{#if stats.totalSolves > 0}
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
{getPerformanceMessage(stats.avgGuesses)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Guesses -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-600 mb-2">{stats.avgGuesses}</div>
|
||||
<div class="text-gray-600">Avg. Guesses</div>
|
||||
<div class="text-sm text-gray-500 mt-1">per solve</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Streak -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-green-600 mb-2">{stats.currentStreak}</div>
|
||||
<div class="text-gray-600">Current Streak</div>
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
{getStreakMessage(stats.currentStreak)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grade Distribution -->
|
||||
{#if stats.totalSolves > 0}
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">Grade Distribution</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{#each Object.entries(stats.gradeDistribution) as [grade, count]}
|
||||
{@const percentage = getGradePercentage(count, stats.totalSolves)}
|
||||
<div class="text-center">
|
||||
<div class="mb-2">
|
||||
<span class="inline-block px-3 py-1 rounded-full text-sm font-semibold {getGradeColor(grade)}">
|
||||
{grade}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-800">{count}</div>
|
||||
<div class="text-sm text-gray-500">{percentage}%</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Streak Info -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">Streak Information</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-green-600 mb-2">{stats.currentStreak}</div>
|
||||
<div class="text-gray-600">Current Streak</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600 mb-2">{stats.bestStreak}</div>
|
||||
<div class="text-gray-600">Best Streak</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Performance -->
|
||||
{#if stats.recentCompletions.length > 0}
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">Recent Performance</h2>
|
||||
<div class="space-y-3">
|
||||
{#each stats.recentCompletions as completion}
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
||||
<div>
|
||||
<span class="font-medium">{formatDate(completion.date)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-600">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span>
|
||||
<span class="px-2 py-1 rounded text-sm font-semibold {getGradeColor(completion.grade)}">
|
||||
{completion.grade}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
40
todo.md
40
todo.md
@@ -1,17 +1,22 @@
|
||||
# in progress
|
||||
|
||||
- Show new/old testament after 3 guesses and section after 7 guesses
|
||||
- Add sections for "first letter", "Canonical/deutero", etc...
|
||||
- Make the UI more "wordle-like" ()
|
||||
- How do you balance rewarding knowledge vs incentivising learning?
|
||||
|
||||
# todo
|
||||
|
||||
- login
|
||||
- login route
|
||||
|
||||
- impossible mode (1904 greek bible) three guesses only.
|
||||
|
||||
- share both classic and impossible mode with both buttons
|
||||
|
||||
- add imposter mode
|
||||
- improve imposter mode
|
||||
|
||||
- Show new/old testament after 3 guesses and section after 7 guesses
|
||||
- Add sections for "first letter", "Canonical/deutero", etc...
|
||||
|
||||
- How do you balance rewarding knowledge vs incentivising learning?
|
||||
|
||||
|
||||
- instructions
|
||||
|
||||
@@ -54,6 +59,29 @@ I created Bibdle from a combination of two things. The first is my lifelong desi
|
||||
|
||||
# done
|
||||
|
||||
## february 2nd
|
||||
|
||||
- created rss feed
|
||||
- fixed "first letter" clue edge cases
|
||||
- updated ranking formula
|
||||
|
||||
## january 28th
|
||||
|
||||
- add percentile stats, update chapter guess UI
|
||||
- fixed middle statline (removed meaningless %)
|
||||
- added instructions
|
||||
- added email button
|
||||
- added test buttons for 3.0 UI/UX
|
||||
- package upgrades
|
||||
|
||||
## january 26th
|
||||
|
||||
- Make the UI more "wordle-like"
|
||||
- added deployment script (./deploy.sh)
|
||||
- added bluesky button
|
||||
- added "first letter" column
|
||||
- added imposter mode, v0.1 (mom likes it) but needs work
|
||||
|
||||
## january 5th
|
||||
|
||||
- created Imposter Mode with four options, identify the verse that is not in the same book as the other three. Needs more testing...
|
||||
@@ -64,6 +92,8 @@ I created Bibdle from a combination of two things. The first is my lifelong desi
|
||||
- For bonus points: guess the verse/psalm number
|
||||
- major UI styling revamp
|
||||
|
||||
-- 2026 --
|
||||
|
||||
## december 30th
|
||||
|
||||
- merged the embeddings/similarity route into production
|
||||
|
||||
Reference in New Issue
Block a user