Compare commits
3 commits
156c210dea
...
838013fb9a
| Author | SHA1 | Date | |
|---|---|---|---|
|
838013fb9a |
|||
|
ec318e9e9a |
|||
|
8a8ec9bc0e |
12 changed files with 115 additions and 60 deletions
6
App.tsx
6
App.tsx
|
|
@ -55,7 +55,8 @@ function App() {
|
||||||
currentCorrectShape,
|
currentCorrectShape,
|
||||||
selectedOption,
|
selectedOption,
|
||||||
currentPlayerScore,
|
currentPlayerScore,
|
||||||
currentStreak
|
currentStreak,
|
||||||
|
currentPlayerId
|
||||||
} = useGame();
|
} = useGame();
|
||||||
|
|
||||||
const currentQ = quiz?.questions[currentQuestionIndex];
|
const currentQ = quiz?.questions[currentQuestionIndex];
|
||||||
|
|
@ -90,7 +91,7 @@ function App() {
|
||||||
|
|
||||||
{gameState === 'LOBBY' ? (
|
{gameState === 'LOBBY' ? (
|
||||||
<Lobby
|
<Lobby
|
||||||
quizTitle={quiz?.title || 'OpenHoot'}
|
quizTitle={quiz?.title || 'Kaboot'}
|
||||||
players={players}
|
players={players}
|
||||||
gamePin={gamePin}
|
gamePin={gamePin}
|
||||||
role={role}
|
role={role}
|
||||||
|
|
@ -138,6 +139,7 @@ function App() {
|
||||||
players={players}
|
players={players}
|
||||||
onNext={nextQuestion}
|
onNext={nextQuestion}
|
||||||
isHost={role === 'HOST'}
|
isHost={role === 'HOST'}
|
||||||
|
currentPlayerId={currentPlayerId}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
// Timer styling logic
|
// Timer styling logic
|
||||||
const isUrgent = timeLeft < 5 && timeLeft > 0;
|
const isUrgent = timeLeft < 5 && timeLeft > 0;
|
||||||
const timerBorderColor = isUrgent ? 'border-red-500' : 'border-white';
|
const timerBorderColor = isUrgent ? 'border-red-500' : 'border-white';
|
||||||
const timerTextColor = isUrgent ? 'text-red-500' : 'text-[#46178f]';
|
const timerTextColor = isUrgent ? 'text-red-500' : 'text-theme-primary';
|
||||||
const timerAnimation = isUrgent ? 'animate-ping' : '';
|
const timerAnimation = isUrgent ? 'animate-ping' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -123,7 +123,7 @@ export const GameScreen: React.FC<GameScreenProps> = ({
|
||||||
{isClient && hasAnswered && (
|
{isClient && hasAnswered && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
className="absolute inset-0 bg-[#46178f]/95 flex flex-col items-center justify-center z-50 p-8 text-center"
|
className="absolute inset-0 bg-theme-primary/95 flex flex-col items-center justify-center z-50 p-8 text-center"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ scale: [1, 1.2, 1] }}
|
animate={{ scale: [1, 1.2, 1] }}
|
||||||
|
|
|
||||||
|
|
@ -35,23 +35,23 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
className="bg-white text-gray-900 p-8 rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] max-w-md w-full border-4 border-white/50"
|
className="bg-white text-gray-900 p-8 rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] max-w-md w-full border-4 border-white/50"
|
||||||
>
|
>
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<div className="bg-[#46178f] p-4 rounded-3xl rotate-3 shadow-lg">
|
<div className="bg-theme-primary p-4 rounded-3xl rotate-3 shadow-lg">
|
||||||
<BrainCircuit size={48} className="text-white" />
|
<BrainCircuit size={48} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-5xl font-black mb-2 text-[#46178f] tracking-tight">OpenHoot</h1>
|
<h1 className="text-5xl font-black mb-2 text-theme-primary tracking-tight">Kaboot</h1>
|
||||||
<p className="text-gray-500 font-bold mb-6">The AI Quiz Party</p>
|
<p className="text-gray-500 font-bold mb-6">The AI Quiz Party</p>
|
||||||
|
|
||||||
<div className="flex bg-gray-100 p-2 rounded-2xl mb-8">
|
<div className="flex bg-gray-100 p-2 rounded-2xl mb-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMode('HOST')}
|
onClick={() => setMode('HOST')}
|
||||||
className={`flex-1 py-3 rounded-xl font-black text-lg transition-all duration-200 ${mode === 'HOST' ? 'bg-white shadow-md text-[#46178f] scale-105' : 'text-gray-400 hover:text-gray-600'}`}
|
className={`flex-1 py-3 rounded-xl font-black text-lg transition-all duration-200 ${mode === 'HOST' ? 'bg-white shadow-md text-theme-primary scale-105' : 'text-gray-400 hover:text-gray-600'}`}
|
||||||
>
|
>
|
||||||
Host
|
Host
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setMode('JOIN')}
|
onClick={() => setMode('JOIN')}
|
||||||
className={`flex-1 py-3 rounded-xl font-black text-lg transition-all duration-200 ${mode === 'JOIN' ? 'bg-white shadow-md text-[#46178f] scale-105' : 'text-gray-400 hover:text-gray-600'}`}
|
className={`flex-1 py-3 rounded-xl font-black text-lg transition-all duration-200 ${mode === 'JOIN' ? 'bg-white shadow-md text-theme-primary scale-105' : 'text-gray-400 hover:text-gray-600'}`}
|
||||||
>
|
>
|
||||||
Join
|
Join
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -65,7 +65,7 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
placeholder="Topic (e.g. 'Space')"
|
placeholder="Topic (e.g. 'Space')"
|
||||||
value={topic}
|
value={topic}
|
||||||
onChange={(e) => setTopic(e.target.value)}
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-[#46178f] focus:ring-4 focus:ring-[#46178f]/20 outline-none transition-all placeholder:font-medium text-center"
|
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|
@ -85,7 +85,7 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onCreateManual}
|
onClick={onCreateManual}
|
||||||
className="w-full bg-white border-2 border-[#46178f] text-[#46178f] py-3 rounded-2xl text-lg font-black hover:bg-purple-50 shadow-[0_4px_0_#46178f] active:shadow-none active:translate-y-[4px] transition-all flex items-center justify-center gap-2"
|
className="w-full bg-white border-2 border-theme-primary text-theme-primary py-3 rounded-2xl text-lg font-black hover:bg-theme-hover shadow-[0_4px_0_var(--theme-primary)] active:shadow-none active:translate-y-[4px] transition-all flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<PenTool size={20} /> Create Manually
|
<PenTool size={20} /> Create Manually
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -97,14 +97,14 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
placeholder="Game PIN"
|
placeholder="Game PIN"
|
||||||
value={pin}
|
value={pin}
|
||||||
onChange={(e) => setPin(e.target.value)}
|
onChange={(e) => setPin(e.target.value)}
|
||||||
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-[#46178f] focus:ring-4 focus:ring-[#46178f]/20 outline-none text-center"
|
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Nickname"
|
placeholder="Nickname"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-[#46178f] focus:ring-4 focus:ring-[#46178f]/20 outline-none text-center"
|
className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
|
||||||
<header className="flex flex-col md:flex-row justify-between items-center bg-white/10 p-6 rounded-[2rem] backdrop-blur-md mb-8 gap-6 border-4 border-white/20 shadow-xl">
|
<header className="flex flex-col md:flex-row justify-between items-center bg-white/10 p-6 rounded-[2rem] backdrop-blur-md mb-8 gap-6 border-4 border-white/20 shadow-xl">
|
||||||
<div className="flex flex-col items-center md:items-start">
|
<div className="flex flex-col items-center md:items-start">
|
||||||
<span className="text-white/80 font-bold uppercase tracking-widest text-sm mb-1">Game PIN</span>
|
<span className="text-white/80 font-bold uppercase tracking-widest text-sm mb-1">Game PIN</span>
|
||||||
<div className="text-5xl md:text-6xl font-black bg-white text-[#46178f] px-8 py-2 rounded-full shadow-[0_6px_0_rgba(0,0,0,0.2)] tracking-wider">
|
<div className="text-5xl md:text-6xl font-black bg-white text-theme-primary px-8 py-2 rounded-full shadow-[0_6px_0_rgba(0,0,0,0.2)] tracking-wider">
|
||||||
{gamePin}
|
{gamePin}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,7 +77,7 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
|
||||||
<button
|
<button
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
disabled={realPlayers.length === 0}
|
disabled={realPlayers.length === 0}
|
||||||
className="bg-white text-[#46178f] px-16 py-5 rounded-full text-3xl font-black hover:scale-105 active:scale-95 transition-all shadow-[0_8px_0_rgba(0,0,0,0.2)] disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none disabled:translate-y-2"
|
className="bg-white text-theme-primary px-16 py-5 rounded-full text-3xl font-black hover:scale-105 active:scale-95 transition-all shadow-[0_8px_0_rgba(0,0,0,0.2)] disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none disabled:translate-y-2"
|
||||||
>
|
>
|
||||||
Start Game
|
Start Game
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -89,7 +89,7 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
|
||||||
initial={{ scale: 0.5 }}
|
initial={{ scale: 0.5 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: 'spring', bounce: 0.6 }}
|
transition={{ type: 'spring', bounce: 0.6 }}
|
||||||
className="bg-white text-[#46178f] p-8 rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] mb-8"
|
className="bg-white text-theme-primary p-8 rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] mb-8"
|
||||||
>
|
>
|
||||||
<User size={80} strokeWidth={2.5} />
|
<User size={80} strokeWidth={2.5} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ export const Podium: React.FC<PodiumProps> = ({ players, onRestart }) => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onRestart}
|
onClick={onRestart}
|
||||||
className="flex items-center gap-3 bg-white text-[#46178f] px-10 py-4 rounded-2xl text-2xl font-black hover:scale-105 transition shadow-[0_8px_0_rgba(0,0,0,0.2)] active:shadow-none active:translate-y-[8px]"
|
className="flex items-center gap-3 bg-white text-theme-primary px-10 py-4 rounded-2xl text-2xl font-black hover:scale-105 transition shadow-[0_8px_0_rgba(0,0,0,0.2)] active:shadow-none active:translate-y-[8px]"
|
||||||
>
|
>
|
||||||
<RotateCcw size={28} /> Play Again
|
<RotateCcw size={28} /> Play Again
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 text-gray-900 p-4 md:p-8 flex flex-col items-center">
|
<div className="min-h-screen bg-gray-100 text-gray-900 p-4 md:p-8 flex flex-col items-center">
|
||||||
<div className="max-w-4xl w-full bg-white rounded-[2rem] shadow-xl overflow-hidden border-4 border-white">
|
<div className="max-w-4xl w-full bg-white rounded-[2rem] shadow-xl overflow-hidden border-4 border-white">
|
||||||
<div className="bg-[#46178f] p-8 text-white flex justify-between items-center relative overflow-hidden">
|
<div className="bg-theme-primary p-8 text-white flex justify-between items-center relative overflow-hidden">
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h2 className="text-4xl font-black font-display">Create Quiz</h2>
|
<h2 className="text-4xl font-black font-display">Create Quiz</h2>
|
||||||
<p className="opacity-80 font-bold">Build your masterpiece</p>
|
<p className="opacity-80 font-bold">Build your masterpiece</p>
|
||||||
|
|
@ -83,7 +83,7 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
className="w-full p-4 border-4 border-gray-200 rounded-2xl text-2xl font-bold focus:border-[#46178f] outline-none transition-colors"
|
className="w-full p-4 border-4 border-gray-200 rounded-2xl text-2xl font-bold focus:border-theme-primary outline-none transition-colors"
|
||||||
placeholder="e.g., The Ultimate Trivia"
|
placeholder="e.g., The Ultimate Trivia"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,7 +92,7 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
|
||||||
{questions.map((q, idx) => (
|
{questions.map((q, idx) => (
|
||||||
<div key={q.id} className="border-4 border-gray-100 p-4 rounded-2xl bg-gray-50 flex justify-between items-center group hover:border-gray-200 transition">
|
<div key={q.id} className="border-4 border-gray-100 p-4 rounded-2xl bg-gray-50 flex justify-between items-center group hover:border-gray-200 transition">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="bg-[#46178f] text-white w-10 h-10 flex items-center justify-center rounded-full font-black">
|
<span className="bg-theme-primary text-white w-10 h-10 flex items-center justify-center rounded-full font-black">
|
||||||
{idx + 1}
|
{idx + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-bold text-lg">{q.text}</span>
|
<span className="font-bold text-lg">{q.text}</span>
|
||||||
|
|
@ -149,17 +149,19 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
|
||||||
placeholder={`Option ${idx + 1}`}
|
placeholder={`Option ${idx + 1}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div className="pl-12">
|
||||||
type="text"
|
<input
|
||||||
value={reasons[idx]}
|
type="text"
|
||||||
onChange={(e) => {
|
value={reasons[idx]}
|
||||||
const newReasons = [...reasons];
|
onChange={(e) => {
|
||||||
newReasons[idx] = e.target.value;
|
const newReasons = [...reasons];
|
||||||
setReasons(newReasons);
|
newReasons[idx] = e.target.value;
|
||||||
}}
|
setReasons(newReasons);
|
||||||
className="w-full p-2 ml-12 text-sm outline-none text-gray-500 bg-gray-50 rounded-lg placeholder:text-gray-400"
|
}}
|
||||||
placeholder={isSelected ? "Why is this correct? (optional)" : "Why is this wrong? (optional)"}
|
className="w-full p-2 text-sm outline-none text-gray-500 bg-gray-50 rounded-lg placeholder:text-gray-400"
|
||||||
/>
|
placeholder={isSelected ? "Why is this correct? (optional)" : "Why is this wrong? (optional)"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ interface ScoreboardProps {
|
||||||
players: Player[];
|
players: Player[];
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
isHost: boolean;
|
isHost: boolean;
|
||||||
|
currentPlayerId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost }) => {
|
export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost, currentPlayerId }) => {
|
||||||
const sortedPlayers = [...players].sort((a, b) => b.score - a.score).slice(0, 5);
|
const playersWithDisplayName = players.map(p => ({
|
||||||
|
...p,
|
||||||
|
displayName: p.id === currentPlayerId ? `${p.name} (You)` : p.name
|
||||||
|
}));
|
||||||
|
const sortedPlayers = [...playersWithDisplayName].sort((a, b) => b.score - a.score).slice(0, 5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen p-8">
|
<div className="flex flex-col h-screen p-8">
|
||||||
|
|
@ -29,7 +34,7 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost
|
||||||
<XAxis type="number" hide />
|
<XAxis type="number" hide />
|
||||||
<YAxis
|
<YAxis
|
||||||
type="category"
|
type="category"
|
||||||
dataKey="name"
|
dataKey="displayName"
|
||||||
tick={{ fontSize: 24, fontWeight: 800, fill: '#333', fontFamily: 'Fredoka' }}
|
tick={{ fontSize: 24, fontWeight: 800, fill: '#333', fontFamily: 'Fredoka' }}
|
||||||
width={200}
|
width={200}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
|
|
@ -39,7 +44,7 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost
|
||||||
{sortedPlayers.map((entry, index) => (
|
{sortedPlayers.map((entry, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
key={`cell-${index}`}
|
key={`cell-${index}`}
|
||||||
fill={entry.id.startsWith('host') ? '#46178f' : '#8884d8'}
|
fill={entry.color}
|
||||||
className="filter drop-shadow-md"
|
className="filter drop-shadow-md"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -47,7 +52,7 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost
|
||||||
dataKey="score"
|
dataKey="score"
|
||||||
position="right"
|
position="right"
|
||||||
offset={15}
|
offset={15}
|
||||||
style={{ fontSize: '24px', fontWeight: '900', fill: '#46178f', fontFamily: 'Fredoka' }}
|
style={{ fontSize: '24px', fontWeight: '900', fill: 'var(--theme-primary)', fontFamily: 'Fredoka' }}
|
||||||
/>
|
/>
|
||||||
</Bar>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
|
|
@ -58,7 +63,7 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost
|
||||||
{isHost ? (
|
{isHost ? (
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="bg-white text-[#46178f] px-12 py-4 rounded-2xl text-2xl font-black shadow-[0_8px_0_rgba(0,0,0,0.2)] hover:scale-105 active:shadow-none active:translate-y-[8px] transition-all"
|
className="bg-white text-theme-primary px-12 py-4 rounded-2xl text-2xl font-black shadow-[0_8px_0_rgba(0,0,0,0.2)] hover:scale-105 active:shadow-none active:translate-y-[8px] transition-all"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
19
constants.ts
19
constants.ts
|
|
@ -5,8 +5,8 @@ export const COLORS = {
|
||||||
blue: 'bg-blue-600',
|
blue: 'bg-blue-600',
|
||||||
yellow: 'bg-yellow-600',
|
yellow: 'bg-yellow-600',
|
||||||
green: 'bg-green-600',
|
green: 'bg-green-600',
|
||||||
purple: 'bg-[#46178f]',
|
purple: 'bg-theme-primary',
|
||||||
purpleLight: 'bg-[#864cbf]',
|
purpleLight: 'bg-theme-primary/70',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SHAPES = {
|
export const SHAPES = {
|
||||||
|
|
@ -23,3 +23,18 @@ export const BOT_NAMES = [
|
||||||
|
|
||||||
export const QUESTION_TIME = 20; // seconds
|
export const QUESTION_TIME = 20; // seconds
|
||||||
export const POINTS_PER_QUESTION = 1000;
|
export const POINTS_PER_QUESTION = 1000;
|
||||||
|
|
||||||
|
export const PLAYER_COLORS = [
|
||||||
|
'#2563eb',
|
||||||
|
'#e21b3c',
|
||||||
|
'#26890c',
|
||||||
|
'#ffa602',
|
||||||
|
'#d89e00',
|
||||||
|
'#0aa3a3',
|
||||||
|
'#b8008a',
|
||||||
|
'#6a4c93',
|
||||||
|
'#ff6b6b',
|
||||||
|
'#4ecdc4',
|
||||||
|
'#45b7d1',
|
||||||
|
'#8b5cf6',
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question } from '../types';
|
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question } from '../types';
|
||||||
import { generateQuiz } from '../services/geminiService';
|
import { generateQuiz } from '../services/geminiService';
|
||||||
import { POINTS_PER_QUESTION, QUESTION_TIME } from '../constants';
|
import { POINTS_PER_QUESTION, QUESTION_TIME, PLAYER_COLORS } from '../constants';
|
||||||
import { Peer, DataConnection } from 'peerjs';
|
import { Peer, DataConnection } from 'peerjs';
|
||||||
|
|
||||||
export const useGame = () => {
|
export const useGame = () => {
|
||||||
|
|
@ -19,6 +19,8 @@ export const useGame = () => {
|
||||||
const [selectedOption, setSelectedOption] = useState<AnswerOption | null>(null);
|
const [selectedOption, setSelectedOption] = useState<AnswerOption | null>(null);
|
||||||
const [currentPlayerScore, setCurrentPlayerScore] = useState(0);
|
const [currentPlayerScore, setCurrentPlayerScore] = useState(0);
|
||||||
const [currentStreak, setCurrentStreak] = useState(0);
|
const [currentStreak, setCurrentStreak] = useState(0);
|
||||||
|
const [currentPlayerId, setCurrentPlayerId] = useState<string | null>(null);
|
||||||
|
const [currentPlayerName, setCurrentPlayerName] = useState<string | null>(null);
|
||||||
|
|
||||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const peerRef = useRef<Peer | null>(null);
|
const peerRef = useRef<Peer | null>(null);
|
||||||
|
|
@ -71,20 +73,23 @@ export const useGame = () => {
|
||||||
const pin = generateGamePin();
|
const pin = generateGamePin();
|
||||||
setGamePin(pin);
|
setGamePin(pin);
|
||||||
|
|
||||||
const peer = new Peer(`openhoot-${pin}`);
|
const peer = new Peer(`kaboot-${pin}`);
|
||||||
peerRef.current = peer;
|
peerRef.current = peer;
|
||||||
|
|
||||||
peer.on('open', (id) => {
|
peer.on('open', (id) => {
|
||||||
const hostPlayer: Player = {
|
const hostPlayer: Player = {
|
||||||
id: 'host',
|
id: 'host',
|
||||||
name: 'Host (You)',
|
name: 'Host',
|
||||||
score: 0,
|
score: 0,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
lastAnswerCorrect: null,
|
lastAnswerCorrect: null,
|
||||||
isBot: false,
|
isBot: false,
|
||||||
avatarSeed: Math.random()
|
avatarSeed: Math.random(),
|
||||||
|
color: PLAYER_COLORS[0]
|
||||||
};
|
};
|
||||||
setPlayers([hostPlayer]);
|
setPlayers([hostPlayer]);
|
||||||
|
setCurrentPlayerId('host');
|
||||||
|
setCurrentPlayerName('Host');
|
||||||
setGameState('LOBBY');
|
setGameState('LOBBY');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -103,21 +108,23 @@ export const useGame = () => {
|
||||||
|
|
||||||
const handleHostData = (conn: DataConnection, data: NetworkMessage) => {
|
const handleHostData = (conn: DataConnection, data: NetworkMessage) => {
|
||||||
if (data.type === 'JOIN') {
|
if (data.type === 'JOIN') {
|
||||||
const newPlayer: Player = {
|
|
||||||
id: conn.peer,
|
|
||||||
name: data.payload.name,
|
|
||||||
score: 0,
|
|
||||||
streak: 0,
|
|
||||||
lastAnswerCorrect: null,
|
|
||||||
isBot: false,
|
|
||||||
avatarSeed: Math.random()
|
|
||||||
};
|
|
||||||
setPlayers(prev => {
|
setPlayers(prev => {
|
||||||
if (prev.find(p => p.id === newPlayer.id)) return prev;
|
if (prev.find(p => p.id === conn.peer)) return prev;
|
||||||
|
const colorIndex = prev.length % PLAYER_COLORS.length;
|
||||||
|
const newPlayer: Player = {
|
||||||
|
id: conn.peer,
|
||||||
|
name: data.payload.name,
|
||||||
|
score: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastAnswerCorrect: null,
|
||||||
|
isBot: false,
|
||||||
|
avatarSeed: Math.random(),
|
||||||
|
color: PLAYER_COLORS[colorIndex]
|
||||||
|
};
|
||||||
return [...prev, newPlayer];
|
return [...prev, newPlayer];
|
||||||
});
|
});
|
||||||
connectionsRef.current.set(conn.peer, conn);
|
connectionsRef.current.set(conn.peer, conn);
|
||||||
conn.send({ type: 'WELCOME', payload: { playerId: conn.peer, quizTitle: 'OpenHoot', players: [] } });
|
conn.send({ type: 'WELCOME', payload: { playerId: conn.peer, quizTitle: 'Kaboot', players: [] } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'ANSWER') {
|
if (data.type === 'ANSWER') {
|
||||||
|
|
@ -247,11 +254,13 @@ export const useGame = () => {
|
||||||
setRole('CLIENT');
|
setRole('CLIENT');
|
||||||
setError(null);
|
setError(null);
|
||||||
setGamePin(pin);
|
setGamePin(pin);
|
||||||
|
setCurrentPlayerName(name);
|
||||||
const peer = new Peer();
|
const peer = new Peer();
|
||||||
peerRef.current = peer;
|
peerRef.current = peer;
|
||||||
|
|
||||||
peer.on('open', () => {
|
peer.on('open', (id) => {
|
||||||
const conn = peer.connect(`openhoot-${pin}`);
|
setCurrentPlayerId(id);
|
||||||
|
const conn = peer.connect(`kaboot-${pin}`);
|
||||||
hostConnectionRef.current = conn;
|
hostConnectionRef.current = conn;
|
||||||
conn.on('open', () => {
|
conn.on('open', () => {
|
||||||
conn.send({ type: 'JOIN', payload: { name } });
|
conn.send({ type: 'JOIN', payload: { name } });
|
||||||
|
|
@ -369,7 +378,7 @@ export const useGame = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak,
|
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId,
|
||||||
startQuizGen, startManualCreation, finalizeManualQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion
|
startQuizGen, startManualCreation, finalizeManualQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
27
index.html
27
index.html
|
|
@ -3,14 +3,35 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OpenHoot</title>
|
<title>Kaboot</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
theme: {
|
||||||
|
primary: 'var(--theme-primary)',
|
||||||
|
'primary-dark': 'var(--theme-primary-dark)',
|
||||||
|
hover: 'var(--theme-hover-bg)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@300;400;600;700&family=Montserrat:wght@400;700;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@300;400;600;700&family=Montserrat:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--theme-primary: #2563eb;
|
||||||
|
--theme-primary-dark: #1e40af;
|
||||||
|
--theme-primary-darker: #1e3a5f;
|
||||||
|
--theme-hover-bg: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Montserrat', sans-serif;
|
font-family: 'Montserrat', sans-serif;
|
||||||
/* Richer purple gradient */
|
background: linear-gradient(180deg, var(--theme-primary) 0%, var(--theme-primary-darker) 100%);
|
||||||
background: linear-gradient(180deg, #46178f 0%, #25094f 100%);
|
|
||||||
color: white;
|
color: white;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
|
||||||
2
tasks.md
2
tasks.md
|
|
@ -1,4 +1,4 @@
|
||||||
# OpenHoot Development Task List
|
# Kaboot Development Task List
|
||||||
|
|
||||||
## Core Setup
|
## Core Setup
|
||||||
- [x] Initialize project structure (React, TypeScript, Vite-like setup)
|
- [x] Initialize project structure (React, TypeScript, Vite-like setup)
|
||||||
|
|
|
||||||
1
types.ts
1
types.ts
|
|
@ -39,6 +39,7 @@ export interface Player {
|
||||||
lastAnswerCorrect: boolean | null;
|
lastAnswerCorrect: boolean | null;
|
||||||
isBot: boolean;
|
isBot: boolean;
|
||||||
avatarSeed: number;
|
avatarSeed: number;
|
||||||
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network Types
|
// Network Types
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue