commit
77084c3aff
@ -13,17 +13,19 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@szhsin/react-menu": "^3.0.2",
|
||||
"@szhsin/react-menu": "^3.1.2",
|
||||
"@types/": "szhsin/react-menu",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/styled-jsx": "^3.4.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eventemitter2": "^6.4.7",
|
||||
"mancala.js": "^0.0.2-beta.3",
|
||||
"notyf": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "6",
|
||||
"styled-jsx": "^5.0.2",
|
||||
"sweetalert": "^2.1.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -12,9 +12,11 @@ import Home from './routes/Home';
|
||||
import { initContext } from './context/context';
|
||||
import { RTMTWS } from './rtmt/rtmt_websocket';
|
||||
import { getColorByBrightness } from './util/ColorUtil';
|
||||
import { ConnectionState } from './models/ConnectionState';
|
||||
import { Theme } from './theme/Theme';
|
||||
import LobyPage from './routes/LobyPage';
|
||||
import swal from 'sweetalert';
|
||||
import { ConnectionState } from './rtmt/rtmt';
|
||||
|
||||
const context = initContext();
|
||||
|
||||
const MancalaApp: FunctionComponent = () => {
|
||||
@ -40,9 +42,15 @@ const MancalaApp: FunctionComponent = () => {
|
||||
setTheme(theme)
|
||||
}
|
||||
const connectToServer = async () => {
|
||||
try {
|
||||
const userKey = await context.userKeyStore.getUserKey();
|
||||
setUserKey(userKey);
|
||||
(context.rtmt as RTMTWS).initWebSocket(userKey);
|
||||
} catch (error) {
|
||||
//TODO: check if it is network error!
|
||||
swal(context.texts.Error + "!", context.texts.ErrorWhenRetrievingInformation, "error");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -74,7 +82,6 @@ const MancalaApp: FunctionComponent = () => {
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
if (!userKey) return <></>;
|
||||
return (
|
||||
<>
|
||||
<BrowserRouter>
|
||||
@ -82,7 +89,7 @@ const MancalaApp: FunctionComponent = () => {
|
||||
<Route index element={<Home context={context} theme={theme} userKey={userKey} />} />
|
||||
<Route path="/" >
|
||||
<Route path="game" >
|
||||
<Route path=":gameId" element={<GamePage context={context} theme={theme} userKey={userKey} connectionState={connectionState} />} ></Route>
|
||||
<Route path=":gameId" element={<GamePage context={context} theme={theme} userKey={userKey} />} ></Route>
|
||||
</Route>
|
||||
<Route path="loby" element={<LobyPage context={context} theme={theme} userKey={userKey} />}>
|
||||
</Route>
|
||||
@ -90,7 +97,7 @@ const MancalaApp: FunctionComponent = () => {
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<FloatingPanel context={context} color={floatingPanelColor} visible={showConnectionState}>
|
||||
<span style={{ color: textColorOnBoard }}>{connectionStateText()}</span>
|
||||
<span style={{ color: textColorOnBoard, transition: 'color 0.5s' }}>{connectionStateText()}</span>
|
||||
</FloatingPanel>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -13,6 +13,7 @@ import { Context } from "../context/context";
|
||||
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
||||
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
||||
import { Game } from "../models/Game";
|
||||
import { Theme } from "../theme/Theme";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||
|
||||
@ -35,6 +36,7 @@ export default class PitAnimator {
|
||||
) {
|
||||
this.context = context;
|
||||
this.onBoardViewModelUpdate = onBoardViewModelUpdate;
|
||||
this.context.themeManager.on("themechange", this.onThemeChange.bind(this));
|
||||
}
|
||||
|
||||
get mancalaGame(): MancalaGame | undefined {
|
||||
@ -190,6 +192,11 @@ export default class PitAnimator {
|
||||
});
|
||||
}
|
||||
|
||||
private onThemeChange(theme: Theme){
|
||||
if(!this.game) return;
|
||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||
}
|
||||
|
||||
public resetAnimationState() {
|
||||
this.animationIndex = -1;
|
||||
this.currentHistoryItem = undefined;
|
||||
@ -204,6 +211,7 @@ export default class PitAnimator {
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.context.themeManager.off("themechange", this.onThemeChange.bind(this));
|
||||
this.resetAnimationState();
|
||||
this.clearCurrentInterval();
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ const Button: FunctionComponent<{
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
background: color,
|
||||
backgroundColor: color,
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
@ -28,6 +28,9 @@ const Button: FunctionComponent<{
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
transition: color 0.5s;
|
||||
transition: background-color 0.5s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`}</style>
|
||||
{text}
|
||||
|
||||
@ -11,6 +11,8 @@ const CircularPanel: FunctionComponent<{
|
||||
div {
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
transition: background-color 0.5s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
@ -99,7 +99,7 @@ const InfoPanel: FunctionComponent<{
|
||||
if (text) {
|
||||
return (
|
||||
<CircularPanel style={style} color={containerColor}>
|
||||
<h4 style={{ margin: "0", color: textColor }}>
|
||||
<h4 style={{ margin: "0", color: textColor, transition: 'color 0.5s' }}>
|
||||
{text}
|
||||
</h4>
|
||||
</CircularPanel>
|
||||
|
||||
@ -14,6 +14,7 @@ const PageContainer: FunctionComponent<{ theme: Theme }> = (props) => {
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
`}</style>
|
||||
{props.children}
|
||||
|
||||
@ -16,17 +16,13 @@ const UserStatus: FunctionComponent<{
|
||||
}> = ({ context, user, layoutMode, visible, style }) => {
|
||||
if (visible === false) return <></>;
|
||||
const textColorOnBoard = getColorByBrightness(
|
||||
context.themeManager.theme.boardColor,
|
||||
context.themeManager.theme.background,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<div style={style} className={layoutMode === "right" ? "flex-rtl" : "flex-ltr"}>
|
||||
<span style={{color: textColorOnBoard}} className="material-symbols-outlined icon" >
|
||||
face_6
|
||||
</span>
|
||||
<Space width='5px' />
|
||||
<span style={{color: textColorOnBoard}} className='text'>{user.isAnonymous ? context.texts.Anonymous : user.name}</span>
|
||||
<span style={{color: textColorOnBoard, transition: 'color 0.5s'}} className='text'>{user.isAnonymous ? context.texts.Anonymous : user.name}</span>
|
||||
<Space width='5px' />
|
||||
<div className={"circle " + (user.isOnline ? "online" : "offline")}></div>
|
||||
<style jsx>{`
|
||||
@ -43,6 +39,8 @@ const UserStatus: FunctionComponent<{
|
||||
min-height: 15px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid ${context.themeManager.theme.boardColor};
|
||||
transition: background-color 0.5s;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
.flex-rtl {
|
||||
display: flex;
|
||||
|
||||
@ -29,7 +29,7 @@ const StoreView: FunctionComponent<{
|
||||
gridRow: gridRow
|
||||
}}>
|
||||
{stones}
|
||||
<span className="store-stone-count-text" style={{ color: textColor }}>
|
||||
<span className="store-stone-count-text" style={{ color: textColor, transition: 'color 0.5s' }}>
|
||||
{stones.length}
|
||||
</span>
|
||||
<style jsx>{`
|
||||
@ -42,6 +42,7 @@ const StoreView: FunctionComponent<{
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
.store-stone-count-text {
|
||||
position: absolute;
|
||||
|
||||
@ -13,6 +13,7 @@ const HeaderBar: FunctionComponent<{ color?: string }> = ({children, color }) =>
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-self: stretch;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
`}</style>
|
||||
</div>)
|
||||
|
||||
@ -4,7 +4,7 @@ import { Context } from '../../context/context';
|
||||
|
||||
const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => {
|
||||
return (
|
||||
<h1 style={{ color: color }} className="header-bar-title">
|
||||
<h1 style={{ color: color, transition: 'color 0.5s' }} className="header-bar-title">
|
||||
{title}
|
||||
<style jsx>{`
|
||||
.header-bar-title {
|
||||
|
||||
@ -8,22 +8,27 @@ import "@szhsin/react-menu/dist/transitions/slide.css"
|
||||
const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => {
|
||||
const { context, textColor } = props;
|
||||
const menuButton = <span
|
||||
style={{ color: textColor }}
|
||||
style={{ color: textColor, cursor: 'pointer', userSelect: 'none' }}
|
||||
className="material-symbols-outlined">
|
||||
light_mode
|
||||
</span>;
|
||||
const menuItems = context.themeManager.themes.map((theme, index) => {
|
||||
const themeBackground = context.themeManager.theme.background;
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
style={{ color: textColor }}
|
||||
//@ts-ignore
|
||||
onMouseOver={(event) => (event.target.style.background = themeBackground)}
|
||||
onMouseOver={(event) => {
|
||||
const htmlElement: HTMLElement = event.target as HTMLElement;
|
||||
if (htmlElement.localName === "li") htmlElement.style.background = "transparent";
|
||||
}}
|
||||
//@ts-ignore
|
||||
onMouseOut={(event) => (event.target.style.background = "transparent")}
|
||||
onMouseOut={(event) => {
|
||||
const htmlElement: HTMLElement = event.target as HTMLElement;
|
||||
if (htmlElement.localName === "li") htmlElement.style.background = "transparent";
|
||||
}}
|
||||
onClick={() => (context.themeManager.theme = theme)}>
|
||||
<div style={{ background: theme.boardColor }} className="theme-color-circle" />
|
||||
<div style={{ background: theme.themePreviewColor }} className="theme-color-circle" />
|
||||
{theme.name}
|
||||
<style jsx>{`
|
||||
.theme-color-circle {
|
||||
|
||||
@ -6,7 +6,7 @@ export type Texts = {
|
||||
YourTurn: string,
|
||||
OpponentTurn: string,
|
||||
GameEnded: string,
|
||||
GameCrashed: string,
|
||||
InternalErrorOccurred: string,
|
||||
YouWon: string,
|
||||
Won: string,
|
||||
YouLost: string,
|
||||
@ -27,6 +27,14 @@ export type Texts = {
|
||||
GameNotFound: string,
|
||||
Loading: string,
|
||||
Playing: string,
|
||||
Error: string,
|
||||
ErrorWhenRetrievingInformation: string,
|
||||
UCanOnlyPlayYourOwnPits: string,
|
||||
UMustWaitUntilCurrentMoveComplete: string,
|
||||
UCanNotPlayEmptyPit: string,
|
||||
AreYouSureToLeaveGame: string,
|
||||
Yes: string,
|
||||
Cancel: string,
|
||||
}
|
||||
|
||||
export const EnUs: Texts = {
|
||||
@ -36,14 +44,14 @@ export const EnUs: Texts = {
|
||||
YourTurn: "Your Turn",
|
||||
OpponentTurn: "Opponent Turn",
|
||||
GameEnded: "Game Ended",
|
||||
GameCrashed: "Game Crashed",
|
||||
InternalErrorOccurred: "An internal error has occurred",
|
||||
YouWon: "You Won",
|
||||
Won: "Won",
|
||||
YouLost: "You Lost",
|
||||
Connecting: "Connecting",
|
||||
Connected: "Connected",
|
||||
CannotConnect: "Can't Connect",
|
||||
ConnectionLost: "Connection Lost",
|
||||
ConnectionLost: "Network Connection Lost",
|
||||
ConnectingAgain: "Connecting Again",
|
||||
ServerError: "Server Error",
|
||||
SearchingOpponet: "Searching Opponet",
|
||||
@ -57,6 +65,14 @@ export const EnUs: Texts = {
|
||||
GameNotFound: "Game Not Found",
|
||||
Loading: "Loading",
|
||||
Playing: "Playing",
|
||||
Error: "Error",
|
||||
ErrorWhenRetrievingInformation: "An error occured when retrieving information!",
|
||||
UCanOnlyPlayYourOwnPits: "You can only play your own pits",
|
||||
UMustWaitUntilCurrentMoveComplete: "You must wait until the current move is complete",
|
||||
UCanNotPlayEmptyPit: "You can not play empty pit",
|
||||
AreYouSureToLeaveGame: "Are you sure to leave game?",
|
||||
Yes: "Yes",
|
||||
Cancel: "Cancel",
|
||||
}
|
||||
|
||||
export const TrTr: Texts = {
|
||||
@ -66,14 +82,14 @@ export const TrTr: Texts = {
|
||||
YourTurn: "Sıra Sende",
|
||||
OpponentTurn: "Sıra Rakipte",
|
||||
GameEnded: "Oyun Bitti",
|
||||
GameCrashed: "Oyunda Hata Oluştu",
|
||||
InternalErrorOccurred: "Dahili bir hata oluştu",
|
||||
YouWon: "Kazandın",
|
||||
Won: "Kazandı",
|
||||
YouLost: "Kaybettin",
|
||||
Connecting: "Bağlanılıyor",
|
||||
Connected: "Bağlandı",
|
||||
CannotConnect: "Bağlanılamadı",
|
||||
ConnectionLost: "Bağlantı Koptu",
|
||||
ConnectionLost: "Ağ Bağlantısı Koptu",
|
||||
ConnectingAgain: "Tekrar Bağlanılıyor",
|
||||
ServerError: "Sunucu Hatası",
|
||||
SearchingOpponet: "Rakip Aranıyor",
|
||||
@ -87,4 +103,12 @@ export const TrTr: Texts = {
|
||||
GameNotFound: "Oyun Bulunamadı",
|
||||
Loading: "Yükleniyor",
|
||||
Playing: "Oynuyor",
|
||||
Error: "Hata",
|
||||
ErrorWhenRetrievingInformation: "Bilgiler toplanırken bir hata oluştu!",
|
||||
UCanOnlyPlayYourOwnPits: "Sadece sana ait olan kuyular ile oynayabilirsin",
|
||||
UMustWaitUntilCurrentMoveComplete: "Devam eden haraketin bitmesini beklemelisin",
|
||||
UCanNotPlayEmptyPit: "Boş kuyu ile oynayamazsın",
|
||||
AreYouSureToLeaveGame: "Oyundan ayrılmak istediğine emin misin?",
|
||||
Yes: "Evet",
|
||||
Cancel: "İptal"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
||||
@ -20,7 +20,6 @@ import UserStatus from '../components/UserStatus';
|
||||
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move, channel_listen_game_events, channel_unlisten_game_events } from '../const/channel_names';
|
||||
import { Context } from '../context/context';
|
||||
import useWindowDimensions from '../hooks/useWindowDimensions';
|
||||
import { ConnectionState } from '../models/ConnectionState';
|
||||
import { GameMove } from '../models/GameMove';
|
||||
import { LoadingState } from '../models/LoadingState';
|
||||
import { Theme } from '../theme/Theme';
|
||||
@ -28,13 +27,15 @@ import { getColorByBrightness } from '../util/ColorUtil';
|
||||
import BoardViewModel from '../viewmodel/BoardViewModel';
|
||||
import Center from '../components/Center';
|
||||
import { Game, GameUsersConnectionInfo } from '../models/Game';
|
||||
import notyf from '../util/Notyf';
|
||||
import swal from 'sweetalert';
|
||||
import Util from '../util/Util';
|
||||
|
||||
const GamePage: FunctionComponent<{
|
||||
context: Context,
|
||||
userKey?: string,
|
||||
theme: Theme,
|
||||
connectionState: ConnectionState
|
||||
}> = ({ context, userKey, theme, connectionState }) => {
|
||||
}> = ({ context, userKey, theme }) => {
|
||||
let params = useParams<{ gameId: string }>();
|
||||
|
||||
const [game, setGame] = useState<Game | undefined>(undefined);
|
||||
@ -89,6 +90,7 @@ const GamePage: FunctionComponent<{
|
||||
}
|
||||
const onGameCrashed = (message: any) => {
|
||||
const newCrashMessage = message as string;
|
||||
notyf.error(context.texts.InternalErrorOccurred);
|
||||
console.error("on_game_crash");
|
||||
console.error(newCrashMessage);
|
||||
}
|
||||
@ -136,10 +138,22 @@ const GamePage: FunctionComponent<{
|
||||
const checkHasAnOngoingAction = () => hasOngoingAction;
|
||||
|
||||
const onLeaveGameClick = () => {
|
||||
if(Util.checkConnectionAndMaybeAlert(context)) return;
|
||||
swal({
|
||||
title: context.texts.AreYouSureToLeaveGame,
|
||||
icon: "warning",
|
||||
buttons: [context.texts.Yes, context.texts.Cancel],
|
||||
dangerMode: true,
|
||||
})
|
||||
.then((cancel) => {
|
||||
if (!cancel) {
|
||||
context.rtmt.sendMessage(channel_leave_game, {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onNewGameClick = () => {
|
||||
if(Util.checkConnectionAndMaybeAlert(context)) return;
|
||||
navigate("/loby")
|
||||
};
|
||||
|
||||
@ -147,21 +161,27 @@ const GamePage: FunctionComponent<{
|
||||
if (!game || isSpectator || !userKey) {
|
||||
return;
|
||||
}
|
||||
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) {
|
||||
notyf.error(context.texts.UCanOnlyPlayYourOwnPits);
|
||||
return;
|
||||
}
|
||||
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
|
||||
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey ||
|
||||
!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
|
||||
if (!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
|
||||
notyf.error(context.texts.OpponentTurn);
|
||||
return;
|
||||
}
|
||||
if (checkHasAnOngoingAction()) {
|
||||
notyf.error(context.texts.UMustWaitUntilCurrentMoveComplete);
|
||||
return;
|
||||
}
|
||||
setHasOngoingAction(true);
|
||||
if(Util.checkConnectionAndMaybeAlert(context)) return;
|
||||
if (!boardViewModel) return;
|
||||
//TODO: this check should be in mancala.js
|
||||
if (pit.stoneCount === 0) {
|
||||
//TODO : warn user
|
||||
notyf.error(context.texts.UCanNotPlayEmptyPit);
|
||||
return;
|
||||
}
|
||||
setHasOngoingAction(true);
|
||||
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
|
||||
context.themeManager.theme.pitSelectedColor;
|
||||
updateBoardViewModel(boardViewModel);
|
||||
@ -210,13 +230,13 @@ const GamePage: FunctionComponent<{
|
||||
const bottomLocatedUser = {
|
||||
id: bottomLocatedUserId,
|
||||
name: "Anonymous",
|
||||
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : connectionState === "connected",
|
||||
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : context.rtmt.connectionState === "connected",
|
||||
isAnonymous: true
|
||||
};
|
||||
const currentUser = isSpectator ? {
|
||||
id: "2",
|
||||
name: "Anonymous",
|
||||
isOnline: connectionState === "connected",
|
||||
isOnline: context.rtmt.connectionState === "connected",
|
||||
isAnonymous: true
|
||||
} : bottomLocatedUser;
|
||||
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
|
||||
@ -263,7 +283,7 @@ const GamePage: FunctionComponent<{
|
||||
|
||||
function renderMobileBoardToolbar() {
|
||||
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||
{buildInfoPanel()}
|
||||
{buildInfoPanel({ visible: isMobile })}
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
@ -273,7 +293,7 @@ const GamePage: FunctionComponent<{
|
||||
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
||||
width: isMobile ? "40vw" : "30vw"
|
||||
}} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} />
|
||||
{buildInfoPanel()}
|
||||
{buildInfoPanel({ visible: !isMobile })}
|
||||
<UserStatus style={{
|
||||
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
||||
width: isMobile ? "40vw" : "30vw"
|
||||
@ -281,7 +301,7 @@ const GamePage: FunctionComponent<{
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
function buildInfoPanel() {
|
||||
function buildInfoPanel(params: { visible: boolean }) {
|
||||
return (
|
||||
<InfoPanel
|
||||
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
|
||||
@ -291,7 +311,7 @@ const GamePage: FunctionComponent<{
|
||||
whitePlayer={topLocatedUser}
|
||||
blackPlayer={bottomLocatedUser}
|
||||
leftPlayer={leftPlayer}
|
||||
visible={!isMobile}
|
||||
visible={params.visible}
|
||||
isSpectator={isSpectator} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
|
||||
import Button from "../components/Button";
|
||||
import { Context } from "../context/context";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Util from "../util/Util";
|
||||
|
||||
const Home: FunctionComponent<{
|
||||
context: Context,
|
||||
@ -21,6 +22,7 @@ const Home: FunctionComponent<{
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNewGameClick = () => {
|
||||
if(Util.checkConnectionAndMaybeAlert(context)) return;
|
||||
navigate("/loby")
|
||||
};
|
||||
|
||||
|
||||
@ -3,10 +3,12 @@ import EventEmitter2, { Listener } from "eventemitter2"
|
||||
export type Bytes = Uint8Array
|
||||
export type OnMessage = (message : Object) => any
|
||||
|
||||
export type ConnectionState = "none" | "connecting" | "error" | "connected" | "closed" | "reconnecting";
|
||||
|
||||
export type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message";
|
||||
|
||||
export interface RTMT extends EventEmitter2 {
|
||||
get connectionState() : ConnectionState;
|
||||
sendMessage: (channel: string, message: Object) => void;
|
||||
addMessageListener(channel: string, callback: (message: any) => void);
|
||||
removeMessageListener(channel: string, callback: (message: any) => void);
|
||||
|
||||
@ -2,7 +2,7 @@ import { decode, encode } from "./encode_decode_message";
|
||||
import { channel_ping, channel_pong } from "../const/channel_names";
|
||||
import { server } from "../const/config";
|
||||
import EventEmitter2, { Listener } from "eventemitter2";
|
||||
import { Bytes, RTMT, RtmtEventTypes } from "./rtmt";
|
||||
import { Bytes, ConnectionState, RTMT, RtmtEventTypes } from "./rtmt";
|
||||
|
||||
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
|
||||
const MESSAGE_CHANNEL_PREFIX = "message_channel";
|
||||
@ -10,24 +10,32 @@ const MESSAGE_CHANNEL_PREFIX = "message_channel";
|
||||
export class RTMTWS extends EventEmitter2 implements RTMT {
|
||||
private webSocket: WebSocket;
|
||||
private pingTimeout?: number = undefined;
|
||||
private _connectionState: ConnectionState = "none";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
get connectionState(): ConnectionState {
|
||||
return this._connectionState;
|
||||
}
|
||||
|
||||
public initWebSocket(userKey: string) {
|
||||
this._connectionState = this._connectionState !== "none" ? "reconnecting" : "connecting";
|
||||
const url = server.wsServerAdress + "?userKey=" + userKey;
|
||||
const webSocket = new WebSocket(url);
|
||||
webSocket.onopen = () => {
|
||||
console.info("(RTMT) WebSocket has opened");
|
||||
this.webSocket = webSocket;
|
||||
this.heartbeat();
|
||||
this._connectionState = "connected";
|
||||
this.emit("open");
|
||||
};
|
||||
webSocket.onclose = () => {
|
||||
console.info("(RTMT) WebSocket has closed");
|
||||
//this.WebSocket = undefined
|
||||
clearTimeout(this.pingTimeout);
|
||||
this._connectionState = "closed";
|
||||
this.emit("close");
|
||||
};
|
||||
webSocket.onmessage = (event: MessageEvent) => {
|
||||
@ -36,6 +44,7 @@ export class RTMTWS extends EventEmitter2 implements RTMT {
|
||||
};
|
||||
webSocket.onerror = (error) => {
|
||||
console.error(error);
|
||||
this._connectionState = "error";
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ const colorSpecial = "#337a44";
|
||||
const darkTheme: Theme = {
|
||||
id: "2",
|
||||
name: "Dark Theme",
|
||||
themePreviewColor: colors.primary,
|
||||
background: colors.primary,
|
||||
appBarBgColor: colors.secondary,
|
||||
textColor: colors.primary,
|
||||
|
||||
@ -3,6 +3,7 @@ import { Theme } from "./Theme";
|
||||
const greyTheme: Theme = {
|
||||
id: "1",
|
||||
name: "Grey Theme",
|
||||
themePreviewColor: "#4D606E",
|
||||
background: "#EEEEEE",
|
||||
appBarBgColor: "#e4e4e4",
|
||||
textColor: "#4D606E",
|
||||
|
||||
@ -5,6 +5,7 @@ const colorSpecial = "#8B8B8B";
|
||||
const lightTheme: Theme = {
|
||||
id: "1",
|
||||
name: "Light Theme",
|
||||
themePreviewColor: "#9B9B9B",
|
||||
background: "#BBBBBB",
|
||||
appBarBgColor: "#7B7B7B",
|
||||
textColor: "#5B5B5B",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
themePreviewColor: string; // for theme switch menu
|
||||
textColor: string;
|
||||
textLightColor: string;
|
||||
background: string;
|
||||
|
||||
4
src/util/Notyf.ts
Normal file
4
src/util/Notyf.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Notyf } from 'notyf';
|
||||
import 'notyf/notyf.min.css';
|
||||
const notyf = new Notyf();
|
||||
export default notyf;
|
||||
@ -1,3 +1,5 @@
|
||||
import { Context } from "../context/context";
|
||||
import notyf from "./Notyf";
|
||||
|
||||
export default class Util {
|
||||
public static range(size: number) {
|
||||
@ -7,4 +9,12 @@ export default class Util {
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
public static checkConnectionAndMaybeAlert(context: Context): boolean {
|
||||
if (context.rtmt.connectionState !== "connected") {
|
||||
notyf.error(context.texts.ConnectionLost);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
31
yarn.lock
31
yarn.lock
@ -1067,10 +1067,10 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@szhsin/react-menu@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-3.0.2.tgz#d22971c53d56e6d404c9d3c98f533907cd8f03dc"
|
||||
integrity sha512-m9Ly+cT+CxQx3xhq90CVaOLQWU7f7UKeMxfDt1gPYV23tDwEe8Zo6PO547qPlAEGEwwb9MdA38U8OyueXKJc2g==
|
||||
"@szhsin/react-menu@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-3.1.2.tgz#3a791e7e6c672d113c298985bec5185e9c7aa8a7"
|
||||
integrity sha512-NUnU429a3jXtRD4xxk8EsR4yRSuhZPWAkI+4P4K63LQPUZGVE7adVKtEmlyOpd8CRQ7aoUz1ZLr1VmR1nZi6GQ==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
react-transition-state "^1.1.4"
|
||||
@ -1418,6 +1418,11 @@ error-ex@^1.3.1:
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es6-object-assign@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
|
||||
integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz"
|
||||
@ -1630,6 +1635,11 @@ node-releases@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
|
||||
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
|
||||
|
||||
notyf@^3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/notyf/-/notyf-3.10.0.tgz#67a64443c69ea0e6495c56ea0f91198860163d06"
|
||||
integrity sha512-Mtnp+0qiZxgrH+TzVlzhWyZceHdAZ/UWK0/ju9U0HQeDpap1mZ8cC7H5wSI5mwgni6yeAjaxsTw0sbMK+aSuHw==
|
||||
|
||||
nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
@ -1738,6 +1748,11 @@ process@^0.11.10:
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
promise-polyfill@^6.0.2:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
|
||||
integrity sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==
|
||||
|
||||
prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
@ -1884,6 +1899,14 @@ svgo@^2.4.0, svgo@^2.5.0:
|
||||
picocolors "^1.0.0"
|
||||
stable "^0.1.8"
|
||||
|
||||
sweetalert@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sweetalert/-/sweetalert-2.1.2.tgz#010baaa80d0dbdc86f96bfcaa96b490728594b79"
|
||||
integrity sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==
|
||||
dependencies:
|
||||
es6-object-assign "^1.1.0"
|
||||
promise-polyfill "^6.0.2"
|
||||
|
||||
term-size@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user