mancala/apps/frontend/src/routes/LobyPage.tsx

138 lines
5.3 KiB
TypeScript
Raw Normal View History

import { CommonMancalaGame } from 'mancala.js';
2022-07-30 14:17:16 +03:00
import * as React from 'react';
import { FunctionComponent, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import AppHeader from '../components/headerbar/AppHeader';
2022-07-30 14:17:16 +03:00
import PageContainer from '../components/PageContainer';
import { BOT_OFFER_DELAY_MS, BotLevel, channel_cancel_new_game, channel_new_bot_game, channel_new_game, channel_on_game_start, channel_on_new_game_rejected, isBotLevel } from '@mancala/core';
import BotLevelPicker from '../components/BotLevelPicker';
2022-07-30 14:17:16 +03:00
import { Context } from '../context/context';
import { Theme } from '@mancala/core';
import { useTranslation } from 'react-i18next';
2022-07-30 14:17:16 +03:00
const LobyPage: FunctionComponent<{
context: Context,
userKey?: string,
theme: Theme
}> = ({ context }) => {
2022-07-30 14:17:16 +03:00
let navigate = useNavigate();
const { t } = useTranslation();
// /loby?bot=<level> starts a game against the bot at once instead of searching for an opponent.
const [searchParams] = useSearchParams();
const botParam = searchParams.get("bot");
const botLevel: BotLevel | undefined = isBotLevel(botParam) ? botParam : undefined;
// Set when the server refuses a new game because the user has one in progress.
const [activeGameId, setActiveGameId] = useState<string | undefined>(undefined);
// Shown when no opponent has been found for a while.
const [botOffered, setBotOffered] = useState(false);
const onNewGameRejected = (message: { gameId: string }) => setActiveGameId(message.gameId);
2022-09-02 00:03:17 +03:00
const onGameStart = (message: Object) => {
const newGame: CommonMancalaGame = message as CommonMancalaGame;
navigate(`/game/${newGame.id}`)
}
const startBotGame = (level: BotLevel) => {
context.rtmt.sendMessage(channel_new_bot_game, { level });
}
2024-03-31 17:54:07 +03:00
const onCancelNewGameClick = () => {
if (context.rtmt.connectionState === "connected") {
context.rtmt.sendMessage(channel_cancel_new_game, "");
}
navigate("/")
}
2022-07-30 14:17:16 +03:00
useEffect(() => {
2022-09-02 00:03:17 +03:00
context.rtmt.addMessageListener(channel_on_game_start, onGameStart);
context.rtmt.addMessageListener(channel_on_new_game_rejected, onNewGameRejected);
let botOfferTimer: ReturnType<typeof setTimeout> | undefined;
if (botLevel) {
startBotGame(botLevel);
} else {
context.rtmt.sendMessage(channel_new_game, {});
botOfferTimer = setTimeout(() => setBotOffered(true), BOT_OFFER_DELAY_MS);
}
2022-09-02 00:03:17 +03:00
return () => {
if (botOfferTimer) clearTimeout(botOfferTimer);
2022-09-02 00:03:17 +03:00
context.rtmt.removeMessageListener(channel_on_game_start, onGameStart);
context.rtmt.removeMessageListener(channel_on_new_game_rejected, onNewGameRejected);
2022-09-02 00:03:17 +03:00
}
2022-07-30 14:17:16 +03:00
}, []);
2024-03-31 17:54:07 +03:00
return (
<PageContainer>
<AppHeader context={context} />
<main className="wrap">
<section className="search panel" aria-live="polite">
{!activeGameId && <span className="dot" />}
<h1>{activeGameId ? t("GameInProgress") : botLevel ? t("PlayWithBot") : t("SearchingOpponent")}</h1>
{!activeGameId && <p>{`${t("PleaseWait")}...`}</p>}
{!activeGameId && botOffered && (
<div className="offer">
<p>{t("NoOpponentYet")}</p>
<BotLevelPicker context={context} onChoose={startBotGame} />
</div>
)}
<div className="actions">
{activeGameId && (
<button className="btn pri" onClick={() => navigate(`/game/${activeGameId}`)}>{t("Continue")}</button>
)}
<button className="btn ghost" onClick={onCancelNewGameClick}>{t("Cancel")}</button>
</div>
</section>
</main>
<style jsx>{`
.search {
align-self: center;
width: 100%;
max-width: 460px;
margin-top: 8vh;
padding: 32px;
box-sizing: border-box;
border-radius: 32px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
.dot {
width: 14px;
height: 14px;
color: var(--accent);
}
h1 {
margin: 0;
font-size: 28px;
letter-spacing: -.02em;
}
p {
margin: 0;
color: var(--muted);
}
.offer {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 8px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
`}</style>
2022-07-30 14:17:16 +03:00
</PageContainer>
);
}
export default LobyPage;