172 lines
7.1 KiB
TypeScript
172 lines
7.1 KiB
TypeScript
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { IconConfettiFilled, IconCopyFilled, IconCircleFilled,
|
|
IconPlayerPlayFilled, IconX } from '@tabler/icons-react'
|
|
import { useStore } from '../store'
|
|
import PartyGame from './PartyGame'
|
|
|
|
export default function PartyPage() {
|
|
const { t } = useTranslation()
|
|
const SUB_LABELS = {
|
|
wrong: t('party.subModes.wrong'),
|
|
bluff: t('party.subModes.bluff'),
|
|
reverse: t('party.subModes.reverse'),
|
|
speed: t('party.subModes.speed'),
|
|
roulette: t('party.subModes.roulette'),
|
|
}
|
|
|
|
const subLabelFor = (labels: Record<string, string>, mode: string): string => {
|
|
return labels[mode] ?? labels.wrong
|
|
}
|
|
const mpRoomCode = useStore(s => s.mpRoomCode)
|
|
const mpPlayers = useStore(s => s.mpPlayers)
|
|
const mpStarted = useStore(s => s.mpStarted)
|
|
const mpMyId = useStore(s => s.mpMyId)
|
|
const mpIsCreator = useStore(s => s.mpIsCreator)
|
|
const mpStartGame = useStore(s => s.mpStartGame)
|
|
const mpLeaveRoom = useStore(s => s.mpLeaveRoom)
|
|
const partySubMode = useStore(s => s.partySubMode)
|
|
const partyPhase = useStore(s => s.partyPhase)
|
|
const partyScores = useStore(s => s.partyScores)
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
const [confirmLeave, setConfirmLeave] = useState(false)
|
|
|
|
const copyCode = () => {
|
|
if (!mpRoomCode) return
|
|
navigator.clipboard.writeText(mpRoomCode)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 1500)
|
|
}
|
|
|
|
const handleLeave = () => {
|
|
if (mpStarted && partyPhase !== 'finished') {
|
|
setConfirmLeave(true)
|
|
} else {
|
|
mpLeaveRoom()
|
|
}
|
|
}
|
|
|
|
const confirmAndLeave = () => {
|
|
if (partyScores.length > 0 && partyPhase !== 'finished') {
|
|
useStore.setState({ partyPhase: 'finished' })
|
|
setTimeout(() => mpLeaveRoom(), 3000)
|
|
} else {
|
|
mpLeaveRoom()
|
|
}
|
|
setConfirmLeave(false)
|
|
}
|
|
|
|
if (mpStarted) {
|
|
return (
|
|
<div className="party-page">
|
|
<div className="party-page-bar">
|
|
<button type="button" className="header-brand" onClick={handleLeave}>{t('app.name')}</button>
|
|
<button type="button" className="party-page-room" onClick={copyCode} aria-label={t('party.roomCode')}>
|
|
<IconConfettiFilled size={14} aria-hidden="true" />
|
|
<span className="party-page-code">{mpRoomCode}</span>
|
|
<span className="party-page-sub">{subLabelFor(SUB_LABELS, partySubMode || 'wrong')}</span>
|
|
{copied && <span className="party-page-copied">{t('party.copied')}</span>}
|
|
</button>
|
|
<div className="party-page-players-compact">
|
|
{mpPlayers.map(p => (
|
|
<IconCircleFilled key={p.id} size={8} style={{ color: p.color }} />
|
|
))}
|
|
<span>{mpPlayers.length}p</span>
|
|
</div>
|
|
<button className="btn btn-sm party-page-leave" onClick={handleLeave}>
|
|
<IconX size={12} /> {t('party.leave')}
|
|
</button>
|
|
</div>
|
|
<PartyGame />
|
|
<AnimatePresence>
|
|
{confirmLeave && (
|
|
<motion.div className="settings-overlay" key="confirm-overlay"
|
|
onClick={() => setConfirmLeave(false)}
|
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
<motion.div className="party-confirm" role="alertdialog" aria-modal="true" onClick={e => e.stopPropagation()}
|
|
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.96, y: 10 }}
|
|
transition={{ duration: 0.2 }}>
|
|
<p className="party-confirm-text">
|
|
{mpIsCreator ? t('party.leaveEndGame') : t('party.leaveConfirm')}
|
|
</p>
|
|
<div className="party-confirm-actions">
|
|
<button className="btn btn-sm" onClick={() => setConfirmLeave(false)}>{t('party.cancel')}</button>
|
|
<button className="btn btn-sm party-confirm-leave" onClick={confirmAndLeave}>
|
|
{mpIsCreator ? t('party.endAndLeave') : t('party.leave')}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="party-page">
|
|
<div className="party-page-bar" style={{ justifyContent: 'flex-start' }}>
|
|
<button type="button" className="header-brand" onClick={mpLeaveRoom}>{t('app.name')}</button>
|
|
</div>
|
|
<motion.div className="party-lobby"
|
|
initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.25 }}>
|
|
<IconConfettiFilled size={32} className="party-lobby-icon" aria-hidden="true" />
|
|
<h2 className="party-lobby-title">{t('party.title')}</h2>
|
|
<p className="party-lobby-sub">{subLabelFor(SUB_LABELS, partySubMode || 'wrong')}</p>
|
|
|
|
<button type="button" className="party-lobby-code" onClick={copyCode} aria-label={t('party.roomCode')}>
|
|
<span className="party-lobby-code-label">{t('party.roomCode')}</span>
|
|
<span className="party-lobby-code-value">{mpRoomCode}</span>
|
|
<span className="party-lobby-code-copy">
|
|
{copied ? t('party.copied') : <IconCopyFilled size={14} />}
|
|
</span>
|
|
</button>
|
|
|
|
<div className="party-lobby-players">
|
|
<div className="mp-section-title">{t('party.players', { count: mpPlayers.length })}</div>
|
|
<AnimatePresence>
|
|
{mpPlayers.map((p, i) => (
|
|
<motion.div key={p.id} className="party-lobby-player"
|
|
initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.15, delay: i * 0.03 }}>
|
|
<IconCircleFilled size={10} style={{ color: p.color }} />
|
|
<span>{p.name}</span>
|
|
{p.id === mpMyId && <span className="mp-player-you">{t('multiplayer.you')}</span>}
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
<AnimatePresence>
|
|
{mpPlayers.length < 2 && (
|
|
<motion.p className="party-lobby-hint" key="hint"
|
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
{t('party.waitingForPlayers')}
|
|
</motion.p>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
<div className="party-lobby-actions">
|
|
{mpIsCreator && (
|
|
<button className="btn btn-primary" onClick={mpStartGame} disabled={mpPlayers.length < 2} style={{ width: '100%' }}>
|
|
<IconPlayerPlayFilled size={14} /> {mpPlayers.length < 2 ? t('party.needMorePlayers') : t('party.startGame')}
|
|
</button>
|
|
)}
|
|
{!mpIsCreator && (
|
|
<p className="party-lobby-hint">{t('party.waitingForHost')}</p>
|
|
)}
|
|
<button className="btn btn-sm party-page-leave" onClick={mpLeaveRoom} style={{ width: '100%' }}>
|
|
<IconX size={14} /> {t('party.leaveRoom')}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)
|
|
}
|