2026-04-12 20:03:20 +02:00
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
|
import Dialog from '@mui/material/Dialog';
|
|
|
|
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
2026-04-19 09:25:58 +02:00
|
|
|
import Avatar from './battle-dialog/Avatar';
|
|
|
|
|
import StatRow from './battle-dialog/StatRow';
|
|
|
|
|
import BonusPoints from './battle-dialog/BonusPoints';
|
2026-04-12 20:03:20 +02:00
|
|
|
|
|
|
|
|
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
|
|
|
|
|
|
|
|
|
const DIALOG_SX = {
|
|
|
|
|
'& .MuiDialog-paper': {
|
|
|
|
|
background: '#07090d',
|
|
|
|
|
backgroundImage: `
|
|
|
|
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
|
|
|
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
|
|
|
|
`,
|
|
|
|
|
backgroundSize: '46px 46px',
|
|
|
|
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0,0,0,0.9)',
|
|
|
|
|
width: '580px',
|
|
|
|
|
maxWidth: '94vw',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
},
|
|
|
|
|
'& .MuiBackdrop-root': {
|
|
|
|
|
background: 'rgba(2, 4, 8, 0.88)',
|
|
|
|
|
backdropFilter: 'blur(4px)',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const RESULT_META = {
|
|
|
|
|
win: {
|
|
|
|
|
label: 'Victory',
|
|
|
|
|
color: '#5ee89a',
|
|
|
|
|
bg: 'rgba(42,158,96,0.15)',
|
|
|
|
|
border: 'rgba(42,158,96,0.4)',
|
|
|
|
|
icon: 'fa-trophy',
|
|
|
|
|
},
|
|
|
|
|
loss: {
|
2026-04-15 14:38:25 +02:00
|
|
|
label: 'Defeated',
|
2026-04-12 20:03:20 +02:00
|
|
|
color: '#f67d52',
|
|
|
|
|
bg: 'rgba(173,10,5,0.15)',
|
|
|
|
|
border: 'rgba(173,10,5,0.4)',
|
|
|
|
|
icon: 'fa-flag',
|
|
|
|
|
},
|
|
|
|
|
draw: {
|
|
|
|
|
label: 'Draw',
|
|
|
|
|
color: '#95cff5',
|
|
|
|
|
bg: 'rgba(149,207,245,0.1)',
|
|
|
|
|
border: 'rgba(149,207,245,0.3)',
|
|
|
|
|
icon: 'fa-minus',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function BattleDialog({ games }) {
|
2026-04-14 18:54:44 +02:00
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const [game, setGame] = useState(null);
|
2026-04-12 20:03:20 +02:00
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handler = e => {
|
|
|
|
|
const row = e.target.closest('[data-game-index]');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
const idx = parseInt(row.dataset.gameIndex, 10);
|
|
|
|
|
if (!isNaN(idx) && games[idx]) {
|
|
|
|
|
setGame(games[idx]);
|
|
|
|
|
setOpen(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('click', handler);
|
|
|
|
|
return () => document.removeEventListener('click', handler);
|
|
|
|
|
}, [games]);
|
|
|
|
|
|
|
|
|
|
if (!game) {
|
|
|
|
|
return <ThemeProvider theme={darkTheme}><Dialog open={false} sx={DIALOG_SX} /></ThemeProvider>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 18:54:44 +02:00
|
|
|
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
|
|
|
|
const resign = game.resign;
|
2026-04-19 18:04:01 +02:00
|
|
|
const maxPoints = Math.max(game.redPoints ?? 0, game.bluePoints ?? 0);
|
2026-04-12 20:03:20 +02:00
|
|
|
const endReason = resign
|
2026-04-14 18:54:44 +02:00
|
|
|
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
2026-04-19 18:04:01 +02:00
|
|
|
: 26 <= maxPoints ? 'Points' : 'Abandoned';
|
2026-04-14 18:54:44 +02:00
|
|
|
const shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
2026-04-19 18:04:01 +02:00
|
|
|
const canContinue = !resign && 26 > maxPoints;
|
|
|
|
|
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
2026-04-12 20:03:20 +02:00
|
|
|
|
2026-04-18 17:56:50 +02:00
|
|
|
const formatDuration = (from, to) => {
|
|
|
|
|
if (!from || !to) return null;
|
|
|
|
|
const diffMs = new Date(to.replace(' ', 'T')) - new Date(from.replace(' ', 'T'));
|
|
|
|
|
if (isNaN(diffMs) || 0 >= diffMs) return null;
|
|
|
|
|
const totalSec = Math.floor(diffMs / 1000);
|
|
|
|
|
const h = Math.floor(totalSec / 3600);
|
|
|
|
|
const m = Math.floor((totalSec % 3600) / 60);
|
|
|
|
|
const s = totalSec % 60;
|
|
|
|
|
if (0 < h) return `${h}h ${m}m ${s}s`;
|
|
|
|
|
if (0 < m) return `${m}m ${s}s`;
|
|
|
|
|
return `${s}s`;
|
|
|
|
|
};
|
|
|
|
|
const duration = formatDuration(game.created, game.date);
|
|
|
|
|
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
|
|
|
|
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
|
|
|
|
: (game.bluePoints ?? 0) > (game.redPoints ?? 0) ? '#95cff5'
|
|
|
|
|
: 'rgba(255,255,255,0.45)';
|
|
|
|
|
|
2026-04-12 20:03:20 +02:00
|
|
|
const handleShare = () => {
|
2026-04-14 18:54:44 +02:00
|
|
|
navigator.clipboard.writeText(shareUrl).then(() => {
|
|
|
|
|
setCopied(true);
|
|
|
|
|
setTimeout(() => setCopied(false), 2200);
|
|
|
|
|
});
|
2026-04-12 20:03:20 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ThemeProvider theme={darkTheme}>
|
|
|
|
|
<Dialog open={open} onClose={() => setOpen(false)} sx={DIALOG_SX}>
|
|
|
|
|
<div className="bd">
|
|
|
|
|
<div className="bd-header">
|
|
|
|
|
<div className="bd-header-left">
|
|
|
|
|
<span className="bd-label">Battle Report</span>
|
|
|
|
|
<h2 className="bd-title">
|
|
|
|
|
<i className="fa fa-crosshairs" /> Match Details
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
2026-04-19 18:04:01 +02:00
|
|
|
{canContinue ? (
|
|
|
|
|
<a
|
|
|
|
|
className="bd-continue"
|
|
|
|
|
href={playUrl}
|
|
|
|
|
aria-label="Continue the game"
|
|
|
|
|
title="Continue the game"
|
|
|
|
|
>
|
|
|
|
|
<i className="fa fa-play" />
|
|
|
|
|
Continue
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
className={`bd-share${copied ? ' bd-share--copied' : ''}`}
|
|
|
|
|
onClick={handleShare}
|
|
|
|
|
aria-label="Copy share link"
|
|
|
|
|
title="Copy share link"
|
|
|
|
|
>
|
|
|
|
|
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
|
|
|
|
{copied ? 'Copied!' : 'Share'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-04-12 20:03:20 +02:00
|
|
|
<button className="bd-close" onClick={() => setOpen(false)} aria-label="Close">
|
|
|
|
|
<i className="fa fa-times" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bd-vs-panel">
|
2026-04-19 09:25:58 +02:00
|
|
|
<Avatar
|
|
|
|
|
name={game.redName} color="red" avatarUrl={game.redAvatar}
|
|
|
|
|
bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0}
|
|
|
|
|
/>
|
2026-04-12 20:03:20 +02:00
|
|
|
<div className="bd-vs-center">
|
|
|
|
|
<div className="bd-vs-score">
|
|
|
|
|
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
|
|
|
|
<span className="bd-vs-score__sep">:</span>
|
|
|
|
|
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
|
|
|
|
</div>
|
2026-04-18 13:44:15 +02:00
|
|
|
<div className="bd-vs-score" style={{ marginBottom: 8 }}>
|
2026-04-19 09:25:58 +02:00
|
|
|
<span style={{
|
|
|
|
|
font: '700 13px \'Rajdhani\', sans-serif',
|
|
|
|
|
color: '#f67d52',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 4,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-18 13:44:15 +02:00
|
|
|
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="bd-vs-score__sep">:</span>
|
2026-04-19 09:25:58 +02:00
|
|
|
<span style={{
|
|
|
|
|
font: '700 13px \'Rajdhani\', sans-serif',
|
|
|
|
|
color: '#95cff5',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 4,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-18 13:44:15 +02:00
|
|
|
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-04-12 20:03:20 +02:00
|
|
|
<div className="bd-vs-label">VS</div>
|
|
|
|
|
<div
|
|
|
|
|
className="bd-result-badge"
|
|
|
|
|
style={{ background: meta.bg, border: `1px solid ${meta.border}`, color: meta.color }}
|
|
|
|
|
>
|
|
|
|
|
<i className={`fa ${meta.icon}`} /> {meta.label}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-19 09:25:58 +02:00
|
|
|
<Avatar
|
|
|
|
|
name={game.blueName} color="blue" avatarUrl={game.blueAvatar}
|
|
|
|
|
bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0}
|
|
|
|
|
/>
|
2026-04-12 20:03:20 +02:00
|
|
|
</div>
|
|
|
|
|
<div className="bd-stats">
|
|
|
|
|
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
2026-04-19 09:10:17 +02:00
|
|
|
{game.created && game.date && game.created !== game.date && (
|
|
|
|
|
<StatRow icon="fa-clock" label="Started" value={game.created} />
|
|
|
|
|
)}
|
2026-04-18 17:56:50 +02:00
|
|
|
{duration && (
|
|
|
|
|
<StatRow icon="fa-hourglass-half" label="Match duration" value={duration} />
|
|
|
|
|
)}
|
2026-04-19 09:10:17 +02:00
|
|
|
<StatRow icon="fa-flag-checkered" label="End reason" value={endReason} />
|
2026-04-18 17:56:50 +02:00
|
|
|
{0 < pointDiff && (
|
2026-04-19 09:25:58 +02:00
|
|
|
<StatRow
|
|
|
|
|
icon="fa-balance-scale" label="Winning margin"
|
|
|
|
|
value={`${pointDiff} mine${1 === pointDiff ? '' : 's'}`} valueColor={winnerColor}
|
|
|
|
|
/>
|
2026-04-18 17:56:50 +02:00
|
|
|
)}
|
2026-04-12 20:03:20 +02:00
|
|
|
<StatRow
|
2026-04-18 17:56:50 +02:00
|
|
|
icon="fa-bomb" label="Red used bomb"
|
2026-04-12 20:03:20 +02:00
|
|
|
value={game.redExplodedBomb ? 'Yes' : 'No'}
|
|
|
|
|
valueColor={game.redExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
|
|
|
|
/>
|
|
|
|
|
<StatRow
|
2026-04-18 17:56:50 +02:00
|
|
|
icon="fa-bomb" label="Blue used bomb"
|
2026-04-12 20:03:20 +02:00
|
|
|
value={game.blueExplodedBomb ? 'Yes' : 'No'}
|
2026-04-18 17:56:50 +02:00
|
|
|
valueColor={game.blueExplodedBomb ? '#95cff5' : 'rgba(255,255,255,0.45)'}
|
2026-04-12 20:03:20 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-19 09:25:58 +02:00
|
|
|
<BonusPoints
|
|
|
|
|
game={game}
|
|
|
|
|
/>
|
2026-04-12 20:03:20 +02:00
|
|
|
</div>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|