Private
Public Access
1
0
Files
MineSeeker/assets/js/mine-seeker/components/grid/GridControl.jsx

124 lines
4.0 KiB
React
Raw Normal View History

/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React, { Fragment, useState } from 'react';
import { useGame } from '@mine-contexts';
import GridField from './GridField';
import UserControl from '../user/UserControl';
import GameTimer from '../GameTimer';
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
import { func, string } from 'prop-types';
const GridControl = ({ gameAssoc, onClick, resign }) => {
const {
overlay, overlayTitle, overlaySubTitle,
webPlayer, activePlayer, bombSelected,
cells, setCells, endRef,
} = useGame();
const [copied, setCopied] = useState(false);
const shareUrl = gameAssoc ? `${window.location.origin}/play/${gameAssoc}` : null;
const endShareUrl = gameAssoc ? `${window.location.origin}/battle/${gameAssoc}` : null;
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
const handleShare = () => {
const url = endRef.current ? endShareUrl : shareUrl;
if (!url) return;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2200);
});
};
const handleHover = (row, col) => {
if (!bombSelected) return;
const activeColor = activePlayer ? 'blue' : 'red';
if (activeColor !== webPlayer) return;
setCells(prev => {
const next = prev.map(r => r.map(c =>
null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c,
));
bombRadius(row, col, prev.length, prev[0]?.length ?? 0).forEach(([r, c], i) => {
if (!next[r]?.[c]) return;
next[r] = [...next[r]];
next[r][c] = { ...next[r][c], bombTargetArea: BOMB_SYMBOLS[i] };
});
return next;
});
};
return (
<Fragment>
<GameTimer />
<div className="game-wrapper">
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
<div className="game-overlay-window">
<h1>{overlayTitle}</h1>
{'string' === typeof overlaySubTitle && (
<h2>{overlaySubTitle}</h2>
)}
{'string' !== typeof overlaySubTitle && (
<Fragment>{overlaySubTitle}</Fragment>
)}
{gameAssoc && endRef.current && (
<div className="game-overlay-actions">
<button
className={`game-overlay-share${copied ? ' copied' : ''}`}
onClick={handleShare}
title="Copy share link"
aria-label="Copy share link"
>
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
{copied ? 'Copied!' : 'Share Battle'}
</button>
<a
className="game-overlay-profile"
href={isAuthenticated ? '/profile' : '/'}
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
>
<i className={`fa ${isAuthenticated ? 'fa-user' : 'fa-house'}`} />
{isAuthenticated ? 'My Profile' : 'Homepage'}
</a>
</div>
)}
</div>
</div>
<UserControl
resign={resign}
/>
<div className="grid-container">
<div className="grid">
{cells.flatMap((row, r) =>
row.map((cell, c) => (
<GridField
key={`${r}_${c}`}
cell={cell}
onClick={() => onClick([r, c])}
onMouseEnter={() => handleHover(r, c)}
/>
)),
)}
</div>
</div>
</div>
</Fragment>
);
};
export default GridControl;
GridControl.propTypes = {
gameAssoc: string,
onClick: func.isRequired,
resign: func.isRequired,
};