Add presenter role for game flow control

This commit is contained in:
Joey Yakimowich-Payne 2026-01-19 14:02:28 -07:00
commit 9ef8f7343d
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
10 changed files with 1412 additions and 17 deletions

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Player } from '../types';
import { motion, AnimatePresence } from 'framer-motion';
import { Sparkles, User, X, Link, Check, QrCode } from 'lucide-react';
import { Sparkles, User, X, Link, Check, QrCode, Crown } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { PlayerAvatar } from './PlayerAvatar';
import toast from 'react-hot-toast';
@ -15,15 +15,19 @@ interface LobbyProps {
onEndGame?: () => void;
currentPlayerId?: string | null;
hostParticipates?: boolean;
presenterId?: string | null;
onSetPresenter?: (playerId: string | null) => void;
}
export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role, onStart, onEndGame, currentPlayerId, hostParticipates = false }) => {
export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role, onStart, onEndGame, currentPlayerId, hostParticipates = false, presenterId, onSetPresenter }) => {
const isHost = role === 'HOST';
const hostPlayer = players.find(p => p.id === 'host');
const realPlayers = players.filter(p => p.id !== 'host');
const currentPlayer = currentPlayerId ? players.find(p => p.id === currentPlayerId) : null;
const [linkCopied, setLinkCopied] = useState(false);
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
const isPresenter = currentPlayerId === presenterId;
const canSelectPresenter = isHost && !hostParticipates && onSetPresenter;
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@ -165,6 +169,12 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
<div className="text-xl md:text-3xl font-bold font-display text-center px-4">Waiting for players to join...</div>
</div>
)}
{realPlayers.length > 0 && !hostParticipates && canSelectPresenter && (
<div className="w-full text-center text-white/60 text-sm mb-2">
<Crown size={14} className="inline mr-1 text-yellow-400" />
Click a player to make them presenter (can advance screens)
</div>
)}
{hostParticipates && hostPlayer && (
<motion.div
key="host"
@ -178,18 +188,30 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
<span className="text-xs bg-black/20 px-2 py-0.5 rounded-full">HOST</span>
</motion.div>
)}
{realPlayers.map((player) => (
{realPlayers.map((player) => {
const isPlayerPresenter = player.id === presenterId;
return (
<motion.div
key={player.id}
initial={{ scale: 0, rotate: -10 }}
animate={{ scale: 1, rotate: 0 }}
exit={{ scale: 0, opacity: 0 }}
className="bg-white text-black px-4 md:px-6 py-2 md:py-3 rounded-full font-black text-base md:text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-2 md:gap-3 border-b-4 border-gray-200"
onClick={() => canSelectPresenter && onSetPresenter(player.id)}
className={`bg-white text-black px-4 md:px-6 py-2 md:py-3 rounded-full font-black text-base md:text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-2 md:gap-3 border-b-4 ${
isPlayerPresenter ? 'border-yellow-400 ring-2 ring-yellow-400' : 'border-gray-200'
} ${canSelectPresenter ? 'cursor-pointer hover:scale-105 transition-transform' : ''}`}
>
{isPlayerPresenter && (
<Crown size={18} className="text-yellow-500 -ml-1" />
)}
<PlayerAvatar seed={player.avatarSeed} size={20} />
{player.name}
{isPlayerPresenter && (
<span className="text-xs bg-yellow-400 text-yellow-900 px-2 py-0.5 rounded-full">PRESENTER</span>
)}
</motion.div>
))}
);
})}
</AnimatePresence>
</div>
@ -223,8 +245,13 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', bounce: 0.6 }}
className="bg-white p-6 md:p-8 rounded-2xl md:rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] mb-4 md:mb-8"
className={`bg-white p-6 md:p-8 rounded-2xl md:rounded-[2rem] shadow-[0_10px_0_rgba(0,0,0,0.1)] mb-4 md:mb-8 relative ${isPresenter ? 'ring-4 ring-yellow-400' : ''}`}
>
{isPresenter && (
<div className="absolute -top-3 -right-3 bg-yellow-400 p-2 rounded-full shadow-lg">
<Crown size={24} className="text-yellow-900" />
</div>
)}
{currentPlayer ? (
<PlayerAvatar seed={currentPlayer.avatarSeed} size={60} className="md:w-20 md:h-20" />
) : (
@ -234,7 +261,17 @@ export const Lobby: React.FC<LobbyProps> = ({ quizTitle, players, gamePin, role,
<h2 className="text-3xl md:text-5xl font-black mb-2 md:mb-4 font-display">
{currentPlayer?.name || "You're in!"}
</h2>
<p className="text-lg md:text-2xl font-bold opacity-80">Waiting for the host to start...</p>
{isPresenter ? (
<div className="flex flex-col items-center gap-2">
<span className="bg-yellow-400 text-yellow-900 px-4 py-1 rounded-full font-bold text-sm flex items-center gap-2">
<Crown size={16} />
You are the Presenter
</span>
<p className="text-lg md:text-xl font-bold opacity-80">You can advance screens during the game</p>
</div>
) : (
<p className="text-lg md:text-2xl font-bold opacity-80">Waiting for the host to start...</p>
)}
</div>
)}
</main>