import React from 'react'; import DraggableCard from './DraggableCard'; import TouchBackend from 'react-dnd-touch-backend'; import { DndProvider } from 'react-dnd'; import MyCardsDropZone from './MyCardsDropZone'; import PlayerDrop from './PlayerDrop'; import CardWrap from './CardWrap'; import BlankPlayerCard from './BlankPlayerCard'; import BlackCardDrop from './BlackCardDrop'; import NamePopup from './NamePopup'; import { ToastContainer, toast, Slide } from 'react-toastify'; import { MAX_PLAYERS } from '../constants'; import { withRouter } from 'react-router-dom'; import styled, { createGlobalStyle, keyframes } from 'styled-components'; import io from 'socket.io-client'; import axios from 'axios'; import queryString from 'query-string'; import { SERVER_URL } from '../constants'; import ChatBox from './ChatBox'; import Tour from 'reactour'; import './Game.css'; import 'react-toastify/dist/ReactToastify.min.css'; export const BlackCard = React.memo(({ text, setUserIsDragging, socket, isMyCardsOpen, isSubmittedTableOpen }) => { return ( <DraggableCard isFlipBroadcasted setUserIsDragging={setUserIsDragging} socket={socket} type="blackCard" bgColor="#000" color="#fff" text={text} screen="main" isMyCardsOpen={isMyCardsOpen} isSubmittedTableOpen={isSubmittedTableOpen} /> ); }); const PickUpPile = React.memo(({ id, text, setUserIsDragging, socket, isMyCardsOpen, isSubmittedTableOpen }) => { return ( <DraggableCard isFlippable={false} setUserIsDragging={setUserIsDragging} socket={socket} id={id} type="whiteCard" bgColor="#fff" color="#000" text={text} screen="main" isMyCardsOpen={isMyCardsOpen} isSubmittedTableOpen={isSubmittedTableOpen} /> ); }); const TourToast = ({ setTourOpen }) => ( <TourToastButton type="button" onClick={setTourOpen}> Tap here to learn how to play! (Recommended) </TourToastButton> ); class Game extends React.PureComponent { socket = null; roomId = null; componentDidMount() { this.setState({ cardDimensions: { width: this.whiteCardRef.current.offsetWidth, height: this.whiteCardRef.current.offsetHeight, top: this.whiteCardRef.current.getBoundingClientRect().top, left: this.whiteCardRef.current.getBoundingClientRect().left, }, }); this.props.reactGA.pageview('/g'); if (!this.socket) { // start socket connection this.socket = io(SERVER_URL); // set the roomId based on the /g/:roomId path this.roomId = this.props.location.pathname.replace('/g/', ''); // let the server know we've joined a room this.socket.emit('join room', { roomId: this.roomId, myName: this.state.myName, }); // confirm that we've joined the right room on the client this.socket.on('joined a room', (theRoom) => { console.log({ theRoom }); // once we've joined a room, lets get the cards const deckQueryString = queryString.parse(this.props.location.search) .deck; // If the whiteCards and blackCards are already set, don't bother hitting this endpoint. if (!this.state.whiteCards.length && !this.state.blackCards.length) { axios .post(`${SERVER_URL}/api/getInitialCards`, { deckName: deckQueryString, roomId: this.roomId, }) .then((res) => { if (!res.data) { return; } const { blackCards: newBlackCards, whiteCards: newWhiteCards, } = res.data; this.socket.emit('set initialCards for game', { whiteCards: newWhiteCards, blackCards: newBlackCards, }); }); // check if the room is marked as private by the first user to connect const isPrivate = queryString.parse(this.props.location.search) .private; if (isPrivate === '1') { this.socket.emit('set game as private'); } } }); const newPlayers = [...this.state.players, { socket: null }]; this.setState({ players: newPlayers, }); } this.socket.on( 'get initialCards for game', ({ whiteCards = [], blackCards = [] }) => { this.setState({ whiteCards, blackCards, }); } ); this.socket.on('disconnect', () => { // @TODO: find a better way to reconnect or recreate the room // after the server restarts or a long period of time and someone tries to reconnect. // when the server no longer has any knowledge of the room window.location.reload(); }); // when a player changes their name, update players state with new name this.socket.on('name change', (players) => { console.log({ players }); this.setState({ players }); }); // when a player disconnects from the server, remove them from state this.socket.on('user disconnected', (players) => { this.setState({ players }); }); // when a new user connects // send that specific user the latest server states this.socket.on( 'new connection', ({ players, blackCards, whiteCards, submittedCards, socketId }) => { if (whiteCards && whiteCards.length > 0) { this.setState({ whiteCards }); } if (blackCards && blackCards.length > 0) { this.setState({ blackCards }); } if (submittedCards && submittedCards.length > 0) { this.setState({ submittedCards }); } console.log('new connection!', players); this.setState(() => ({ players, socketConnected: true, })); } ); // when a new user connects, let every client know. this.socket.on('user connected', ({ players }) => { this.setState({ players }); }); this.socket.on('dropped in my cards', ({ players, whiteCards }) => { this.setState({ whiteCards, players }); }); this.socket.on('update players', (players) => { this.setState({ players }); }); this.socket.on('update submittedCards', (submittedCards) => { this.setState({ submittedCards }); }); this.socket.on('submitted a card', ({ submittedCards, players }) => { this.setState({ submittedCards, players }); }); this.socket.on('player rejoins', (players) => { const playerWithWhiteCards = players.find( (player) => this.socket.id === player.id ); if (playerWithWhiteCards.whiteCards) { this.setState({ myCards: playerWithWhiteCards.whiteCards }); } this.setState({ players }); }); this.socket.on('dropped in player drop', ({ players, blackCards }) => { this.setState({ players, blackCards }); }); this.socket.on( 'draw seven white cards update', ({ players, whiteCards, sevenWhiteCards, socketId }) => { this.setState({ players, whiteCards, }); if (this.socket.id === socketId) { this.setState({ myCards: sevenWhiteCards, }); } } ); } componentDidUpdate(prevProps, prevState) { if (prevState.players !== this.state.players) { const lengths = this.state.players.map((player) => player.blackCards ? player.blackCards.length : -1 ); const winner = Math.max(...lengths); const numberOfWinners = lengths.filter((length) => length === winner) .length; const index = this.state.players.findIndex( (player) => player.blackCards && player.blackCards.length === winner ); if (winner === 0 || numberOfWinners > 1) { return this.setState({ winningPlayerIndex: -1 }); } this.setState({ winningPlayerIndex: index }); } if (prevState.animationOver !== this.state.animationOver) { if (this.state.animationOver) { toast.success(<TourToast setTourOpen={this.setTourOpen} />, { toastId: 'TourToast', position: toast.POSITION.TOP_CENTER, autoClose: false, }); } } } componentWillUnmount() { this.socket.off('get initialCards for game'); this.socket.off('disconnect'); this.socket.off('name change'); this.socket.off('user disconnected'); this.socket.off('new connection'); this.socket.off('user connected'); this.socket.off('dropped in my cards'); this.socket.off('update players'); this.socket.off('update submittedCards'); this.socket.off('submitted a card'); this.socket.off('player rejoins'); this.socket.off('dropped in player drop'); this.socket.off('draw seven white cards update'); this.socket.off('joined a room'); } state = { blackCardWidth: null, blackCards: [], whiteCards: [], myCards: [], myName: localStorage.getItem('cas-name') || '', players: [], submittedCards: [], currentHost: 0, showNamePopup: true, userIsDragging: null, nameError: '', winningPlayerIndex: -1, socketConnected: false, chatOpen: false, unreadCount: 0, animationOver: false, isTourOpen: false, isMyCardsOpen: false, isSubmittedTableOpen: false, }; whiteCardRef = React.createRef(); getTheCurrentHost = (index) => this.setState({ currentHost: index }); addCardToPlayer = (passedInCard, playerDroppedOn) => { if (!this.state.userIsDragging) { return; } // get the players state, the player index, and give that the passedInCard (players[index].blackCards.push(passedInCard)) this.setState( (prevState) => { // update player card property with new card const newPlayers = [...prevState.players].map((player) => { if (player.id === playerDroppedOn.id) { if ( playerDroppedOn.blackCards && playerDroppedOn.blackCards.length ) { // check if blackCard already exists with player if ( !player.blackCards.some( (blackCard) => blackCard.text === passedInCard.text ) ) { player.blackCards = [...player.blackCards, { ...passedInCard }]; } } else { player.blackCards = [{ ...passedInCard }]; } } else { if (player.blackCards) { // if another player already has the blackCard, remove it from them player.blackCards = player.blackCards.filter((blackCard) => { return blackCard.text !== passedInCard.text; }); } } return player; }); // remove blackcard from blackcards if this is from the main deck // and not from another player slot ('blackCardFromPlayer') if (passedInCard.type === 'blackCard') { const indexOfPassedInCard = prevState.blackCards.findIndex( (blackCard) => blackCard === passedInCard.text ); const newBlackCards = [...prevState.blackCards]; newBlackCards.splice(indexOfPassedInCard, 1); return { players: newPlayers, blackCards: newBlackCards, }; } return { players: newPlayers, }; }, () => { // send event that a card was moved to someones deck to the server this.socket.emit('dropped in player drop', { players: this.state.players, blackCards: this.state.blackCards, }); } ); }; addCardToMyCards = (passedInCard) => { if (this.state.myCards.length === 7 || !this.state.userIsDragging) { return; } this.setState((prevState) => ({ myCards: [...prevState.myCards, passedInCard], })); // send event that a card was moved to someones deck to the server this.socket.emit('dropped in my cards', { passedInCard, socketId: this.socket.id, }); }; addBlackCardBackToPile = (passedInCard) => { if (!this.state.userIsDragging) { return; } // add passedInCard to the front of the blackCards array const newBlackCards = [...this.state.blackCards]; newBlackCards.unshift(passedInCard); // find player with blackCard and remove from their blackCards array const newPlayers = this.state.players.map((player) => { if (player.blackCards && player.blackCards.length) { const newPlayerBlackCards = player.blackCards.filter((blackCard) => { return blackCard.text !== passedInCard.text; }); return { ...player, blackCards: newPlayerBlackCards }; } return player; }); this.setState({ blackCards: newBlackCards, players: newPlayers, }); // update blackCards for everyone this.socket.emit('dropped in player drop', { blackCards: newBlackCards, players: newPlayers, }); }; submitACard = (passedInCard) => { if (this.state.submittedCards.length === MAX_PLAYERS - 1) { return; } // remove passedInCard from myCards const passedInCardIndex = this.state.myCards.findIndex( (card) => card.text === passedInCard.text ); const newMyCards = [...this.state.myCards]; newMyCards.splice(passedInCardIndex, 1); // update players and myCards this.setState({ myCards: newMyCards, }); this.props.reactGA.event({ category: `Game ${this.roomId}`, action: 'Player submitted a card', label: passedInCard.text, }); this.socket.emit('submitted a card', { socketId: this.socket.id, passedInCard, newMyCards, }); }; discardACard = (passedInCard) => { if (!this.state.userIsDragging) { return; } this.socket.emit('update submittedCards', passedInCard); }; getBlankPlayerCards(players) { const length = MAX_PLAYERS - players.length; const arr = Array.from({ length }, (_, i) => i); return arr; } updateMyName = (e) => { const myName = e.target.value.toUpperCase().trim(); this.setState({ myName }); // send event that a user just changed their name this.socket.emit('name change', { id: this.socket.id, name: myName }); }; handleSubmit = (e) => { e.preventDefault(); if (!this.socket.connected) { this.setState({ nameError: 'Cannot connect to server. Try again.' }); return; } if (this.state.myName.trim().length < 2) { this.setState({ nameError: 'Please submit a name at least 2 characters long.', }); return; } // This handles a case for slower connections so users cannot // enter the game and do stuff before the socket is connected if (!this.state.socketConnected) { this.setState({ nameError: 'Please try again in a few seconds.', }); return; } if ( this.state.players.find( (player) => player.name === this.state.myName && player.id !== this.socket.id ) ) { this.setState({ nameError: 'Name taken. Please choose another name.' }); return; } const doesPlayerExist = this.state.players.find( (player) => player.id === this.socket.id ); // not sure the main cause, but hoping this prevents // users from entering the game without being set up as a player. if (!doesPlayerExist) { this.setState({ nameError: 'Looks like you were disconnected. \nPlease refresh the page.', }); return; } localStorage.setItem('cas-name', this.state.myName); this.setState((prevState) => { // once we update our name, let's update our player in players const newPlayers = prevState.players.map((player) => { if (player.id === this.socket.id) { const newPlayer = { ...player }; newPlayer.name = this.state.myName; return newPlayer; } return player; }); // and then let the other clients know this.socket.emit('name submit', { players: newPlayers, myName: this.state.myName, id: this.socket.id, }); this.props.reactGA.event({ category: `Game ${this.roomId}`, action: 'Submitted A Name', label: this.state.myName, }); return { showNamePopup: false, players: newPlayers, nameError: '', }; }); }; setUserIsDragging = (type) => { this.setState({ userIsDragging: type }); }; copyLink = () => { // Web Share API for cool browsers // @TODO: This is crashing chrome for some reason // if (navigator && navigator.share) { // navigator // .share({ // title: "Cards of Personality Game", // url: this.inviteInputRef.current, // }) // .then(() => { // console.log("Thanks for sharing!"); // }) // .catch(console.error); // } // Generic copy to clipboard this.inviteInputRef.current.select(); document.execCommand('copy'); // Clear the text selection if (window.getSelection) { if (window.getSelection().empty) { // Chrome window.getSelection().empty(); } else if (window.getSelection().removeAllRanges) { // Firefox window.getSelection().removeAllRanges(); } } else if (document.selection) { // IE? document.selection.empty(); } // Pop a success toast toast.success('Copied to clipboard!', { toastId: 'copy-toast', position: toast.POSITION.TOP_CENTER, autoClose: 2000, }); }; inviteInputRef = React.createRef(); setChatOpen = (bool) => { this.setState({ chatOpen: bool }); }; setUnreadCount = (count) => { if (count) { this.setState((prevState) => ({ unreadCount: prevState.unreadCount + 1, })); return; } this.setState({ unreadCount: 0 }); }; setAnimationOver = () => { this.setState({ animationOver: true, }); }; setTourOpen = () => { this.setState({ isTourOpen: true, }); }; setTourClosed = () => { this.setState({ isTourOpen: false, isSubmittedTableOpen: false, isMyCardsOpen: false, }); }; setMyCardsOpen = (bool) => { if (bool && typeof bool === 'boolean') { return this.setState({ isMyCardsOpen: bool, }); } this.setState((prevState) => ({ isMyCardsOpen: !prevState.isMyCardsOpen, })); }; setSubmittedTableOpen = (bool) => { if (bool && typeof bool === 'boolean') { return this.setState({ isSubmittedTableOpen: bool, }); } this.setState((prevState) => ({ isSubmittedTableOpen: !prevState.isSubmittedTableOpen, })); }; // Tutorial Overlay Steps getSteps = () => { return [ { selector: '.Game-bigBlackCard', content: 'To start, the first player to join is the judge and begins the round by tapping the black card and reading it aloud.', position: 'left', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.MyCardsDropBar', content: 'Each player looks at their seven white cards by tapping this bar.', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.MyCardsContainer-scrollingWrap', content: 'Each player except the judge submits a single white card that makes the funniest play with the black card.', action: () => { if (!this.state.isMyCardsOpen) { this.setMyCardsOpen(true); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.SubmittedCardsBar', content: 'Submit your card by dragging it and dropping it on the bottom bar. Tap the bottom bar to proceed to the Submitted Cards screen.', action: () => { if (!this.state.isMyCardsOpen) { this.setMyCardsOpen(true); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.SubmittedCardsTable-scrollingWrap', content: 'Once everyone has submitted a card, the judge re-reads the black card and taps to flip each submitted white card one by one to read aloud for all to admire or condemn.', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (!this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(true); } }, }, { selector: '.DiscardButton', content: 'The judge announces a winner and now everyone can discard their submitted card by dragging it to the bottom bar.', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (!this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(true); } }, }, { selector: '.PlayerOneSlot', content: 'The player who submitted the winning card can now drag the big black card to their player slot. The first player to collect 7 black cards in their player slot wins.', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.PlayerTwoSlot', content: 'The next player is now the judge and proceeds to flip the next black card.', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, { selector: '.WhiteCardPile', content: 'Don\'t forget to drag another white card to your deck after each round. A player can have up to 7 white cards in their deck at all times!', action: () => { if (this.state.isMyCardsOpen) { this.setMyCardsOpen(false); } if (this.state.isSubmittedTableOpen) { this.setSubmittedTableOpen(false); } }, }, ]; }; render() { return ( <> <div className={`Game ${this.state.isTourOpen ? 'is-tourActive' : ''}`}> <GlobalStyle /> {this.state.showNamePopup && ( <NamePopup handleSubmit={this.handleSubmit} inviteInputRef={this.inviteInputRef} roomId={this.roomId} copyLink={this.copyLink} updateMyName={this.updateMyName} myName={this.state.myName} nameError={this.state.nameError} reactGA={this.props.reactGA} /> )} <DndProvider backend={TouchBackend} options={{ enableMouseEvents: true }} > <Table> <CardsWrap> <Piles> <CardWrap isPickUpPile className="Game-bigBlackCard"> <BlackCardDrop addBlackCardBackToPile={this.addBlackCardBackToPile} > {this.state.blackCards .slice( Math.max( this.state.blackCards.length - (MAX_PLAYERS + 1), 0 ) ) .map((text, index) => ( <BlackCard setUserIsDragging={this.setUserIsDragging} key={text} id={index} text={text} socket={this.socket} isMyCardsOpen={this.state.isMyCardsOpen} isSubmittedTableOpen={this.state.isSubmittedTableOpen} /> ))} </BlackCardDrop> </CardWrap> <CardWrap isPickUpPile innerRef={this.whiteCardRef} className="WhiteCardPile"> {this.state.whiteCards .slice( Math.max( this.state.whiteCards.length - (MAX_PLAYERS + 1), 0 ) ) .map((text, index) => ( <PickUpPile setUserIsDragging={this.setUserIsDragging} key={text} id={index} text={text} socket={this.socket} isMyCardsOpen={this.state.isMyCardsOpen} isSubmittedTableOpen={this.state.isSubmittedTableOpen} /> ))} {!this.state.showNamePopup && this.state.myCards.length > 0 && !this.state.animationOver && ( <AnimatedDraw cardDimensions={this.state.cardDimensions} myCards={this.state.myCards} onAnimationEnd={this.setAnimationOver} > <DraggableCard bgColor="#fff" isBroadcastingDrag={false} isFlipBroadcasted={false} color="#000" type="whiteCard" setUserIsDragging={this.setUserIsDragging} isFlippable={false} /> </AnimatedDraw> )} </CardWrap> </Piles> <PlayerDecks className="Table-playerDecks"> {this.state.players && this.state.players.map(({ name }, index) => ( <PlayerDrop className={index === 0 ? 'PlayerOneSlot' : index === 1 ? 'PlayerTwoSlot' : ''} setUserIsDragging={this.setUserIsDragging} userIsDragging={this.state.userIsDragging} key={index} index={index} socket={this.socket} addCardToPlayer={this.addCardToPlayer} players={this.state.players} myName={this.state.myName} winningPlayerIndex={this.state.winningPlayerIndex} isMyCardsOpen={this.state.isMyCardsOpen} isSubmittedTableOpen={this.state.isSubmittedTableOpen} /> ))} {this.getBlankPlayerCards(this.state.players).map( (num, index) => ( <BlankPlayerCard className={this.state.players && this.state.players.length === 1 && index === 0 ? 'PlayerTwoSlot' : ''} key={num} index={index} count={this.state.players.length} /> ) )} </PlayerDecks> </CardsWrap> <MyCardsDropZone setUserIsDragging={this.setUserIsDragging} blackCards={this.state.blackCards} userIsDragging={this.state.userIsDragging} socket={this.socket} discardACard={this.discardACard} addCardToMyCards={this.addCardToMyCards} submitACard={this.submitACard} submittedCards={this.state.submittedCards} myCards={this.state.myCards} myName={this.state.myName} setChatOpen={this.setChatOpen} unreadCount={this.state.unreadCount} setMyCardsOpen={this.setMyCardsOpen} setSubmittedTableOpen={this.setSubmittedTableOpen} isMyCardsOpen={this.state.isMyCardsOpen} isSubmittedTableOpen={this.state.isSubmittedTableOpen} /> </Table> </DndProvider> <ToastContainer limit={1} hideProgressBar closeOnClick transition={Slide} pauseOnFocusLoss={false} /> <ChatBox chatOpen={this.state.chatOpen} setChatOpen={this.setChatOpen} socket={this.socket} myName={this.state.myName} setUnreadCount={this.setUnreadCount} reactGA={this.props.reactGA} roomId={this.roomId} /> </div> <Tour steps={this.getSteps()} closeWithMask={false} isOpen={this.state.isTourOpen} onRequestClose={this.setTourClosed} highlightedMaskClassName="is-highlighted" maskClassName="MaskOverlay" rounded={8} accentColor="#2cce9f" lastStepNextButton={<DoneButton type="button">Got it!</DoneButton>} /> </> ); } } const GlobalStyle = createGlobalStyle` html { overflow: hidden; position: fixed; width: 100%; } body { height: 100%; background: #dcdbdb; border: 0; padding: 0; } .Toastify__toast--success { background: #2cce9f; border-radius: 8px; color: #000; max-width: 200px; margin: 1em auto; font: inherit; } .Toastify__close-button { color: #000; } `; const TourToastButton = styled.button` appearance: none; background: transparent; border: 0; &:focus { outline: 0; } `; const DoneButton = styled.button` appearance: none; background: #2cce9f; border: 0; border-radius: 8px; padding: .5em; &:focus { outline: 0; } &:hover, &:focus { opacity: .5; } `; const moveToBottom = (cardDimensions) => keyframes` 0% { transform: translate3d(0, 0, 0); opacity: 1; } 99% { opacity: 1; } 100% { transform: translate3d(calc(${ window ? `${window.innerWidth / 2 - 25}px` : 0 } - ${cardDimensions?.left}px - 50%), calc(${ window ? `${window.innerHeight - 25}px` : 0 } - ${cardDimensions?.top}px - 50%), 0); opacity: 0; } `; const AnimatedDraw = styled.div` position: fixed; pointer-events: none; z-index: 999; animation: 0.2s ${(props) => moveToBottom(props.cardDimensions)} ease-out ${(props) => props.myCards.length || 7} forwards; width: ${(props) => props.cardDimensions.width ? `${props.cardDimensions.width}px` : 0}; height: ${(props) => props.cardDimensions.height ? `${props.cardDimensions.height}px` : 0}; `; const Table = styled.div` display: flex; flex-direction: column; height: 100%; `; const Piles = styled.div` display: flex; width: calc(40% - 0.25em); justify-content: center; align-items: center; > div:first-child { margin-right: 1em; } @media (min-width: 1600px) { margin-right: 2em; } @media (max-width: 500px) and (orientation: portrait) { width: 100%; margin: 0.5em 0; order: 1; } `; const PlayerDecks = styled.div` display: flex; flex-wrap: wrap; width: calc(60% - 0.25em); justify-content: center; align-content: center; margin-right: -0.5em; font-size: 0.7rem; /* some devices with small viewport height like Moto G2 need to make the player slots smaller. they can take up full space at this height */ @media (min-height: 556px) { justify-content: center; } @media (max-width: 500px) and (orientation: portrait) { width: calc(100% + 1em); margin: 0.5em -0.5em 0.5em; } `; const CardsWrap = styled.div` display: flex; flex-grow: 1; padding: 1em; justify-content: space-between; max-height: calc(100vh - 50px); @media (min-width: 1600px) { padding: 0; } @media (max-width: 500px) and (orientation: portrait) { max-height: none; margin-bottom: 50px; flex-direction: column; width: 100%; justify-content: center; } `; export default withRouter(Game);