Merge pull request #26 from jhalitaksoy/feature/user-status

Feature/user status
This commit is contained in:
Halit Aksoy 2022-07-23 00:53:47 +03:00 committed by GitHub
commit b395c6974d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 268 additions and 51 deletions

View File

@ -27,7 +27,7 @@ const Button: FunctionComponent<{
margin: 5px; margin: 5px;
padding: 10px; padding: 10px;
border: none; border: none;
border-radius: 4vw; border-radius: 30px;
} }
`}</style> `}</style>
{text} {text}

21
src/components/Center.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from 'react';
import { FunctionComponent } from 'react';
const Center: FunctionComponent = ({children}) => {
return (
<div className='center'>
{children}
<style jsx>{`
.center {
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
`}</style>
</div>
);
}
export default Center;

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import { FunctionComponent } from 'react';
const CircularPanel: FunctionComponent<{
color: string;
style?: React.CSSProperties
}> = (props) => {
return (
<div style={Object.assign({ background: props.color }, props.style)}>
<style jsx>{`
div {
padding: 10px 20px;
border-radius: 30px;
}
`}
</style>
{props.children}
</div>
);
}
export default CircularPanel;

View File

@ -7,7 +7,7 @@ const FloatingPanel: FunctionComponent<{
color: string; color: string;
visible: boolean; visible: boolean;
}> = (props) => { }> = (props) => {
if(!props.visible) return <></> if(props.visible === false) return <></>
return ( return (
<div style={{ <div style={{
backgroundColor: props.color, backgroundColor: props.color,
@ -21,6 +21,7 @@ const FloatingPanel: FunctionComponent<{
border-top-right-radius: 1vw; border-top-right-radius: 1vw;
min-width: 10vw; min-width: 10vw;
min-height: 1vw; min-height: 1vw;
z-index: 100;
} }
`}</style> `}</style>
{props.children} {props.children}

View File

@ -3,6 +3,7 @@ import * as React from "react";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Context } from "../context/context"; import { Context } from "../context/context";
import { getColorByBrightness } from "../util/ColorUtil"; import { getColorByBrightness } from "../util/ColorUtil";
import CircularPanel from "./CircularPanel";
function getInfoPanelTextByGameState(params: { function getInfoPanelTextByGameState(params: {
context: Context; context: Context;
@ -10,7 +11,6 @@ function getInfoPanelTextByGameState(params: {
crashMessage?: string; crashMessage?: string;
userKey?: string; userKey?: string;
userKeyWhoLeave?: string; userKeyWhoLeave?: string;
searchingOpponent: boolean;
}): string | undefined { }): string | undefined {
const { const {
context, context,
@ -18,11 +18,8 @@ function getInfoPanelTextByGameState(params: {
crashMessage, crashMessage,
userKey, userKey,
userKeyWhoLeave, userKeyWhoLeave,
searchingOpponent,
} = params; } = params;
if (searchingOpponent) { if (crashMessage) {
return context.texts.SearchingOpponent + " " + context.texts.PleaseWait;
} else if (crashMessage) {
return context.texts.GameCrashed + " " + crashMessage; return context.texts.GameCrashed + " " + crashMessage;
} else if (userKeyWhoLeave) { } else if (userKeyWhoLeave) {
let message = context.texts.OpponentLeavesTheGame; let message = context.texts.OpponentLeavesTheGame;
@ -50,44 +47,24 @@ function getInfoPanelTextByGameState(params: {
return undefined; return undefined;
} }
const InfoPanelContainer: FunctionComponent<{
context: Context;
color: string;
}> = (props) => {
return (
<div
style={{
background: props.color
}}
>
<style jsx>{`
div {
padding: 1vw 2vw;
margin-top: 1vw;
border-radius: 10vw;
}
`}
</style>
{props.children}
</div>
);
};
const InfoPanel: FunctionComponent<{ const InfoPanel: FunctionComponent<{
context: Context; context: Context;
game?: MancalaGame; game?: MancalaGame;
crashMessage?: string; crashMessage?: string;
userKey?: string; userKey?: string;
userKeyWhoLeave?: string; userKeyWhoLeave?: string;
searchingOpponent: boolean; style?: React.CSSProperties;
visible?: boolean;
}> = ({ }> = ({
context, context,
game, game,
crashMessage, crashMessage,
userKey, userKey,
userKeyWhoLeave, userKeyWhoLeave,
searchingOpponent, style,
visible
}) => { }) => {
if (visible === false) return <></>;
const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false; const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false;
const containerColor = isUserTurn const containerColor = isUserTurn
? context.themeManager.theme.playerTurnColor ? context.themeManager.theme.playerTurnColor
@ -102,16 +79,15 @@ const InfoPanel: FunctionComponent<{
game, game,
crashMessage, crashMessage,
userKey, userKey,
userKeyWhoLeave, userKeyWhoLeave
searchingOpponent,
}); });
if (text) { if (text) {
return ( return (
<InfoPanelContainer context={context} color={containerColor}> <CircularPanel style={style} color={containerColor}>
<h4 style={{ margin: "0", color: textColor }}> <h4 style={{ margin: "0", color: textColor }}>
{text} {text}
</h4> </h4>
</InfoPanelContainer> </CircularPanel>
); );
} else { } else {
return (<div></div>) return (<div></div>)

10
src/components/Space.tsx Normal file
View File

@ -0,0 +1,10 @@
import * as React from 'react';
import { FunctionComponent } from 'react';
const Space: FunctionComponent<{ width?: string, height?: string }> = ({ width, height }) => {
return (
<div style={{ width, height}}/>
);
}
export default Space;

View File

@ -0,0 +1,73 @@
import * as React from 'react';
import { FunctionComponent } from 'react';
import { Context } from '../context/context';
import { User } from '../models/User';
import { getColorByBrightness } from '../util/ColorUtil';
import Space from './Space';
export type LayoutMode = "right" | "left";
const UserStatus: FunctionComponent<{
context: Context,
user: User,
layoutMode: LayoutMode,
visible?: boolean,
style?: React.CSSProperties
}> = ({ context, user, layoutMode, visible, style }) => {
if (visible === false) return <></>;
const textColorOnBoard = getColorByBrightness(
context.themeManager.theme.boardColor,
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>
<Space width='5px' />
<div className={"circle " + (user.isOnline ? "online" : "offline")}></div>
<style jsx>{`
.online {
background-color: ${context.themeManager.theme.boardColor};
}
.offline {
background-color: transparent;
}
.circle {
width: 15px;
height: 15px;
min-width: 15px;
min-height: 15px;
border-radius: 15px;
border: 2px solid ${context.themeManager.theme.boardColor};
}
.flex-rtl {
display: flex;
flex-direction: row-reverse;
align-items: center;
}
.flex-ltr {
display: flex;
flex-direction: row;
align-items: center;
}
.text {
font-weight: bold;
text-overflow: ellipsis;
overflow: hidden;
}
.icon {
color : "grey";
width: 32px;
height: 32px;
font-size: 32px;
}
`}</style>
</div>
);
}
export default UserStatus;

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import { FunctionComponent } from 'react';
const BoardToolbar: FunctionComponent<{ visible?: boolean, style?: React.CSSProperties }> = ({ children, visible, style }) => {
if(visible === false) return <></>;
return (
<div style={style} className='toolbar'>
{children}
<style jsx>{`
.toolbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
align-self: stretch;
}
`}</style>
</div>
);
}
export default BoardToolbar;

View File

@ -56,7 +56,6 @@ const BoardView: FunctionComponent<{
{isPlayer2 ? player2Pits : player1Pits} {isPlayer2 ? player2Pits : player1Pits}
<style jsx>{` <style jsx>{`
.board { .board {
margin: 1vw;
padding: 2vw; padding: 2vw;
display: grid; display: grid;
grid-template-columns: repeat(8, 11vw); grid-template-columns: repeat(8, 11vw);

View File

@ -8,3 +8,4 @@ export const channel_on_game_crashed = "on_game_crashed"
export const channel_on_game_user_leave = "on_game_user_leave" export const channel_on_game_user_leave = "on_game_user_leave"
export const channel_ping = "ping" export const channel_ping = "ping"
export const channel_pong = "pong" export const channel_pong = "pong"
export const channel_on_user_connection_change = "channel_on_user_connection_change"

View File

@ -20,7 +20,8 @@ export type Texts = {
YouLeftTheGame: string, YouLeftTheGame: string,
SearchingOpponent: string, SearchingOpponent: string,
PleaseWait : string, PleaseWait : string,
GameDraw : string GameDraw : string,
Anonymous: string,
} }
export const EnUs: Texts = { export const EnUs: Texts = {
@ -44,7 +45,8 @@ export const EnUs: Texts = {
YouLeftTheGame: "You Left The Game", YouLeftTheGame: "You Left The Game",
SearchingOpponent: "Searching Opponent", SearchingOpponent: "Searching Opponent",
PleaseWait : "Please Wait", PleaseWait : "Please Wait",
GameDraw : "Draw" GameDraw : "Draw",
Anonymous: "Anonymous",
} }
export const TrTr: Texts = { export const TrTr: Texts = {
@ -68,5 +70,6 @@ export const TrTr: Texts = {
YouLeftTheGame: "Sen Oyundan Ayrıldın", YouLeftTheGame: "Sen Oyundan Ayrıldın",
SearchingOpponent: "Rakip Aranıyor", SearchingOpponent: "Rakip Aranıyor",
PleaseWait: "Lütfen Bekleyin", PleaseWait: "Lütfen Bekleyin",
GameDraw : "Berabere" GameDraw : "Berabere",
Anonymous: "Anonim"
} }

View File

@ -0,0 +1,26 @@
import { useState, useEffect } from 'react';
//https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height
};
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
}

6
src/models/User.ts Normal file
View File

@ -0,0 +1,6 @@
export interface User {
id: string;
name: string;
isOnline: boolean;
isAnonymous: boolean;
}

View File

@ -0,0 +1,4 @@
export interface UserConnectionInfo {
userId: string;
isOnline: boolean;
}

View File

@ -10,6 +10,7 @@ import {
channel_on_game_start, channel_on_game_start,
channel_on_game_update, channel_on_game_update,
channel_on_game_user_leave, channel_on_game_user_leave,
channel_on_user_connection_change,
} from "../const/channel_names"; } from "../const/channel_names";
import InfoPanel from "../components/InfoPanel"; import InfoPanel from "../components/InfoPanel";
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js"; import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
@ -19,7 +20,7 @@ import BoardViewModel from "../viewmodel/BoardViewModel";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { getColorByBrightness } from "../util/ColorUtil"; import { getColorByBrightness } from "../util/ColorUtil";
import { Theme } from "../theme/Theme"; import { Theme } from "../theme/Theme";
import HeaderBar from "../components/HeaderBar"; import HeaderBar from "../components/headerbar/HeaderBar";
import FloatingPanel from "../components/FloatingPanel"; import FloatingPanel from "../components/FloatingPanel";
import PageContainer from "../components/PageContainer"; import PageContainer from "../components/PageContainer";
import Row from "../components/Row"; import Row from "../components/Row";
@ -27,6 +28,12 @@ import HeaderbarIcon from "../components/headerbar/HeaderbarIcon";
import HeaderbarTitle from "../components/headerbar/HeaderbarTitle"; import HeaderbarTitle from "../components/headerbar/HeaderbarTitle";
import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu"; import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
import Button from "../components/Button"; import Button from "../components/Button";
import BoardToolbar from "../components/board/BoardToolbar";
import UserStatus from "../components/UserStatus";
import Center from "../components/Center";
import CircularPanel from "../components/CircularPanel";
import useWindowDimensions from "../hooks/useWindowDimensions";
import { UserConnectionInfo } from "../models/UserConnectionInfo";
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting"; type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
@ -56,6 +63,10 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
// We have to block future actions if there is an ongoing action. // We have to block future actions if there is an ongoing action.
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false); const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
const { height, width } = useWindowDimensions();
const [isOpponentOnline, setIsOpponentOnline] = useState<boolean>(false);
const onConnectionDone = () => { const onConnectionDone = () => {
setConnetionState("connected"); setConnetionState("connected");
}; };
@ -116,6 +127,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
setUserKeyWhoLeave(userKeyWhoLeave); setUserKeyWhoLeave(userKeyWhoLeave);
setHasOngoingAction(false); setHasOngoingAction(false);
}); });
context.rtmt.listenMessage(channel_on_user_connection_change, (message: any) => {
const userConnectionInfo = message as UserConnectionInfo;
//todo: change this when implementing watch the game feature
setIsOpponentOnline(userConnectionInfo.isOnline);
})
}; };
const updateBoardViewModel = (boardViewModel: BoardViewModel) => { const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
@ -137,6 +154,8 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
return index; return index;
}; };
const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id;
const checkHasAnOngoingAction = () => hasOngoingAction; const checkHasAnOngoingAction = () => hasOngoingAction;
React.useEffect(() => { React.useEffect(() => {
@ -209,6 +228,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
context.themeManager.theme.textLightColor context.themeManager.theme.textLightColor
); );
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.state == "ended"); 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;
return ( return (
<PageContainer theme={theme!}> <PageContainer theme={theme!}>
<FloatingPanel context={context} color={context.themeManager.theme.boardColor} visible={showConnectionState}> <FloatingPanel context={context} color={context.themeManager.theme.boardColor} visible={showConnectionState}>
@ -228,14 +253,36 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} /> onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
</Row> </Row>
</HeaderBar> </HeaderBar>
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
<InfoPanel <InfoPanel
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
context={context}
game={game}
crashMessage={crashMessage}
userKey={userKey}
userKeyWhoLeave={userKeyWhoLeave} />
</BoardToolbar>
<BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
<UserStatus style={{
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="left" user={opponentUser} visible={showBoardView || false} />
<InfoPanel
style={{
marginTop: "0.5rem", marginBottom: "0.5rem",
}}
context={context} context={context}
game={game} game={game}
crashMessage={crashMessage} crashMessage={crashMessage}
userKey={userKey} userKey={userKey}
userKeyWhoLeave={userKeyWhoLeave} userKeyWhoLeave={userKeyWhoLeave}
searchingOpponent={searchingOpponent} /> visible={!isMobile} />
{game && boardViewModel && userKey && ( <UserStatus style={{
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="right" user={user} visible={showBoardView || false} />
</BoardToolbar>
{showBoardView ? (
<BoardView <BoardView
userKey={userKey} userKey={userKey}
game={game} game={game}
@ -243,6 +290,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
boardViewModel={boardViewModel} boardViewModel={boardViewModel}
context={context} context={context}
onPitSelect={onPitSelect} /> onPitSelect={onPitSelect} />
) : searchingOpponent && (
<Center>
<CircularPanel color={context.themeManager.theme.boardColor}>
<h4 style={{ margin: "0", color: textColorOnBoard }}>{`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}</h4>
</CircularPanel>
</Center>
)} )}
</PageContainer> </PageContainer>
); );