diff options
Diffstat (limited to 'frontend/src')
23 files changed, 559 insertions, 0 deletions
diff --git a/frontend/src/components/errors/Error404.js b/frontend/src/components/errors/Error404.js new file mode 100644 index 00000000..b657482d --- /dev/null +++ b/frontend/src/components/errors/Error404.js @@ -0,0 +1,8 @@ +import React from 'react' +import { Link } from 'react-router' + +const Error404 = () => <div> + <h1>No Match</h1> + <Link to="/">Take me back home ! 🏠</Link> +</div> +export default Error404
\ No newline at end of file diff --git a/frontend/src/components/modals/username.js b/frontend/src/components/modals/username.js new file mode 100644 index 00000000..61b52114 --- /dev/null +++ b/frontend/src/components/modals/username.js @@ -0,0 +1,40 @@ +import React from 'react' +import { + Overlay, + Panel, + PanelHeader, + PanelFooter, + Button, + Input, + Close, + Space +} from 'rebass' + +const Modal = ({ modalOpen, toggleModal }) => ( + <Overlay open={modalOpen} onDismiss={toggleModal('usernameModal')}> + <Panel theme="info"> + <PanelHeader> + What's your username ? + <Space auto /> + <Close onClick={toggleModal('usernameModal')} /> + </PanelHeader> + <Input + label="Username" + name="username" + placeholder="Cesar92" + rounded + type="text" + /> + <PanelFooter> + <Space auto /> + <Button + theme="success" + onClick={toggleModal('usernameModal')} + children="Ok" + /> + </PanelFooter> + </Panel> + </Overlay> +) + +export default Modal diff --git a/frontend/src/containers/App/actions.js b/frontend/src/containers/App/actions.js new file mode 100644 index 00000000..cfb617d5 --- /dev/null +++ b/frontend/src/containers/App/actions.js @@ -0,0 +1,5 @@ +import { INITIALIZE_WS } from "./constants" + +export const initializeWs = () => ({ + type: INITIALIZE_WS +}) diff --git a/frontend/src/containers/App/constants.js b/frontend/src/containers/App/constants.js new file mode 100644 index 00000000..be31f8cc --- /dev/null +++ b/frontend/src/containers/App/constants.js @@ -0,0 +1 @@ +export const INITIALIZE_WS = 'app/INITIALIZE_WS' diff --git a/frontend/src/containers/App/index.js b/frontend/src/containers/App/index.js new file mode 100644 index 00000000..70f99b6b --- /dev/null +++ b/frontend/src/containers/App/index.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { + Banner, + Heading, + Space, + Button, + InlineForm, + Text +} from 'rebass' +import { Flex } from 'reflexbox' +import Modal from '../../components/modals/username' +import GameBrowser from '../GameBrowser' + +class App extends Component { + state = { + usernameModal: false, + } + + componentDidMount() { + + } + + toggleModal = (key) => { + return (e) => { + const val = !this.state[key] + this.setState({ [key]: val }) + } + } + + createGame = (e) => { + e.preventDefault() + if (this._gameName !== undefined) { + this.props.createGame(this._gameName) + } + } + + render() { + return ( + <div> + <Banner + align="center" + style={{minHeight: '30vh'}} + backgroundImage="https://images.unsplash.com/photo-1431207446535-a9296cf995b1?dpr=1&auto=format&fit=crop&w=1199&h=799&q=80&cs=tinysrgb&crop=" + > + <Heading level={1}>Seven Wonders</Heading> + </Banner> + <Flex align="center" p={1}> + <InlineForm + buttonLabel="Create Game" + label="Game name" + name="game_name" + onChange={(e) => this._gameName = e.target.value} + onClick={this.createGame} + > + + </InlineForm> + <Space auto /> + <Text><b>Username:</b> Cesar92</Text> + <Space x={1} /> + <Button + onClick={this.toggleModal('usernameModal')} + children="Change"/> + </Flex> + <GameBrowser /> + <Modal toggleModal={this.toggleModal} modalOpen={this.state.usernameModal} /> + </div> + ) + } +} + +const mapStateToProps = (state) => ({ + +}) + +import { initializeWs } from "./actions"; +import { createGame } from '../GameBrowser/actions' +const mapDispatchToProps = { + initializeWs, + createGame +} + +export default connect(mapStateToProps, mapDispatchToProps)(App) diff --git a/frontend/src/containers/App/saga.js b/frontend/src/containers/App/saga.js new file mode 100644 index 00000000..0c212142 --- /dev/null +++ b/frontend/src/containers/App/saga.js @@ -0,0 +1,28 @@ +import { put, take } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' + +function createSocketChannel(socket) { + return eventChannel(emit => { + const errorHandler = event => emit(JSON.parse(event.body)) + + const userErrors = socket.subscribe('/user/queue/errors', errorHandler) + + const unsubscribe = () => { + userErrors.unsubscribe() + } + + return unsubscribe + }) +} + +export function* watchOnErrors(socketConnection) { + const { socket } = socketConnection + const socketChannel = createSocketChannel(socket) + + while (true) { + const payload = yield take(socketChannel) + yield put({ type: 'USER_ERROR', payload }) + } +} + +export default watchOnErrors diff --git a/frontend/src/containers/GameBrowser/actions.js b/frontend/src/containers/GameBrowser/actions.js new file mode 100644 index 00000000..376973b4 --- /dev/null +++ b/frontend/src/containers/GameBrowser/actions.js @@ -0,0 +1,16 @@ +import { NEW_GAME, JOIN_GAME, CREATE_GAME } from './constants' + +export const newGame = (game) => ({ + type: NEW_GAME, + game +}) + +export const joinGame = (id) => ({ + type: JOIN_GAME, + id +}) + +export const createGame = (name) => ({ + type: CREATE_GAME, + name +}) diff --git a/frontend/src/containers/GameBrowser/constants.js b/frontend/src/containers/GameBrowser/constants.js new file mode 100644 index 00000000..36f701b7 --- /dev/null +++ b/frontend/src/containers/GameBrowser/constants.js @@ -0,0 +1,3 @@ +export const NEW_GAME = 'gameBrowser/NEW_GAME' +export const JOIN_GAME = 'gameBrowser/JOIN_GAME' +export const CREATE_GAME = 'gameBrowser/CREATE_GAME' diff --git a/frontend/src/containers/GameBrowser/index.js b/frontend/src/containers/GameBrowser/index.js new file mode 100644 index 00000000..9deb720b --- /dev/null +++ b/frontend/src/containers/GameBrowser/index.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { Flex } from 'reflexbox' +import { Text, Space } from 'rebass' + +class GameBrowser extends Component { + + listGames = (games) => { + return games.valueSeq().map((game, index) => { + return (<Flex key={index}> + <Text>{game.get('name')}</Text> + <Space auto /> + <a href="#">Join</a> + </Flex>) + }) + } + + render() { + return ( + <div> + {this.listGames(this.props.games)} + </div> + ) + } +} + +const mapStateToProps = (state) => ({ + games: state.games +}) + +export default connect(mapStateToProps, {})(GameBrowser) diff --git a/frontend/src/containers/GameBrowser/reducer.js b/frontend/src/containers/GameBrowser/reducer.js new file mode 100644 index 00000000..4fb3390a --- /dev/null +++ b/frontend/src/containers/GameBrowser/reducer.js @@ -0,0 +1,13 @@ +import { Map } from 'immutable' +import { NEW_GAME } from './constants' + +const initialState = Map({}) + +export default function reducer(state = initialState, action) { + switch (action.type) { + case NEW_GAME: + return state.set(action.game.get('id'), action.game) + default: + return state + } +} diff --git a/frontend/src/containers/GameBrowser/saga.js b/frontend/src/containers/GameBrowser/saga.js new file mode 100644 index 00000000..4cd3d207 --- /dev/null +++ b/frontend/src/containers/GameBrowser/saga.js @@ -0,0 +1,75 @@ +import { call, put, take } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' +import { fromJS } from 'immutable' +import { push } from 'react-router-redux' + +import { NEW_GAME, JOIN_GAME, CREATE_GAME } from './constants' +import { newGame, joinGame } from './actions' + +function createSocketChannel(socket) { + return eventChannel(emit => { + const makeHandler = (type) => (event) => { + const response = fromJS(JSON.parse(event.body)) + + emit({ + type, + response + }) + } + + const newGameHandler = makeHandler(NEW_GAME) + const joinGameHandler = makeHandler(JOIN_GAME) + + const newGame = socket.subscribe('/topic/games', newGameHandler) + const joinGame = socket.subscribe('/user/queue/join-game', joinGameHandler) + + const unsubscribe = () => { + newGame.unsubscribe() + joinGame.unsubscribe() + } + + return unsubscribe + }) +} + +export function* watchGames(socketConnection) { + + const { socket } = socketConnection + const socketChannel = createSocketChannel(socket) + + while (true) { + const { type, response } = yield take(socketChannel) + + switch (type) { + case NEW_GAME: + yield put(newGame(response)) + break; + case JOIN_GAME: + yield put(joinGame(response)) + break; + default: + console.error('Unknown type') + } + } +} + +export function* createGame(socketConnection) { + const { name } = yield take(CREATE_GAME) + const { socket } = socketConnection + + socket.send("/app/lobby/create-game", JSON.stringify({ + 'gameName': name, + 'playerName': 'Cesar92' + }), {}) +} + +export function* gameBrowserSaga(socketConnection) { + yield put(push('/lobby')) + + yield [ + call(watchGames, socketConnection), + call(createGame, socketConnection) + ] +} + +export default gameBrowserSaga diff --git a/frontend/src/containers/HomePage/actions.js b/frontend/src/containers/HomePage/actions.js new file mode 100644 index 00000000..e06d6fa2 --- /dev/null +++ b/frontend/src/containers/HomePage/actions.js @@ -0,0 +1,6 @@ +export const ENTER_GAME = 'homePage/ENTER_GAME' + +export const enterGame = (username) => ({ + type: ENTER_GAME, + username +}) diff --git a/frontend/src/containers/HomePage/index.js b/frontend/src/containers/HomePage/index.js new file mode 100644 index 00000000..c8e33239 --- /dev/null +++ b/frontend/src/containers/HomePage/index.js @@ -0,0 +1,39 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { Heading, InlineForm } from 'rebass' + +class HomePage extends Component { + + play = (e) => { + e.preventDefault() + if (this._username !== undefined) { + this.props.enterGame(this._username) + } + } + + render() { + return ( + <div> + <Heading>Enter your username to start playing!</Heading> + <InlineForm + buttonLabel="Play now!" + label="Username" + name="username" + onChange={(e) => this._username = e.target.value} + onClick={this.play} + /> + </div> + ) + } +} + +const mapStateToProps = (state) => ({ + +}) + +import { enterGame } from './actions' +const mapDispatchToProps = { + enterGame +} + +export default connect(mapStateToProps, mapDispatchToProps)(HomePage) diff --git a/frontend/src/containers/HomePage/saga.js b/frontend/src/containers/HomePage/saga.js new file mode 100644 index 00000000..0fbe8a45 --- /dev/null +++ b/frontend/src/containers/HomePage/saga.js @@ -0,0 +1,57 @@ +import { call, put, take } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' +import { ENTER_GAME } from './actions' +import { setUsername } from '../UserRepo/actions' + +import gameBrowserSaga from '../GameBrowser/saga' + +function* sendUsername(socketConnection) { + const { username: playerName } = yield take(ENTER_GAME) + const { socket } = socketConnection + + socket.send("/app/chooseName", JSON.stringify({ + playerName + }), {}) +} + +function createSocketChannel(socket) { + return eventChannel(emit => { + const receiveUsername = socket.subscribe('/user/queue/nameChoice', event => { + emit(JSON.parse(event.body)) + }) + + const unsubscribe = () => { + receiveUsername.unsubscribe() + } + + return unsubscribe + }) +} + +function* validateUsername(socketConnection) { + const { socket } = socketConnection + const socketChannel = createSocketChannel(socket) + + const response = yield take(socketChannel) + + if (response.error) { + return false + } + + yield put(setUsername(response.userName, response.displayName, response.index)) + yield call(gameBrowserSaga, socketConnection) + return true +} + +function* homeSaga(socketConnection) { + let validated = false + do { + const [, usernameValid] = yield [ + call(sendUsername, socketConnection), + call(validateUsername, socketConnection) + ] + validated = usernameValid + } while (!validated) +} + +export default homeSaga diff --git a/frontend/src/containers/UserRepo/actions.js b/frontend/src/containers/UserRepo/actions.js new file mode 100644 index 00000000..dc06035b --- /dev/null +++ b/frontend/src/containers/UserRepo/actions.js @@ -0,0 +1,8 @@ +export const SET_USERNAME = 'homePage/SET_USERNAME' + +export const setUsername = (userName, displayName, index) => ({ + type: SET_USERNAME, + userName, + index, + displayName +}) diff --git a/frontend/src/containers/UserRepo/reducer.js b/frontend/src/containers/UserRepo/reducer.js new file mode 100644 index 00000000..82960a58 --- /dev/null +++ b/frontend/src/containers/UserRepo/reducer.js @@ -0,0 +1,18 @@ +import { SET_USERNAME } from './actions' +import { fromJS } from 'immutable' +const initialState = fromJS({ + username: '', + displayName: '', + id: null +}) + +export default (state = initialState, action) => { + switch (action.type) { + case SET_USERNAME: + return state.set('username', action.userName) + .set('displayName', action.displayName) + .set('id', action.index) + default: + return state + } +} diff --git a/frontend/src/global-styles.css b/frontend/src/global-styles.css new file mode 100644 index 00000000..4b644b66 --- /dev/null +++ b/frontend/src/global-styles.css @@ -0,0 +1,10 @@ +* { box-sizing: border-box; } + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + line-height: 1.5; + color: #111; + background-color: #fff; +}
\ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 00000000..3edce222 --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,26 @@ +import 'babel-polyfill' + +import React from 'react' +import ReactDOM from 'react-dom' +import { Router, Route } from 'react-router' +import { Provider } from 'react-redux' +import configureStore from './store' + +const initialState = {} +const { store, history } = configureStore(initialState) + +import './global-styles.css' +import HomePage from './containers/HomePage' +import Error404 from './components/errors/Error404' + +ReactDOM.render( + <Provider store={store}> + <Router history={history}> + <div className="app"> + <Route path="/" component={HomePage}/> + <Route path="*" component={Error404}/> + </div> + </Router> + </Provider>, + document.getElementById('root') +); diff --git a/frontend/src/layouts/.gitkeep b/frontend/src/layouts/.gitkeep new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/frontend/src/layouts/.gitkeep diff --git a/frontend/src/reducers.js b/frontend/src/reducers.js new file mode 100644 index 00000000..d9db899b --- /dev/null +++ b/frontend/src/reducers.js @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux' +import { routerReducer } from 'react-router-redux' + +import gamesReducer from './containers/GameBrowser/reducer' +import userReducer from './containers/UserRepo/reducer' + +export default function createReducer() { + return combineReducers({ + games: gamesReducer, + routing: routerReducer, + user: userReducer, + }) +} diff --git a/frontend/src/sagas.js b/frontend/src/sagas.js new file mode 100644 index 00000000..58ef73ee --- /dev/null +++ b/frontend/src/sagas.js @@ -0,0 +1,25 @@ +import { fork, call } from 'redux-saga/effects' + +import createWsConnection from './utils/createWebSocketConnection' + +import errorSaga from './containers/App/saga' +import homeSaga from './containers/HomePage/saga' + +function* wsAwareSagas() { + let wsConnection + try { + wsConnection = yield call(createWsConnection) + } catch (error) { + console.error('Could not connect to socket') + return + } + + yield fork(errorSaga, wsConnection) + yield fork(homeSaga, wsConnection) +} + +export default function* rootSaga() { + yield [ + call(wsAwareSagas) + ] +} diff --git a/frontend/src/store.js b/frontend/src/store.js new file mode 100644 index 00000000..e9ac401e --- /dev/null +++ b/frontend/src/store.js @@ -0,0 +1,40 @@ +import { createStore, applyMiddleware, compose } from 'redux' +import { browserHistory} from 'react-router' +import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux' + +import createReducer from './reducers' +import createSagaMiddleware from 'redux-saga' +import rootSaga from './sagas' + +export default function configureStore(initialState = {}) { + const sagaMiddleware = createSagaMiddleware() + const history = browserHistory + + const middlewares = [ + sagaMiddleware, + routerMiddleware(history) + ] + + const enhancers = [ + applyMiddleware(...middlewares) + ] + + const composeEnhancers = + process.env.NODE_ENV !== 'production' && + typeof window === 'object' && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; + + const store = createStore( + createReducer(), + initialState, + composeEnhancers(...enhancers) + ) + + sagaMiddleware.run(rootSaga) + + return { + store, + history: syncHistoryWithStore(history, store) + } +} diff --git a/frontend/src/utils/createWebSocketConnection.js b/frontend/src/utils/createWebSocketConnection.js new file mode 100644 index 00000000..b0924976 --- /dev/null +++ b/frontend/src/utils/createWebSocketConnection.js @@ -0,0 +1,14 @@ +import SockJS from 'sockjs-client' +import Stomp from 'webstomp-client' +const wsURL = 'http://localhost:8080/seven-wonders-websocket' + +const createConnection = (headers = {}) => new Promise((resolve, reject) => { + let socket = Stomp.over(new SockJS(wsURL), { + debug: process.env.NODE_ENV !== "production" + }) + socket.connect(headers, (frame) => { + return resolve({ frame, socket }) + }, reject) +}) + +export default createConnection |