Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<meta name="theme-color" content="#2563eb" />
<meta name="description" content="Social Bingo - Find people who match the questions!" />
<title>Soc Ops - Social Bingo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@700;900&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
Expand Down
15 changes: 14 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useState } from 'react';
import { useBingoGame } from './hooks/useBingoGame';
import { StartScreen } from './components/StartScreen';
import { GameScreen } from './components/GameScreen';
import { BingoModal } from './components/BingoModal';
import { QuizWizardScreen } from './components/QuizWizardScreen';

function App() {
const [quizWizardActive, setQuizWizardActive] = useState(false);

const {
gameState,
board,
Expand All @@ -15,8 +19,17 @@ function App() {
dismissModal,
} = useBingoGame();

if (quizWizardActive) {
return <QuizWizardScreen onReset={() => setQuizWizardActive(false)} />;
}

if (gameState === 'start') {
return <StartScreen onStart={startGame} />;
return (
<StartScreen
onStartBingo={startGame}
onStartQuizWizard={() => setQuizWizardActive(true)}
/>
);
}

return (
Expand Down
230 changes: 230 additions & 0 deletions src/components/QuizWizardScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { useState, useMemo, useRef, useEffect } from 'react';
import { questions } from '../data/questions';

interface QuizWizardScreenProps {
onReset: () => void;
}

function shuffleArray<T>(arr: T[]): T[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}

export function QuizWizardScreen({ onReset }: QuizWizardScreenProps) {
const shuffled = useMemo(() => shuffleArray(questions), []);
const total = shuffled.length;

const [currentIndex, setCurrentIndex] = useState(0);
const [foundCount, setFoundCount] = useState(0);
const [done, setDone] = useState(false);
const [animKey, setAnimKey] = useState(0);
const cardRef = useRef<HTMLDivElement>(null);

// Animate card in on each question change
useEffect(() => {
const el = cardRef.current;
if (!el) return;
el.style.animation = 'none';
void el.offsetHeight; // reflow
el.style.animation = 'wiz-slide-in 0.28s ease-out both';
}, [animKey]);

const advance = (found: boolean) => {
if (found) setFoundCount((c) => c + 1);
if (currentIndex + 1 >= total) {
setDone(true);
} else {
setCurrentIndex((i) => i + 1);
setAnimKey((k) => k + 1);
}
};

const progressPct = Math.round((currentIndex / total) * 100);
const stepLabel = String(currentIndex + 1).padStart(2, '0');
const totalLabel = String(total).padStart(2, '0');

if (done) {
return (
<div
className="flex flex-col items-center justify-center min-h-full p-6"
style={{ background: 'var(--color-wiz-bg)' }}
>
<div
className="w-full max-w-sm text-center p-8"
style={{
border: '4px solid var(--color-wiz-ink)',
background: 'var(--color-wiz-card)',
animation: 'wiz-pop 0.4s ease-out both',
}}
>
<p
className="text-xs tracking-[0.25em] uppercase mb-3"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'var(--color-wiz-red)' }}
>
Finished!
</p>
<p
className="text-7xl font-black leading-none mb-1"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'var(--color-wiz-ink)' }}
>
{foundCount}
</p>
<p
className="text-base mb-6"
style={{ fontFamily: 'var(--font-wiz-body)', color: 'var(--color-wiz-ink)' }}
>
out of {total} people found
</p>
<button
onClick={onReset}
className="w-full py-4 text-sm tracking-[0.2em] uppercase transition-opacity active:opacity-70"
style={{
fontFamily: 'var(--font-wiz-display)',
background: 'var(--color-wiz-ink)',
color: 'var(--color-wiz-bg)',
border: 'none',
}}
>
Back to Start
</button>
</div>
</div>
);
}

return (
<div
className="flex flex-col min-h-full"
style={{ background: 'var(--color-wiz-bg)' }}
>
{/* Header */}
<header
className="flex items-center justify-between px-4 py-3"
style={{ background: 'var(--color-wiz-ink)' }}
>
<button
onClick={onReset}
className="text-xs tracking-widest uppercase py-1 px-2 transition-opacity active:opacity-60"
style={{
fontFamily: 'var(--font-wiz-display)',
color: 'var(--color-wiz-bg)',
border: '1px solid rgba(240,235,224,0.3)',
}}
>
← Back
</button>

<span
className="text-xs tracking-[0.3em] uppercase"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'var(--color-wiz-bg)' }}
>
Quiz Wizard
</span>

<span
className="text-sm"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'var(--color-wiz-red)' }}
>
{stepLabel}/{totalLabel}
</span>
</header>

{/* Progress bar */}
<div
className="w-full h-1"
style={{ background: 'rgba(13,13,13,0.15)' }}
>
<div
className="h-full transition-all duration-300"
style={{ width: `${progressPct}%`, background: 'var(--color-wiz-red)' }}
/>
</div>

{/* Body */}
<div className="flex-1 flex flex-col justify-between p-5 gap-4">

