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

View File

@ -3,6 +3,7 @@ import * as React from "react";
import { FunctionComponent } from "react";
import { Context } from "../context/context";
import { getColorByBrightness } from "../util/ColorUtil";
import CircularPanel from "./CircularPanel";
function getInfoPanelTextByGameState(params: {
context: Context;
@ -10,7 +11,6 @@ function getInfoPanelTextByGameState(params: {
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
searchingOpponent: boolean;
}): string | undefined {
const {
context,
@ -18,11 +18,8 @@ function getInfoPanelTextByGameState(params: {
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
} = params;
if (searchingOpponent) {
return context.texts.SearchingOpponent + " " + context.texts.PleaseWait;
} else if (crashMessage) {
if (crashMessage) {
return context.texts.GameCrashed + " " + crashMessage;
} else if (userKeyWhoLeave) {
let message = context.texts.OpponentLeavesTheGame;
@ -50,44 +47,24 @@ function getInfoPanelTextByGameState(params: {
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<{
context: Context;
game?: MancalaGame;
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
searchingOpponent: boolean;
style?: React.CSSProperties;
visible?: boolean;
}> = ({
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
style,
visible
}) => {
if (visible === false) return <></>;
const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false;
const containerColor = isUserTurn
? context.themeManager.theme.playerTurnColor
@ -102,16 +79,15 @@ const InfoPanel: FunctionComponent<{
game,
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
userKeyWhoLeave
});
if (text) {
return (
<InfoPanelContainer context={context} color={containerColor}>
<CircularPanel style={style} color={containerColor}>
<h4 style={{ margin: "0", color: textColor }}>
{text}
</h4>
</InfoPanelContainer>
</CircularPanel>
);
} else {
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}
<style jsx>{`
.board {
margin: 1vw;
padding: 2vw;
display: grid;
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_ping = "ping"
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,
SearchingOpponent: string,
PleaseWait : string,
GameDraw : string
GameDraw : string,
Anonymous: string,
}
export const EnUs: Texts = {
@ -44,7 +45,8 @@ export const EnUs: Texts = {
YouLeftTheGame: "You Left The Game",
SearchingOpponent: "Searching Opponent",
PleaseWait : "Please Wait",
GameDraw : "Draw"
GameDraw : "Draw",
Anonymous: "Anonymous",
}
export const TrTr: Texts = {
@ -68,5 +70,6 @@ export const TrTr: Texts = {
YouLeftTheGame: "Sen Oyundan Ayrıldın",
SearchingOpponent: "Rakip Aranıyor",
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_update,
channel_on_game_user_leave,
channel_on_user_connection_change,
} from "../const/channel_names";
import InfoPanel from "../components/InfoPanel";
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
@ -19,7 +20,7 @@ import BoardViewModel from "../viewmodel/BoardViewModel";
import { v4 } from "uuid";
import { getColorByBrightness } from "../util/ColorUtil";
import { Theme } from "../theme/Theme";
import HeaderBar from "../components/HeaderBar";
import HeaderBar from "../components/headerbar/HeaderBar";
import FloatingPanel from "../components/FloatingPanel";
import PageContainer from "../components/PageContainer";
import Row from "../components/Row";
@ -27,6 +28,12 @@ import HeaderbarIcon from "../components/headerbar/HeaderbarIcon";
import HeaderbarTitle from "../components/headerbar/HeaderbarTitle";
import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
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";
@ -56,6 +63,10 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
// We have to block future actions if there is an ongoing action.
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
const { height, width } = useWindowDimensions();
const [isOpponentOnline, setIsOpponentOnline] = useState<boolean>(false);
const onConnectionDone = () => {
setConnetionState("connected");
};
@ -116,6 +127,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
setUserKeyWhoLeave(userKeyWhoLeave);
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) => {
@ -137,6 +154,8 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
return index;
};
const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id;
const checkHasAnOngoingAction = () => hasOngoingAction;
React.useEffect(() => {
@ -168,7 +187,7 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
};
const onPitSelect = (index: number, pit: Pit) => {
if(checkHasAnOngoingAction()) {
if (checkHasAnOngoingAction()) {
return;
}
setHasOngoingAction(true);
@ -209,6 +228,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
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;
return (
<PageContainer theme={theme!}>
<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} />
</Row>
</HeaderBar>
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
<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}
game={game}
crashMessage={crashMessage}
userKey={userKey}
userKeyWhoLeave={userKeyWhoLeave}
searchingOpponent={searchingOpponent} />
{game && boardViewModel && userKey && (
visible={!isMobile} />
<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
userKey={userKey}
game={game}
@ -243,6 +290,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
boardViewModel={boardViewModel}
context={context}
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>
);