feat: Initialize BuzzMaster project structure

Sets up the foundational project structure for the BuzzMaster Live Quiz Platform. This includes:

- **Project Initialization:** Creates `package.json` with necessary dependencies (React, React Router DOM, Vite, TypeScript).
- **Vite Configuration:** Configures Vite for development and building, including server settings and environment variable handling.
- **HTML Entry Point:** Sets up `index.html` with basic structure, Tailwind CSS, Google Fonts, and ESM import maps.
- **React Entry Point:** Configures `index.tsx` to render the main `App` component.
- **TypeScript Configuration:** Defines `tsconfig.json` for the project.
- **Git Ignore:** Adds standard files and directories to `.gitignore`.
- **README and Metadata:** Includes a basic `README.md` and `metadata.json` describing the project.
- **Type Definitions:** Establishes core type definitions in `types.ts` for game state, user roles, players, teams, and questions.
- **App Component:** Creates a basic `App.tsx` component with routing and initial game state management, including logic for local storage fallback and API sync.
- **Component Stubs:** Adds placeholder components for `PlayerView`, `AdminDashboard`, and `SpectatorView`.
This commit is contained in:
Philip
2026-05-18 16:07:02 -07:00
parent 27b22b0f73
commit e50e6f83dc
18 changed files with 1046 additions and 8 deletions
+207
View File
@@ -0,0 +1,207 @@
import React, { useState } from 'react';
import { GameData, GameState, Player, Question } from '../types';
interface AdminDashboardProps {
game: GameData;
updateGame: (data: Partial<GameData>) => void;
}
const AdminDashboard: React.FC<AdminDashboardProps> = ({ game, updateGame }) => {
const [activeTab, setActiveTab] = useState<'control' | 'lobby' | 'questions'>('control');
const approvePlayer = (id: string) => {
const updated = game.players.map(p => p.id === id ? { ...p, status: 'APPROVED' } : p);
updateGame({ players: updated as Player[] });
};
const startGame = () => {
updateGame({ state: GameState.LOBBY });
};
const startCountdown = () => {
updateGame({ state: GameState.COUNTDOWN, countdownValue: 3 });
let count = 3;
const interval = setInterval(() => {
count -= 1;
if (count <= 0) {
clearInterval(interval);
updateGame({ state: GameState.QUESTION, countdownValue: 0, activeBuzzerQueue: [] });
} else {
updateGame({ countdownValue: count });
}
}, 1000);
};
const adjudicate = (isCorrect: boolean) => {
const firstBuzzer = game.activeBuzzerQueue[0];
if (!firstBuzzer) return;
const currentQ = game.questions[game.currentQuestionIndex];
if (isCorrect) {
const updatedPlayers = game.players.map(p => {
if (p.id === firstBuzzer.playerId) {
return { ...p, score: p.score + currentQ.points, correctAnswers: p.correctAnswers + 1 };
}
return p;
});
updateGame({
state: GameState.REVEAL,
players: updatedPlayers as Player[]
});
} else {
const updatedQueue = game.activeBuzzerQueue.slice(1);
const updatedPlayers = game.players.map(p => {
if (p.id === firstBuzzer.playerId) {
return { ...p, wrongAnswers: p.wrongAnswers + 1 };
}
return p;
});
updateGame({
activeBuzzerQueue: updatedQueue,
players: updatedPlayers as Player[]
});
}
};
const nextQuestion = () => {
const nextIdx = game.currentQuestionIndex + 1;
if (nextIdx >= game.questions.length) {
updateGame({ state: GameState.FINAL_STATS });
} else {
updateGame({ state: GameState.SCOREBOARD, currentQuestionIndex: nextIdx });
}
};
const currentQ = game.questions[game.currentQuestionIndex];
return (
<div className="flex h-screen bg-slate-900 overflow-hidden">
{/* Sidebar */}
<div className="w-64 bg-slate-950 border-r border-white/5 flex flex-col">
<div className="p-6 font-bold text-xl border-b border-white/5">HOST PANEL</div>
<nav className="flex-1 p-4 space-y-2">
<NavBtn active={activeTab === 'control'} onClick={() => setActiveTab('control')} label="Control Center" />
<NavBtn active={activeTab === 'lobby'} onClick={() => setActiveTab('lobby')} label="Lobby & Players" />
<NavBtn active={activeTab === 'questions'} onClick={() => setActiveTab('questions')} label="Question List" />
</nav>
<div className="p-4 border-t border-white/5 space-y-2">
<SoundBtn label="DING" color="bg-green-600" />
<SoundBtn label="BUZZ" color="bg-red-600" />
</div>
</div>
{/* Main Area */}
<main className="flex-1 p-8 overflow-y-auto">
{activeTab === 'lobby' && (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Lobby Management</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{game.players.map(p => (
<div key={p.id} className="glass p-4 rounded-xl flex items-center justify-between">
<div>
<div className="font-bold">{p.name}</div>
<div className="text-sm opacity-50">Team: {p.teamId}</div>
</div>
{p.status === 'PENDING' ? (
<button onClick={() => approvePlayer(p.id)} className="px-4 py-2 bg-blue-600 rounded-lg text-sm">Approve</button>
) : (
<span className="text-green-500 text-sm font-bold">ACTIVE</span>
)}
</div>
))}
</div>
{game.state === GameState.LOBBY && (
<button onClick={startGame} className="px-8 py-3 bg-indigo-600 rounded-xl font-bold">Finalize Lobby & Start</button>
)}
</div>
)}
{activeTab === 'control' && (
<div className="space-y-8">
<div className="flex justify-between items-start">
<div>
<h2 className="text-4xl font-black mb-2">Question {game.currentQuestionIndex + 1}</h2>
<p className="text-xl opacity-70 max-w-2xl">{currentQ.text}</p>
<p className="text-green-400 mt-2 font-bold">A: {currentQ.answer}</p>
</div>
<div className="text-right">
<div className="text-sm opacity-50">Points Value</div>
<div className="text-3xl font-bold">{currentQ.points}</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Controls */}
<div className="glass p-6 rounded-3xl border border-white/10 space-y-4">
<h3 className="text-xl font-bold border-b border-white/5 pb-2">Game Actions</h3>
<div className="grid grid-cols-2 gap-4">
<button
disabled={game.state !== GameState.SCOREBOARD && game.state !== GameState.LOBBY}
onClick={startCountdown}
className="p-6 bg-blue-600 disabled:opacity-30 rounded-2xl font-bold"
>
START COUNTDOWN
</button>
<button
disabled={game.state !== GameState.REVEAL}
onClick={nextQuestion}
className="p-6 bg-indigo-600 disabled:opacity-30 rounded-2xl font-bold"
>
NEXT SLIDE
</button>
</div>
<div className="space-y-2 mt-4">
<h4 className="text-sm font-bold opacity-50">Adjudication</h4>
<div className="flex gap-4">
<button onClick={() => adjudicate(true)} className="flex-1 py-4 bg-green-600 rounded-xl font-bold">CORRECT</button>
<button onClick={() => adjudicate(false)} className="flex-1 py-4 bg-red-600 rounded-xl font-bold">WRONG</button>
</div>
</div>
</div>
{/* Buzzer Queue */}
<div className="glass p-6 rounded-3xl border border-white/10">
<h3 className="text-xl font-bold border-b border-white/5 pb-2 mb-4">Buzzer Queue</h3>
<div className="space-y-3">
{game.activeBuzzerQueue.length === 0 && <div className="text-center py-8 opacity-40">Waiting for buzzes...</div>}
{game.activeBuzzerQueue.map((buzz, i) => {
const p = game.players.find(pl => pl.id === buzz.playerId);
return (
<div key={buzz.playerId} className={`p-4 rounded-xl flex justify-between items-center ${i === 0 ? 'bg-green-500/20 border border-green-500/50' : 'bg-white/5'}`}>
<div className="flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-white/10 flex items-center justify-center text-xs">{i + 1}</span>
<span className="font-bold">{p?.name || 'Unknown'}</span>
</div>
<div className="text-xs opacity-50">{buzz.timestamp % 10000}ms</div>
</div>
);
})}
</div>
</div>
</div>
</div>
)}
</main>
</div>
);
};
const NavBtn: React.FC<{ active: boolean; label: string; onClick: () => void }> = ({ active, label, onClick }) => (
<button
onClick={onClick}
className={`w-full text-left p-3 rounded-lg transition-colors ${active ? 'bg-blue-600 text-white font-bold' : 'hover:bg-white/5 text-white/60'}`}
>
{label}
</button>
);
const SoundBtn: React.FC<{ label: string; color: string }> = ({ label, color }) => (
<button className={`w-full py-2 ${color} rounded-lg text-xs font-bold shadow-lg active:scale-95 transition-transform`}>
{label}
</button>
);
export default AdminDashboard;
+141
View File
@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { GameData, GameState, Player } from '../types';
interface PlayerViewProps {
game: GameData;
updateGame: (data: Partial<GameData>) => void;
currentUser: Player | null;
setCurrentUser: (p: Player) => void;
}
const PlayerView: React.FC<PlayerViewProps> = ({ game, updateGame, currentUser, setCurrentUser }) => {
const [name, setName] = useState('');
const [teamName, setTeamName] = useState('');
const handleJoin = (e: React.FormEvent) => {
e.preventDefault();
if (!name || !teamName) return;
const newPlayer: Player = {
id: Math.random().toString(36).substr(2, 9),
name,
teamId: teamName, // Simplified team logic
status: 'PENDING',
score: 0,
correctAnswers: 0,
wrongAnswers: 0,
avgBuzzerMs: 0
};
setCurrentUser(newPlayer);
updateGame({ players: [...game.players, newPlayer] });
};
const handleBuzz = () => {
if (!currentUser || game.state !== GameState.QUESTION) return;
const timestamp = Date.now();
const alreadyBuzzed = game.activeBuzzerQueue.some(b => b.playerId === currentUser.id);
if (!alreadyBuzzed) {
updateGame({
state: GameState.BUZZED,
activeBuzzerQueue: [...game.activeBuzzerQueue, { playerId: currentUser.id, timestamp }]
});
}
};
if (!currentUser) {
return (
<div className="flex flex-col items-center justify-center p-6 min-h-screen bg-slate-950">
<form onSubmit={handleJoin} className="w-full max-w-sm space-y-4 glass p-8 rounded-3xl border border-white/10">
<h2 className="text-2xl font-bold text-center mb-6">Join the Game</h2>
<div>
<label className="block text-sm font-medium mb-1 opacity-70">Your Name</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full p-4 bg-white/5 border border-white/10 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="e.g. John Doe"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 opacity-70">Team Name</label>
<input
type="text"
value={teamName}
onChange={e => setTeamName(e.target.value)}
className="w-full p-4 bg-white/5 border border-white/10 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="e.g. The Nerds"
/>
</div>
<button type="submit" className="w-full py-4 bg-blue-600 rounded-xl font-bold text-lg hover:bg-blue-500 transition-colors">
Enter Lobby
</button>
</form>
</div>
);
}
if (currentUser.status === 'PENDING') {
return (
<div className="flex flex-col items-center justify-center p-6 min-h-screen text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500 mb-6"></div>
<h2 className="text-2xl font-bold mb-2">Waiting for Host...</h2>
<p className="opacity-70">Hang tight, {currentUser.name}! The host will approve you shortly.</p>
</div>
);
}
const isFirst = game.activeBuzzerQueue[0]?.playerId === currentUser.id;
const hasBuzzed = game.activeBuzzerQueue.some(b => b.playerId === currentUser.id);
return (
<div className="flex flex-col items-center p-4 min-h-screen bg-slate-900 overflow-hidden">
<div className="w-full flex justify-between items-center mb-8 px-4 py-2 glass rounded-2xl border border-white/5">
<div className="font-bold">{currentUser.name}</div>
<div className="text-blue-400 font-bold">{teamName}</div>
</div>
<div className="flex-1 flex flex-col items-center justify-center w-full">
{game.state === GameState.LOBBY && (
<div className="text-center italic opacity-60">Game will start soon...</div>
)}
{game.state === GameState.COUNTDOWN && (
<div className="text-8xl font-black text-blue-500 animate-bounce">{game.countdownValue}</div>
)}
{game.state === GameState.QUESTION && (
<button
onClick={handleBuzz}
className="w-64 h-64 rounded-full bg-red-600 shadow-2xl active:scale-90 transition-all border-[12px] border-red-800 buzzer-ready flex items-center justify-center text-3xl font-black italic uppercase"
>
BUZZ!
</button>
)}
{game.state === GameState.BUZZED && (
<div className={`w-64 h-64 rounded-full flex items-center justify-center text-2xl font-bold border-[12px] ${isFirst ? 'bg-green-600 border-green-800' : 'bg-slate-700 border-slate-800 opacity-50'}`}>
{isFirst ? 'YOUR TURN!' : hasBuzzed ? 'LOCKED' : 'WAITING'}
</div>
)}
{(game.state === GameState.REVEAL || game.state === GameState.SCOREBOARD) && (
<div className="text-center">
<h3 className="text-2xl font-bold mb-2">Well Played!</h3>
<p className="opacity-60">Get ready for the next one.</p>
</div>
)}
</div>
<div className="w-full mt-4 text-center text-xs opacity-40">
Lat: ~15ms
</div>
</div>
);
};
export default PlayerView;
+143
View File
@@ -0,0 +1,143 @@
import React from 'react';
import { GameData, GameState } from '../types';
interface SpectatorViewProps {
game: GameData;
}
const SpectatorView: React.FC<SpectatorViewProps> = ({ game }) => {
const currentQ = game.questions[game.currentQuestionIndex];
return (
<div className="h-screen w-full bg-slate-900 flex flex-col items-center justify-center overflow-hidden p-12">
{/* Background Ambience */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-0 -left-20 w-96 h-96 bg-blue-500 rounded-full blur-[120px] animate-pulse"></div>
<div className="absolute bottom-0 -right-20 w-96 h-96 bg-purple-500 rounded-full blur-[120px] animate-pulse"></div>
</div>
<div className="relative z-10 w-full max-w-6xl h-full flex flex-col items-center justify-center text-center">
{game.state === GameState.LOBBY && (
<div className="space-y-12">
<h1 className="text-7xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-500">
JOIN THE QUIZ!
</h1>
<div className="flex justify-center gap-12">
<div className="p-8 bg-white rounded-3xl shadow-2xl">
{/* Simulate QR Code */}
<div className="w-48 h-48 bg-slate-900 rounded-xl flex items-center justify-center">
<div className="grid grid-cols-4 gap-2">
{[...Array(16)].map((_, i) => (
<div key={i} className={`w-6 h-6 rounded-sm ${Math.random() > 0.5 ? 'bg-white' : 'bg-transparent'}`}></div>
))}
</div>
</div>
</div>
<div className="text-left flex flex-col justify-center">
<div className="text-3xl font-bold opacity-60">Visit URL:</div>
<div className="text-5xl font-black text-blue-400">QUIZ.LIVE/BM-123</div>
</div>
</div>
<div className="pt-12">
<h3 className="text-2xl font-bold opacity-40 mb-4">PLAYERS JOINED:</h3>
<div className="flex flex-wrap justify-center gap-4">
{game.players.filter(p => p.status === 'APPROVED').map(p => (
<span key={p.id} className="px-6 py-2 glass border border-white/10 rounded-full font-bold animate-bounce" style={{ animationDelay: `${Math.random()}s` }}>
{p.name}
</span>
))}
</div>
</div>
</div>
)}
{game.state === GameState.COUNTDOWN && (
<div className="text-[20rem] font-black italic text-transparent bg-clip-text bg-gradient-to-b from-blue-400 to-indigo-800 animate-ping">
{game.countdownValue}
</div>
)}
{game.state === GameState.QUESTION && (
<div className="space-y-12 animate-in fade-in zoom-in duration-500">
<div className="px-8 py-4 bg-blue-600 rounded-2xl text-2xl font-bold inline-block">QUESTION {game.currentQuestionIndex + 1}</div>
<h2 className="text-7xl font-bold leading-tight drop-shadow-2xl">
{currentQ.text}
</h2>
<div className="text-3xl font-bold text-white/40 italic">Waiting for buzzers...</div>
</div>
)}
{game.state === GameState.BUZZED && (
<div className="space-y-12 w-full">
<h2 className="text-4xl opacity-50 font-bold">{currentQ.text}</h2>
<div className="flex flex-col items-center">
<div className="text-2xl font-bold mb-4 px-6 py-2 bg-yellow-500 rounded-xl text-slate-900">PLAYER BUZZED!</div>
<div className="text-9xl font-black text-white px-12 py-8 bg-blue-600 rounded-[3rem] shadow-[0_0_100px_rgba(37,99,235,0.6)] border-8 border-blue-400">
{game.players.find(p => p.id === game.activeBuzzerQueue[0]?.playerId)?.name || 'Someone'}
</div>
</div>
</div>
)}
{game.state === GameState.REVEAL && (
<div className="space-y-12 text-center animate-in slide-in-from-bottom duration-700">
<h2 className="text-4xl opacity-50 font-bold">THE ANSWER IS...</h2>
<div className="text-9xl font-black text-green-400 drop-shadow-[0_0_30px_rgba(74,222,128,0.5)]">
{currentQ.answer}
</div>
<div className="text-4xl font-bold text-white">Points awarded to {game.players.find(p => p.id === game.activeBuzzerQueue[0]?.playerId)?.teamId}!</div>
</div>
)}
{game.state === GameState.SCOREBOARD && (
<div className="w-full max-w-4xl space-y-8">
<h2 className="text-6xl font-black mb-12 italic tracking-tighter underline decoration-blue-500 underline-offset-8">LEADERBOARD</h2>
<div className="space-y-4">
{game.players
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((p, i) => (
<div key={p.id} className="flex items-center justify-between p-6 glass rounded-2xl border border-white/10 shadow-xl overflow-hidden relative">
<div className="absolute left-0 top-0 bottom-0 w-2 bg-blue-500"></div>
<div className="flex items-center gap-6">
<span className="text-4xl font-black opacity-20">{i+1}</span>
<span className="text-3xl font-bold">{p.name}</span>
<span className="text-xl opacity-40">({p.teamId})</span>
</div>
<div className="text-4xl font-black text-blue-400">{p.score} pts</div>
</div>
))}
</div>
</div>
)}
{game.state === GameState.FINAL_STATS && (
<div className="space-y-12 w-full">
<h1 className="text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-500">GAME OVER!</h1>
<div className="grid grid-cols-3 gap-8">
<StatBox label="The Flash" sub="Fastest Buzzer" value="Sarah" />
<StatBox label="The Brain" sub="Most Correct" value="John" />
<StatBox label="The Guesser" sub="Most Misses" value="Mike" />
</div>
<div className="mt-12 p-8 glass rounded-[3rem] border border-white/20">
<h2 className="text-4xl font-bold mb-4">🏆 FINAL CHAMPIONS 🏆</h2>
<div className="text-7xl font-black text-blue-400 uppercase">THE NERDS</div>
</div>
</div>
)}
</div>
</div>
);
};
const StatBox: React.FC<{ label: string; sub: string; value: string }> = ({ label, sub, value }) => (
<div className="glass p-8 rounded-3xl border border-white/5 space-y-2">
<div className="text-sm font-bold text-blue-400 uppercase tracking-widest">{sub}</div>
<div className="text-xl font-bold opacity-60">{label}</div>
<div className="text-4xl font-black">{value}</div>
</div>
);
export default SpectatorView;