166 lines
No EOL
7.3 KiB
TypeScript
166 lines
No EOL
7.3 KiB
TypeScript
import React from 'react';
|
|
import { Question, AnswerOption, GameState, GameRole } from '../types';
|
|
import { COLORS, SHAPES } from '../constants';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
interface GameScreenProps {
|
|
question?: Question;
|
|
timeLeft: number;
|
|
totalQuestions: number;
|
|
currentQuestionIndex: number;
|
|
gameState: GameState;
|
|
role: GameRole;
|
|
onAnswer: (isCorrect: boolean) => void;
|
|
hasAnswered: boolean;
|
|
lastPointsEarned: number | null;
|
|
hostPlays?: boolean;
|
|
}
|
|
|
|
export const GameScreen: React.FC<GameScreenProps> = ({
|
|
question,
|
|
timeLeft,
|
|
totalQuestions,
|
|
currentQuestionIndex,
|
|
gameState,
|
|
role,
|
|
onAnswer,
|
|
hasAnswered,
|
|
hostPlays = true,
|
|
}) => {
|
|
const isClient = role === 'CLIENT';
|
|
const isSpectator = role === 'HOST' && !hostPlays;
|
|
const displayOptions = question?.options || [];
|
|
const timeLeftSeconds = Math.ceil(timeLeft / 1000);
|
|
|
|
const isUrgent = timeLeftSeconds <= 5 && timeLeftSeconds > 0;
|
|
const timerBorderColor = isUrgent ? 'border-red-500' : 'border-white';
|
|
const timerTextColor = isUrgent ? 'text-red-500' : 'text-theme-primary';
|
|
const timerAnimation = isUrgent ? 'animate-ping' : '';
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen max-h-screen overflow-hidden relative">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center p-2 md:p-6 shrink-0">
|
|
<div className="bg-white/20 backdrop-blur-md px-3 md:px-6 py-1 md:py-2 rounded-xl md:rounded-2xl font-black text-sm md:text-xl shadow-sm border-2 border-white/10">
|
|
{currentQuestionIndex + 1} / {totalQuestions}
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-white/20 rounded-full blur-xl animate-pulse"></div>
|
|
<div className={`bg-white ${timerTextColor} rounded-full w-14 h-14 md:w-20 md:h-20 flex items-center justify-center text-2xl md:text-4xl font-black shadow-[0_6px_0_rgba(0,0,0,0.2)] border-4 ${timerBorderColor} ${timerAnimation} relative z-10 transition-colors duration-300`}>
|
|
{timeLeftSeconds}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white/20 backdrop-blur-md px-3 md:px-6 py-1 md:py-2 rounded-xl md:rounded-2xl font-black text-sm md:text-xl shadow-sm border-2 border-white/10">
|
|
{isClient ? 'Player' : isSpectator ? 'Spectator' : 'Host'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Question Area */}
|
|
<div className="shrink-0 flex flex-col items-center justify-center p-2 md:p-8 text-center relative z-10">
|
|
{question && (
|
|
<motion.div
|
|
key={question.id}
|
|
initial={{ y: 20, opacity: 0, scale: 0.95 }}
|
|
animate={{ y: 0, opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
className="bg-white text-black p-4 md:p-12 rounded-2xl md:rounded-[2rem] shadow-[0_12px_0_rgba(0,0,0,0.1)] max-w-5xl w-full border-b-4 md:border-b-8 border-gray-200"
|
|
>
|
|
<h2 className="text-lg md:text-5xl font-black text-[#333] font-display leading-tight">
|
|
{question.text}
|
|
</h2>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Answer Grid */}
|
|
<div className="grid grid-cols-2 gap-2 md:gap-6 p-2 md:p-6 flex-1 min-h-0">
|
|
{displayOptions.map((option, idx) => {
|
|
const ShapeIcon = SHAPES[option.shape];
|
|
const colorClass = COLORS[option.color];
|
|
|
|
let opacityClass = "opacity-100";
|
|
let scaleClass = "scale-100";
|
|
let cursorClass = "";
|
|
|
|
if (isSpectator) {
|
|
cursorClass = "cursor-default";
|
|
} else if (hasAnswered) {
|
|
opacityClass = "opacity-50 cursor-not-allowed grayscale";
|
|
scaleClass = "scale-95";
|
|
}
|
|
|
|
return (
|
|
<motion.button
|
|
key={idx}
|
|
initial={{ y: 50, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
transition={{ delay: idx * 0.03, type: 'spring', stiffness: 500, damping: 30 }}
|
|
disabled={hasAnswered || isSpectator}
|
|
onClick={() => !isSpectator && onAnswer(option as any)}
|
|
className={`
|
|
${colorClass} ${opacityClass} ${scaleClass} ${cursorClass}
|
|
rounded-3xl shadow-[0_8px_0_rgba(0,0,0,0.2)]
|
|
flex flex-col md:flex-row items-center justify-center md:justify-start
|
|
p-4 md:p-8
|
|
${!isSpectator ? 'active:shadow-none active:translate-y-[8px] active:scale-95' : ''}
|
|
transition-all duration-300 relative group overflow-hidden border-b-8 border-black/10
|
|
`}
|
|
>
|
|
<ShapeIcon className="absolute -right-6 -bottom-6 w-32 h-32 text-black/10 rotate-12 group-hover:rotate-45 transition-transform duration-500" />
|
|
|
|
<div className="flex-shrink-0 mb-2 md:mb-0 md:mr-6 bg-black/20 p-3 md:p-4 rounded-2xl shadow-inner">
|
|
<ShapeIcon className="w-8 h-8 md:w-12 md:h-12 text-white" fill="currentColor" />
|
|
</div>
|
|
|
|
<span className="text-lg md:text-3xl font-black text-white text-center md:text-left drop-shadow-md leading-tight relative z-10">
|
|
{option.text}
|
|
</span>
|
|
</motion.button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* "Answer Sent" Overlay */}
|
|
<AnimatePresence>
|
|
{isClient && hasAnswered && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="absolute inset-0 bg-black/20 backdrop-blur-[2px] flex flex-col items-center justify-center z-50 p-4"
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
className="bg-theme-primary/40 backdrop-blur-xl border border-white/20 p-8 md:p-12 rounded-[2.5rem] shadow-[0_20px_50px_rgba(0,0,0,0.5)] text-center max-w-md w-full relative overflow-hidden"
|
|
>
|
|
<div className="absolute -inset-full bg-gradient-to-tr from-transparent via-white/10 to-transparent rotate-45 animate-pulse pointer-events-none" />
|
|
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-white/10 to-transparent pointer-events-none" />
|
|
|
|
<motion.div
|
|
animate={{
|
|
y: [0, -15, 0],
|
|
rotate: [0, 5, -5, 0],
|
|
scale: [1, 1.1, 1]
|
|
}}
|
|
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
|
className="text-7xl mb-6 drop-shadow-2xl relative z-10 inline-block filter"
|
|
>
|
|
🚀
|
|
</motion.div>
|
|
<h2 className="text-4xl font-black text-white font-display mb-3 drop-shadow-lg relative z-10 tracking-tight">
|
|
Answer Sent!
|
|
</h2>
|
|
<p className="text-lg font-bold text-white/80 relative z-10">
|
|
Cross your fingers...
|
|
</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}; |