chg: dev: increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4
This commit is contained in:
@@ -255,13 +255,45 @@
|
||||
transition: all 200ms ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(to bottom, rgba(45, 138, 168, 0.9) 0%, rgba(35, 111, 135, 0.95) 100%);
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 14px rgba(35, 111, 135, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&.opd-join--waiting {
|
||||
background: linear-gradient(to bottom, rgba(26, 80, 104, 0.6) 0%, rgba(15, 50, 70, 0.7) 100%);
|
||||
border-color: rgba(35, 111, 135, 0.3);
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
opacity: 1;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-declined {
|
||||
font: 600 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 120, 120, 0.85);
|
||||
letter-spacing: 0.5px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(180, 60, 60, 0.3);
|
||||
background: rgba(180, 60, 60, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-note {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
|
||||
const DIALOG_SX = {
|
||||
@@ -45,6 +45,9 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
||||
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
||||
const [declinedMsg, setDeclinedMsg] = useState('');
|
||||
const declinedTimerRef = useRef(null);
|
||||
|
||||
const addPlayer = useCallback(entry => {
|
||||
setPlayers(prev =>
|
||||
@@ -103,6 +106,31 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
||||
return () => es.close();
|
||||
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setChallengingGameAssoc(null);
|
||||
clearTimeout(declinedTimerRef.current);
|
||||
setDeclinedMsg('Challenge was not accepted.');
|
||||
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
||||
};
|
||||
window.addEventListener('challenge-declined', handler);
|
||||
return () => {
|
||||
window.removeEventListener('challenge-declined', handler);
|
||||
clearTimeout(declinedTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleChallenge = player => {
|
||||
if (challengingGameAssoc) return;
|
||||
setChallengingGameAssoc(player.gameAssoc);
|
||||
setDeclinedMsg('');
|
||||
fetch('/api/game/challenge/' + player.gameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ challengerGameAssoc: currentGameAssoc }),
|
||||
}).catch(() => setChallengingGameAssoc(null));
|
||||
};
|
||||
|
||||
const visible = players
|
||||
.filter(p => p.gameAssoc !== currentGameAssoc)
|
||||
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
||||
@@ -177,24 +205,38 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && shown.map(player => (
|
||||
<div key={player.gameAssoc} className="opd-row">
|
||||
<div className="opd-avatar">
|
||||
{player.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="opd-info">
|
||||
<span className="opd-name">{player.name}</span>
|
||||
<span className="opd-since">
|
||||
<i className="fa fa-clock-o" />
|
||||
{' '}Waiting {formatSince(player.since)}
|
||||
</span>
|
||||
</div>
|
||||
<a className="opd-join" href={`/play/${player.gameAssoc}`}>
|
||||
<i className="fa fa-play" />
|
||||
Join
|
||||
</a>
|
||||
{declinedMsg && (
|
||||
<div className="opd-declined">
|
||||
<i className="fa fa-times-circle" />
|
||||
{' '}{declinedMsg}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{!loading && shown.map(player => {
|
||||
const isWaiting = challengingGameAssoc === player.gameAssoc;
|
||||
return (
|
||||
<div key={player.gameAssoc} className="opd-row">
|
||||
<div className="opd-avatar">
|
||||
{player.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="opd-info">
|
||||
<span className="opd-name">{player.name}</span>
|
||||
<span className="opd-since">
|
||||
<i className="fa fa-clock-o" />
|
||||
{' '}Waiting {formatSince(player.since)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={`opd-join${isWaiting ? ' opd-join--waiting' : ''}`}
|
||||
onClick={() => handleChallenge(player)}
|
||||
disabled={!!challengingGameAssoc}
|
||||
>
|
||||
<i className={`fa ${isWaiting ? 'fa-spinner fa-spin' : 'fa-play'}`} />
|
||||
{isWaiting ? 'Waiting...' : 'Join'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!loading && hasMore && (
|
||||
|
||||
@@ -130,6 +130,51 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
||||
showOverlay('The connection has been lost w/ your friend...', 'Please, restart the game!');
|
||||
};
|
||||
|
||||
const wChallenge = payload => {
|
||||
const { challengerName, challengerGameAssoc } = payload;
|
||||
|
||||
const handleAccept = () => {
|
||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted: true, targetGameAssoc: gameAssoc }),
|
||||
}).then(() => {
|
||||
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted: false, targetGameAssoc: gameAssoc }),
|
||||
}).then(() => {
|
||||
showOverlay('We are waiting for your opponent...', gameAssoc ? (
|
||||
<WaitingOverlayContent
|
||||
shareUrl={window.location.origin + '/play/' + gameAssoc}
|
||||
currentGameAssoc={gameAssoc}
|
||||
/>
|
||||
) : '');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
showOverlay(
|
||||
challengerName + ' wants to challenge you!',
|
||||
<div className="resign">
|
||||
<a onClick={handleAccept}>Accept</a>
|
||||
<a onClick={handleDecline}>Decline</a>
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
|
||||
const wChallengeResponse = payload => {
|
||||
if (payload.accepted) {
|
||||
window.location.href = '/play/' + payload.targetGameAssoc;
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('challenge-declined'));
|
||||
}
|
||||
};
|
||||
|
||||
const wTopic = payload => {
|
||||
if (webPlayerRef.current !== payload.data.player) {
|
||||
if (null === payload.data.resign) {
|
||||
@@ -152,6 +197,12 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
||||
};
|
||||
|
||||
const handleMercureMessage = payload => {
|
||||
if (undefined !== payload.type) {
|
||||
if ('challenge' === payload.type) wChallenge(payload);
|
||||
else if ('challenge-response' === payload.type) wChallengeResponse(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (undefined !== payload.data) {
|
||||
wTopic(payload);
|
||||
} else if (undefined === payload.msg) {
|
||||
|
||||
Reference in New Issue
Block a user