From b7945b24f6e9fed52ac2dbdd7168e26ed5cd248e Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Thu, 28 Jul 2022 18:09:19 +0300 Subject: [PATCH 01/12] remove unused parameter --- src/animation/PitAnimator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/animation/PitAnimator.ts b/src/animation/PitAnimator.ts index 38cf540..2f4d442 100644 --- a/src/animation/PitAnimator.ts +++ b/src/animation/PitAnimator.ts @@ -42,7 +42,7 @@ export default class PitAnimator { this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game)); } - public setUpdatedGame(game: MancalaGame, forceClear = false) { + public setUpdatedGame(game: MancalaGame) { this.resetAnimationState(); if (!this.game) { this.setNewGame(game); @@ -68,7 +68,7 @@ export default class PitAnimator { } onAnimate() { - if(!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return; + if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return; if (this.animationIndex === this.currentHistoryItem.gameSteps.length) { this.clearCurrentInterval(); this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game)); @@ -92,7 +92,7 @@ export default class PitAnimator { boardViewModel: BoardViewModel, gameStep: GameStep ) { - if(!this.currentHistoryItem || !this.game) return; + if (!this.currentHistoryItem || !this.game) return; const pitViewModel = boardViewModel.pits[index]; if (this.animationIndex === 0) { //This is one stone move case, TODO: beautify it later From 8ce2381c4b6af2467c4eff6472edd0b8279b3ca0 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 12:05:01 +0300 Subject: [PATCH 02/12] add react-router dependency --- package.json | 1 + yarn.lock | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9781d67..0567cb1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "mancala.js": "^0.0.2-beta.3", "react": "^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-router-dom": "6", "styled-jsx": "^5.0.2", "uuid": "^8.3.2" }, diff --git a/yarn.lock b/yarn.lock index 9c9e032..c13b937 100644 --- a/yarn.lock +++ b/yarn.lock @@ -171,6 +171,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf" integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA== +"@babel/runtime@^7.7.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" @@ -1446,6 +1453,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +history@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + htmlnano@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878" @@ -1744,6 +1758,21 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== +react-router-dom@6: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== + dependencies: + history "^5.2.0" + react-router "6.3.0" + +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react-transition-state@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac" @@ -1756,7 +1785,7 @@ react-transition-state@^1.1.4: dependencies: loose-envify "^1.1.0" -regenerator-runtime@^0.13.7: +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: version "0.13.9" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== From fb4f38330fa6ee40452644532c2c3c4ce14eb201 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 12:01:50 +0300 Subject: [PATCH 03/12] add MancalaApp and refactor connection state floating panel --- src/App.tsx | 7 +-- src/MancalaApp.tsx | 92 +++++++++++++++++++++++++++++++++++ src/context/context.tsx | 4 +- src/models/ConnectionState.ts | 1 + src/routes/GamePage.tsx | 18 +++++++ src/routes/Home.tsx | 63 +++--------------------- 6 files changed, 121 insertions(+), 64 deletions(-) create mode 100644 src/MancalaApp.tsx create mode 100644 src/models/ConnectionState.ts create mode 100644 src/routes/GamePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 5edb8b9..d079d3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,6 @@ -import { initContext } from './context/context'; -initContext(); - import * as React from 'react'; import { createRoot } from 'react-dom/client'; const container = document.getElementById('main'); const root = createRoot(container!); -import Home from './routes/Home'; -root.render(); \ No newline at end of file +import MancalaApp from './MancalaApp'; +root.render(); \ No newline at end of file diff --git a/src/MancalaApp.tsx b/src/MancalaApp.tsx new file mode 100644 index 0000000..80427d0 --- /dev/null +++ b/src/MancalaApp.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import { FunctionComponent, useState } from 'react'; +import { + BrowserRouter, + Routes, + Route, +} from "react-router-dom"; +import FloatingPanel from './components/FloatingPanel'; +import GamePage from './routes/GamePage'; +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'; +const context = initContext(); + +const MancalaApp: FunctionComponent = () => { + const [userKey, setUserKey] = useState(undefined); + + const [connectionState, setConnetionState] = useState("connecting"); + + const onConnectionDone = () => { + setConnetionState("connected"); + }; + const onConnectionLost = () => { + connectToServer("reconnecting"); + }; + const onConnectionError = (event: Event) => { + setConnetionState("error"); + }; + const connectToServer = (connectionState: ConnectionState) => { + setConnetionState(connectionState); + context.userKeyStore.getUserKey((userKey: string) => { + setUserKey(userKey); + const rtmtws = context.rtmt as RTMTWS; + if (rtmtws) { + rtmtws.initWebSocket( + userKey, + onConnectionDone, + onConnectionLost, + onConnectionError + ); + } else { + console.error("context.rtmt is not RTMTWS"); + } + }); + }; + React.useEffect(() => { + connectToServer("connecting"); + return () => { + // todo: dispose rtmt.dispose + //context.rtmt.dispose(); + }; + }, []); + + const showConnectionState = connectionState != "connected"; + const floatingPanelColor = context.themeManager.theme.boardColor; + const connectionStateText = () => { + const map: { [key: string]: string } = { + connecting: context.texts.Connecting, + connected: context.texts.Connected, + error: context.texts.CannotConnect, + reconnecting: context.texts.ConnectingAgain, + }; + return map[connectionState]; + }; + const textColorOnBoard = getColorByBrightness( + context.themeManager.theme.boardColor, + context.themeManager.theme.textColor, + context.themeManager.theme.textLightColor + ); + return ( + <> + + + } /> + + + } > + + + + + + {connectionStateText()} + + + ); +} + +export default MancalaApp; \ No newline at end of file diff --git a/src/context/context.tsx b/src/context/context.tsx index 28d2485..4825b3c 100644 --- a/src/context/context.tsx +++ b/src/context/context.tsx @@ -11,7 +11,7 @@ export type Context = { themeManager: ThemeManager; }; -export const initContext = () => { +export const initContext = () : Context => { const rtmt = new RTMTWS(); const userKeyStore = new UserKeyStoreImpl(); const texts = TrTr; @@ -23,5 +23,3 @@ export const initContext = () => { themeManager, }; }; - -export const context: Context = initContext(); diff --git a/src/models/ConnectionState.ts b/src/models/ConnectionState.ts new file mode 100644 index 0000000..5010944 --- /dev/null +++ b/src/models/ConnectionState.ts @@ -0,0 +1 @@ +export type ConnectionState = "connecting" | "error" | "connected" | "reconnecting"; \ No newline at end of file diff --git a/src/routes/GamePage.tsx b/src/routes/GamePage.tsx new file mode 100644 index 0000000..8c02d4b --- /dev/null +++ b/src/routes/GamePage.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { FunctionComponent } from 'react'; +import { useParams } from 'react-router'; +import { Context } from '../context/context'; + +const GamePage: FunctionComponent<{ context: Context }> = ({ context }) => { + let params = useParams<{gameId : string}>(); + return ( +
+ Game Route {params.gameId} + +
+ ); +} + +export default GamePage; \ No newline at end of file diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 0485672..3ff6022 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { FunctionComponent, useEffect, useState } from "react"; import BoardView from "../components/board/BoardView"; -import { context } from "../context/context"; import { RTMTWS } from "../rtmt/rtmt_websocket"; import { channel_game_move, @@ -21,7 +20,6 @@ import { v4 } from "uuid"; import { getColorByBrightness } from "../util/ColorUtil"; import { Theme } from "../theme/Theme"; import HeaderBar from "../components/headerbar/HeaderBar"; -import FloatingPanel from "../components/FloatingPanel"; import PageContainer from "../components/PageContainer"; import Row from "../components/Row"; import HeaderbarIcon from "../components/headerbar/HeaderbarIcon"; @@ -34,14 +32,14 @@ import Center from "../components/Center"; import CircularPanel from "../components/CircularPanel"; import useWindowDimensions from "../hooks/useWindowDimensions"; import { UserConnectionInfo } from "../models/UserConnectionInfo"; +import { Context } from "../context/context"; +import { ConnectionState } from "../models/ConnectionState"; -type ConnectionState = "connecting" | "error" | "connected" | "reconnecting"; - -const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { - const [userKey, setUserKey] = useState(undefined); - - const [connectionState, setConnetionState] = - useState("connecting"); +const Home: FunctionComponent<{ + context: Context, + userKey?: string, + connectionState: ConnectionState +}> = ({ context, userKey, connectionState }) => { const [searchingOpponent, setSearchingOpponent] = useState(false); @@ -67,36 +65,6 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { const [isOpponentOnline, setIsOpponentOnline] = useState(false); - const onConnectionDone = () => { - setConnetionState("connected"); - }; - - const onConnectionLost = () => { - connectToServer("reconnecting"); - }; - - const onConnectionError = (event: Event) => { - setConnetionState("error"); - }; - - const connectToServer = (connectionState: ConnectionState) => { - setConnetionState(connectionState); - context.userKeyStore.getUserKey((userKey: string) => { - setUserKey(userKey); - const rtmtws = context.rtmt as RTMTWS; - if (rtmtws) { - rtmtws.initWebSocket( - userKey, - onConnectionDone, - onConnectionLost, - onConnectionError - ); - } else { - console.error("context.rtmt is not RTMTWS"); - } - }); - }; - const listenMessages = (pitAnimator: PitAnimator) => { context.rtmt.listenMessage(channel_on_game_start, (message: Object) => { const newGame: CommonMancalaGame = message as CommonMancalaGame; @@ -163,7 +131,6 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { const pitAnimator = new PitAnimator(context, updateBoardViewModel); setPitAnimator(pitAnimator); listenMessages(pitAnimator); - connectToServer("connecting"); return () => { pitAnimator.dispose(); }; @@ -204,19 +171,6 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { context.rtmt.sendMessage(channel_game_move, gameMove); }; - const showConnectionState = connectionState != "connected"; - - const connectionStateText = () => { - let map: { [key: string]: string } = { - connecting: context.texts.Connecting, - connected: context.texts.Connected, - error: context.texts.CannotConnect, - reconnecting: context.texts.ConnectingAgain, - }; - - return map[connectionState]; - }; - const textColorOnBoard = getColorByBrightness( context.themeManager.theme.boardColor, context.themeManager.theme.textColor, @@ -236,9 +190,6 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { return ( - - {connectionStateText()} - From 300db9a687a9ecbad50c30e099af6dd6540bf55f Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 14:17:16 +0300 Subject: [PATCH 04/12] add loby page --- src/MancalaApp.tsx | 14 +++++++-- src/routes/LobyPage.tsx | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/routes/LobyPage.tsx diff --git a/src/MancalaApp.tsx b/src/MancalaApp.tsx index 80427d0..3d049c8 100644 --- a/src/MancalaApp.tsx +++ b/src/MancalaApp.tsx @@ -13,13 +13,18 @@ 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'; const context = initContext(); const MancalaApp: FunctionComponent = () => { + const [userKey, setUserKey] = useState(undefined); const [connectionState, setConnetionState] = useState("connecting"); + const [theme, setTheme] = useState(context.themeManager.theme); + const onConnectionDone = () => { setConnetionState("connected"); }; @@ -48,6 +53,9 @@ const MancalaApp: FunctionComponent = () => { }; React.useEffect(() => { connectToServer("connecting"); + context.themeManager.onThemeChange = (theme: Theme) => { + setTheme(theme); + } return () => { // todo: dispose rtmt.dispose //context.rtmt.dispose(); @@ -74,10 +82,12 @@ const MancalaApp: FunctionComponent = () => { <> - } /> + } /> - } > + } > + + }> diff --git a/src/routes/LobyPage.tsx b/src/routes/LobyPage.tsx new file mode 100644 index 0000000..386f13e --- /dev/null +++ b/src/routes/LobyPage.tsx @@ -0,0 +1,68 @@ +import { CommonMancalaGame, MancalaGame } from 'mancala.js'; +import * as React from 'react'; +import { FunctionComponent, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Center from '../components/Center'; +import CircularPanel from '../components/CircularPanel'; +import HeaderBar from '../components/headerbar/HeaderBar'; +import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; +import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; +import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; +import PageContainer from '../components/PageContainer'; +import Row from '../components/Row'; +import { channel_on_game_start } from '../const/channel_names'; +import { Context } from '../context/context'; +import { Theme } from '../theme/Theme'; +import { getColorByBrightness } from '../util/ColorUtil'; + +const LobyPage: FunctionComponent<{ + context: Context, + userKey?: string, + theme: Theme +}> = ({ context, userKey, theme }) => { + + let navigate = useNavigate(); + + useEffect(() => { + listenMessages(); + context.rtmt.sendMessage("new_game", {}); + }, []); + + const listenMessages = () => { + context.rtmt.listenMessage(channel_on_game_start, (message: Object) => { + const newGame: CommonMancalaGame = message as CommonMancalaGame; + navigate(`/game/${newGame.id}`) + }); + } + + const textColorOnAppBar = getColorByBrightness( + context.themeManager.theme.appBarBgColor, + context.themeManager.theme.textColor, + context.themeManager.theme.textLightColor + ); + const textColorOnBoard = getColorByBrightness( + context.themeManager.theme.boardColor, + context.themeManager.theme.textColor, + context.themeManager.theme.textLightColor + ); + return ( + + + + + + + + + + +
+ +

{`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}

+
+
+
+ ); +} + +export default LobyPage; \ No newline at end of file From 4bda2a35b4642ead99899334f12f132c25c9a580 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 16:18:48 +0300 Subject: [PATCH 05/12] add config --- src/const/config.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/const/config.ts diff --git a/src/const/config.ts b/src/const/config.ts new file mode 100644 index 0000000..95fea6b --- /dev/null +++ b/src/const/config.ts @@ -0,0 +1,15 @@ +export const useLocalServer = false; + +export type Server = { + serverAdress: string; + wsServerAdress: string; +}; + +export const server: Server = useLocalServer ? { + serverAdress: "http://localhost:5000", + wsServerAdress: "ws://localhost:5000", +} : { + serverAdress: "https://segin.one/mancala-backend-beta", + wsServerAdress: "wss://segin.one/mancala-backend-beta/", +}; + From af98005ea1c147936556769aa0e6fba925cb3f88 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 16:19:07 +0300 Subject: [PATCH 06/12] refactor http service --- src/service/HttpService.ts | 23 ++++++++++++++++ src/service/http_service.ts | 30 --------------------- src/store/KeyStore.ts | 53 +++++++++++++++++++++++++++++++++++++ src/store/key_store.ts | 51 ----------------------------------- 4 files changed, 76 insertions(+), 81 deletions(-) create mode 100644 src/service/HttpService.ts delete mode 100644 src/service/http_service.ts create mode 100644 src/store/KeyStore.ts delete mode 100644 src/store/key_store.ts diff --git a/src/service/HttpService.ts b/src/service/HttpService.ts new file mode 100644 index 0000000..c07c2f7 --- /dev/null +++ b/src/service/HttpService.ts @@ -0,0 +1,23 @@ +import { server } from "../const/config"; + +export interface HttpService { + get: (route: string) => Promise; +} + +export class HttpServiceImpl implements HttpService { + public serverAdress: string; + + constructor(serverAdress: string) { + this.serverAdress = serverAdress; + } + + public async get(route: string): Promise { + const url = server.serverAdress + route; + const requestOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }; + const response = await fetch(url, requestOptions); + return response; + } +} diff --git a/src/service/http_service.ts b/src/service/http_service.ts deleted file mode 100644 index fd7d5eb..0000000 --- a/src/service/http_service.ts +++ /dev/null @@ -1,30 +0,0 @@ -export type Server = { - serverAdress: string; - wsServerAdress: string; -}; - -export const server: Server = { - serverAdress: "https://segin.one/mancala-backend-beta", - wsServerAdress: "wss://segin.one/mancala-backend-beta/", -}; - -const useLocal = false; - -if (useLocal) { - server.serverAdress = "http://localhost:5000"; - server.wsServerAdress = "ws://localhost:5000"; -} - -interface HttpService { - get: (route: string, succes: () => any, error: () => any) => any; -} - -class HttpServiceImpl implements HttpService { - public serverAdress: string; - - constructor(serverAdress: string) { - this.serverAdress = serverAdress; - } - - public get(route: string, succes: () => any, error: () => any): any {} -} diff --git a/src/store/KeyStore.ts b/src/store/KeyStore.ts new file mode 100644 index 0000000..6923ff6 --- /dev/null +++ b/src/store/KeyStore.ts @@ -0,0 +1,53 @@ +import { HttpService } from "../service/HttpService" + +const user_key = "user_key" + +export interface UserKeyStore { + getUserKey: () => Promise; +} + +export class UserKeyStoreImpl implements UserKeyStore { + private httpService: HttpService; + private keyStoreHttp: UserKeyStore; + private keyStoreLocalStorage = new UserKeyStoreLocalStorage() + + constructor(props: { httpService: HttpService }) { + this.httpService = props.httpService; + this.keyStoreHttp = new UserKeyStoreLocalHttp({ httpService: this.httpService }); + } + + public async getUserKey(): Promise { + const maybeUserKey = await this.keyStoreLocalStorage.getUserKey(); + if (maybeUserKey === undefined) { + const userKey = await this.keyStoreHttp.getUserKey() + this.keyStoreLocalStorage.storeUserKey(userKey) + return Promise.resolve(userKey); + } else { + return Promise.resolve(maybeUserKey); + } + } +} + +export class UserKeyStoreLocalHttp implements UserKeyStore { + httpService: HttpService; + + constructor(params: { httpService: HttpService }) { + this.httpService = params.httpService; + } + + public async getUserKey(): Promise { + const response = await this.httpService.get("/register/") + return response.text(); + } +} + +export class UserKeyStoreLocalStorage { + public getUserKey(): Promise { + const userKey = localStorage.getItem(user_key) + return Promise.resolve(userKey === null ? undefined : userKey) + } + + public storeUserKey(userKey: string): void { + localStorage.setItem(user_key, userKey) + } +} diff --git a/src/store/key_store.ts b/src/store/key_store.ts deleted file mode 100644 index a5784e7..0000000 --- a/src/store/key_store.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { server } from "../service/http_service" - -const key = "user_key" - -export type OnUserKeyFound = (userKey: string) => any - -export interface UserKeyStore { - getUserKey: (callback: OnUserKeyFound) => any -} - -export class UserKeyStoreImpl implements UserKeyStore { - private keyStoreHttp = new UserKeyStoreLocalHttp() - private keyStoreLocalStoreage = new UserKeyStoreLocalStoreage() - - public getUserKey(callback: (userKey: string) => any): any { - this.keyStoreLocalStoreage.getUserKey((userKey)=>{ - if(userKey){ - callback(userKey) - }else{ - this.keyStoreHttp.getUserKey((userKey)=>{ - this.keyStoreLocalStoreage.storeUserKey(userKey) - callback(userKey) - }) - } - }) - } -} - -export class UserKeyStoreLocalHttp implements UserKeyStore { - public getUserKey(callback: (userKey: string) => any): any { - const url = server.serverAdress + "/register/" - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }; - fetch(url, requestOptions) - .then(response => response.text()) - .then(data => callback(data)); - } -} - -export class UserKeyStoreLocalStoreage implements UserKeyStore { - public getUserKey(callback: (userKey: string) => any): any { - const userKey = localStorage.getItem(key) - callback(userKey) - } - - public storeUserKey(userKey : string) : any { - localStorage.setItem(key, userKey) - } -} From 3947f28195325f2fdeaad2541649725723200f3d Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 16:19:25 +0300 Subject: [PATCH 07/12] add unlistenMessage to rtmt --- src/rtmt/rtmt.ts | 1 + src/rtmt/rtmt_websocket.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rtmt/rtmt.ts b/src/rtmt/rtmt.ts index efb6b70..bc04659 100644 --- a/src/rtmt/rtmt.ts +++ b/src/rtmt/rtmt.ts @@ -4,4 +4,5 @@ export type OnMessage = (message : Object) => any export interface RTMT{ sendMessage : (channel : string, message : Object) => any listenMessage : (channel : string, callback : OnMessage) => any + unlistenMessage : (channel : string, callback : OnMessage) => any } \ No newline at end of file diff --git a/src/rtmt/rtmt_websocket.ts b/src/rtmt/rtmt_websocket.ts index 3f93536..180d3c2 100644 --- a/src/rtmt/rtmt_websocket.ts +++ b/src/rtmt/rtmt_websocket.ts @@ -1,12 +1,12 @@ import { decode, encode } from "./encode_decode_message"; import { Bytes, OnMessage, RTMT } from "./rtmt"; -import { server } from "../service/http_service"; import { channel_ping, channel_pong } from "../const/channel_names"; +import { server } from "../const/config"; const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000; export class RTMTWS implements RTMT { - private messageChannels: Map; + private messageChannels: Map; private ws: WebSocket; private pingTimeout?: number = undefined; @@ -69,10 +69,16 @@ export class RTMTWS implements RTMT { this.ws.send(data); } + // todo: support multible listeners listenMessage(channel: string, callback: OnMessage) { this.messageChannels.set(channel, callback); } + // todo: support multible listeners + unlistenMessage(channel : string, callback : OnMessage) { + this.messageChannels.set(channel, undefined); + } + onWebSocketMessage(rtmt: RTMTWS, event: MessageEvent) { const { channel, message } = decode(event.data); rtmt.onMessage(channel, message); From 644f839933bf3431f0b0cbd5361f45ae71d090fe Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sat, 30 Jul 2022 16:32:13 +0300 Subject: [PATCH 08/12] add GameStore --- src/MancalaApp.tsx | 31 +++++++++++++++---------------- src/context/context.tsx | 13 ++++++++++--- src/store/GameStore.ts | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 src/store/GameStore.ts diff --git a/src/MancalaApp.tsx b/src/MancalaApp.tsx index 3d049c8..2329499 100644 --- a/src/MancalaApp.tsx +++ b/src/MancalaApp.tsx @@ -34,22 +34,21 @@ const MancalaApp: FunctionComponent = () => { const onConnectionError = (event: Event) => { setConnetionState("error"); }; - const connectToServer = (connectionState: ConnectionState) => { + const connectToServer = async (connectionState: ConnectionState) => { setConnetionState(connectionState); - context.userKeyStore.getUserKey((userKey: string) => { - setUserKey(userKey); - const rtmtws = context.rtmt as RTMTWS; - if (rtmtws) { - rtmtws.initWebSocket( - userKey, - onConnectionDone, - onConnectionLost, - onConnectionError - ); - } else { - console.error("context.rtmt is not RTMTWS"); - } - }); + const userKey = await context.userKeyStore.getUserKey(); + setUserKey(userKey); + const rtmtws = context.rtmt as RTMTWS; + if (rtmtws) { + rtmtws.initWebSocket( + userKey, + onConnectionDone, + onConnectionLost, + onConnectionError + ); + } else { + console.error("context.rtmt is not RTMTWS"); + } }; React.useEffect(() => { connectToServer("connecting"); @@ -85,7 +84,7 @@ const MancalaApp: FunctionComponent = () => { } /> - } > + } > }> diff --git a/src/context/context.tsx b/src/context/context.tsx index 4825b3c..ee35337 100644 --- a/src/context/context.tsx +++ b/src/context/context.tsx @@ -1,7 +1,10 @@ +import { server } from "../const/config"; import { Texts, TrTr } from "../const/texts"; import { RTMT } from "../rtmt/rtmt"; import { RTMTWS } from "../rtmt/rtmt_websocket"; -import { UserKeyStore, UserKeyStoreImpl } from "../store/key_store"; +import { HttpServiceImpl } from "../service/HttpService"; +import { GameStore, GameStoreImpl } from "../store/GameStore"; +import { UserKeyStore, UserKeyStoreImpl } from "../store/KeyStore"; import ThemeManager from "../theme/ThemeManager"; export type Context = { @@ -9,11 +12,14 @@ export type Context = { userKeyStore: UserKeyStore; texts: Texts; themeManager: ThemeManager; + gameStore: GameStore; }; -export const initContext = () : Context => { +export const initContext = (): Context => { const rtmt = new RTMTWS(); - const userKeyStore = new UserKeyStoreImpl(); + const httpService = new HttpServiceImpl(server.serverAdress); + const userKeyStore = new UserKeyStoreImpl({ httpService }); + const gameStore = new GameStoreImpl({ httpService }); const texts = TrTr; const themeManager = new ThemeManager(); return { @@ -21,5 +27,6 @@ export const initContext = () : Context => { userKeyStore: userKeyStore, texts: texts, themeManager, + gameStore }; }; diff --git a/src/store/GameStore.ts b/src/store/GameStore.ts new file mode 100644 index 0000000..66f4c6c --- /dev/null +++ b/src/store/GameStore.ts @@ -0,0 +1,25 @@ +import { CommonMancalaGame, MancalaGame } from "mancala.js"; +import { HttpService } from "../service/HttpService"; + +export interface GameStore { + get(id: string): Promise; +} + +export class GameStoreImpl implements GameStore { + httpService: HttpService; + constructor(props: { httpService: HttpService }) { + this.httpService = props.httpService; + } + + async get(id: string): Promise { + try { + const response = await this.httpService.get(`/game/${id}`); + const json = await response.json(); + return MancalaGame.createFromMancalaGame(json as CommonMancalaGame); + } catch (error) { + // todo check error + Promise.resolve(undefined); + } + } + +} \ No newline at end of file From 32e520a7dd06c2e4e6822e277cef039f77e21221 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sun, 31 Jul 2022 00:57:32 +0300 Subject: [PATCH 09/12] add loading state model and component --- src/components/LoadingComponent.tsx | 28 ++++++++++++++++++ src/const/texts.ts | 8 ++++- src/models/LoadingState.tsx | 46 +++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/components/LoadingComponent.tsx create mode 100644 src/models/LoadingState.tsx diff --git a/src/components/LoadingComponent.tsx b/src/components/LoadingComponent.tsx new file mode 100644 index 0000000..3c9495e --- /dev/null +++ b/src/components/LoadingComponent.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { FunctionComponent } from 'react'; +import { Context } from '../context/context'; +import { LoadingState } from '../models/LoadingState'; +import { getColorByBrightness } from '../util/ColorUtil'; +import CircularPanel from './CircularPanel'; + +const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState }> = ({ context, loadingState }) => { + if (loadingState.isUnset() || loadingState.isLoaded()) { + return <> + } + if (loadingState.isLoading() || loadingState.isError()) { + const textColorOnBoard = getColorByBrightness( + context.themeManager.theme.boardColor, + context.themeManager.theme.textColor, + context.themeManager.theme.textLightColor + ); + const text = loadingState.isLoading() ? context.texts.Loading +"..." : loadingState.errorMessage; + return ( + +

{`${text}`}

+
+ ); + } + return <> +} + +export default LoadingComponent; \ No newline at end of file diff --git a/src/const/texts.ts b/src/const/texts.ts index b9b1133..ac2de91 100644 --- a/src/const/texts.ts +++ b/src/const/texts.ts @@ -22,6 +22,8 @@ export type Texts = { PleaseWait : string, GameDraw : string, Anonymous: string, + GameNotFound: string, + Loading: string, } export const EnUs: Texts = { @@ -47,6 +49,8 @@ export const EnUs: Texts = { PleaseWait : "Please Wait", GameDraw : "Draw", Anonymous: "Anonymous", + GameNotFound: "Game Not Found", + Loading: "Loading", } export const TrTr: Texts = { @@ -71,5 +75,7 @@ export const TrTr: Texts = { SearchingOpponent: "Rakip Aranıyor", PleaseWait: "Lütfen Bekleyin", GameDraw : "Berabere", - Anonymous: "Anonim" + Anonymous: "Anonim", + GameNotFound: "Oyun Bulunamadı", + Loading: "Yükleniyor", } \ No newline at end of file diff --git a/src/models/LoadingState.tsx b/src/models/LoadingState.tsx new file mode 100644 index 0000000..dfe1e4e --- /dev/null +++ b/src/models/LoadingState.tsx @@ -0,0 +1,46 @@ +export type LoadingStateType = "unset" | "loading" | "loaded" | "error"; + +export class LoadingState { + state: LoadingStateType; + + errorMessage?: string; + + value?: T; + + constructor(props: { state?: LoadingStateType, errorMessage?: string, value?: T }) { + this.state = props.state ? props.state : "unset"; + this.errorMessage = props.errorMessage; + this.value = props.value; + } + + public static Unset() { + return new LoadingState({ state: "unset" }); + } + + public static Loading() { + return new LoadingState({ state: "loading" }); + } + + public static Error(props: { errorMessage: string }) { + const { errorMessage } = props; + return new LoadingState({ state: "error", errorMessage }); + } + + public static Loaded(props: { value?: T }) { + const { value } = props; + return new LoadingState({ state: "loaded", value }); + } + + public isUnset() : boolean { + return this.state === "unset"; + } + public isLoading() : boolean { + return this.state === "loading"; + } + public isError() : boolean { + return this.state === "error"; + } + public isLoaded() : boolean { + return this.state === "loaded"; + } +} \ No newline at end of file From 25fa771254d0d3b5893b805a8fdbcc5c2755df79 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sun, 31 Jul 2022 00:58:45 +0300 Subject: [PATCH 10/12] add Link to header bar in LobyPage --- src/routes/LobyPage.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/routes/LobyPage.tsx b/src/routes/LobyPage.tsx index 386f13e..c20ae89 100644 --- a/src/routes/LobyPage.tsx +++ b/src/routes/LobyPage.tsx @@ -1,7 +1,7 @@ import { CommonMancalaGame, MancalaGame } from 'mancala.js'; import * as React from 'react'; import { FunctionComponent, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import Center from '../components/Center'; import CircularPanel from '../components/CircularPanel'; import HeaderBar from '../components/headerbar/HeaderBar'; @@ -49,8 +49,12 @@ const LobyPage: FunctionComponent<{ - - + + + + + + From 44090516e218a0bb9d1cc40f752d17658f12bc40 Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Sun, 31 Jul 2022 00:59:18 +0300 Subject: [PATCH 11/12] add GamePage --- src/routes/GamePage.tsx | 246 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 236 insertions(+), 10 deletions(-) diff --git a/src/routes/GamePage.tsx b/src/routes/GamePage.tsx index 8c02d4b..36df155 100644 --- a/src/routes/GamePage.tsx +++ b/src/routes/GamePage.tsx @@ -1,17 +1,243 @@ +import { CommonMancalaGame, MancalaGame, Pit } from 'mancala.js'; import * as React from 'react'; -import { FunctionComponent } from 'react'; -import { useParams } from 'react-router'; +import { FunctionComponent, useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import { v4 } from 'uuid'; +import PitAnimator from '../animation/PitAnimator'; +import BoardToolbar from '../components/board/BoardToolbar'; +import BoardView from '../components/board/BoardView'; +import Button from '../components/Button'; +import HeaderBar from '../components/headerbar/HeaderBar'; +import HeaderbarIcon from '../components/headerbar/HeaderbarIcon'; +import HeaderbarTitle from '../components/headerbar/HeaderbarTitle'; +import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu'; +import InfoPanel from '../components/InfoPanel'; +import LoadingComponent from '../components/LoadingComponent'; +import PageContainer from '../components/PageContainer'; +import Row from '../components/Row'; +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 } 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 { UserConnectionInfo } from '../models/UserConnectionInfo'; +import { Theme } from '../theme/Theme'; +import { getColorByBrightness } from '../util/ColorUtil'; +import BoardViewModel from '../viewmodel/BoardViewModel'; +import Center from '../components/Center'; + +const GamePage: FunctionComponent<{ + context: Context, + userKey?: string, + theme: Theme, + connectionState: ConnectionState +}> = ({ context, userKey, theme, connectionState }) => { + let params = useParams<{ gameId: string }>(); + + const [game, setGame] = useState(undefined); + + const [crashMessage, setCrashMessage] = useState(undefined); + + const [userKeyWhoLeave, setUserKeyWhoLeave] = useState(undefined); + + const [boardViewModel, setBoardViewModel] = useState(undefined); + + const [boardId, setBoardId] = useState("-1"); + + const [pitAnimator, setPitAnimator] = useState(undefined); + + // It is a flag for ongoing action such as send game move. + // We have to block future actions if there is an ongoing action. + const [hasOngoingAction, setHasOngoingAction] = useState(false); + + const [isOpponentOnline, setIsOpponentOnline] = useState(false); + + const { height, width } = useWindowDimensions(); + + const navigate = useNavigate(); + + const [gameLoadingState, setLoadingStateGame] = useState>(LoadingState.Unset()); + + const onGameUpdate = (pitAnimator: PitAnimator, message: Object) => { + const newGame: CommonMancalaGame = message as CommonMancalaGame; + const mancalaGame = MancalaGame.createFromMancalaGame(newGame); + setGame(mancalaGame); + pitAnimator.setUpdatedGame(mancalaGame); + setHasOngoingAction(false); + } + const onGameCrashed = (message: any) => { + const newCrashMessage = message as string; + console.error("on_game_crash"); + console.error(newCrashMessage); + setCrashMessage(newCrashMessage); + } + const onGameUserLeave = (message: any) => { + const userKeyWhoLeave = message; + setUserKeyWhoLeave(userKeyWhoLeave); + setHasOngoingAction(false); + }; + const onUserConnectionChange = (message: any) => { + const userConnectionInfo = message as UserConnectionInfo; + //todo: change this when implementing watch the game feature + setIsOpponentOnline(userConnectionInfo.isOnline); + }; + + const listenMessages = (pitAnimator: PitAnimator) : () => void => { + const _onGameUpdate = (message: object) => onGameUpdate(pitAnimator, message); + context.rtmt.listenMessage(channel_on_game_update, _onGameUpdate); + context.rtmt.listenMessage(channel_on_game_crashed, onGameCrashed); + context.rtmt.listenMessage(channel_on_game_user_leave, onGameUserLeave); + context.rtmt.listenMessage(channel_on_user_connection_change, onUserConnectionChange) + return () => { + context.rtmt.unlistenMessage(channel_on_game_update, _onGameUpdate); + context.rtmt.unlistenMessage(channel_on_game_crashed, onGameCrashed); + context.rtmt.unlistenMessage(channel_on_game_user_leave, onGameUserLeave); + context.rtmt.unlistenMessage(channel_on_user_connection_change, onUserConnectionChange); + } + }; + + const updateBoardViewModel = (boardViewModel: BoardViewModel) => { + boardViewModel.id = v4(); + setBoardId(boardViewModel.id); + setBoardViewModel(boardViewModel); + }; + + const getBoardIndex = (index: number) => { + if (!game) return -1; + if (userKey === game.player2Id) return index + game.board.pits.length / 2; + return index; + }; + + const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id; + + const checkHasAnOngoingAction = () => hasOngoingAction; + + const onLeaveGameClick = () => { + context.rtmt.sendMessage(channel_leave_game, {}); + }; + + const onNewGameClick = () => { + navigate("/loby") + }; + + const onPitSelect = (index: number, pit: Pit) => { + if (checkHasAnOngoingAction()) { + return; + } + setHasOngoingAction(true); + if (!boardViewModel) return; + //TODO : stoneCount comes from view model! + if (pit.stoneCount === 0) { + //TODO : warn user + return; + } + boardViewModel.pits[getBoardIndex(index)].pitColor = + context.themeManager.theme.pitSelectedColor; + updateBoardViewModel(boardViewModel); + const gameMove: GameMove = { index: index }; + context.rtmt.sendMessage(channel_game_move, gameMove); + }; + + React.useEffect(() => { + let pitAnimator: PitAnimator | undefined; + let unlistenMessages: ()=>void; + setLoadingStateGame(LoadingState.Loading()) + context.gameStore.get(params.gameId!!).then((game) => { + if (game) { + setGame(game); + setHasOngoingAction(false); + pitAnimator = new PitAnimator(context, updateBoardViewModel); + pitAnimator.setNewGame(game); + setPitAnimator(pitAnimator); + unlistenMessages = listenMessages(pitAnimator); + setLoadingStateGame(LoadingState.Loaded({ value: game })) + } else { + setLoadingStateGame(LoadingState.Error({ errorMessage: context.texts.GameNotFound })) + } + }) + return () => { + unlistenMessages?.(); + pitAnimator?.dispose(); + }; + }, []); + + const textColorOnAppBar = getColorByBrightness( + context.themeManager.theme.appBarBgColor, + context.themeManager.theme.textColor, + context.themeManager.theme.textLightColor + ); + const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.state == "ended"); + const showBoardView = game && boardViewModel && userKey && true; + const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: isOpponentOnline, isAnonymous: true }; + const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true }; + + const isMobile = width < 600; -const GamePage: FunctionComponent<{ context: Context }> = ({ context }) => { - let params = useParams<{gameId : string}>(); return ( -
- Game Route {params.gameId} - -
+ + + + + + + + + + + + +