mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
242 lines
5.1 KiB
Svelte
242 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
|
|
interface ImposterData {
|
|
verses: string[];
|
|
refs: string[];
|
|
imposterIndex: number;
|
|
}
|
|
|
|
let data: ImposterData | null = null;
|
|
let clicked: boolean[] = [];
|
|
let gameOver = false;
|
|
let loading = true;
|
|
let error: string | null = null;
|
|
|
|
async function loadGame() {
|
|
try {
|
|
const res = await fetch("/api/imposter");
|
|
if (!res.ok) {
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
}
|
|
data = (await res.json()) as ImposterData;
|
|
clicked = new Array(data.verses.length).fill(false);
|
|
gameOver = false;
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : "Unknown error";
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function handleClick(index: number) {
|
|
if (gameOver || !data || clicked[index]) return;
|
|
clicked[index] = true;
|
|
if (index !== data.imposterIndex) {
|
|
clicked[data.imposterIndex] = true;
|
|
}
|
|
gameOver = true;
|
|
}
|
|
|
|
function newGame() {
|
|
loading = true;
|
|
error = null;
|
|
data = null;
|
|
loadGame();
|
|
}
|
|
|
|
onMount(loadGame);
|
|
|
|
function formatVerse(verse: string): string {
|
|
let formatted = verse;
|
|
|
|
// Handle unbalanced opening/closing punctuation
|
|
const pairs: [string, string][] = [
|
|
["(", ")"],
|
|
["[", "]"],
|
|
["{", "}"],
|
|
['"', '"'],
|
|
["'", "'"],
|
|
["\u201C", "\u201D"], // \u201C
|
|
["\u2018", "\u2019"], // \u2018
|
|
];
|
|
for (const [open, close] of pairs) {
|
|
if (formatted.startsWith(open) && !formatted.includes(close)) {
|
|
formatted += "..." + close;
|
|
break;
|
|
}
|
|
}
|
|
for (const [open, close] of pairs) {
|
|
if (formatted.endsWith(close) && !formatted.includes(open)) {
|
|
formatted = open + "..." + formatted;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (/^[a-z]/.test(formatted)) {
|
|
formatted = "..." + formatted;
|
|
}
|
|
formatted = formatted.replace(/[,:;-—]$/, "...");
|
|
return formatted;
|
|
}
|
|
</script>
|
|
|
|
<div class="imposter-game">
|
|
{#if loading}
|
|
<p class="loading">Loading verses...</p>
|
|
{:else if error}
|
|
<div class="error">
|
|
<p>Error: {error}</p>
|
|
<button on:click={newGame}>Retry</button>
|
|
</div>
|
|
{:else if data}
|
|
<!-- <div class="instructions">
|
|
<p>Click the verse that doesn't belong (from a different book).</p>
|
|
</div> -->
|
|
<div class="verses">
|
|
{#each data.verses as verse, i}
|
|
<div class="verse-item">
|
|
<button
|
|
class="verse-button"
|
|
class:clicked={clicked[i]}
|
|
class:correct={clicked[i] && i === data.imposterIndex}
|
|
class:wrong={clicked[i] && i !== data.imposterIndex}
|
|
on:click={() => handleClick(i)}
|
|
disabled={gameOver}
|
|
>
|
|
{formatVerse(verse)}
|
|
</button>
|
|
{#if gameOver}
|
|
<div class="ref">{data.refs[i]}</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{#if gameOver}
|
|
<div class="result">
|
|
<button on:click={newGame}>New Game</button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.imposter-game {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
padding: 2rem;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.loading,
|
|
.error {
|
|
text-align: center;
|
|
}
|
|
|
|
.instructions {
|
|
text-align: center;
|
|
font-style: italic;
|
|
color: #666;
|
|
}
|
|
|
|
.verses {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
gap: 1.5rem;
|
|
width: 100%;
|
|
}
|
|
|
|
.verse-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.verse-button {
|
|
padding: 1.5rem;
|
|
font-size: 1.1rem;
|
|
line-height: 1.4;
|
|
border: 3px solid #ddd;
|
|
background: #fafafa;
|
|
cursor: pointer;
|
|
border-radius: 12px;
|
|
transition: all 0.3s ease;
|
|
min-height: 100px;
|
|
text-align: left;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.verse-button:hover:not(.clicked):not(:disabled) {
|
|
border-color: #007bff;
|
|
background: #f8f9ff;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
|
}
|
|
|
|
.verse-button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.verse-button.clicked {
|
|
cursor: default;
|
|
}
|
|
|
|
.correct {
|
|
background: #d4edda !important;
|
|
border-color: #28a745 !important;
|
|
color: #155724;
|
|
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.3);
|
|
}
|
|
|
|
.wrong {
|
|
background: #f8d7da !important;
|
|
border-color: #dc3545 !important;
|
|
color: #721c24;
|
|
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.3);
|
|
}
|
|
|
|
.ref {
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
color: #555;
|
|
padding-top: 0.25rem;
|
|
}
|
|
|
|
.verse-button.correct ~ .ref {
|
|
color: #28a745;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.verse-button.wrong ~ .ref {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.result {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.result button,
|
|
.error button {
|
|
padding: 0.75rem 2rem;
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.result button:hover,
|
|
.error button:hover {
|
|
background: #0056b3;
|
|
}
|
|
</style>
|