165 lines
7.6 KiB
TypeScript
165 lines
7.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Player } from '../types';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Trophy, Medal, RotateCcw, List, X } from 'lucide-react';
|
|
import confetti from 'canvas-confetti';
|
|
import { PlayerAvatar } from './PlayerAvatar';
|
|
|
|
interface PodiumProps {
|
|
players: Player[];
|
|
onRestart: () => void;
|
|
}
|
|
|
|
export const Podium: React.FC<PodiumProps> = ({ players, onRestart }) => {
|
|
const [showRankings, setShowRankings] = useState(false);
|
|
const sorted = [...players].sort((a, b) => b.score - a.score);
|
|
const winner = sorted[0];
|
|
const second = sorted[1];
|
|
const third = sorted[2];
|
|
|
|
useEffect(() => {
|
|
const duration = 3 * 1000;
|
|
const animationEnd = Date.now() + duration;
|
|
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
|
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min;
|
|
|
|
const interval: any = setInterval(function() {
|
|
const timeLeft = animationEnd - Date.now();
|
|
if (timeLeft <= 0) return clearInterval(interval);
|
|
const particleCount = 50 * (timeLeft / duration);
|
|
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
|
|
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
|
|
}, 250);
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col items-center justify-center p-4 overflow-hidden">
|
|
<h1 className="text-4xl md:text-6xl font-black text-white mb-6 md:mb-12 font-display drop-shadow-[0_5px_0_rgba(0,0,0,0.3)] tracking-wide">Podium</h1>
|
|
|
|
<div className="flex items-end justify-center gap-2 md:gap-8 mb-6 md:mb-12 w-full max-w-4xl h-48 md:h-96 px-2">
|
|
{/* Second Place */}
|
|
{second && (
|
|
<motion.div
|
|
initial={{ height: 0 }}
|
|
animate={{ height: "60%" }}
|
|
transition={{ type: 'spring', bounce: 0.5, delay: 0.5 }}
|
|
className="w-1/3 bg-gray-200 rounded-t-2xl md:rounded-t-[3rem] flex flex-col items-center justify-end p-2 md:p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
|
>
|
|
<div className="absolute -top-12 md:-top-20 flex flex-col items-center">
|
|
<span className="text-sm md:text-xl font-bold mb-1 md:mb-2 text-white drop-shadow-md text-center leading-tight">{second.name}</span>
|
|
<PlayerAvatar seed={second.avatarSeed} size={40} />
|
|
</div>
|
|
<span className="text-xl md:text-3xl font-black text-gray-500 mb-2 md:mb-4">{second.score}</span>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* First Place */}
|
|
{winner && (
|
|
<motion.div
|
|
initial={{ height: 0 }}
|
|
animate={{ height: "80%" }}
|
|
transition={{ type: 'spring', bounce: 0.6, delay: 1 }}
|
|
className="w-1/3 bg-yellow-400 rounded-t-2xl md:rounded-t-[3rem] flex flex-col items-center justify-end p-2 md:p-6 relative z-10 shadow-2xl border-x-4 border-t-4 border-white/50"
|
|
>
|
|
<div className="absolute -top-16 md:-top-28 flex flex-col items-center">
|
|
<span className="text-lg md:text-3xl font-bold mb-1 md:mb-2 text-yellow-100 drop-shadow-md text-center leading-tight">{winner.name}</span>
|
|
<PlayerAvatar seed={winner.avatarSeed} size={56} className="ring-4 ring-yellow-300" />
|
|
</div>
|
|
<span className="text-3xl md:text-5xl font-black text-yellow-900 mb-2 md:mb-6">{winner.score}</span>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Third Place */}
|
|
{third && (
|
|
<motion.div
|
|
initial={{ height: 0 }}
|
|
animate={{ height: "40%" }}
|
|
transition={{ type: 'spring', bounce: 0.5, delay: 0 }}
|
|
className="w-1/3 bg-orange-400 rounded-t-2xl md:rounded-t-[3rem] flex flex-col items-center justify-end p-2 md:p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
|
>
|
|
<div className="absolute -top-12 md:-top-20 flex flex-col items-center">
|
|
<span className="text-sm md:text-xl font-bold mb-1 md:mb-2 text-white drop-shadow-md text-center leading-tight">{third.name}</span>
|
|
<PlayerAvatar seed={third.avatarSeed} size={40} />
|
|
</div>
|
|
<span className="text-xl md:text-3xl font-black text-orange-900 mb-2 md:mb-4">{third.score}</span>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => setShowRankings(true)}
|
|
className="flex items-center gap-2 bg-white/20 text-white px-5 py-4 rounded-2xl text-lg font-bold hover:bg-white/30 transition"
|
|
>
|
|
<List size={22} /> All Rankings
|
|
</button>
|
|
<button
|
|
onClick={onRestart}
|
|
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
|
|
</button>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{showRankings && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
onClick={() => setShowRankings(false)}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
className="bg-white rounded-3xl p-6 max-w-md w-full max-h-[80vh] overflow-hidden flex flex-col shadow-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-2xl font-black text-gray-900">Final Rankings</h2>
|
|
<button
|
|
onClick={() => setShowRankings(false)}
|
|
className="p-2 hover:bg-gray-100 rounded-full transition"
|
|
>
|
|
<X size={24} className="text-gray-500" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="overflow-y-auto flex-1 -mx-2 px-2">
|
|
{sorted.map((player, index) => (
|
|
<div
|
|
key={player.id}
|
|
className={`flex items-center gap-3 p-3 rounded-xl mb-2 ${
|
|
index === 0 ? 'bg-yellow-100' :
|
|
index === 1 ? 'bg-gray-100' :
|
|
index === 2 ? 'bg-orange-100' :
|
|
'bg-gray-50'
|
|
}`}
|
|
>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-black text-sm ${
|
|
index === 0 ? 'bg-yellow-400 text-yellow-900' :
|
|
index === 1 ? 'bg-gray-300 text-gray-700' :
|
|
index === 2 ? 'bg-orange-400 text-orange-900' :
|
|
'bg-gray-200 text-gray-600'
|
|
}`}>
|
|
{index + 1}
|
|
</div>
|
|
<PlayerAvatar seed={player.avatarSeed} size={36} />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-gray-900 truncate">{player.name}</p>
|
|
</div>
|
|
<div className="font-black text-lg text-gray-700">{player.score}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
};
|