summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/errors/Error404.js8
-rw-r--r--frontend/src/components/modals/username.js40
-rw-r--r--frontend/src/containers/App/actions.js5
-rw-r--r--frontend/src/containers/App/constants.js1
-rw-r--r--frontend/src/containers/App/index.js83
-rw-r--r--frontend/src/containers/App/saga.js28
-rw-r--r--frontend/src/containers/GameBrowser/actions.js16
-rw-r--r--frontend/src/containers/GameBrowser/constants.js3
-rw-r--r--frontend/src/containers/GameBrowser/index.js31
-rw-r--r--frontend/src/containers/GameBrowser/reducer.js13
-rw-r--r--frontend/src/containers/GameBrowser/saga.js75
-rw-r--r--frontend/src/containers/HomePage/actions.js6
-rw-r--r--frontend/src/containers/HomePage/index.js39
-rw-r--r--frontend/src/containers/HomePage/saga.js57
-rw-r--r--frontend/src/containers/UserRepo/actions.js8
-rw-r--r--frontend/src/containers/UserRepo/reducer.js18
-rw-r--r--frontend/src/global-styles.css10
-rw-r--r--frontend/src/index.js26
-rw-r--r--frontend/src/layouts/.gitkeep0
-rw-r--r--frontend/src/reducers.js13
-rw-r--r--frontend/src/sagas.js25
-rw-r--r--frontend/src/store.js40
-rw-r--r--frontend/src/utils/createWebSocketConnection.js14
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
bgstack15