{/* Step counter */}
<div className="flex items-baseline gap-2">
<span
className="text-5xl font-black leading-none"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'var(--color-wiz-red)' }}
>
{stepLabel}
</span>
<span
className="text-xs tracking-[0.2em] uppercase"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'rgba(13,13,13,0.4)' }}
>
of {totalLabel}
</span>
</div>

{/* Question card */}
<div
ref={cardRef}
className="flex-1 flex items-center justify-center p-6"
style={{
border: '3px solid var(--color-wiz-ink)',
background: 'var(--color-wiz-card)',
animation: 'wiz-slide-in 0.28s ease-out both',
}}
>
<p
className="text-center text-xl leading-snug"
style={{ fontFamily: 'var(--font-wiz-body)', color: 'var(--color-wiz-ink)' }}
>
{shuffled[currentIndex]}
</p>
</div>

{/* Instruction */}
<p
className="text-center text-xs tracking-widest uppercase"
style={{ fontFamily: 'var(--font-wiz-display)', color: 'rgba(13,13,13,0.4)' }}
>
Find someone who…
</p>

{/* Action buttons */}
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => advance(true)}
className="py-4 text-xs tracking-[0.15em] uppercase transition-opacity active:opacity-70"
style={{
fontFamily: 'var(--font-wiz-display)',
background: 'var(--color-wiz-red)',
color: '#fff',
border: '3px solid var(--color-wiz-red)',
}}
>
✓ Found!
</button>
<button
onClick={() => advance(false)}
className="py-4 text-xs tracking-[0.15em] uppercase transition-opacity active:opacity-70"
style={{
fontFamily: 'var(--font-wiz-display)',
background: 'transparent',
color: 'var(--color-wiz-ink)',
border: '3px solid var(--color-wiz-ink)',
}}
>
Skip →
</button>
</div>

{/* Found counter */}
<p
className="text-center text-xs tracking-widest"
style={{ fontFamily: 'var(--font-wiz-body)', color: 'rgba(13,13,13,0.5)' }}
>
{foundCount} found so far
</p>
</div>
</div>
);
}
47 changes: 30 additions & 17 deletions src/components/StartScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
interface StartScreenProps {
onStart: () => void;
onStartBingo: () => void;
onStartQuizWizard: () => void;
}

export function StartScreen({ onStart }: StartScreenProps) {
export function StartScreen({ onStartBingo, onStartQuizWizard }: StartScreenProps) {
return (
<div className="flex flex-col items-center justify-center min-h-full p-6 bg-gray-50">
<div className="text-center max-w-sm">
<div className="text-center max-w-sm w-full">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Soc Ops</h1>
<p className="text-lg text-gray-600 mb-8">Social Bingo</p>

<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 mb-8">
<h2 className="font-semibold text-gray-800 mb-3">How to play</h2>
<ul className="text-left text-gray-600 text-sm space-y-2">
<li>• Find people who match the questions</li>
<li>• Tap a square when you find a match</li>
<li>• Get 5 in a row to win!</li>
</ul>

<p className="text-sm text-gray-500 mb-4 tracking-wide uppercase font-medium">Choose a mode</p>

<div className="flex flex-col gap-3 mb-6">
<button
onClick={onStartBingo}
className="w-full bg-white border-2 border-gray-200 rounded-lg p-5 text-left active:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between mb-1">
<span className="font-bold text-gray-900 text-lg">Bingo</span>
<span className="text-2xl">🎯</span>
</div>
<p className="text-sm text-gray-500">5×5 grid • Get 5 in a row to win</p>
</button>

<button
onClick={onStartQuizWizard}
className="w-full bg-white border-2 border-gray-200 rounded-lg p-5 text-left active:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between mb-1">
<span className="font-bold text-gray-900 text-lg">Quiz Wizard</span>
<span className="text-2xl">🃏</span>
</div>
<p className="text-sm text-gray-500">One question at a time • Find everyone</p>
</button>
</div>

<button
onClick={onStart}
className="w-full bg-accent text-white font-semibold py-4 px-8 rounded-lg text-lg active:bg-accent-light transition-colors"
>
Start Game
</button>
<p className="text-xs text-gray-400">Find people who match each question</p>
</div>
</div>
);
Expand Down
19 changes: 19 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
--color-marked: #dcfce7;
--color-marked-border: #22c55e;
--color-bingo: #fbbf24;

/* Quiz Wizard theme */
--color-wiz-bg: #f0ebe0;
--color-wiz-ink: #0d0d0d;
--color-wiz-red: #c9281a;
--color-wiz-card: #ffffff;
--font-wiz-display: 'Unbounded', sans-serif;
--font-wiz-body: 'Courier Prime', monospace;
}

html,
Expand All @@ -19,3 +27,14 @@ body {
font-family: system-ui, -apple-system, sans-serif;
-webkit-tap-highlight-color: transparent;
}

@keyframes wiz-slide-in {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}

@keyframes wiz-pop {
0% { transform: scale(1); }
40% { transform: scale(1.06); }
100% { transform: scale(1); }
}