121 lines
No EOL
4.2 KiB
TypeScript
121 lines
No EOL
4.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Player } from '../types';
|
|
import { motion, useSpring, useTransform } from 'framer-motion';
|
|
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, LabelList } from 'recharts';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { PlayerAvatar } from './PlayerAvatar';
|
|
|
|
const AnimatedScoreLabel: React.FC<{
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
value: number;
|
|
}> = ({ x, y, width, value }) => {
|
|
const spring = useSpring(0, { duration: 500 });
|
|
const display = useTransform(spring, (latest) => Math.round(latest));
|
|
const [displayValue, setDisplayValue] = useState(0);
|
|
|
|
useEffect(() => {
|
|
spring.set(value);
|
|
const unsubscribe = display.on('change', (v) => setDisplayValue(v));
|
|
return () => unsubscribe();
|
|
}, [value, spring, display]);
|
|
|
|
return (
|
|
<text
|
|
x={x + width + 15}
|
|
y={y}
|
|
dy={35}
|
|
fill="black"
|
|
fontSize={24}
|
|
fontWeight={900}
|
|
fontFamily="Fredoka"
|
|
>
|
|
{displayValue}
|
|
</text>
|
|
);
|
|
};
|
|
|
|
interface ScoreboardProps {
|
|
players: Player[];
|
|
onNext: () => void;
|
|
isHost: boolean;
|
|
currentPlayerId: string | null;
|
|
}
|
|
|
|
export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost, currentPlayerId }) => {
|
|
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 (
|
|
<div className="flex flex-col h-screen p-8">
|
|
<header className="text-center mb-12">
|
|
<h1 className="text-5xl font-black text-white font-display drop-shadow-md">Scoreboard</h1>
|
|
</header>
|
|
|
|
<div className="flex-1 bg-white rounded-[3rem] shadow-[0_20px_0_rgba(0,0,0,0.2)] p-12 flex text-gray-900 max-w-6xl w-full mx-auto relative z-10 border-8 border-white/50">
|
|
<div className="flex flex-col justify-around py-4 pr-4">
|
|
{sortedPlayers.map((player) => (
|
|
<div key={player.id} className="flex items-center gap-3 h-[50px]">
|
|
<PlayerAvatar seed={player.avatarSeed} size={24} />
|
|
<span className="font-black text-xl font-display whitespace-nowrap">{player.displayName}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex-1">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={sortedPlayers}
|
|
layout="vertical"
|
|
margin={{ top: 20, right: 100, left: 0, bottom: 5 }}
|
|
>
|
|
<XAxis type="number" hide />
|
|
<YAxis type="category" dataKey="displayName" hide />
|
|
<Bar dataKey="score" radius={[0, 20, 20, 0]} barSize={50} animationDuration={1500}>
|
|
{sortedPlayers.map((entry, index) => (
|
|
<Cell
|
|
key={`cell-${index}`}
|
|
fill={entry.color}
|
|
className="filter drop-shadow-md"
|
|
/>
|
|
))}
|
|
<LabelList
|
|
dataKey="score"
|
|
position="right"
|
|
offset={15}
|
|
content={({ x, y, width, value }) => (
|
|
<AnimatedScoreLabel
|
|
x={x as number}
|
|
y={y as number}
|
|
width={width as number}
|
|
value={value as number}
|
|
/>
|
|
)}
|
|
/>
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 flex justify-end max-w-6xl w-full mx-auto">
|
|
{isHost ? (
|
|
<button
|
|
onClick={onNext}
|
|
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
|
|
</button>
|
|
) : (
|
|
<div className="flex items-center gap-3 bg-white/10 px-8 py-4 rounded-2xl backdrop-blur-md border-2 border-white/20">
|
|
<Loader2 className="animate-spin w-8 h-8" />
|
|
<span className="text-xl font-bold">Waiting for host...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |