developmaunt -> mauster #3
31
src/layout/components/Chat.tsx
Normal file
31
src/layout/components/Chat.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {GHButton} from "./Button";
|
||||
import React, {FormEvent, FunctionComponent, RefObject} from "react";
|
||||
|
||||
interface Props{
|
||||
chatRef: RefObject<HTMLOListElement>;
|
||||
handleSend: (message: string) => void;
|
||||
}
|
||||
|
||||
const Chat: FunctionComponent<Props> = ({chatRef, handleSend}) => {
|
||||
|
||||
const handleChat = (form: FormEvent<HTMLFormElement>) => {
|
||||
form.preventDefault();
|
||||
const data = new FormData(form.currentTarget);
|
||||
const chatInput = data.get("chat-input") as string;
|
||||
if(!chatInput) return;
|
||||
handleSend(chatInput);
|
||||
form.currentTarget.reset();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ol className={"chat__list"} ref={chatRef}/>
|
||||
<form className={"chat-form"} onSubmit={handleChat}>
|
||||
<input className={"chat-form__input"} type="text" placeholder={"Chat here..."} id={"chat-input"} name={"chat-input"}/>
|
||||
<GHButton type={"submit"}>Send</GHButton>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
@@ -2,7 +2,7 @@ import React, {FunctionComponent} from "react";
|
||||
import Deck from "./Deck";
|
||||
import Hand from "./Hand";
|
||||
import {GHButton} from "./Button";
|
||||
import {Player} from "../pages/Room";
|
||||
import {GameAction, Player} from "../pages/Room";
|
||||
|
||||
export interface GameState {
|
||||
Me: Player;
|
||||
@@ -15,16 +15,52 @@ export interface GameState {
|
||||
|
||||
interface Props {
|
||||
gameState: GameState
|
||||
handleCardSend: (cardString: string) => void;
|
||||
handleDraw: () => void;
|
||||
handleChoice: (choice: string) => void;
|
||||
handleGameAction: (action: GameAction) => void;
|
||||
}
|
||||
|
||||
const CHOICES = ['SPADES', 'HEARTS', 'DIAMONDS', 'CLUBS'];
|
||||
|
||||
const Game: FunctionComponent<Props> = ({gameState, handleCardSend, handleDraw, handleChoice}) => {
|
||||
const Game: FunctionComponent<Props> = ({gameState, handleGameAction}) => {
|
||||
|
||||
const handleChoice = (choice: string) => {
|
||||
handleGameAction({Action: 'CHOOSE', Data: choice});
|
||||
}
|
||||
|
||||
const handleDraw = () => {
|
||||
handleGameAction({Action: 'DRAW', Data: ""});
|
||||
}
|
||||
|
||||
const handleCardSend = (cardString: string) => {
|
||||
handleGameAction({Action: "PLAYCARD", Data: JSON.stringify({
|
||||
CardType: cardString.split(' ')[0],
|
||||
CardValue: cardString.split(' ')[1]
|
||||
})})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="game">
|
||||
<ol className={"game__players"}>
|
||||
{
|
||||
gameState?.Me?.Id &&
|
||||
gameState.Players.map((player) => {
|
||||
const isCurrentPlayer = player.Id === gameState.CurrentPlayer.Id;
|
||||
const isMe = player.Id === gameState.Me.Id;
|
||||
|
||||
const style =
|
||||
{
|
||||
fontWeight: isMe ? 'bold' : 'normal',
|
||||
outline: isCurrentPlayer ? '2px solid red' : 'none'
|
||||
}
|
||||
|
||||
return <li key={player.Id} style={style}>
|
||||
<div className={"game__players__info"}>
|
||||
<h3>{player.Name} {isMe && '(You)'}</h3>
|
||||
<p>{player.CardsLeft} left</p>
|
||||
</div>
|
||||
</li>
|
||||
})
|
||||
}
|
||||
</ol>
|
||||
{
|
||||
gameState.CurrentCard &&
|
||||
<Deck currentCard={gameState.CurrentCard} actionOnClick={handleDraw}/>
|
||||
@@ -37,18 +73,6 @@ const Game: FunctionComponent<Props> = ({gameState, handleCardSend, handleDraw,
|
||||
gameState.CurrentState === 'CHOOSE' &&
|
||||
CHOICES.map(choice => <GHButton key={choice} onClick={() => handleChoice(choice)}>{choice}</GHButton>)
|
||||
}
|
||||
<ul>
|
||||
{
|
||||
gameState?.Me?.Id &&
|
||||
gameState.Players.map((player) => {
|
||||
const isCurrentPlayer = player.Id === gameState.CurrentPlayer.Id;
|
||||
const isMe = player.Id === gameState.Me.Id;
|
||||
return <li key={player.Id} style={{fontWeight: isCurrentPlayer ? 'bold' : 'normal'}}>
|
||||
{player.Name} {isMe && '(You)'}
|
||||
</li>
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
9
src/layout/components/Lobby.tsx
Normal file
9
src/layout/components/Lobby.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const Lobby = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Lobby</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Lobby;
|
||||
@@ -1,19 +1,16 @@
|
||||
import useWebSocket from "react-use-websocket";
|
||||
import React, {FormEvent} from "react";
|
||||
import React, {useEffect} from "react";
|
||||
import {useNavigate, useParams} from "react-router";
|
||||
import {GHButton} from "../components/Button";
|
||||
import useTitle from "../../utils/hooks/TitleHook";
|
||||
import Game, {GameState} from "../components/Game";
|
||||
|
||||
interface ChatMessage {
|
||||
Time: Date;
|
||||
Sender: string;
|
||||
Message: string;
|
||||
}
|
||||
import Lobby from "../components/Lobby";
|
||||
import Chat from "../components/Chat";
|
||||
|
||||
export interface Player {
|
||||
Name: string;
|
||||
Id: string;
|
||||
CardsLeft: number;
|
||||
}
|
||||
|
||||
interface SocketMessage {
|
||||
@@ -21,6 +18,11 @@ interface SocketMessage {
|
||||
Payload: any;
|
||||
}
|
||||
|
||||
export interface GameAction {
|
||||
Action: string;
|
||||
Data: string;
|
||||
}
|
||||
|
||||
const Room = () => {
|
||||
|
||||
useTitle('Mau!');
|
||||
@@ -33,15 +35,31 @@ const Room = () => {
|
||||
const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}/${playerName}`;
|
||||
|
||||
const [gameState, setGameState] = React.useState<GameState>({
|
||||
Me: {Name: playerName, Id: ''},
|
||||
Me: {Name: playerName, Id: '', CardsLeft: 0},
|
||||
CurrentState: '',
|
||||
Hand: [],
|
||||
CurrentCard: '',
|
||||
CurrentPlayer: {Name: "", Id: ''},
|
||||
CurrentPlayer: {Name: "", Id: '', CardsLeft: 0},
|
||||
Players: []
|
||||
});
|
||||
const [chatMessages, setChatMessages] = React.useState<ChatMessage[]>([]);
|
||||
const [chatInput, setChatInput] = React.useState<string>('');
|
||||
|
||||
const [chatScroll, setChatScroll] = React.useState<boolean>(true);
|
||||
const chatRef = React.useRef<HTMLOListElement>(null);
|
||||
|
||||
const addChatMessage = (message: {Sender: string, Message: string}) => {
|
||||
if(!chatRef.current) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = chatRef.current;
|
||||
|
||||
const newChatElement = document.createElement('li');
|
||||
newChatElement.innerHTML = `<strong>${message.Sender}:</strong> ${message.Message}`;
|
||||
chatRef.current.appendChild(newChatElement);
|
||||
|
||||
const isNearBottom = scrollTop + clientHeight + newChatElement.scrollHeight * 2 >= scrollHeight;
|
||||
if (chatScroll && isNearBottom) {
|
||||
newChatElement.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
|
||||
const websocket = useWebSocket(WS_URL, {
|
||||
onOpen: () => {
|
||||
@@ -51,17 +69,11 @@ const Room = () => {
|
||||
const data = JSON.parse(event.data);
|
||||
const payload = JSON.parse(data.Payload);
|
||||
if (data.Type === 'GAME') setGameState(payload);
|
||||
if (data.Type === 'CHAT') setChatMessages(prev => [...prev, {
|
||||
Time: new Date(payload.Time),
|
||||
Sender: payload.Sender,
|
||||
Message: payload.Message
|
||||
}]);
|
||||
if (data.Type === 'CHAT') addChatMessage({Sender: payload.Sender, Message: payload.Message})
|
||||
}
|
||||
});
|
||||
|
||||
const handleLeaveRoom = () => {
|
||||
const socket = websocket.getWebSocket();
|
||||
if (socket) socket.close();
|
||||
navigateTo('/');
|
||||
}
|
||||
|
||||
@@ -69,62 +81,51 @@ const Room = () => {
|
||||
websocket.sendMessage(JSON.stringify(message));
|
||||
}
|
||||
|
||||
const handleCardSend = (card: string) => {
|
||||
handleSend({
|
||||
Type: "GAME",
|
||||
Payload: JSON.stringify({
|
||||
Action: "PLAYCARD",
|
||||
Data: JSON.stringify({
|
||||
CardType: card.split(' ')[0],
|
||||
CardValue: card.split(' ')[1]
|
||||
})
|
||||
})
|
||||
})
|
||||
const handleGameAction = (action: GameAction) => {
|
||||
handleSend({Type: "GAME", Payload: JSON.stringify(action)});
|
||||
}
|
||||
|
||||
const handleChoice = (choice: string) => {
|
||||
handleSend({
|
||||
Type: "GAME",
|
||||
Payload: JSON.stringify({
|
||||
Action: "CHOOSE",
|
||||
Data: choice
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const handleDraw = () => {
|
||||
handleSend({
|
||||
Type: "GAME",
|
||||
Payload: JSON.stringify({
|
||||
Action: "DRAW",
|
||||
Data: ""
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const handleChat = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const handleChatMessage = (chatMessage: string) => {
|
||||
handleSend({
|
||||
Type: "CHAT",
|
||||
Payload: chatInput
|
||||
Payload: chatMessage
|
||||
});
|
||||
setChatInput('');
|
||||
}
|
||||
|
||||
const isLobby = false;
|
||||
const isGame = true;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const socket = websocket.getWebSocket();
|
||||
if (socket) socket.close();
|
||||
}
|
||||
// this is a cleanup function and should not include any dependencies that would cause it to run more than once
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Room {roomId}</h1>
|
||||
<div className={"room"}>
|
||||
<aside className={"room-aside"}>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Mau-Mau</h1>
|
||||
<h2>Room {roomId?.split("-")[0]}</h2>
|
||||
</hgroup>
|
||||
<GHButton onClick={handleLeaveRoom}>Leave Room</GHButton>
|
||||
<Game gameState={gameState} handleCardSend={handleCardSend} handleDraw={handleDraw} handleChoice={handleChoice}/>
|
||||
<form onSubmit={handleChat}>
|
||||
<input type="text" placeholder={"Chat"} value={chatInput} onChange={(e) => setChatInput(e.target.value)} />
|
||||
<GHButton type={"submit"}>Send</GHButton>
|
||||
</form>
|
||||
<ul>
|
||||
{chatMessages.map((message) => (
|
||||
<li key={message.Time.getUTCMilliseconds()}>({message.Time.toLocaleTimeString()}) {message.Sender}: {message.Message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</header>
|
||||
<Chat chatRef={chatRef} handleSend={handleChatMessage}/>
|
||||
</aside>
|
||||
<main className={"room-main"}>
|
||||
{
|
||||
isLobby &&
|
||||
<Lobby/>
|
||||
}
|
||||
{
|
||||
isGame &&
|
||||
<Game gameState={gameState} handleGameAction={handleGameAction}/>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ body {
|
||||
}
|
||||
|
||||
#root, .app {
|
||||
@extend .dark;
|
||||
background-color: #131313;
|
||||
color: #ffffff;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@import "button";
|
||||
@import "card";
|
||||
@import "chat";
|
||||
@import "hand";
|
||||
@import "game";
|
||||
@import "deck";
|
||||
28
src/styles/layout/components/chat.scss
Normal file
28
src/styles/layout/components/chat.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.chat {
|
||||
|
||||
&__list {
|
||||
overflow-y: hidden;
|
||||
word-wrap: break-word;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
|
||||
&:hover {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
&-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__input {
|
||||
background-color: #2e2e2e;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/styles/layout/components/game.scss
Normal file
25
src/styles/layout/components/game.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
.game {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
place-items: center;
|
||||
|
||||
&__players {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
.hand {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
//justify-content: center;
|
||||
flex-flow: nowrap !important;
|
||||
max-width: 100%;
|
||||
flex-flow: row nowrap !important;
|
||||
overflow-x: scroll;
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@import "mainlobby";
|
||||
@import "room";
|
||||
12
src/styles/layout/pages/room.scss
Normal file
12
src/styles/layout/pages/room.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.room {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
height: 100%;
|
||||
|
||||
&-aside {
|
||||
display: grid;
|
||||
min-width: 20%;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
@import "pointer";
|
||||
@import "theme";
|
||||
@@ -1,8 +0,0 @@
|
||||
.dark {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
.light {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
Reference in New Issue
Block a user