Merge pull request #32 from jhalitaksoy/fix/ui-tweaks

Fix/UI tweaks
This commit is contained in:
Halit Aksoy 2022-09-04 11:09:27 +03:00 committed by GitHub
commit 77084c3aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 179 additions and 54 deletions

View File

@ -13,17 +13,19 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@szhsin/react-menu": "^3.0.2", "@szhsin/react-menu": "^3.1.2",
"@types/": "szhsin/react-menu", "@types/": "szhsin/react-menu",
"@types/eventemitter2": "^4.1.0", "@types/eventemitter2": "^4.1.0",
"@types/styled-jsx": "^3.4.4", "@types/styled-jsx": "^3.4.4",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"eventemitter2": "^6.4.7", "eventemitter2": "^6.4.7",
"mancala.js": "^0.0.2-beta.3", "mancala.js": "^0.0.2-beta.3",
"notyf": "^3.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "6", "react-router-dom": "6",
"styled-jsx": "^5.0.2", "styled-jsx": "^5.0.2",
"sweetalert": "^2.1.2",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -12,9 +12,11 @@ import Home from './routes/Home';
import { initContext } from './context/context'; import { initContext } from './context/context';
import { RTMTWS } from './rtmt/rtmt_websocket'; import { RTMTWS } from './rtmt/rtmt_websocket';
import { getColorByBrightness } from './util/ColorUtil'; import { getColorByBrightness } from './util/ColorUtil';
import { ConnectionState } from './models/ConnectionState';
import { Theme } from './theme/Theme'; import { Theme } from './theme/Theme';
import LobyPage from './routes/LobyPage'; import LobyPage from './routes/LobyPage';
import swal from 'sweetalert';
import { ConnectionState } from './rtmt/rtmt';
const context = initContext(); const context = initContext();
const MancalaApp: FunctionComponent = () => { const MancalaApp: FunctionComponent = () => {
@ -40,9 +42,15 @@ const MancalaApp: FunctionComponent = () => {
setTheme(theme) setTheme(theme)
} }
const connectToServer = async () => { const connectToServer = async () => {
const userKey = await context.userKeyStore.getUserKey(); try {
setUserKey(userKey); const userKey = await context.userKeyStore.getUserKey();
(context.rtmt as RTMTWS).initWebSocket(userKey); 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(() => { React.useEffect(() => {
@ -74,7 +82,6 @@ const MancalaApp: FunctionComponent = () => {
context.themeManager.theme.textColor, context.themeManager.theme.textColor,
context.themeManager.theme.textLightColor context.themeManager.theme.textLightColor
); );
if (!userKey) return <></>;
return ( return (
<> <>
<BrowserRouter> <BrowserRouter>
@ -82,7 +89,7 @@ const MancalaApp: FunctionComponent = () => {
<Route index element={<Home context={context} theme={theme} userKey={userKey} />} /> <Route index element={<Home context={context} theme={theme} userKey={userKey} />} />
<Route path="/" > <Route path="/" >
<Route path="game" > <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>
<Route path="loby" element={<LobyPage context={context} theme={theme} userKey={userKey} />}> <Route path="loby" element={<LobyPage context={context} theme={theme} userKey={userKey} />}>
</Route> </Route>
@ -90,7 +97,7 @@ const MancalaApp: FunctionComponent = () => {
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
<FloatingPanel context={context} color={floatingPanelColor} visible={showConnectionState}> <FloatingPanel context={context} color={floatingPanelColor} visible={showConnectionState}>
<span style={{ color: textColorOnBoard }}>{connectionStateText()}</span> <span style={{ color: textColorOnBoard, transition: 'color 0.5s' }}>{connectionStateText()}</span>
</FloatingPanel> </FloatingPanel>
</> </>
); );

View File

@ -13,6 +13,7 @@ import { Context } from "../context/context";
import BoardViewModelFactory from "../factory/BoardViewModelFactory"; import BoardViewModelFactory from "../factory/BoardViewModelFactory";
import { PitViewModelFactory } from "../factory/PitViewModelFactory"; import { PitViewModelFactory } from "../factory/PitViewModelFactory";
import { Game } from "../models/Game"; import { Game } from "../models/Game";
import { Theme } from "../theme/Theme";
import { getColorByBrightness } from "../util/ColorUtil"; import { getColorByBrightness } from "../util/ColorUtil";
import BoardViewModel from "../viewmodel/BoardViewModel"; import BoardViewModel from "../viewmodel/BoardViewModel";
@ -35,6 +36,7 @@ export default class PitAnimator {
) { ) {
this.context = context; this.context = context;
this.onBoardViewModelUpdate = onBoardViewModelUpdate; this.onBoardViewModelUpdate = onBoardViewModelUpdate;
this.context.themeManager.on("themechange", this.onThemeChange.bind(this));
} }
get mancalaGame(): MancalaGame | undefined { 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() { public resetAnimationState() {
this.animationIndex = -1; this.animationIndex = -1;
this.currentHistoryItem = undefined; this.currentHistoryItem = undefined;
@ -204,6 +211,7 @@ export default class PitAnimator {
} }
public dispose() { public dispose() {
this.context.themeManager.off("themechange", this.onThemeChange.bind(this));
this.resetAnimationState(); this.resetAnimationState();
this.clearCurrentInterval(); this.clearCurrentInterval();
} }

View File

@ -18,7 +18,7 @@ const Button: FunctionComponent<{
<button <button
onClick={onClick} onClick={onClick}
style={{ style={{
background: color, backgroundColor: color,
color: textColor, color: textColor,
}} }}
> >
@ -28,6 +28,9 @@ const Button: FunctionComponent<{
padding: 10px; padding: 10px;
border: none; border: none;
border-radius: 30px; border-radius: 30px;
transition: color 0.5s;
transition: background-color 0.5s;
white-space: nowrap;
} }
`}</style> `}</style>
{text} {text}

View File

@ -11,6 +11,8 @@ const CircularPanel: FunctionComponent<{
div { div {
padding: 10px 20px; padding: 10px 20px;
border-radius: 30px; border-radius: 30px;
transition: background-color 0.5s;
white-space: nowrap;
} }
`} `}
</style> </style>

View File

@ -99,7 +99,7 @@ const InfoPanel: FunctionComponent<{
if (text) { if (text) {
return ( return (
<CircularPanel style={style} color={containerColor}> <CircularPanel style={style} color={containerColor}>
<h4 style={{ margin: "0", color: textColor }}> <h4 style={{ margin: "0", color: textColor, transition: 'color 0.5s' }}>
{text} {text}
</h4> </h4>
</CircularPanel> </CircularPanel>

View File

@ -14,6 +14,7 @@ const PageContainer: FunctionComponent<{ theme: Theme }> = (props) => {
align-items: center; align-items: center;
flex: 1; flex: 1;
min-height: 400px; min-height: 400px;
transition: background-color 0.5s;
} }
`}</style> `}</style>
{props.children} {props.children}

View File

@ -16,17 +16,13 @@ const UserStatus: FunctionComponent<{
}> = ({ context, user, layoutMode, visible, style }) => { }> = ({ context, user, layoutMode, visible, style }) => {
if (visible === false) return <></>; if (visible === false) return <></>;
const textColorOnBoard = getColorByBrightness( const textColorOnBoard = getColorByBrightness(
context.themeManager.theme.boardColor, context.themeManager.theme.background,
context.themeManager.theme.textColor, context.themeManager.theme.textColor,
context.themeManager.theme.textLightColor context.themeManager.theme.textLightColor
); );
return ( return (
<div style={style} className={layoutMode === "right" ? "flex-rtl" : "flex-ltr"}> <div style={style} className={layoutMode === "right" ? "flex-rtl" : "flex-ltr"}>
<span style={{color: textColorOnBoard}} className="material-symbols-outlined icon" > <span style={{color: textColorOnBoard, transition: 'color 0.5s'}} className='text'>{user.isAnonymous ? context.texts.Anonymous : user.name}</span>
face_6
</span>
<Space width='5px' />
<span style={{color: textColorOnBoard}} className='text'>{user.isAnonymous ? context.texts.Anonymous : user.name}</span>
<Space width='5px' /> <Space width='5px' />
<div className={"circle " + (user.isOnline ? "online" : "offline")}></div> <div className={"circle " + (user.isOnline ? "online" : "offline")}></div>
<style jsx>{` <style jsx>{`
@ -43,6 +39,8 @@ const UserStatus: FunctionComponent<{
min-height: 15px; min-height: 15px;
border-radius: 15px; border-radius: 15px;
border: 2px solid ${context.themeManager.theme.boardColor}; border: 2px solid ${context.themeManager.theme.boardColor};
transition: background-color 0.5s;
transition: color 0.5s;
} }
.flex-rtl { .flex-rtl {
display: flex; display: flex;

View File

@ -29,7 +29,7 @@ const StoreView: FunctionComponent<{
gridRow: gridRow gridRow: gridRow
}}> }}>
{stones} {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} {stones.length}
</span> </span>
<style jsx>{` <style jsx>{`
@ -42,6 +42,7 @@ const StoreView: FunctionComponent<{
align-content: center; align-content: center;
flex-wrap: wrap; flex-wrap: wrap;
position: relative; position: relative;
transition: background-color 0.5s;
} }
.store-stone-count-text { .store-stone-count-text {
position: absolute; position: absolute;

View File

@ -13,6 +13,7 @@ const HeaderBar: FunctionComponent<{ color?: string }> = ({children, color }) =>
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
align-self: stretch; align-self: stretch;
transition: background-color 0.5s;
} }
`}</style> `}</style>
</div>) </div>)

View File

@ -4,7 +4,7 @@ import { Context } from '../../context/context';
const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => { const HeaderbarTitle: FunctionComponent<{ title: string, color: string }> = ({ title, color }) => {
return ( return (
<h1 style={{ color: color }} className="header-bar-title"> <h1 style={{ color: color, transition: 'color 0.5s' }} className="header-bar-title">
{title} {title}
<style jsx>{` <style jsx>{`
.header-bar-title { .header-bar-title {

View File

@ -8,22 +8,27 @@ import "@szhsin/react-menu/dist/transitions/slide.css"
const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => { const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => {
const { context, textColor } = props; const { context, textColor } = props;
const menuButton = <span const menuButton = <span
style={{ color: textColor }} style={{ color: textColor, cursor: 'pointer', userSelect: 'none' }}
className="material-symbols-outlined"> className="material-symbols-outlined">
light_mode light_mode
</span>; </span>;
const menuItems = context.themeManager.themes.map((theme, index) => { const menuItems = context.themeManager.themes.map((theme, index) => {
const themeBackground = context.themeManager.theme.background;
return ( return (
<MenuItem <MenuItem
key={index} key={index}
style={{ color: textColor }} style={{ color: textColor }}
//@ts-ignore //@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 //@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)}> 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} {theme.name}
<style jsx>{` <style jsx>{`
.theme-color-circle { .theme-color-circle {

View File

@ -6,7 +6,7 @@ export type Texts = {
YourTurn: string, YourTurn: string,
OpponentTurn: string, OpponentTurn: string,
GameEnded: string, GameEnded: string,
GameCrashed: string, InternalErrorOccurred: string,
YouWon: string, YouWon: string,
Won: string, Won: string,
YouLost: string, YouLost: string,
@ -21,12 +21,20 @@ export type Texts = {
YouLeftTheGame: string, YouLeftTheGame: string,
UserLeftTheGame: string, UserLeftTheGame: string,
SearchingOpponent: string, SearchingOpponent: string,
PleaseWait : string, PleaseWait: string,
GameDraw : string, GameDraw: string,
Anonymous: string, Anonymous: string,
GameNotFound: string, GameNotFound: string,
Loading: string, Loading: string,
Playing: string, Playing: string,
Error: string,
ErrorWhenRetrievingInformation: string,
UCanOnlyPlayYourOwnPits: string,
UMustWaitUntilCurrentMoveComplete: string,
UCanNotPlayEmptyPit: string,
AreYouSureToLeaveGame: string,
Yes: string,
Cancel: string,
} }
export const EnUs: Texts = { export const EnUs: Texts = {
@ -36,14 +44,14 @@ export const EnUs: Texts = {
YourTurn: "Your Turn", YourTurn: "Your Turn",
OpponentTurn: "Opponent Turn", OpponentTurn: "Opponent Turn",
GameEnded: "Game Ended", GameEnded: "Game Ended",
GameCrashed: "Game Crashed", InternalErrorOccurred: "An internal error has occurred",
YouWon: "You Won", YouWon: "You Won",
Won: "Won", Won: "Won",
YouLost: "You Lost", YouLost: "You Lost",
Connecting: "Connecting", Connecting: "Connecting",
Connected: "Connected", Connected: "Connected",
CannotConnect: "Can't Connect", CannotConnect: "Can't Connect",
ConnectionLost: "Connection Lost", ConnectionLost: "Network Connection Lost",
ConnectingAgain: "Connecting Again", ConnectingAgain: "Connecting Again",
ServerError: "Server Error", ServerError: "Server Error",
SearchingOpponet: "Searching Opponet", SearchingOpponet: "Searching Opponet",
@ -51,12 +59,20 @@ export const EnUs: Texts = {
YouLeftTheGame: "You Left The Game", YouLeftTheGame: "You Left The Game",
UserLeftTheGame: "Left The Game", UserLeftTheGame: "Left The Game",
SearchingOpponent: "Searching Opponent", SearchingOpponent: "Searching Opponent",
PleaseWait : "Please Wait", PleaseWait: "Please Wait",
GameDraw : "Draw", GameDraw: "Draw",
Anonymous: "Anonymous", Anonymous: "Anonymous",
GameNotFound: "Game Not Found", GameNotFound: "Game Not Found",
Loading: "Loading", Loading: "Loading",
Playing: "Playing", 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 = { export const TrTr: Texts = {
@ -66,14 +82,14 @@ export const TrTr: Texts = {
YourTurn: "Sıra Sende", YourTurn: "Sıra Sende",
OpponentTurn: "Sıra Rakipte", OpponentTurn: "Sıra Rakipte",
GameEnded: "Oyun Bitti", GameEnded: "Oyun Bitti",
GameCrashed: "Oyunda Hata Oluştu", InternalErrorOccurred: "Dahili bir hata oluştu",
YouWon: "Kazandın", YouWon: "Kazandın",
Won: "Kazandı", Won: "Kazandı",
YouLost: "Kaybettin", YouLost: "Kaybettin",
Connecting: "Bağlanılıyor", Connecting: "Bağlanılıyor",
Connected: "Bağlandı", Connected: "Bağlandı",
CannotConnect: "Bağlanılamadı", CannotConnect: "Bağlanılamadı",
ConnectionLost: "Bağlantı Koptu", ConnectionLost: "Bağlantısı Koptu",
ConnectingAgain: "Tekrar Bağlanılıyor", ConnectingAgain: "Tekrar Bağlanılıyor",
ServerError: "Sunucu Hatası", ServerError: "Sunucu Hatası",
SearchingOpponet: "Rakip Aranıyor", SearchingOpponet: "Rakip Aranıyor",
@ -82,9 +98,17 @@ export const TrTr: Texts = {
UserLeftTheGame: "Oyundan Ayrıldı", UserLeftTheGame: "Oyundan Ayrıldı",
SearchingOpponent: "Rakip Aranıyor", SearchingOpponent: "Rakip Aranıyor",
PleaseWait: "Lütfen Bekleyin", PleaseWait: "Lütfen Bekleyin",
GameDraw : "Berabere", GameDraw: "Berabere",
Anonymous: "Anonim", Anonymous: "Anonim",
GameNotFound: "Oyun Bulunamadı", GameNotFound: "Oyun Bulunamadı",
Loading: "Yükleniyor", Loading: "Yükleniyor",
Playing: "Oynuyor", 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"
} }

View File

@ -1 +0,0 @@
export type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";

View File

@ -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 { 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 { Context } from '../context/context';
import useWindowDimensions from '../hooks/useWindowDimensions'; import useWindowDimensions from '../hooks/useWindowDimensions';
import { ConnectionState } from '../models/ConnectionState';
import { GameMove } from '../models/GameMove'; import { GameMove } from '../models/GameMove';
import { LoadingState } from '../models/LoadingState'; import { LoadingState } from '../models/LoadingState';
import { Theme } from '../theme/Theme'; import { Theme } from '../theme/Theme';
@ -28,13 +27,15 @@ import { getColorByBrightness } from '../util/ColorUtil';
import BoardViewModel from '../viewmodel/BoardViewModel'; import BoardViewModel from '../viewmodel/BoardViewModel';
import Center from '../components/Center'; import Center from '../components/Center';
import { Game, GameUsersConnectionInfo } from '../models/Game'; import { Game, GameUsersConnectionInfo } from '../models/Game';
import notyf from '../util/Notyf';
import swal from 'sweetalert';
import Util from '../util/Util';
const GamePage: FunctionComponent<{ const GamePage: FunctionComponent<{
context: Context, context: Context,
userKey?: string, userKey?: string,
theme: Theme, theme: Theme,
connectionState: ConnectionState }> = ({ context, userKey, theme }) => {
}> = ({ context, userKey, theme, connectionState }) => {
let params = useParams<{ gameId: string }>(); let params = useParams<{ gameId: string }>();
const [game, setGame] = useState<Game | undefined>(undefined); const [game, setGame] = useState<Game | undefined>(undefined);
@ -89,6 +90,7 @@ const GamePage: FunctionComponent<{
} }
const onGameCrashed = (message: any) => { const onGameCrashed = (message: any) => {
const newCrashMessage = message as string; const newCrashMessage = message as string;
notyf.error(context.texts.InternalErrorOccurred);
console.error("on_game_crash"); console.error("on_game_crash");
console.error(newCrashMessage); console.error(newCrashMessage);
} }
@ -136,10 +138,22 @@ const GamePage: FunctionComponent<{
const checkHasAnOngoingAction = () => hasOngoingAction; const checkHasAnOngoingAction = () => hasOngoingAction;
const onLeaveGameClick = () => { const onLeaveGameClick = () => {
context.rtmt.sendMessage(channel_leave_game, {}); 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 = () => { const onNewGameClick = () => {
if(Util.checkConnectionAndMaybeAlert(context)) return;
navigate("/loby") navigate("/loby")
}; };
@ -147,21 +161,27 @@ const GamePage: FunctionComponent<{
if (!game || isSpectator || !userKey) { if (!game || isSpectator || !userKey) {
return; return;
} }
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) {
notyf.error(context.texts.UCanOnlyPlayYourOwnPits);
return;
}
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2); const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey || if (!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) { notyf.error(context.texts.OpponentTurn);
return; return;
} }
if (checkHasAnOngoingAction()) { if (checkHasAnOngoingAction()) {
notyf.error(context.texts.UMustWaitUntilCurrentMoveComplete);
return; return;
} }
setHasOngoingAction(true); if(Util.checkConnectionAndMaybeAlert(context)) return;
if (!boardViewModel) return; if (!boardViewModel) return;
//TODO: this check should be in mancala.js //TODO: this check should be in mancala.js
if (pit.stoneCount === 0) { if (pit.stoneCount === 0) {
//TODO : warn user notyf.error(context.texts.UCanNotPlayEmptyPit);
return; return;
} }
setHasOngoingAction(true);
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor = boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
context.themeManager.theme.pitSelectedColor; context.themeManager.theme.pitSelectedColor;
updateBoardViewModel(boardViewModel); updateBoardViewModel(boardViewModel);
@ -210,13 +230,13 @@ const GamePage: FunctionComponent<{
const bottomLocatedUser = { const bottomLocatedUser = {
id: bottomLocatedUserId, id: bottomLocatedUserId,
name: "Anonymous", name: "Anonymous",
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : connectionState === "connected", isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : context.rtmt.connectionState === "connected",
isAnonymous: true isAnonymous: true
}; };
const currentUser = isSpectator ? { const currentUser = isSpectator ? {
id: "2", id: "2",
name: "Anonymous", name: "Anonymous",
isOnline: connectionState === "connected", isOnline: context.rtmt.connectionState === "connected",
isAnonymous: true isAnonymous: true
} : bottomLocatedUser; } : bottomLocatedUser;
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined; const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
@ -263,7 +283,7 @@ const GamePage: FunctionComponent<{
function renderMobileBoardToolbar() { function renderMobileBoardToolbar() {
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}> return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
{buildInfoPanel()} {buildInfoPanel({ visible: isMobile })}
</BoardToolbar>; </BoardToolbar>;
} }
@ -273,7 +293,7 @@ const GamePage: FunctionComponent<{
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw", marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw" width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} /> }} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} />
{buildInfoPanel()} {buildInfoPanel({ visible: !isMobile })}
<UserStatus style={{ <UserStatus style={{
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw", marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw" width: isMobile ? "40vw" : "30vw"
@ -281,7 +301,7 @@ const GamePage: FunctionComponent<{
</BoardToolbar>; </BoardToolbar>;
} }
function buildInfoPanel() { function buildInfoPanel(params: { visible: boolean }) {
return ( return (
<InfoPanel <InfoPanel
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }} style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
@ -291,7 +311,7 @@ const GamePage: FunctionComponent<{
whitePlayer={topLocatedUser} whitePlayer={topLocatedUser}
blackPlayer={bottomLocatedUser} blackPlayer={bottomLocatedUser}
leftPlayer={leftPlayer} leftPlayer={leftPlayer}
visible={!isMobile} visible={params.visible}
isSpectator={isSpectator} /> isSpectator={isSpectator} />
); );
} }

View File

@ -11,6 +11,7 @@ import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
import Button from "../components/Button"; import Button from "../components/Button";
import { Context } from "../context/context"; import { Context } from "../context/context";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import Util from "../util/Util";
const Home: FunctionComponent<{ const Home: FunctionComponent<{
context: Context, context: Context,
@ -21,6 +22,7 @@ const Home: FunctionComponent<{
const navigate = useNavigate(); const navigate = useNavigate();
const onNewGameClick = () => { const onNewGameClick = () => {
if(Util.checkConnectionAndMaybeAlert(context)) return;
navigate("/loby") navigate("/loby")
}; };

View File

@ -3,10 +3,12 @@ import EventEmitter2, { Listener } from "eventemitter2"
export type Bytes = Uint8Array export type Bytes = Uint8Array
export type OnMessage = (message : Object) => any 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 type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message";
export interface RTMT extends EventEmitter2 { export interface RTMT extends EventEmitter2 {
get connectionState() : ConnectionState;
sendMessage: (channel: string, message: Object) => void; sendMessage: (channel: string, message: Object) => void;
addMessageListener(channel: string, callback: (message: any) => void); addMessageListener(channel: string, callback: (message: any) => void);
removeMessageListener(channel: string, callback: (message: any) => void); removeMessageListener(channel: string, callback: (message: any) => void);

View File

@ -2,7 +2,7 @@ import { decode, encode } from "./encode_decode_message";
import { channel_ping, channel_pong } from "../const/channel_names"; import { channel_ping, channel_pong } from "../const/channel_names";
import { server } from "../const/config"; import { server } from "../const/config";
import EventEmitter2, { Listener } from "eventemitter2"; 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 PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
const MESSAGE_CHANNEL_PREFIX = "message_channel"; const MESSAGE_CHANNEL_PREFIX = "message_channel";
@ -10,24 +10,32 @@ const MESSAGE_CHANNEL_PREFIX = "message_channel";
export class RTMTWS extends EventEmitter2 implements RTMT { export class RTMTWS extends EventEmitter2 implements RTMT {
private webSocket: WebSocket; private webSocket: WebSocket;
private pingTimeout?: number = undefined; private pingTimeout?: number = undefined;
private _connectionState: ConnectionState = "none";
constructor() { constructor() {
super(); super();
} }
get connectionState(): ConnectionState {
return this._connectionState;
}
public initWebSocket(userKey: string) { public initWebSocket(userKey: string) {
this._connectionState = this._connectionState !== "none" ? "reconnecting" : "connecting";
const url = server.wsServerAdress + "?userKey=" + userKey; const url = server.wsServerAdress + "?userKey=" + userKey;
const webSocket = new WebSocket(url); const webSocket = new WebSocket(url);
webSocket.onopen = () => { webSocket.onopen = () => {
console.info("(RTMT) WebSocket has opened"); console.info("(RTMT) WebSocket has opened");
this.webSocket = webSocket; this.webSocket = webSocket;
this.heartbeat(); this.heartbeat();
this._connectionState = "connected";
this.emit("open"); this.emit("open");
}; };
webSocket.onclose = () => { webSocket.onclose = () => {
console.info("(RTMT) WebSocket has closed"); console.info("(RTMT) WebSocket has closed");
//this.WebSocket = undefined //this.WebSocket = undefined
clearTimeout(this.pingTimeout); clearTimeout(this.pingTimeout);
this._connectionState = "closed";
this.emit("close"); this.emit("close");
}; };
webSocket.onmessage = (event: MessageEvent) => { webSocket.onmessage = (event: MessageEvent) => {
@ -36,6 +44,7 @@ export class RTMTWS extends EventEmitter2 implements RTMT {
}; };
webSocket.onerror = (error) => { webSocket.onerror = (error) => {
console.error(error); console.error(error);
this._connectionState = "error";
this.emit("error", error); this.emit("error", error);
} }
} }

View File

@ -13,6 +13,7 @@ const colorSpecial = "#337a44";
const darkTheme: Theme = { const darkTheme: Theme = {
id: "2", id: "2",
name: "Dark Theme", name: "Dark Theme",
themePreviewColor: colors.primary,
background: colors.primary, background: colors.primary,
appBarBgColor: colors.secondary, appBarBgColor: colors.secondary,
textColor: colors.primary, textColor: colors.primary,

View File

@ -3,6 +3,7 @@ import { Theme } from "./Theme";
const greyTheme: Theme = { const greyTheme: Theme = {
id: "1", id: "1",
name: "Grey Theme", name: "Grey Theme",
themePreviewColor: "#4D606E",
background: "#EEEEEE", background: "#EEEEEE",
appBarBgColor: "#e4e4e4", appBarBgColor: "#e4e4e4",
textColor: "#4D606E", textColor: "#4D606E",

View File

@ -5,6 +5,7 @@ const colorSpecial = "#8B8B8B";
const lightTheme: Theme = { const lightTheme: Theme = {
id: "1", id: "1",
name: "Light Theme", name: "Light Theme",
themePreviewColor: "#9B9B9B",
background: "#BBBBBB", background: "#BBBBBB",
appBarBgColor: "#7B7B7B", appBarBgColor: "#7B7B7B",
textColor: "#5B5B5B", textColor: "#5B5B5B",

View File

@ -1,6 +1,7 @@
export type Theme = { export type Theme = {
id: string; id: string;
name: string; name: string;
themePreviewColor: string; // for theme switch menu
textColor: string; textColor: string;
textLightColor: string; textLightColor: string;
background: string; background: string;

4
src/util/Notyf.ts Normal file
View File

@ -0,0 +1,4 @@
import { Notyf } from 'notyf';
import 'notyf/notyf.min.css';
const notyf = new Notyf();
export default notyf;

View File

@ -1,10 +1,20 @@
import { Context } from "../context/context";
import notyf from "./Notyf";
export default class Util { export default class Util {
public static range(size: number) { public static range(size: number) {
var ans : number[] = []; var ans: number[] = [];
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
ans.push(i); ans.push(i);
} }
return ans; return ans;
} }
public static checkConnectionAndMaybeAlert(context: Context): boolean {
if (context.rtmt.connectionState !== "connected") {
notyf.error(context.texts.ConnectionLost);
return true;
}
return false;
}
} }

View File

@ -1067,10 +1067,10 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@szhsin/react-menu@^3.0.2": "@szhsin/react-menu@^3.1.2":
version "3.0.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-3.0.2.tgz#d22971c53d56e6d404c9d3c98f533907cd8f03dc" resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-3.1.2.tgz#3a791e7e6c672d113c298985bec5185e9c7aa8a7"
integrity sha512-m9Ly+cT+CxQx3xhq90CVaOLQWU7f7UKeMxfDt1gPYV23tDwEe8Zo6PO547qPlAEGEwwb9MdA38U8OyueXKJc2g== integrity sha512-NUnU429a3jXtRD4xxk8EsR4yRSuhZPWAkI+4P4K63LQPUZGVE7adVKtEmlyOpd8CRQ7aoUz1ZLr1VmR1nZi6GQ==
dependencies: dependencies:
prop-types "^15.7.2" prop-types "^15.7.2"
react-transition-state "^1.1.4" react-transition-state "^1.1.4"
@ -1418,6 +1418,11 @@ error-ex@^1.3.1:
dependencies: dependencies:
is-arrayish "^0.2.1" 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: escalade@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" 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" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== 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: nth-check@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" 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" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== 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: prop-types@^15.7.2:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" 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" picocolors "^1.0.0"
stable "^0.1.8" 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: term-size@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"