kaboot/components/RevealScreen.tsx

266 lines
11 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;
hostParticipates?: boolean;
}
export const RevealScreen: React.FC<RevealScreenProps> = ({
isCorrect,
pointsEarned,
newScore,
streak,
correctOption,
selectedOption,
role,
onNext,
isPresenter = false,
onPresenterAdvance,
hostParticipates = false
}) => {
const isHost = role === 'HOST';
const canAdvance = isHost || isPresenter;
// When host is participating, show client-style feedback view instead of presentation view
const showPlayerFeedback = !isHost || hostParticipates;
// Trigger confetti for correct answers
useEffect(() => {
if (isCorrect && !isHost) {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
colors: ['#22c55e', '#ffffff', '#fbbf24']
});
}
}, [isCorrect, isHost]);
// Host spectating (not participating) - show presentation view with big answer card
if (isHost && !hostParticipates) {
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-4 md: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-lg md:text-4xl font-bold uppercase tracking-widest mb-4 md: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-6 md:p-12 rounded-2xl md:rounded-[3rem] shadow-[0_10px_0_rgba(0,0,0,0.3)] md:shadow-[0_20px_0_rgba(0,0,0,0.3)] flex flex-col items-center max-w-4xl w-full border-4 md:border-8 border-white/20 relative z-10`}
>
<div className="bg-black/20 p-3 md:p-6 rounded-full mb-3 md:mb-6">
<ShapeIcon className="w-10 h-10 md:w-20 md:h-20" fill="currentColor" />
</div>
<h1 className="text-3xl 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-3 md:mt-6 text-base md:text-2xl text-center opacity-90 bg-black/20 px-4 md:px-6 py-2 md:py-4 rounded-xl md: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-6 md:mt-12 bg-white text-gray-900 px-6 md:px-8 py-3 md:py-4 rounded-xl md:rounded-2xl text-base md: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 className="w-5 h-5 md:w-7 md:h-7" 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 ${(isHost || 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>
)}
{/* Host participating gets the continue button */}
{isHost && onNext && (
<motion.button
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
onClick={onNext}
className="fixed bottom-6 left-1/2 -translate-x-1/2 bg-white text-gray-900 px-6 md:px-8 py-3 md:py-4 rounded-xl md:rounded-2xl text-base md: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 className="w-5 h-5 md:w-7 md:h-7" strokeWidth={3} />
</motion.button>
)}
{/* Presenter (non-host) gets the continue button */}
{!isHost && 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-6 md:px-8 py-3 md:py-4 rounded-xl md:rounded-2xl text-base md: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 className="w-5 h-5 md:w-7 md:h-7" strokeWidth={3} />
</motion.button>
)}
</div>
);
};