246 lines
9.4 KiB
TypeScript
246 lines
9.4 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { motion, useSpring, useTransform } from 'framer-motion';
|
|
import { Check, X, Flame, ChevronRight } from 'lucide-react';
|
|
import { AnswerOption, Player, GameRole } from '../types';
|
|
import { SHAPES, COLORS } from '../constants';
|
|
import confetti from 'canvas-confetti';
|
|
|
|
const AnimatedCounter: React.FC<{ from: number; to: number }> = ({ from, to }) => {
|
|
const springValue = useSpring(from, {
|
|
stiffness: 50,
|
|
damping: 20,
|
|
});
|
|
const [displayValue, setDisplayValue] = useState(from);
|
|
|
|
useEffect(() => {
|
|
springValue.set(to);
|
|
}, [to, springValue]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = springValue.on('change', (latest) => {
|
|
setDisplayValue(Math.round(Number(latest)));
|
|
});
|
|
return unsubscribe;
|
|
}, [springValue]);
|
|
|
|
return <span>{displayValue}</span>;
|
|
};
|
|
|
|
interface RevealScreenProps {
|
|
isCorrect: boolean;
|
|
pointsEarned: number;
|
|
newScore: number;
|
|
streak: number;
|
|
correctOption: AnswerOption;
|
|
selectedOption?: AnswerOption | null;
|
|
role: GameRole;
|
|
onNext?: () => void;
|
|
isPresenter?: boolean;
|
|
onPresenterAdvance?: () => void;
|
|
}
|
|
|
|
export const RevealScreen: React.FC<RevealScreenProps> = ({
|
|
isCorrect,
|
|
pointsEarned,
|
|
newScore,
|
|
streak,
|
|
correctOption,
|
|
selectedOption,
|
|
role,
|
|
onNext,
|
|
isPresenter = false,
|
|
onPresenterAdvance
|
|
}) => {
|
|
const isHost = role === 'HOST';
|
|
const canAdvance = isHost || isPresenter;
|
|
|
|
// Trigger confetti for correct answers
|
|
useEffect(() => {
|
|
if (isCorrect && !isHost) {
|
|
confetti({
|
|
particleCount: 100,
|
|
spread: 70,
|
|
origin: { y: 0.6 },
|
|
colors: ['#22c55e', '#ffffff', '#fbbf24']
|
|
});
|
|
}
|
|
}, [isCorrect, isHost]);
|
|
|
|
if (isHost) {
|
|
const ShapeIcon = SHAPES[correctOption.shape];
|
|
const colorClass = COLORS[correctOption.color];
|
|
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-screen bg-gray-900 text-white p-8 relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10 pointer-events-none"></div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -50 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="text-4xl font-bold uppercase tracking-widest mb-12 opacity-80 relative z-10"
|
|
>
|
|
The correct answer is
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ scale: 0, rotate: -10 }}
|
|
animate={{ scale: 1, rotate: 0 }}
|
|
transition={{ type: "spring", bounce: 0.5 }}
|
|
className={`${colorClass} p-12 rounded-[3rem] shadow-[0_20px_0_rgba(0,0,0,0.3)] flex flex-col items-center max-w-4xl w-full border-8 border-white/20 relative z-10`}
|
|
>
|
|
<div className="bg-black/20 p-6 rounded-full mb-6">
|
|
<ShapeIcon size={80} fill="currentColor" />
|
|
</div>
|
|
<h1 className="text-5xl md:text-7xl font-black font-display text-center drop-shadow-md leading-tight">
|
|
{correctOption.text}
|
|
</h1>
|
|
{correctOption.reason && (
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="mt-6 text-xl md:text-2xl text-center opacity-90 bg-black/20 px-6 py-4 rounded-2xl max-w-2xl"
|
|
>
|
|
{correctOption.reason}
|
|
</motion.p>
|
|
)}
|
|
</motion.div>
|
|
|
|
{onNext && (
|
|
<motion.button
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.5 }}
|
|
onClick={onNext}
|
|
className="mt-12 bg-white text-gray-900 px-8 py-4 rounded-2xl text-xl font-black shadow-[0_6px_0_rgba(0,0,0,0.3)] active:shadow-none active:translate-y-[6px] transition-all flex items-center gap-2 hover:bg-gray-100 relative z-10 cursor-pointer"
|
|
>
|
|
Continue to Scoreboard
|
|
<ChevronRight size={28} strokeWidth={3} />
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// -- CLIENT VIEW --
|
|
const bgColor = isCorrect ? 'bg-[#22c55e]' : 'bg-[#ef4444]';
|
|
const darkerColor = isCorrect ? 'bg-[#15803d]' : 'bg-[#b91c1c]';
|
|
const ShapeIcon = SHAPES[correctOption.shape];
|
|
|
|
return (
|
|
<div className={`flex flex-col items-center justify-center h-screen ${bgColor} text-white p-6 relative overflow-hidden transition-colors duration-500`}>
|
|
|
|
{/* Dynamic Background Circles */}
|
|
<motion.div
|
|
animate={{ scale: [1, 1.2, 1], opacity: [0.1, 0.2, 0.1] }}
|
|
transition={{ repeat: Infinity, duration: 4 }}
|
|
className="absolute w-[800px] h-[800px] bg-white rounded-full blur-3xl opacity-10 pointer-events-none"
|
|
/>
|
|
|
|
<motion.div
|
|
initial={{ scale: 0.5, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
transition={{ type: "spring", bounce: 0.6 }}
|
|
className="flex flex-col items-center z-10"
|
|
>
|
|
<div className="bg-white p-6 rounded-full shadow-[0_10px_0_rgba(0,0,0,0.2)] mb-8">
|
|
{isCorrect ? (
|
|
<Check size={80} className="text-[#22c55e]" strokeWidth={4} />
|
|
) : (
|
|
<X size={80} className="text-[#ef4444]" strokeWidth={4} />
|
|
)}
|
|
</div>
|
|
|
|
<h1 className="text-6xl md:text-8xl font-black font-display mb-4 drop-shadow-lg text-center">
|
|
{isCorrect ? "Correct!" : "Incorrect"}
|
|
</h1>
|
|
|
|
<motion.div
|
|
initial={{ y: 20, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
transition={{ delay: 0.2 }}
|
|
className="flex flex-col items-center"
|
|
>
|
|
{isCorrect ? (
|
|
<div className="flex flex-col items-center gap-4 mb-8">
|
|
<div className="bg-black/20 px-8 py-4 rounded-3xl backdrop-blur-sm border-4 border-white/30 flex items-center gap-4">
|
|
<span className="text-4xl font-black">+{pointsEarned}</span>
|
|
<span className="font-bold uppercase opacity-80">Points</span>
|
|
</div>
|
|
{correctOption.reason && (
|
|
<p className="text-center text-lg opacity-90 max-w-md bg-black/10 px-4 py-2 rounded-xl">
|
|
{correctOption.reason}
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-2xl font-bold opacity-90 mb-8 max-w-md text-center">
|
|
Don't worry, you can catch up in the next round!
|
|
</div>
|
|
)}
|
|
|
|
{/* Streak Indicator */}
|
|
{streak > 1 && isCorrect && (
|
|
<div className="flex items-center gap-2 text-yellow-200 font-black text-2xl animate-pulse">
|
|
<Flame fill="currentColor" />
|
|
<span>Answer Streak: {streak}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8 bg-black/20 px-6 py-2 rounded-xl text-xl font-bold">
|
|
Total Score: <AnimatedCounter from={newScore - (pointsEarned || 0)} to={newScore} />
|
|
</div>
|
|
</motion.div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
{!isCorrect && (
|
|
<div className={`absolute bottom-0 left-0 right-0 overflow-hidden z-20 ${isPresenter ? 'pb-20' : ''}`}>
|
|
<motion.div
|
|
initial={{ y: "100%" }}
|
|
animate={{ y: 0 }}
|
|
transition={{ delay: 0.5, type: 'spring' }}
|
|
className="bg-black/30 backdrop-blur-md p-6 pb-12"
|
|
>
|
|
{selectedOption && selectedOption.reason && (
|
|
<div className="mb-4 pb-4 border-b border-white/20">
|
|
<p className="text-center text-sm font-bold uppercase tracking-widest mb-2 opacity-70">Why your answer was wrong</p>
|
|
<p className="text-center text-sm opacity-90 max-w-md mx-auto">
|
|
{selectedOption.reason}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<p className="text-center text-sm font-bold uppercase tracking-widest mb-4 opacity-70">The correct answer was</p>
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`${COLORS[correctOption.color]} p-3 rounded-xl shadow-lg`}>
|
|
<ShapeIcon size={24} fill="currentColor" />
|
|
</div>
|
|
<span className="text-2xl font-black">{correctOption.text}</span>
|
|
</div>
|
|
{correctOption.reason && (
|
|
<p className="text-center text-sm opacity-80 max-w-md mt-2">
|
|
{correctOption.reason}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
|
|
{isPresenter && onPresenterAdvance && (
|
|
<motion.button
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.5 }}
|
|
onClick={onPresenterAdvance}
|
|
className="fixed bottom-6 left-1/2 -translate-x-1/2 bg-white text-gray-900 px-8 py-4 rounded-2xl text-xl font-black shadow-[0_6px_0_rgba(0,0,0,0.3)] active:shadow-none active:translate-y-[6px] transition-all flex items-center gap-2 hover:bg-gray-100 z-30 cursor-pointer"
|
|
>
|
|
Continue to Scoreboard
|
|
<ChevronRight size={28} strokeWidth={3} />
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|