Merge pull request #26 from jhalitaksoy/feature/user-status
Feature/user status
This commit is contained in:
commit
b395c6974d
@ -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
21
src/components/Center.tsx
Normal 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;
|
||||
22
src/components/CircularPanel.tsx
Normal file
22
src/components/CircularPanel.tsx
Normal 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;
|
||||
@ -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}
|
||||
|
||||
@ -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
10
src/components/Space.tsx
Normal 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;
|
||||
73
src/components/UserStatus.tsx
Normal file
73
src/components/UserStatus.tsx
Normal 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;
|
||||
22
src/components/board/BoardToolbar.tsx
Normal file
22
src/components/board/BoardToolbar.tsx
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -7,4 +7,5 @@ export const channel_on_game_end = "on_game_end"
|
||||
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_pong = "pong"
|
||||
export const channel_on_user_connection_change = "channel_on_user_connection_change"
|
||||
@ -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"
|
||||
}
|
||||
26
src/hooks/useWindowDimensions.ts
Normal file
26
src/hooks/useWindowDimensions.ts
Normal 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
6
src/models/User.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
isOnline: boolean;
|
||||
isAnonymous: boolean;
|
||||
}
|
||||
4
src/models/UserConnectionInfo.ts
Normal file
4
src/models/UserConnectionInfo.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface UserConnectionInfo {
|
||||
userId: string;
|
||||
isOnline: boolean;
|
||||
}
|
||||
@ -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>
|
||||
<InfoPanel
|
||||
context={context}
|
||||
game={game}
|
||||
crashMessage={crashMessage}
|
||||
userKey={userKey}
|
||||
userKeyWhoLeave={userKeyWhoLeave}
|
||||
searchingOpponent={searchingOpponent} />
|
||||
{game && boardViewModel && userKey && (
|
||||
<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}
|
||||
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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user