Update UI
This commit is contained in:
parent
838013fb9a
commit
2aafe8fce9
4 changed files with 119 additions and 42 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Player } from '../types';
|
import { Player } from '../types';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { User, Sparkles } from 'lucide-react';
|
import { Sparkles, User } from 'lucide-react';
|
||||||
|
import { PlayerAvatar } from './PlayerAvatar';
|
||||||
|
|
||||||
interface LobbyProps {
|
interface LobbyProps {
|
||||||
quizTitle: string;
|
quizTitle: string;
|
||||||
|
|
@ -60,9 +61,7 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
|
||||||
exit={{ scale: 0, opacity: 0 }}
|
exit={{ scale: 0, opacity: 0 }}
|
||||||
className="bg-white text-black px-6 py-3 rounded-full font-black text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-3 border-b-4 border-gray-200"
|
className="bg-white text-black px-6 py-3 rounded-full font-black text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-3 border-b-4 border-gray-200"
|
||||||
>
|
>
|
||||||
<div className={`p-2 rounded-full bg-gradient-to-br from-purple-400 to-blue-500 text-white`}>
|
<PlayerAvatar seed={player.avatarSeed} size={20} />
|
||||||
<User size={20} />
|
|
||||||
</div>
|
|
||||||
{player.name}
|
{player.name}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
58
components/PlayerAvatar.tsx
Normal file
58
components/PlayerAvatar.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Cat, Dog, Bird, Fish, Rabbit, Turtle,
|
||||||
|
Ghost, Skull, Heart, Star, Moon, Sun,
|
||||||
|
Flame, Zap, Cloud, Snowflake, Leaf, Flower2,
|
||||||
|
Crown, Diamond, Gem, Trophy, Target, Anchor,
|
||||||
|
Rocket, Plane, Car, Bike, Train, Ship
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const ICONS = [
|
||||||
|
Cat, Dog, Bird, Fish, Rabbit, Turtle,
|
||||||
|
Ghost, Skull, Heart, Star, Moon, Sun,
|
||||||
|
Flame, Zap, Cloud, Snowflake, Leaf, Flower2,
|
||||||
|
Crown, Diamond, Gem, Trophy, Target, Anchor,
|
||||||
|
Rocket, Plane, Car, Bike, Train, Ship
|
||||||
|
];
|
||||||
|
|
||||||
|
const GRADIENT_PAIRS = [
|
||||||
|
['#f472b6', '#c026d3'],
|
||||||
|
['#fb923c', '#ea580c'],
|
||||||
|
['#facc15', '#ca8a04'],
|
||||||
|
['#4ade80', '#16a34a'],
|
||||||
|
['#2dd4bf', '#0d9488'],
|
||||||
|
['#38bdf8', '#0284c7'],
|
||||||
|
['#818cf8', '#6366f1'],
|
||||||
|
['#f87171', '#dc2626'],
|
||||||
|
['#a78bfa', '#7c3aed'],
|
||||||
|
['#fb7185', '#e11d48'],
|
||||||
|
['#34d399', '#059669'],
|
||||||
|
['#60a5fa', '#2563eb'],
|
||||||
|
];
|
||||||
|
|
||||||
|
interface PlayerAvatarProps {
|
||||||
|
seed: number;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayerAvatar: React.FC<PlayerAvatarProps> = ({ seed, size = 24, className = '' }) => {
|
||||||
|
const iconIndex = Math.floor(seed * 1000) % ICONS.length;
|
||||||
|
const gradientIndex = Math.floor(seed * 10000) % GRADIENT_PAIRS.length;
|
||||||
|
|
||||||
|
const Icon = ICONS[iconIndex];
|
||||||
|
const [colorFrom, colorTo] = GRADIENT_PAIRS[gradientIndex];
|
||||||
|
const gradientId = `avatar-gradient-${seed.toString().replace('.', '-')}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`rounded-full flex items-center justify-center ${className}`}
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colorFrom}, ${colorTo})`,
|
||||||
|
padding: size * 0.3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={size} className="text-white" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import { Player } from '../types';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Trophy, Medal, RotateCcw } from 'lucide-react';
|
import { Trophy, Medal, RotateCcw } from 'lucide-react';
|
||||||
import confetti from 'canvas-confetti';
|
import confetti from 'canvas-confetti';
|
||||||
|
import { PlayerAvatar } from './PlayerAvatar';
|
||||||
|
|
||||||
interface PodiumProps {
|
interface PodiumProps {
|
||||||
players: Player[];
|
players: Player[];
|
||||||
|
|
@ -45,9 +46,9 @@ export const Podium: React.FC<PodiumProps> = ({ players, onRestart }) => {
|
||||||
transition={{ type: 'spring', bounce: 0.5, delay: 0.5 }}
|
transition={{ type: 'spring', bounce: 0.5, delay: 0.5 }}
|
||||||
className="w-1/3 bg-gray-200 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
className="w-1/3 bg-gray-200 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-16 flex flex-col items-center">
|
<div className="absolute -top-20 flex flex-col items-center">
|
||||||
<span className="text-xl font-bold mb-2 text-white drop-shadow-md">{second.name}</span>
|
<span className="text-xl font-bold mb-2 text-white drop-shadow-md">{second.name}</span>
|
||||||
<div className="bg-gray-300 p-4 rounded-full text-white shadow-lg"><Medal size={32} /></div>
|
<PlayerAvatar seed={second.avatarSeed} size={40} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-3xl font-black text-gray-500 mb-4">{second.score}</span>
|
<span className="text-3xl font-black text-gray-500 mb-4">{second.score}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -61,9 +62,9 @@ export const Podium: React.FC<PodiumProps> = ({ players, onRestart }) => {
|
||||||
transition={{ type: 'spring', bounce: 0.6, delay: 1 }}
|
transition={{ type: 'spring', bounce: 0.6, delay: 1 }}
|
||||||
className="w-1/3 bg-yellow-400 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative z-10 shadow-2xl border-x-4 border-t-4 border-white/50"
|
className="w-1/3 bg-yellow-400 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative z-10 shadow-2xl border-x-4 border-t-4 border-white/50"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-24 flex flex-col items-center">
|
<div className="absolute -top-28 flex flex-col items-center">
|
||||||
<span className="text-3xl font-bold mb-2 text-yellow-100 drop-shadow-md">{winner.name}</span>
|
<span className="text-3xl font-bold mb-2 text-yellow-100 drop-shadow-md">{winner.name}</span>
|
||||||
<div className="bg-yellow-500 p-6 rounded-full text-white shadow-lg scale-110"><Trophy size={48} /></div>
|
<PlayerAvatar seed={winner.avatarSeed} size={56} className="ring-4 ring-yellow-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-5xl font-black text-yellow-900 mb-6">{winner.score}</span>
|
<span className="text-5xl font-black text-yellow-900 mb-6">{winner.score}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -77,9 +78,9 @@ export const Podium: React.FC<PodiumProps> = ({ players, onRestart }) => {
|
||||||
transition={{ type: 'spring', bounce: 0.5, delay: 0 }}
|
transition={{ type: 'spring', bounce: 0.5, delay: 0 }}
|
||||||
className="w-1/3 bg-orange-400 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
className="w-1/3 bg-orange-400 rounded-t-[3rem] flex flex-col items-center justify-end p-6 relative border-x-4 border-t-4 border-white/50 shadow-xl"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-16 flex flex-col items-center">
|
<div className="absolute -top-20 flex flex-col items-center">
|
||||||
<span className="text-xl font-bold mb-2 text-white drop-shadow-md">{third.name}</span>
|
<span className="text-xl font-bold mb-2 text-white drop-shadow-md">{third.name}</span>
|
||||||
<div className="bg-orange-500 p-4 rounded-full text-white shadow-lg"><Medal size={32} /></div>
|
<PlayerAvatar seed={third.avatarSeed} size={40} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-3xl font-black text-orange-900 mb-4">{third.score}</span>
|
<span className="text-3xl font-black text-orange-900 mb-4">{third.score}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Player } from '../types';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, LabelList } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, LabelList } from 'recharts';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { PlayerAvatar } from './PlayerAvatar';
|
||||||
|
|
||||||
interface ScoreboardProps {
|
interface ScoreboardProps {
|
||||||
players: Player[];
|
players: Player[];
|
||||||
|
|
@ -24,39 +25,57 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost,
|
||||||
<h1 className="text-5xl font-black text-white font-display drop-shadow-md">Scoreboard</h1>
|
<h1 className="text-5xl font-black text-white font-display drop-shadow-md">Scoreboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 bg-white rounded-[3rem] shadow-[0_20px_0_rgba(0,0,0,0.2)] p-12 flex flex-col text-gray-900 max-w-6xl w-full mx-auto relative z-10 border-8 border-white/50">
|
<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">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div className="flex flex-col justify-around py-4 pr-4">
|
||||||
<BarChart
|
{sortedPlayers.map((player) => (
|
||||||
data={sortedPlayers}
|
<div key={player.id} className="flex items-center gap-3 h-[50px]">
|
||||||
layout="vertical"
|
<PlayerAvatar seed={player.avatarSeed} size={24} />
|
||||||
margin={{ top: 20, right: 120, left: 20, bottom: 5 }}
|
<span className="font-black text-xl font-display whitespace-nowrap">{player.displayName}</span>
|
||||||
>
|
</div>
|
||||||
<XAxis type="number" hide />
|
))}
|
||||||
<YAxis
|
</div>
|
||||||
type="category"
|
<div className="flex-1">
|
||||||
dataKey="displayName"
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
tick={{ fontSize: 24, fontWeight: 800, fill: '#333', fontFamily: 'Fredoka' }}
|
<BarChart
|
||||||
width={200}
|
data={sortedPlayers}
|
||||||
tickLine={false}
|
layout="vertical"
|
||||||
axisLine={false}
|
margin={{ top: 20, right: 100, left: 0, bottom: 5 }}
|
||||||
/>
|
>
|
||||||
<Bar dataKey="score" radius={[0, 20, 20, 0]} barSize={50} animationDuration={1500}>
|
<XAxis type="number" hide />
|
||||||
{sortedPlayers.map((entry, index) => (
|
<YAxis type="category" dataKey="displayName" hide />
|
||||||
<Cell
|
<Bar dataKey="score" radius={[0, 20, 20, 0]} barSize={50} animationDuration={1500}>
|
||||||
key={`cell-${index}`}
|
{sortedPlayers.map((entry, index) => (
|
||||||
fill={entry.color}
|
<Cell
|
||||||
className="filter drop-shadow-md"
|
key={`cell-${index}`}
|
||||||
|
fill={entry.color}
|
||||||
|
className="filter drop-shadow-md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<LabelList
|
||||||
|
dataKey="score"
|
||||||
|
position="right"
|
||||||
|
offset={15}
|
||||||
|
content={({ x, y, width, value, index }) => {
|
||||||
|
const player = sortedPlayers[index as number];
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={(x as number) + (width as number) + 15}
|
||||||
|
y={y as number}
|
||||||
|
dy={35}
|
||||||
|
fill={player?.color || 'var(--theme-primary)'}
|
||||||
|
fontSize={24}
|
||||||
|
fontWeight={900}
|
||||||
|
fontFamily="Fredoka"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
</Bar>
|
||||||
<LabelList
|
</BarChart>
|
||||||
dataKey="score"
|
</ResponsiveContainer>
|
||||||
position="right"
|
</div>
|
||||||
offset={15}
|
|
||||||
style={{ fontSize: '24px', fontWeight: '900', fill: 'var(--theme-primary)', fontFamily: 'Fredoka' }}
|
|
||||||
/>
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex justify-end max-w-6xl w-full mx-auto">
|
<div className="mt-8 flex justify-end max-w-6xl w-full mx-auto">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue