developmaunt -> mauster #3

Merged
DTieman merged 7 commits from developmaunt into mauster 2024-04-24 18:05:41 +00:00
13 changed files with 222 additions and 98 deletions
Showing only changes of commit 4959e197bb - Show all commits

View 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;

View File

@@ -2,7 +2,7 @@ import React, {FunctionComponent} from "react";
import Deck from "./Deck"; import Deck from "./Deck";
import Hand from "./Hand"; import Hand from "./Hand";
import {GHButton} from "./Button"; import {GHButton} from "./Button";
import {Player} from "../pages/Room"; import {GameAction, Player} from "../pages/Room";
export interface GameState { export interface GameState {
Me: Player; Me: Player;
@@ -15,16 +15,52 @@ export interface GameState {
interface Props { interface Props {
gameState: GameState gameState: GameState
handleCardSend: (cardString: string) => void; handleGameAction: (action: GameAction) => void;
handleDraw: () => void;
handleChoice: (choice: string) => void;
} }
const CHOICES = ['SPADES', 'HEARTS', 'DIAMONDS', 'CLUBS']; 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 ( return (
<div className="game"> <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 && gameState.CurrentCard &&
<Deck currentCard={gameState.CurrentCard} actionOnClick={handleDraw}/> <Deck currentCard={gameState.CurrentCard} actionOnClick={handleDraw}/>
@@ -37,18 +73,6 @@ const Game: FunctionComponent<Props> = ({gameState, handleCardSend, handleDraw,
gameState.CurrentState === 'CHOOSE' && gameState.CurrentState === 'CHOOSE' &&
CHOICES.map(choice => <GHButton key={choice} onClick={() => handleChoice(choice)}>{choice}</GHButton>) 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> </div>
) )
} }

View File

@@ -0,0 +1,9 @@
const Lobby = () => {
return (
<div>
<h1>Lobby</h1>
</div>
);
}
export default Lobby;

View File

@@ -1,19 +1,16 @@
import useWebSocket from "react-use-websocket"; import useWebSocket from "react-use-websocket";
import React, {FormEvent} from "react"; import React, {useEffect} from "react";
import {useNavigate, useParams} from "react-router"; import {useNavigate, useParams} from "react-router";
import {GHButton} from "../components/Button"; import {GHButton} from "../components/Button";
import useTitle from "../../utils/hooks/TitleHook"; import useTitle from "../../utils/hooks/TitleHook";
import Game, {GameState} from "../components/Game"; import Game, {GameState} from "../components/Game";
import Lobby from "../components/Lobby";
interface ChatMessage { import Chat from "../components/Chat";
Time: Date;
Sender: string;
Message: string;
}
export interface Player { export interface Player {
Name: string; Name: string;
Id: string; Id: string;
CardsLeft: number;
} }
interface SocketMessage { interface SocketMessage {
@@ -21,6 +18,11 @@ interface SocketMessage {
Payload: any; Payload: any;
} }
export interface GameAction {
Action: string;
Data: string;
}
const Room = () => { const Room = () => {
useTitle('Mau!'); useTitle('Mau!');
@@ -33,15 +35,31 @@ const Room = () => {
const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}/${playerName}`; const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}/${playerName}`;
const [gameState, setGameState] = React.useState<GameState>({ const [gameState, setGameState] = React.useState<GameState>({
Me: {Name: playerName, Id: ''}, Me: {Name: playerName, Id: '', CardsLeft: 0},
CurrentState: '', CurrentState: '',
Hand: [], Hand: [],
CurrentCard: '', CurrentCard: '',
CurrentPlayer: {Name: "", Id: ''}, CurrentPlayer: {Name: "", Id: '', CardsLeft: 0},
Players: [] 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, { const websocket = useWebSocket(WS_URL, {
onOpen: () => { onOpen: () => {
@@ -51,17 +69,11 @@ const Room = () => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
const payload = JSON.parse(data.Payload); const payload = JSON.parse(data.Payload);
if (data.Type === 'GAME') setGameState(payload); if (data.Type === 'GAME') setGameState(payload);
if (data.Type === 'CHAT') setChatMessages(prev => [...prev, { if (data.Type === 'CHAT') addChatMessage({Sender: payload.Sender, Message: payload.Message})
Time: new Date(payload.Time),
Sender: payload.Sender,
Message: payload.Message
}]);
} }
}); });
const handleLeaveRoom = () => { const handleLeaveRoom = () => {
const socket = websocket.getWebSocket();
if (socket) socket.close();
navigateTo('/'); navigateTo('/');
} }
@@ -69,62 +81,51 @@ const Room = () => {
websocket.sendMessage(JSON.stringify(message)); websocket.sendMessage(JSON.stringify(message));
} }
const handleCardSend = (card: string) => { const handleGameAction = (action: GameAction) => {
handleSend({ handleSend({Type: "GAME", Payload: JSON.stringify(action)});
Type: "GAME",
Payload: JSON.stringify({
Action: "PLAYCARD",
Data: JSON.stringify({
CardType: card.split(' ')[0],
CardValue: card.split(' ')[1]
})
})
})
} }
const handleChoice = (choice: string) => { const handleChatMessage = (chatMessage: 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();
handleSend({ handleSend({
Type: "CHAT", 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 ( return (
<div> <div className={"room"}>
<h1>Room {roomId}</h1> <aside className={"room-aside"}>
<header>
<hgroup>
<h1>Mau-Mau</h1>
<h2>Room {roomId?.split("-")[0]}</h2>
</hgroup>
<GHButton onClick={handleLeaveRoom}>Leave Room</GHButton> <GHButton onClick={handleLeaveRoom}>Leave Room</GHButton>
<Game gameState={gameState} handleCardSend={handleCardSend} handleDraw={handleDraw} handleChoice={handleChoice}/> </header>
<form onSubmit={handleChat}> <Chat chatRef={chatRef} handleSend={handleChatMessage}/>
<input type="text" placeholder={"Chat"} value={chatInput} onChange={(e) => setChatInput(e.target.value)} /> </aside>
<GHButton type={"submit"}>Send</GHButton> <main className={"room-main"}>
</form> {
<ul> isLobby &&
{chatMessages.map((message) => ( <Lobby/>
<li key={message.Time.getUTCMilliseconds()}>({message.Time.toLocaleTimeString()}) {message.Sender}: {message.Message}</li> }
))} {
</ul> isGame &&
<Game gameState={gameState} handleGameAction={handleGameAction}/>
}
</main>
</div> </div>
) )
} }

View File

@@ -19,7 +19,8 @@ body {
} }
#root, .app { #root, .app {
@extend .dark; background-color: #131313;
color: #ffffff;
padding: 1rem; padding: 1rem;
height: 100%; height: 100%;
} }

View File

@@ -1,4 +1,6 @@
@import "button"; @import "button";
@import "card"; @import "card";
@import "chat";
@import "hand"; @import "hand";
@import "game";
@import "deck"; @import "deck";

View 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;
}
}
}

View 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%;
}
}
}

View File

@@ -1,8 +1,7 @@
.hand { .hand {
display: flex; display: flex;
flex-direction: row; max-width: 100%;
//justify-content: center; flex-flow: row nowrap !important;
flex-flow: nowrap !important;
overflow-x: scroll; overflow-x: scroll;
padding: 0.5rem; padding: 0.5rem;
gap: 0.5rem; gap: 0.5rem;

View File

@@ -1 +1,2 @@
@import "mainlobby"; @import "mainlobby";
@import "room";

View 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;
}
}

View File

@@ -1,2 +1 @@
@import "pointer"; @import "pointer";
@import "theme";

View File

@@ -1,8 +0,0 @@
.dark {
background-color: #333;
color: #fff;
}
.light {
background-color: #fff;
color: #333;
}