Add presenter role for game flow control
This commit is contained in:
parent
99977bc8e6
commit
9ef8f7343d
10 changed files with 1412 additions and 17 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ interface RevealScreenProps {
|
|||
selectedOption?: AnswerOption | null;
|
||||
role: GameRole;
|
||||
onNext?: () => void;
|
||||
isPresenter?: boolean;
|
||||
onPresenterAdvance?: () => void;
|
||||
}
|
||||
|
||||
export const RevealScreen: React.FC<RevealScreenProps> = ({
|
||||
|
|
@ -45,9 +47,12 @@ export const RevealScreen: React.FC<RevealScreenProps> = ({
|
|||
correctOption,
|
||||
selectedOption,
|
||||
role,
|
||||
onNext
|
||||
onNext,
|
||||
isPresenter = false,
|
||||
onPresenterAdvance
|
||||
}) => {
|
||||
const isHost = role === 'HOST';
|
||||
const canAdvance = isHost || isPresenter;
|
||||
|
||||
// Trigger confetti for correct answers
|
||||
useEffect(() => {
|
||||
|
|
@ -191,7 +196,7 @@ export const RevealScreen: React.FC<RevealScreenProps> = ({
|
|||
</motion.div>
|
||||
|
||||
{!isCorrect && (
|
||||
<div className="absolute bottom-0 left-0 right-0 overflow-hidden z-20">
|
||||
<div className={`absolute bottom-0 left-0 right-0 overflow-hidden z-20 ${isPresenter ? 'pb-20' : ''}`}>
|
||||
<motion.div
|
||||
initial={{ y: "100%" }}
|
||||
animate={{ y: 0 }}
|
||||
|
|
@ -223,6 +228,19 @@ export const RevealScreen: React.FC<RevealScreenProps> = ({
|
|||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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-8 py-4 rounded-2xl 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 size={28} strokeWidth={3} />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -210,6 +210,8 @@ interface ScoreboardProps {
|
|||
onNext: () => void;
|
||||
isHost: boolean;
|
||||
currentPlayerId: string | null;
|
||||
isPresenter?: boolean;
|
||||
onPresenterAdvance?: () => void;
|
||||
}
|
||||
|
||||
interface AnimatedPlayerState extends Player {
|
||||
|
|
@ -219,7 +221,8 @@ interface AnimatedPlayerState extends Player {
|
|||
initialIndex: number;
|
||||
}
|
||||
|
||||
export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost, currentPlayerId }) => {
|
||||
export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost, currentPlayerId, isPresenter = false, onPresenterAdvance }) => {
|
||||
const canAdvance = isHost || isPresenter;
|
||||
// Initialize players sorted by previousScore to start
|
||||
const [animatedPlayers, setAnimatedPlayers] = useState<AnimatedPlayerState[]>(() => {
|
||||
const playersWithMeta = players.map(p => ({
|
||||
|
|
@ -341,9 +344,9 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost,
|
|||
</div>
|
||||
|
||||
<div className="mt-4 md:mt-8 flex justify-center md:justify-end max-w-4xl w-full mx-auto shrink-0 z-20">
|
||||
{isHost ? (
|
||||
{canAdvance ? (
|
||||
<button
|
||||
onClick={onNext}
|
||||
onClick={isHost ? onNext : onPresenterAdvance}
|
||||
className="bg-white text-theme-primary px-8 md:px-12 py-3 md:py-4 rounded-xl md:rounded-2xl text-xl md: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 flex items-center gap-2"
|
||||
>
|
||||
Next <Trophy size={24} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue