Files
cruciverb/frontend/src/components/PartyPage.tsx
T
2026-04-13 15:22:41 +03:00

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>
)
}