├── Procfile
├── .gitignore
├── README.md
├── server
├── poker
│ ├── sidePot.js
│ ├── player.js
│ ├── __test__
│ │ ├── deck.test.js
│ │ ├── winner.test.js
│ │ ├── betting.test.js
│ │ ├── seat.test.js
│ │ ├── multiplayer.test.js
│ │ ├── table.test.js
│ │ └── raise.test.js
│ ├── seat.js
│ └── deck.js
├── db
│ └── models
│ │ ├── hand.js
│ │ ├── userHand.js
│ │ ├── group.js
│ │ ├── groupMember.js
│ │ ├── groupInvite.js
│ │ ├── user.js
│ │ ├── index.js
│ │ └── __test__
│ │ └── user.test.js
├── index.html
├── utils
│ └── index.js
├── routes
│ ├── groupInvites.js
│ ├── searchUsers.js
│ ├── index.js
│ ├── hands.js
│ ├── auth.js
│ └── groups.js
├── index.js
└── socket
│ └── index.js
├── client
├── modules
│ ├── utils
│ │ └── theme.js
│ └── components
│ │ ├── index.js
│ │ ├── Text.js
│ │ ├── Modal.js
│ │ ├── Link.js
│ │ ├── Button.js
│ │ ├── Panel.js
│ │ └── UserSearchModal.js
├── app
│ ├── components
│ │ ├── Playground
│ │ │ └── index.js
│ │ ├── NoMatch.jsx
│ │ ├── Game
│ │ │ ├── Pieces
│ │ │ │ ├── Background.jsx
│ │ │ │ ├── ChipStack.jsx
│ │ │ │ ├── Board.jsx
│ │ │ │ ├── Card.jsx
│ │ │ │ └── ChipPile.jsx
│ │ │ ├── Seats
│ │ │ │ ├── Bet.jsx
│ │ │ │ ├── EmptySeat.jsx
│ │ │ │ ├── Hand.jsx
│ │ │ │ ├── ShotClock.jsx
│ │ │ │ ├── SeatedPlayer.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── ChatAndInfo
│ │ │ │ ├── Message.jsx
│ │ │ │ ├── SitOutCheckbox.js
│ │ │ │ ├── Spectators.jsx
│ │ │ │ ├── Chat.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── Actions
│ │ │ │ ├── PotSizeButton.jsx
│ │ │ │ ├── RaiseSlider.jsx
│ │ │ │ ├── ActionButtons.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── TableControls
│ │ │ │ └── index.js
│ │ │ ├── Game.jsx
│ │ │ └── BuyinModal.js
│ │ ├── lobby
│ │ │ └── MainMenu
│ │ │ │ ├── Player.jsx
│ │ │ │ ├── PlayerList.jsx
│ │ │ │ ├── Table.jsx
│ │ │ │ ├── TableList.jsx
│ │ │ │ └── index.jsx
│ │ ├── Lobby
│ │ │ ├── TopNav.js
│ │ │ ├── BottomNav.js
│ │ │ └── index.jsx
│ │ ├── App.jsx
│ │ ├── Groups
│ │ │ ├── CreateGroupModal.jsx
│ │ │ ├── Group.jsx
│ │ │ └── index.js
│ │ ├── Signup.jsx
│ │ ├── Login.jsx
│ │ └── HandHistory
│ │ │ ├── index.js
│ │ │ └── Hand.jsx
│ ├── actions
│ │ ├── ui.js
│ │ ├── lobby.js
│ │ ├── hands.js
│ │ ├── searchUsers.js
│ │ ├── groups.js
│ │ └── user.js
│ ├── reducers
│ │ ├── ui.js
│ │ ├── searchUsers.js
│ │ ├── hands.js
│ │ ├── groups.js
│ │ ├── lobby.js
│ │ └── user.js
│ ├── store
│ │ └── store.js
│ └── index.js
└── scss
│ ├── card.scss
│ ├── main.scss
│ ├── table.scss
│ ├── seats.scss
│ └── small_table.scss
├── .eslintrc.js
├── .flowconfig
├── .gitattributes
├── .vscode
└── launch.json
├── webpack.config.js
└── package.json
/Procfile:
--------------------------------------------------------------------------------
1 | server
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | npm-debug.log
3 | .vscode
4 | .env
5 | /server/db/config
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # poker_playhouse
2 | Realtime multiplayer poker
3 | Node.js, React, Redux, Socket.IO, Express, Sequelize, PostgreSQL
--------------------------------------------------------------------------------
/server/poker/sidePot.js:
--------------------------------------------------------------------------------
1 | class SidePot {
2 | constructor() {
3 | this.amount = 0
4 | this.players = []
5 | }
6 | }
7 |
8 | module.exports = SidePot;
--------------------------------------------------------------------------------
/client/modules/utils/theme.js:
--------------------------------------------------------------------------------
1 | export default {
2 | colors: {
3 | blue: '#2196f3',
4 | green: '#5cb85c',
5 | lightblue: '#5bc0de',
6 | orange: '#f0ad4e',
7 | red: '##d9534f',
8 | },
9 | }
--------------------------------------------------------------------------------
/client/app/components/Playground/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react';
3 |
4 | export default function Playground() {
5 | return (
6 |
7 | hello world
8 |
9 | )
10 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }]
5 | },
6 | "extends": ['plugin:react/recommended']
7 | }
--------------------------------------------------------------------------------
/server/poker/player.js:
--------------------------------------------------------------------------------
1 | class Player {
2 | constructor(socketId, id, name, bankroll) {
3 | this.socketId = socketId,
4 | this.id = id,
5 | this.name = name,
6 | this.bankroll = bankroll
7 | }
8 | }
9 |
10 | module.exports = Player
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ./.gitignore
3 | ./.gitattributes
4 | ./node_modules/
5 |
6 | [include]
7 | ./client/app/components/*.js
8 |
9 | [libs]
10 |
11 | [lints]
12 |
13 | [options]
14 | module.name_mapper='^app/\(.*\)' -> '/client/modules/\1'
15 |
16 | [strict]
17 |
--------------------------------------------------------------------------------
/client/app/components/NoMatch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class NoMatch extends React.Component {
4 | render() {
5 | return (
6 |
7 |
Oops, you must be in the wrong place...
8 |
9 | )
10 | }
11 | }
12 |
13 | export default NoMatch
14 |
--------------------------------------------------------------------------------
/client/modules/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Button } from './Button';
2 | export { default as Link } from './Link';
3 | export { default as Modal } from './Modal';
4 | export { default as Panel } from './Panel';
5 | export { default as Text } from './Text';
6 | export { default as UserSearchModal } from './UserSearchModal';
--------------------------------------------------------------------------------
/server/db/models/hand.js:
--------------------------------------------------------------------------------
1 | module.exports = function (sequelize, DataTypes) {
2 | const Hand = sequelize.define('Hand', {
3 | history: {
4 | type: DataTypes.JSON,
5 | allowNull: false,
6 | },
7 | });
8 |
9 | Hand.associate = models => {
10 | Hand.hasMany(models.UserHand, { foreignKey: 'hand_id' });
11 | };
12 |
13 | return Hand;
14 | };
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/client/app/actions/ui.js:
--------------------------------------------------------------------------------
1 | export const TOGGLE_LEFT_COLUMN = 'TOGGLE_LEFT_COLUMN'
2 | export const TOGGLE_RIGHT_COLUMN = 'TOGGLE_RIGHT_COLUMN'
3 | export const TOGGLE_GRID_VIEW = 'TOGGLE_GRID_VIEW'
4 |
5 | export function toggleLeftColumn() {
6 | return {
7 | type: TOGGLE_LEFT_COLUMN
8 | }
9 | }
10 |
11 | export function toggleRightColumn() {
12 | return {
13 | type: TOGGLE_RIGHT_COLUMN
14 | }
15 | }
16 |
17 | export function toggleGridView() {
18 | return {
19 | type: TOGGLE_GRID_VIEW
20 | }
21 | }
--------------------------------------------------------------------------------
/server/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Poker Playhouse
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/modules/components/Text.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { css } from 'emotion'
4 |
5 | type Props = {
6 | children: React.Node,
7 | small?: boolean,
8 | large?: boolean,
9 | bold?: boolean,
10 | }
11 |
12 | const Text = ({ children, small, large, bold }: Props) => (
13 |
18 | {children}
19 |
20 | )
21 |
22 | export default Text
--------------------------------------------------------------------------------
/client/app/components/Game/Pieces/Background.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { blue } from 'material-ui/styles/colors'
4 |
5 | const styles = {
6 | outer: {
7 | background: blue[700]
8 | }
9 | }
10 | function Background() {
11 | return (
12 |
13 | {[1, 2, 3].map(num => (
14 |
17 | ))}
18 |
19 | )
20 | }
21 |
22 | export default Background
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/Bet.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ChipPile from '../Pieces/ChipPile'
3 |
4 | type Props = {
5 | seat: {
6 | bet: number,
7 | turn: boolean,
8 | lastAction: ?string,
9 | }
10 | }
11 | const Bet = ({ seat }: Props) => (
12 |
13 |
14 |
15 | {seat.lastAction && !seat.turn &&
16 | {seat.lastAction + ' - '}
17 | }
18 | ${seat.bet.toFixed(2)}
19 |
20 |
21 | )
22 |
23 | export default Bet
--------------------------------------------------------------------------------
/client/modules/components/Modal.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { css } from 'emotion'
3 |
4 | import Dialog from 'material-ui/Dialog';
5 |
6 | const container = css`
7 | padding: 24px;
8 | `;
9 |
10 | type Props = {
11 | open: boolean,
12 | children: React.Node,
13 | }
14 | class Modal extends React.Component {
15 | render() {
16 | return (
17 |
22 | )
23 | }
24 | }
25 |
26 | export default Modal;
--------------------------------------------------------------------------------
/client/app/components/lobby/MainMenu/Player.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 |
4 | type Props = {
5 | player: {
6 | name?: ?string,
7 | },
8 | active: boolean,
9 | }
10 | class Player extends React.Component {
11 | render() {
12 | const { player, active } = this.props
13 | const style = {
14 | fontWeight: active ? 'bold' : 'normal'
15 | }
16 |
17 | return (
18 |
19 | {player.name}
20 | {active && (you)}
21 |
22 | )
23 | }
24 | }
25 |
26 | export default Player
--------------------------------------------------------------------------------
/client/app/components/Game/ChatAndInfo/Message.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 |
4 | const styles = {
5 | timeStamp: {
6 | color: '#ccc',
7 | },
8 | }
9 |
10 | type Props = {
11 | message: {
12 | message: string,
13 | timestamp: string,
14 | from: string,
15 | },
16 | }
17 | const Message = ({ message }: Props) => (
18 |
19 | {message.timestamp}
20 | {message.from &&
21 | [{message.from}]
22 | }
23 | : {message.message}
24 |
25 | )
26 |
27 | export default Message
--------------------------------------------------------------------------------
/server/poker/__test__/deck.test.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect
2 | var Deck = require('../deck')
3 |
4 | describe('Deck', () => {
5 | describe('#count()', () => {
6 | it('should initialize with 52 cards', () => {
7 | let deck = new Deck()
8 |
9 | expect(deck.count()).to.be.equal(52)
10 | })
11 | })
12 |
13 | describe('#draw()', () => {
14 | it('should return a card', () => {
15 | let deck = new Deck()
16 | let card = deck.draw()
17 |
18 | expect(card).to.have.property('suit')
19 | expect(card).to.have.property('rank')
20 | })
21 | })
22 | })
--------------------------------------------------------------------------------
/server/db/models/userHand.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | const UserHand = sequelize.define('UserHand', {
3 | id: {
4 | type: DataTypes.INTEGER,
5 | primaryKey: true,
6 | autoIncrement: true,
7 | },
8 | user_id: {
9 | type: DataTypes.INTEGER,
10 | },
11 | hand_id: {
12 | type: DataTypes.INTEGER,
13 | },
14 | });
15 |
16 | UserHand.associate = models => {
17 | UserHand.belongsTo(models.User, { foreignKey: 'user_id' })
18 | UserHand.belongsTo(models.Hand, { foreignKey: 'hand_id' })
19 | };
20 |
21 | return UserHand;
22 | };
--------------------------------------------------------------------------------
/server/db/models/group.js:
--------------------------------------------------------------------------------
1 | module.exports = function (sequelize, DataTypes) {
2 | const Group = sequelize.define('Group', {
3 | creator_id: {
4 | type: DataTypes.INTEGER,
5 | },
6 | name: {
7 | type: DataTypes.STRING,
8 | allowNull: false
9 | },
10 | code: {
11 | type: DataTypes.STRING,
12 | allowNull: false
13 | },
14 | });
15 |
16 | Group.associate = models => {
17 | Group.hasMany(models.GroupMember, { foreignKey: 'group_id' });
18 | Group.hasMany(models.GroupInvite, { foreignKey: 'group_id', as: 'group_invites' });
19 | };;
20 |
21 | return Group;
22 | };
23 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceRoot}\\index.js",
12 | "cwd": "${workspaceRoot}"
13 | },
14 | {
15 | "type": "node",
16 | "request": "attach",
17 | "name": "Attach to Process",
18 | "port": 5858
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/client/app/components/Game/Actions/PotSizeButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Button from 'material-ui/Button'
3 | import { withStyles, createStyleSheet } from 'material-ui/styles'
4 |
5 | const styleSheet = createStyleSheet('Actions', theme => ({
6 | button: {
7 | flex: 1,
8 | margin: '3px',
9 | padding: '0px',
10 | minWidth: '0px',
11 | fontSize: '12px',
12 | fontFamily: 'Montserrat, sans-serif',
13 | }
14 | }))
15 |
16 | const PotSizeButton = ({ text, onRaiseClick, classes }) => (
17 |
24 | )
25 |
26 | export default withStyles(styleSheet)(PotSizeButton)
27 |
28 |
--------------------------------------------------------------------------------
/client/modules/components/Link.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { css } from 'emotion';
4 | import { IndexLink } from 'react-router';
5 |
6 | const linkStyle = css`
7 | text-decoration: none;
8 | color: #aaa;
9 | `;
10 | const activeClass = css`
11 | margin-right: 20px;
12 | font-weight: bold;
13 | color: #000;
14 | `;
15 |
16 | type Props = {
17 | children?: ?React.Node,
18 | to: string,
19 | style: Object,
20 | };
21 |
22 | const MyLink = ({ children, to, style }: Props) => (
23 |
29 | {children}
30 |
31 | );
32 |
33 | MyLink.defaultProps = {
34 | style: {},
35 | }
36 |
37 | export default MyLink;
--------------------------------------------------------------------------------
/server/db/models/groupMember.js:
--------------------------------------------------------------------------------
1 | module.exports = function (sequelize, DataTypes) {
2 | const GroupMember = sequelize.define('GroupMember', {
3 | group_id: {
4 | type: DataTypes.INTEGER,
5 | },
6 | user_id: {
7 | type: DataTypes.INTEGER,
8 | allowNull: false
9 | },
10 | is_admin: {
11 | type: DataTypes.BOOLEAN,
12 | allowNull: false,
13 | defaultValue: false,
14 | },
15 | bankroll: {
16 | type: DataTypes.FLOAT,
17 | allowNull: true,
18 | defaultValue: 0.00,
19 | }
20 | });
21 |
22 | GroupMember.associate = models => {
23 | GroupMember.belongsTo(models.User, { foreignKey: 'user_id' });
24 | GroupMember.belongsTo(models.Group, { foreignKey: 'group_id' });
25 | };
26 |
27 | return GroupMember;
28 | };
29 |
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/EmptySeat.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import Paper from 'material-ui/Paper'
4 | import { blueGrey, cyan } from 'material-ui/styles/colors'
5 |
6 | type Props = {
7 | seatId: string,
8 | onSeatClick: () => void
9 | }
10 | const EmptySeat = ({ seatId, onSeatClick }: Props) => (
11 |
12 |
13 | SIT HERE
14 |
15 |
16 |
17 |
{seatId}
18 |
$0.00
19 |
20 |
21 | )
22 |
23 | export default EmptySeat
24 |
--------------------------------------------------------------------------------
/server/utils/index.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 |
3 | function generateToken(user) {
4 | var u = {
5 | id: user.id,
6 | username: user.username,
7 | location: user.location,
8 | bankroll: user.bankroll,
9 | createdAt: user.createdAt,
10 | updatedAt: user.updatedAt
11 | }
12 |
13 | return token = jwt.sign(u, process.env.JWT_SECRET, {
14 | expiresIn: 60 * 60 * 24 // expires in 24 hours
15 | })
16 | }
17 |
18 | function getCleanUser(user) {
19 | if (!user) return {}
20 |
21 | return {
22 | id: user.id,
23 | username: user.username,
24 | location: user.location,
25 | bankroll: user.bankroll,
26 | createdAt: user.createdAt,
27 | updatedAt: user.updatedAt
28 | }
29 | }
30 |
31 | module.exports = {
32 | generateToken,
33 | getCleanUser
34 | }
--------------------------------------------------------------------------------
/server/routes/groupInvites.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/models')
2 | const jwt = require('jsonwebtoken');
3 |
4 | module.exports = {
5 | fetchInvites(req, res) {
6 | jwt.verify(req.body.accessToken, process.env.JWT_SECRET, async (err, decoded) => {
7 | if (err) {
8 | return res.status(404).json({
9 | error: true,
10 | message: 'Invalid access token'
11 | })
12 | }
13 |
14 | const user = await db.User.findById(decoded.id)
15 | if (!user) throw new Error('User not found');
16 |
17 | const invites = await db.GroupInvite.findAll({
18 | where: { invited_id: user.id },
19 | });
20 |
21 | res.send({ invites });
22 | })
23 | },
24 | // createInvite(req, res) {
25 |
26 | // },
27 | // deleteInvite(req, res) {
28 |
29 | // },
30 | }
--------------------------------------------------------------------------------
/client/app/components/Lobby/TopNav.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { Link } from 'app/components';
4 | import { css } from 'emotion';
5 |
6 | const outer = css`
7 | position: absolute;
8 | top: 0;
9 | width: 100%;
10 | background: #fff;
11 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
12 | `
13 | const inner = css`
14 | display: flex;
15 | padding: 20px;
16 | align-items: center;
17 | `
18 | const linkStyle = { marginRight: '20px' };
19 |
20 | const TopNav = () => (
21 |
22 |
23 | Lobby
24 | Hand history
25 | Groups
26 |
27 |
28 | )
29 |
30 | export default TopNav;
--------------------------------------------------------------------------------
/client/app/reducers/ui.js:
--------------------------------------------------------------------------------
1 | import {
2 | TOGGLE_LEFT_COLUMN,
3 | TOGGLE_RIGHT_COLUMN,
4 | TOGGLE_GRID_VIEW
5 | } from '../actions/ui'
6 |
7 | const initialState = {
8 | leftColumnShowing: true,
9 | rightColumnShowing: true,
10 | gridViewOn: false
11 | }
12 |
13 | function ui(state = initialState, action) {
14 | switch (action.type) {
15 | case TOGGLE_LEFT_COLUMN:
16 | return {
17 | ...state,
18 | leftColumnShowing: !state.leftColumnShowing
19 | }
20 | case TOGGLE_RIGHT_COLUMN:
21 | return {
22 | ...state,
23 | rightColumnShowing: !state.rightColumnShowing
24 | }
25 | case TOGGLE_GRID_VIEW:
26 | return {
27 | ...state,
28 | gridViewOn: !state.gridViewOn
29 | }
30 | default:
31 | return state
32 | }
33 | }
34 |
35 | export default ui
--------------------------------------------------------------------------------
/server/routes/searchUsers.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/models');
2 | const jwt = require('jsonwebtoken');
3 |
4 | module.exports = {
5 | search(req, res) {
6 | jwt.verify(req.query.accessToken, process.env.JWT_SECRET, async (err, decoded) => {
7 | if (err) {
8 | return res.status(404).json({
9 | error: true,
10 | message: 'JWT invalid'
11 | })
12 | }
13 |
14 | const user = await db.User.findById(decoded.id)
15 | if (!user) throw new Error('User not found');
16 |
17 | const query = req.query.searchQuery;
18 | if (!query) res.send({ searchResults: [] })
19 |
20 | const searchResults = await db.User.findAll({
21 | where: { username: { $iLike: `%${query}%` } },
22 | limit: 20,
23 | });
24 |
25 | res.send({ searchResults });
26 | });
27 | },
28 | }
--------------------------------------------------------------------------------
/server/db/models/groupInvite.js:
--------------------------------------------------------------------------------
1 | module.exports = function (sequelize, DataTypes) {
2 | const GroupInvite = sequelize.define('GroupInvite', {
3 | group_id: {
4 | type: DataTypes.INTEGER,
5 | },
6 | inviter_id: {
7 | type: DataTypes.INTEGER,
8 | allowNull: false
9 | },
10 | invited_id: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false
13 | },
14 | accepted_at: {
15 | type: DataTypes.DATE,
16 | allowNull: true,
17 | },
18 | });
19 |
20 | GroupInvite.associate = models => {
21 | GroupInvite.belongsTo(models.User, { foreignKey: 'inviter_id', as: 'inviter' });
22 | GroupInvite.belongsTo(models.User, { foreignKey: 'invited_id', as: 'invited' });
23 | GroupInvite.belongsTo(models.Group, { foreignKey: 'group_id', as: 'group' });
24 | };
25 |
26 | return GroupInvite;
27 | };
28 |
--------------------------------------------------------------------------------
/client/app/components/Game/Pieces/ChipStack.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withStyles, createStyleSheet } from 'material-ui/styles'
3 |
4 | const styleSheet = createStyleSheet('ChipStack', () => ({
5 | chip: {
6 | width: '20px',
7 | height: '3px',
8 | margin: '1px 3px',
9 | }
10 | }))
11 |
12 | type Props = {
13 | number: number,
14 | color: string,
15 | classes: Object,
16 | }
17 | const ChipStack = ({
18 | number,
19 | color,
20 | classes
21 | }: Props) => {
22 | if (number === 0) {
23 | return null
24 | }
25 |
26 | const chips = Array.from(Array(parseInt(number)).keys()).map(num => (
27 |
31 |
32 | ))
33 |
34 | return {chips}
35 | }
36 |
37 | export default withStyles(styleSheet)(ChipStack)
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const authRoute = require('./auth');
4 | const handsRoute = require('./hands');
5 | const groupsRoute = require('./groups');
6 | const searchUsersRoute = require('./searchUsers');
7 |
8 | if (!process.env.JWT_SECRET) {
9 | console.error('ERROR!: Please set JWT_SECRET before running the app. \n run: export JWT_SECRET=...')
10 | process.exit();
11 | }
12 |
13 | router.post('/signup', authRoute.signup);
14 | router.post('/login', authRoute.login);
15 | router.post('/verify_jwt', authRoute.verifyToken);
16 |
17 | router.post('/hand-history/:page', handsRoute.handHistory);
18 |
19 | router.get('/groups', groupsRoute.fetchGroups);
20 | router.post('/groups', groupsRoute.createGroup);
21 | router.delete('/groups/:groupId', groupsRoute.deleteGroup);
22 |
23 | router.get('/users/search', searchUsersRoute.search);
24 |
25 | module.exports = router;
--------------------------------------------------------------------------------
/server/db/models/user.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | const User = sequelize.define('User', {
3 | username: {
4 | type: DataTypes.STRING,
5 | unique: true,
6 | allowNull: false
7 | },
8 | password: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | location: {
13 | type: DataTypes.STRING,
14 | allowNull: true
15 | },
16 | bankroll: {
17 | type: DataTypes.FLOAT,
18 | allowNull: true,
19 | defaultValue: 100.00
20 | }
21 | });
22 |
23 | User.associate = models => {
24 | User.hasMany(models.UserHand, { foreignKey: 'user_id' });
25 | User.hasMany(models.GroupMember, { foreignKey: 'user_id' });
26 | User.hasMany(models.GroupInvite, { foreignKey: 'inviter_id', as: 'sent_invites' })
27 | User.hasMany(models.GroupInvite, { foreignKey: 'invited_id', as: 'received_invites' })
28 | };
29 |
30 | return User;
31 | };
--------------------------------------------------------------------------------
/client/app/reducers/searchUsers.js:
--------------------------------------------------------------------------------
1 | import {
2 | SEARCH_USERS_REQUEST,
3 | SEARCH_USERS_SUCCESS,
4 | SEARCH_USERS_FAILURE,
5 | } from '../actions/searchUsers';
6 |
7 | const initialState = {
8 | isFetching: false,
9 | searchResults: [],
10 | errorMessage: '',
11 | }
12 |
13 | function searchUsers(state = initialState, action) {
14 | switch (action.type) {
15 | case SEARCH_USERS_REQUEST:
16 | return {
17 | ...state,
18 | isFetching: true,
19 | errorMessage: '',
20 | }
21 | case SEARCH_USERS_SUCCESS:
22 | return {
23 | searchResults: action.searchResults,
24 | isFetching: false,
25 | errorMessage: '',
26 | }
27 | case SEARCH_USERS_FAILURE:
28 | return {
29 | ...state,
30 | isFetching: false,
31 | errorMessage: action.message.response.data.message,
32 | }
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export default searchUsers
--------------------------------------------------------------------------------
/client/app/components/Lobby/BottomNav.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { css } from 'emotion';
4 |
5 | import { Button } from 'app/components'
6 |
7 | const outer = css`
8 | position: absolute;
9 | bottom: 0;
10 | width: 100%;
11 | background: #2c2e3e;
12 | `
13 | const inner = css`
14 | display: flex;
15 | padding: 20px;
16 | align-items: center;
17 | justify-content: space-between;
18 | color: #eee;
19 | `
20 |
21 | type Props = {
22 | name: string,
23 | bankroll: number,
24 | logout: () => void,
25 | }
26 | const BottomNav = ({ name, bankroll, logout }: Props) => (
27 |
28 |
29 |
30 | Logged in as {name}
31 | Balance: ${bankroll.toFixed(2)}
32 |
33 |
34 |
35 |
36 | )
37 |
38 | export default BottomNav;
--------------------------------------------------------------------------------
/client/app/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux'
2 | import { hashHistory } from 'react-router'
3 | import { routerMiddleware, routerReducer } from 'react-router-redux'
4 | import { createLogger } from 'redux-logger'
5 | import thunkMiddleware from 'redux-thunk'
6 |
7 | import user from '../reducers/user'
8 | import lobby from '../reducers/lobby'
9 | import ui from '../reducers/ui'
10 | import hands from '../reducers/hands'
11 | import groups from '../reducers/groups'
12 | import searchUsers from '../reducers/searchUsers'
13 |
14 | const rootReducer = combineReducers({
15 | user,
16 | lobby,
17 | ui,
18 | hands,
19 | groups,
20 | searchUsers,
21 | routing: routerReducer
22 | })
23 |
24 | const masterMiddleware = applyMiddleware(
25 | thunkMiddleware,
26 | createLogger({ collapsed: true }),
27 | routerMiddleware(hashHistory)
28 | )
29 |
30 | const store = createStore(
31 | rootReducer,
32 | masterMiddleware
33 | )
34 |
35 | export default store
--------------------------------------------------------------------------------
/client/app/reducers/hands.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_HANDS,
3 | REQUEST_HANDS_SUCCESS,
4 | REQUEST_HANDS_FAILURE,
5 | } from '../actions/hands'
6 |
7 | const initialState = {
8 | isFetching: false,
9 | hands: [],
10 | pages: 0,
11 | count: 0,
12 | errorMessage: '',
13 | }
14 |
15 | function hands(state = initialState, action) {
16 | switch (action.type) {
17 | case REQUEST_HANDS:
18 | return {
19 | ...state,
20 | isFetching: true,
21 | errorMessage: '',
22 | }
23 | case REQUEST_HANDS_SUCCESS:
24 | return {
25 | ...state,
26 | isFetching: false,
27 | hands: action.hands,
28 | pages: action.pages,
29 | count: action.count,
30 | errorMessage: '',
31 | }
32 | case REQUEST_HANDS_FAILURE:
33 | return {
34 | ...state,
35 | isFetching: false,
36 | errorMessage: action.message.response.data.message,
37 | }
38 | default:
39 | return state
40 | }
41 | }
42 |
43 | export default hands
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/Hand.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { css } from 'emotion'
4 | import Card from '../Pieces/Card'
5 |
6 | const hand = css`
7 | position: absolute;
8 | text-align: center;
9 | width: 100%;
10 | bottom: 128px;
11 |
12 | .card {
13 | position: absolute;
14 | transition: 0.2s all;
15 | }
16 | .card:first-child {
17 | top: 0px;
18 | left: 38px;
19 | }
20 | .card:last-child {
21 | top: 0px;
22 | right: 38px;
23 | }
24 |
25 | &:hover {
26 | .card:first-child {
27 | top: -40px;
28 | left: 10px;
29 | }
30 | .card:last-child {
31 | top: -40px;
32 | right: 10px;
33 | }
34 | }
35 | `
36 |
37 | type Props = {
38 | seat: {
39 | hand: Array<{
40 | rank: string,
41 | suit: string,
42 | }>,
43 | },
44 | }
45 | const Hand = (props: Props) => (
46 |
47 |
48 |
49 |
50 | );
51 |
52 | export default Hand
--------------------------------------------------------------------------------
/client/app/components/lobby/MainMenu/PlayerList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Player from './Player'
3 |
4 | const styles = {
5 | playerList: {
6 | margin: 0,
7 | padding: 0,
8 | listStyleType: 'none',
9 | },
10 | }
11 |
12 | class PlayerList extends React.Component {
13 | render() {
14 | const { user, players } = this.props
15 |
16 | if (Object.keys(players).length > 0) {
17 | return (
18 |
19 |
20 | {Object.keys(players).map(id => {
21 | let active = user.username === players[id].name ? true : false
22 |
23 | return (
24 |
29 | )
30 | })}
31 |
32 |
33 | )
34 | } else {
35 | return Players
36 | }
37 | }
38 | }
39 |
40 | export default PlayerList
--------------------------------------------------------------------------------
/client/modules/components/Button.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import Button from 'material-ui/Button'
4 |
5 | const buttonStyle = {
6 | fontFamily: 'Montserrat, sans-serif',
7 | color: '#fff',
8 | fontSize: '16px',
9 | padding: '6px 18px',
10 | minWidth: '0',
11 | minHeight: '0',
12 | }
13 |
14 | type Props = {
15 | onClick: () => void,
16 | className: any,
17 | disabled: boolean,
18 | children: React.Node,
19 | style?: Object,
20 | flat?: boolean,
21 | secondary?: boolean,
22 | className?: string,
23 | }
24 |
25 | const MyButton = ({ onClick, disabled, children, style, flat, secondary, className }: Props) => {
26 | const xStyle = {...style};
27 | if (flat) {
28 | xStyle.boxShadow = 'none';
29 | }
30 |
31 | return (
32 |
42 | )
43 | }
44 |
45 | export default MyButton
--------------------------------------------------------------------------------
/server/db/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const env = process.env.NODE_ENV || 'development';
6 | const config = require(__dirname + '/../config/config.json')[env];
7 | const db = {};
8 |
9 | let sequelize;
10 | if (config.use_env_variable) {
11 | sequelize = new Sequelize(process.env[config.use_env_variable], config);
12 | } else {
13 | sequelize = new Sequelize(config.database, config.username, config.password, config);
14 | }
15 |
16 | fs
17 | .readdirSync(__dirname)
18 | .filter(file => {
19 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
20 | })
21 | .forEach(file => {
22 | const model = sequelize['import'](path.join(__dirname, file));
23 | db[model.name] = model;
24 | });
25 |
26 | Object.keys(db).forEach(modelName => {
27 | if (db[modelName].associate) {
28 | db[modelName].associate(db);
29 | }
30 | });
31 |
32 | db.sequelize = sequelize;
33 | db.Sequelize = Sequelize;
34 |
35 | module.exports = db;
36 |
--------------------------------------------------------------------------------
/client/scss/card.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | display: inline-block;
3 | font-weight: bold;
4 | // background: #eee;
5 | width: 75px;
6 | height: 105px;
7 | overflow: hidden;
8 | margin: 3px;
9 | vertical-align: top;
10 | box-sizing: border-box;
11 | // box-shadow: $box-shadow;
12 | font-family: 'Oswald', sans-serif;
13 | border-radius: 4px;
14 |
15 | .card-back {
16 | width: 69px;
17 | height: 99px;
18 | margin: 3px;
19 | background-color: #295dc3;
20 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23ffffff' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
21 | }
22 | > div {
23 | display: inline-block;
24 | }
25 | .small-picture {
26 | vertical-align: top;
27 | padding-left: 5px;
28 | float: left;
29 |
30 | div:first-child {
31 | font-size: 1.2em;
32 | }
33 | div:last-child {
34 | margin-top: -15px;
35 | font-size: 1.5em;
36 | }
37 | }
38 | .big-picture {
39 | font-size: 5em;
40 | float: right;
41 | padding-right: 5px;
42 | }
43 | }
--------------------------------------------------------------------------------
/client/app/components/Game/ChatAndInfo/SitOutCheckbox.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react';
3 | import Checkbox from 'material-ui/Checkbox';
4 |
5 | const styles = {
6 | container: {
7 | position: 'absolute',
8 | top: '-42px',
9 | right: '-8px',
10 | display: 'flex',
11 | alignItems: 'center',
12 | }
13 | }
14 |
15 | type Props = {
16 | socket: Object,
17 | table: {
18 | id: string,
19 | },
20 | seat: {
21 | id: string,
22 | sittingOut: boolean,
23 | },
24 | };
25 |
26 | class SitOutCheckbox extends React.Component {
27 | handleClick = () => {
28 | const { socket, table, seat } = this.props
29 | const socketMessage = seat.sittingOut ? 'sitting_in' : 'sitting_out'
30 |
31 | socket.emit(socketMessage, {
32 | tableId: table.id,
33 | seatId: seat.id
34 | })
35 | }
36 |
37 | render () {
38 | const { seat } = this.props
39 | if (!seat) return null
40 |
41 | return (
42 |
43 | Sit out next hand
44 | this.handleClick()} />
45 |
46 | )
47 | }
48 | }
49 |
50 | export default SitOutCheckbox
--------------------------------------------------------------------------------
/client/app/actions/lobby.js:
--------------------------------------------------------------------------------
1 | export const RECEIVE_LOBBY_INFO = 'RECEIVE_LOBBY_INFO'
2 | export const TABLES_UPDATED = 'TABLES_UPDATED'
3 | export const PLAYERS_UPDATED = 'PLAYERS_UPDATED'
4 | export const TABLE_JOINED = 'TABLE_JOINED'
5 | export const TABLE_LEFT = 'TABLE_LEFT'
6 | export const TABLE_UPDATED = 'TABLE_UPDATED'
7 |
8 | export function receiveLobbyInfo(tables, players, socketId) {
9 | return {
10 | type: RECEIVE_LOBBY_INFO,
11 | tables,
12 | players,
13 | socketId
14 | }
15 | }
16 |
17 | export function tablesUpdated(tables) {
18 | return {
19 | type: TABLES_UPDATED,
20 | tables
21 | }
22 | }
23 |
24 | export function playersUpdated(players) {
25 | return {
26 | type: PLAYERS_UPDATED,
27 | players
28 | }
29 | }
30 |
31 | export function tableJoined(tables, tableId) {
32 | return {
33 | type: TABLE_JOINED,
34 | tables,
35 | tableId
36 | }
37 | }
38 |
39 | export function tableLeft(tables, tableId) {
40 | return {
41 | type: TABLE_LEFT,
42 | tables,
43 | tableId
44 | }
45 | }
46 |
47 | export function tableUpdated(table, message, from) {
48 | return {
49 | type: TABLE_UPDATED,
50 | table,
51 | message,
52 | from
53 | }
54 | }
--------------------------------------------------------------------------------
/client/app/components/Game/Pieces/Board.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import Card from './Card'
4 | import ChipPile from './ChipPile'
5 |
6 | type Props = {
7 | table: {
8 | board: Array<{
9 | rank: string,
10 | suit: string,
11 | }>,
12 | mainPot: number,
13 | pot: number,
14 | },
15 | }
16 | class Board extends React.Component {
17 | render() {
18 | let renderedCards = this.props.table.board.slice(0)
19 | let { table } = this.props
20 |
21 | while (renderedCards.length < 5) {
22 | renderedCards.push({
23 | rank: '0',
24 | suit: '0'
25 | })
26 | }
27 |
28 | return (
29 |
30 | {table.mainPot > 0 &&
31 |
32 |
33 |
Main pot: ${table.mainPot.toFixed(2)}
34 |
35 | }
36 |
37 | {renderedCards.map((card, index) =>
38 |
39 | )}
40 |
41 |
42 | Total pot: ${table.pot.toFixed(2)}
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | export default Board
50 |
--------------------------------------------------------------------------------
/client/app/components/Game/ChatAndInfo/Spectators.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import Player from '../../Lobby/MainMenu/Player'
4 |
5 | const styles = {
6 | playerList: {
7 | margin: 0,
8 | padding: 0,
9 | listStyleType: 'none',
10 | },
11 | }
12 |
13 | type Props = {
14 | user: {
15 | username: string,
16 | },
17 | table: {
18 | players: Array<{
19 | name?: ?string,
20 | socketId: string,
21 | }>,
22 | },
23 | }
24 | class Spectators extends React.Component {
25 | render() {
26 | const { user, table } = this.props
27 |
28 | if (table.players.length > 0) {
29 | return (
30 |
31 |
32 | {table.players.map(player => {
33 | let active = user.username === player.name ? true : false
34 |
35 | return (
36 |
41 | )
42 | })}
43 |
44 |
45 | )
46 | } else {
47 | return
48 | }
49 | }
50 | }
51 |
52 | export default Spectators
53 |
--------------------------------------------------------------------------------
/client/modules/components/Panel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { css } from 'emotion'
4 | import cx from 'classnames';
5 |
6 | const container = css`
7 | border: 1px solid #eee;
8 | background: #fff;
9 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
10 | border-radius: 3px;
11 | `
12 | const darkStyle = css`
13 | background: #eee;
14 | `
15 | const headerStyle = css`
16 | padding: 16px 24px;
17 | border-bottom: 1px solid #eee;
18 | `
19 | const darkHeader = css`
20 | padding: 16px 24px;
21 | border-bottom: 1px solid #ddd;
22 | `
23 | const body = css`
24 | padding: 16px 24px;
25 | `
26 |
27 | type Props = {
28 | dark?: true,
29 | header?: ?React.Node,
30 | children: React.Node,
31 | className?: string,
32 | }
33 |
34 | const Panel = ({ dark, header, children, className }: Props) => {
35 | return (
36 |
37 | {header && (
38 |
39 | {typeof header === 'string'
40 | ?
{header}
41 | : header}
42 |
43 | )}
44 |
45 | {children}
46 |
47 |
48 | )
49 | }
50 |
51 | export default Panel
--------------------------------------------------------------------------------
/client/app/components/App.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { hashHistory } from 'react-router'
4 | import { connect } from 'react-redux'
5 | import { css } from 'emotion'
6 |
7 | import { tokenLogin } from '../actions/user'
8 |
9 | import io from 'socket.io-client'
10 | const socket = io('/')
11 |
12 | const container = css`
13 | height: 100vh;
14 | background: #fafafa;
15 | `
16 |
17 | type Props = {
18 | token: string,
19 | tokenLogin: (token: string) => void,
20 | children?: React.Element,
21 | }
22 | class App extends React.Component {
23 | componentDidMount() {
24 | const { token, tokenLogin } = this.props
25 |
26 | if (token) {
27 | tokenLogin(token)
28 | } else {
29 | if (hashHistory.getCurrentLocation().pathname !== '/login') {
30 | hashHistory.push('/login')
31 | }
32 | }
33 | }
34 |
35 | render() {
36 | const { children } = this.props;
37 | if (!children) return null;
38 |
39 | return {React.cloneElement(children, { socket })}
;
40 | }
41 | }
42 |
43 | function mapStateToProps(state) {
44 | return { token: state.user.token }
45 | }
46 |
47 | const mapDispatchToProps = ({ tokenLogin })
48 |
49 | export default connect(
50 | mapStateToProps,
51 | mapDispatchToProps
52 | )(App)
53 |
--------------------------------------------------------------------------------
/client/app/actions/hands.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
3 |
4 | export const REQUEST_HANDS = 'REQUEST_HANDS'
5 | export const REQUEST_HANDS_SUCCESS = 'REQUEST_HANDS_SUCCESS'
6 | export const REQUEST_HANDS_FAILURE = 'REQUEST_HANDS_FAILURE'
7 |
8 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api'
9 |
10 | export function requestHands() {
11 | return {
12 | type: REQUEST_HANDS,
13 | }
14 | }
15 |
16 | export function requestHandsSuccess(hands, count, pages) {
17 | return {
18 | type: REQUEST_HANDS_SUCCESS,
19 | hands,
20 | count,
21 | pages,
22 | }
23 | }
24 |
25 | export function requestHandsFailure(message) {
26 | return {
27 | type: REQUEST_HANDS_FAILURE,
28 | message,
29 | }
30 | }
31 |
32 | export function fetchHandHistory(token, page) {
33 | return function(dispatch) {
34 | dispatch(requestHands())
35 |
36 | return axios.post(`${ROOT_URL}/hand-history/${page}`, { token })
37 | .then(res => {
38 | const { hands, count, pages } = res.data
39 | dispatch(requestHandsSuccess(hands, count, pages))
40 | })
41 | .catch(err => {
42 | console.log(err)
43 | dispatch(requestHandsFailure(err))
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/app/actions/searchUsers.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
3 |
4 | export const SEARCH_USERS_REQUEST = 'SEARCH_USERS_REQUEST';
5 | export const SEARCH_USERS_SUCCESS = 'SEARCH_USERS_SUCCESS';
6 | export const SEARCH_USERS_FAILURE = 'SEARCH_USERS_FAILURE';
7 |
8 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api'
9 |
10 | export function searchUsersRequest() {
11 | return {
12 | type: SEARCH_USERS_REQUEST,
13 | }
14 | }
15 |
16 | export function searchUsersSuccess(searchResults) {
17 | return {
18 | type: SEARCH_USERS_SUCCESS,
19 | searchResults,
20 | }
21 | }
22 |
23 | export function searchUsersFailure(message) {
24 | return {
25 | type: SEARCH_USERS_FAILURE,
26 | message,
27 | }
28 | }
29 |
30 | export function searchUsers(token, searchQuery) {
31 | return function (dispatch) {
32 | dispatch(searchUsersRequest())
33 |
34 | return axios.get(`${ROOT_URL}/users/search?accessToken=${token}&searchQuery=${searchQuery}`)
35 | .then(res => {
36 | const { searchResults } = res.data;
37 | dispatch(searchUsersSuccess(searchResults))
38 | })
39 | .catch(err => {
40 | console.log(err)
41 | dispatch(searchUsersFailure(err))
42 | })
43 | }
44 | }
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const http = require('http')
3 | const bodyParser = require('body-parser')
4 | const socketIo = require('socket.io')
5 | const webpack = require('webpack')
6 | const path = require('path')
7 | const webpackConfig = require('../webpack.config.js')
8 | const routes = require('./routes/index')
9 | const db = require('./db/models')
10 | const gameSocket = require('./socket');
11 |
12 | const app = express()
13 | const server = http.createServer(app)
14 | const io = socketIo(server)
15 | const compiler = webpack(webpackConfig)
16 |
17 | app.use(express.static(__dirname))
18 | app.use(bodyParser.json())
19 | app.use(bodyParser.urlencoded({ extended: true }))
20 |
21 | if (process.env.NODE_ENV === 'development') {
22 | const webpackDevMiddleware = require('webpack-dev-middleware')
23 | const webpackHotMiddleware = require('webpack-hot-middleware')
24 | app.use(webpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath }))
25 | app.use(webpackHotMiddleware(compiler))
26 | } else if (process.env.NODE_ENV === 'production') {
27 | app.get('/', (res) => {
28 | res.sendFile(path.join(__dirname + 'index.html'))
29 | })
30 | }
31 |
32 | app.use('/api', routes);
33 | io.on('connection', socket => gameSocket.init(socket, io));
34 | db.sequelize.sync();
35 |
36 | server.listen(process.env.PORT)
--------------------------------------------------------------------------------
/client/app/components/lobby/MainMenu/Table.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { css } from 'emotion'
4 | import { Button } from 'app/components';
5 |
6 | const activeStyle = css`font-weight: bold;`;
7 | const disabledStyle = css`color: #aaa;`;
8 |
9 | type Props = {
10 | table: {
11 | id: number,
12 | name: string,
13 | minBet: number,
14 | players: Array<{}>,
15 | maxPlayers: number,
16 | },
17 | onTableClick: (tableId: number) => void,
18 | active: boolean,
19 | hasTableOpen: boolean,
20 | }
21 | class Table extends React.Component {
22 | render() {
23 | const { table, onTableClick, active, hasTableOpen } = this.props
24 | let className;
25 | if (active) {
26 | className = activeStyle;
27 | } else {
28 | className = hasTableOpen ? disabledStyle : '';
29 | }
30 | return (
31 |
32 | | {table.name} |
33 | ${table.minBet.toFixed(2)} / ${(table.minBet * 2).toFixed(2)} |
34 | {table.players.length} / {table.maxPlayers} |
35 |
36 |
42 | |
43 |
44 | )
45 | }
46 | }
47 |
48 | export default Table
--------------------------------------------------------------------------------
/server/poker/seat.js:
--------------------------------------------------------------------------------
1 | class Seat {
2 | constructor(id, player, buyin, stack) {
3 | this.id = id
4 | this.player = player
5 | this.buyin = buyin
6 | this.stack = stack
7 | this.hand = []
8 | this.bet = 0
9 | this.turn = false
10 | this.checked = true
11 | this.folded = true
12 | this.lastAction = null
13 | this.sittingOut = false
14 | }
15 | fold() {
16 | this.bet = 0
17 | this.folded = true
18 | this.lastAction = 'FOLD'
19 | this.turn = false
20 | }
21 | check() {
22 | this.checked = true
23 | this.lastAction = 'CHECK'
24 | this.turn = false
25 | }
26 | raise(amount) {
27 | const reRaiseAmount = amount - this.bet
28 | if (reRaiseAmount > this.stack) { return }
29 |
30 | this.bet = amount
31 | this.stack -= reRaiseAmount
32 | this.turn = false
33 | this.lastAction = 'RAISE'
34 | }
35 | placeBlind(amount) {
36 | this.bet = amount
37 | this.stack -= amount
38 | }
39 | callRaise(amount) {
40 | let amountCalled = amount - this.bet
41 | if (amountCalled >= this.stack) {
42 | amountCalled = this.stack
43 | }
44 |
45 | this.bet += amountCalled
46 | this.stack -= amountCalled
47 | this.turn = false
48 | this.lastAction = 'CALL'
49 | }
50 | winHand(amount) {
51 | this.bet = 0
52 | this.stack += amount
53 | this.turn = false
54 | this.lastAction = 'WINNER'
55 | }
56 | }
57 |
58 | module.exports = Seat
--------------------------------------------------------------------------------
/client/app/components/Game/ChatAndInfo/Chat.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Message from './Message'
3 | import { blueGrey } from 'material-ui/styles/colors'
4 | import { withStyles, createStyleSheet } from 'material-ui/styles'
5 | import Input from 'material-ui/Input/Input'
6 |
7 | const styleSheet = createStyleSheet('GameChat', theme => ({
8 | container: {
9 | height: '100%',
10 | },
11 | messages: {
12 | fontFamily: 'Montserrat, sans-serif',
13 | height: '144px',
14 | padding: '3px',
15 | color: '#fff',
16 | background: blueGrey[900],
17 | overflowY: 'auto',
18 | },
19 | input: {
20 | fontFamily: 'Montserrat, sans-serif',
21 | display: 'block',
22 | padding: '0px 3px',
23 | margin: '4px 0px',
24 | background: blueGrey[900],
25 | color: '#fff',
26 | overflow: 'hidden',
27 | }
28 | }))
29 |
30 | const Chat = ({
31 | user,
32 | table,
33 | messages,
34 | onTableMessage,
35 | classes
36 | }) => (
37 |
38 |
39 | {messages.map((message, index) =>
40 |
41 | )}
42 |
43 |
44 |
50 |
51 | )
52 |
53 | export default withStyles(styleSheet)(Chat)
--------------------------------------------------------------------------------
/client/app/components/Groups/CreateGroupModal.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { css } from 'emotion'
3 |
4 | import Input from 'material-ui/Input';
5 | import { Modal, Button } from 'app/components';
6 |
7 | const buttons = css`
8 | text-align: right;
9 | `;
10 | const inputStyle = {
11 | width: '100%',
12 | fontSize: '20px',
13 | fontFamily: 'Montserrat, sans-serif',
14 | marginBottom: '32px',
15 | };
16 |
17 | type Props = {
18 | open: boolean,
19 | createGroup: (groupAttrs: Object) => void,
20 | closeModal: () => void,
21 | };
22 | class CreateGroupModal extends React.Component {
23 | handleSubmit = () => {
24 | this.props.createGroup({ name: this.groupName.value });
25 | };
26 |
27 | render() {
28 | return (
29 |
30 | What's the name of your group?
31 | { this.groupName = ref }}
33 | type="text"
34 | style={inputStyle}
35 | />
36 |
37 |
44 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default CreateGroupModal
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/ShotClock.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { css } from 'emotion'
4 |
5 | const shotClock = css`
6 | position: absolute;
7 | width: 100%;
8 | bottom: -3px;
9 |
10 | div {
11 | float: right;
12 | height: 3px;
13 | transition: 0.2s all;
14 | }
15 | `
16 |
17 | type Props = {
18 | seconds: number
19 | }
20 | type State = {
21 | seconds: number
22 | }
23 | class ShotClock extends React.Component {
24 | interval: any
25 |
26 | constructor(props: Props) {
27 | super(props)
28 |
29 | this.state = {
30 | seconds: props.seconds
31 | }
32 | }
33 |
34 | componentDidMount() {
35 | this.interval = setInterval(() => {
36 | if (this.state.seconds === 0) {
37 | // Todo: this should fire some event that ends the player's turn
38 | } else {
39 | this.setState({ seconds: this.state.seconds - 1 })
40 | }
41 | }, 1000)
42 | }
43 |
44 | componentWillUnmount() {
45 | clearInterval(this.interval)
46 | }
47 |
48 | render() {
49 | const { seconds } = this.state;
50 | if (seconds === 0) return null
51 |
52 | let background = '#ffeb3b'
53 | if (seconds <= 5) {
54 | background = '#ff5722';
55 | } else if (seconds <= 10) {
56 | background = '#ff9800';
57 | }
58 |
59 | return (
60 |
63 | );
64 | }
65 | }
66 |
67 | export default ShotClock
68 |
--------------------------------------------------------------------------------
/client/app/components/Game/TableControls/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from 'emotion'
3 |
4 | import Button from 'material-ui/Button'
5 | import Icon from 'material-ui/Icon'
6 |
7 | const styles = {
8 | container: {
9 | position: 'absolute',
10 | top: '-30px',
11 | left: '250px',
12 | },
13 | button: {
14 | minWidth: '10px',
15 | minHeight: '10px',
16 | padding: '2px',
17 | },
18 | }
19 |
20 | type Props = {
21 | onLeaveClick: () => void,
22 | onStandClick: () => void,
23 | onRotateClockwise: () => void,
24 | onRotateCounterClockwise: () => void,
25 | }
26 | function TableControls(props: Props) {
27 | const {
28 | onLeaveClick,
29 | onStandClick,
30 | onRotateClockwise,
31 | onRotateCounterClockwise,
32 | } = props
33 |
34 | return (
35 |
36 |
42 |
48 |
55 |
62 |
63 | )
64 | }
65 |
66 | export default TableControls
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/SeatedPlayer.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import Hand from './Hand'
4 | import ShotClock from './ShotClock'
5 | import Bet from './Bet'
6 | import Paper from 'material-ui/Paper'
7 | import { blueGrey, cyan } from 'material-ui/styles/colors'
8 |
9 | type Props = {
10 | user: {
11 | username: string,
12 | },
13 | seat: {
14 | id: number,
15 | player: {
16 | name: string,
17 | },
18 | hand: Array<{
19 | rank: string,
20 | suit: string,
21 | }>,
22 | bet: number,
23 | stack: number,
24 | },
25 | isButton: boolean,
26 | }
27 | class SeatedPlayer extends React.Component {
28 | render() {
29 | const { user, seat, isButton } = this.props
30 |
31 | return (
32 |
33 | {seat.bet > 0 &&
}
34 |
35 | {seat.hand.length > 0 &&
36 |
37 | }
38 |
39 |
40 |
41 | ${seat.stack.toFixed(2)}
42 |
43 |
44 |
45 |
46 | {seat.id}
47 |
48 |
49 | {seat.player.name}
50 | {user.username === seat.player.name ? ' (me)' : ''}
51 | {isButton && B}
52 |
53 |
54 |
55 | {seat.turn && }
56 |
57 |
58 | )
59 | }
60 | }
61 |
62 | export default SeatedPlayer
63 |
--------------------------------------------------------------------------------
/server/db/models/__test__/user.test.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | const Sequelize = require('sequelize');
3 |
4 | describe('User model', () => {
5 | let db;
6 | let User;
7 | let createdUser;
8 |
9 | before(async () => {
10 | db = new Sequelize('postgresql://localhost/poker_friends_test', { logging: false });
11 | User = db.define('user', {
12 | username: Sequelize.STRING,
13 | password: Sequelize.STRING,
14 | location: Sequelize.STRING,
15 | bankroll: Sequelize.FLOAT,
16 | });
17 | })
18 |
19 | beforeEach(async () => {
20 | await User.sync();
21 | createdUser = await User.create({
22 | username: 'Byron',
23 | bankroll: 100,
24 | });
25 | })
26 |
27 | afterEach(async () => {
28 | await User.drop();
29 | })
30 |
31 | describe('finding a user by id', () => {
32 | it('should return the user', async () => {
33 | const user = await User.findById(createdUser.id)
34 | expect(user.username).to.equal('Byron')
35 | expect(user.bankroll).to.equal(100)
36 | })
37 | })
38 |
39 | describe('finding a user by name', () => {
40 | it('should return the user', async () => {
41 | const user = await User.findOne({ where: { username: createdUser.username } })
42 | expect(user.username).to.equal('Byron');
43 | expect(user.bankroll).to.equal(100);
44 | });
45 | });
46 |
47 | describe('updating the user', () => {
48 | it('should update the user', async () => {
49 | await User.update(
50 | { bankroll: 110 },
51 | { where: { id: createdUser.id }}
52 | )
53 | const user = await User.findById(createdUser.id)
54 | expect(user.username).to.equal('Byron')
55 | expect(user.bankroll).to.equal(110)
56 | });
57 | });
58 | });
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var path = require('path')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | module.exports = {
6 | entry: ['./client/app/index.js', './client/scss/main.scss', 'webpack-hot-middleware/client', 'webpack/hot/dev-server'],
7 | output: {
8 | path: path.resolve(__dirname, 'server'),
9 | publicPath: '/',
10 | filename: 'bundle.js'
11 | },
12 | resolve: {
13 | extensions: ['.js', '.jsx'],
14 | modules: [
15 | path.resolve('./client/modules'),
16 | path.resolve('./node_modules'),
17 | ],
18 | alias: {
19 | 'app': path.resolve('./client/modules'),
20 | },
21 | },
22 | devtool: 'source-maps',
23 | module: {
24 | rules: [
25 | {
26 | test: /\.jsx?$/,
27 | exclude: /node_modules/,
28 | loaders: [
29 | 'react-hot-loader',
30 | 'babel-loader?presets[]=es2015,presets[]=react,presets[]=stage-0,plugins[]=transform-class-properties'
31 | ]
32 | },
33 | { // regular css files
34 | test: /\.css$/,
35 | loader: ExtractTextPlugin.extract({
36 | use: 'css-loader?importLoaders=1',
37 | }),
38 | },
39 | { // sass / scss loader for webpack
40 | test: /\.(sass|scss)$/,
41 | loader: ExtractTextPlugin.extract(['css-loader', 'sass-loader'])
42 | },
43 | {
44 | test: /\.(jpe?g|png|gif|svg)$/i,
45 | loaders: [
46 | 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]',
47 | 'image-webpack-loader?bypassOnDebug&optimizationLevel=7&interlaced=false'
48 | ]
49 | }
50 | ]
51 | },
52 | plugins: [
53 | new ExtractTextPlugin({
54 | filename: 'dist/[name].bundle.css',
55 | allChunks: true,
56 | }),
57 | new webpack.HotModuleReplacementPlugin(),
58 | new webpack.NoEmitOnErrorsPlugin()
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/client/app/components/lobby/MainMenu/TableList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from 'emotion';
3 |
4 | import Table from './Table'
5 | import { Panel } from 'app/components';
6 |
7 | const tableList = css`
8 | text-align: center;
9 | font-size: 16px;
10 | border-spacing: 0 5px;
11 | `
12 | const tableHeader = css`
13 | padding-bottom: 10px;
14 | font-weight: 600;
15 | font-size: 20px;
16 | `
17 |
18 | type Props = {
19 | tables: {
20 | [id: string]: Object,
21 | },
22 | openTables: Array<{}>,
23 | onTableClick: () => void,
24 | hasTableOpen: boolean,
25 | }
26 |
27 | class TableList extends React.Component {
28 | render() {
29 | const { tables, openTables, onTableClick, hasTableOpen } = this.props
30 | if (Object.keys(tables).length > 0) {
31 | return (
32 |
33 |
34 |
35 |
36 | | Name |
37 | Stakes |
38 | Players |
39 | |
40 |
41 |
42 |
43 | {Object.keys(tables).map((id) => {
44 | const active = openTables.indexOf(id.toString()) !== -1 ? true : false
45 | return (
46 |
53 | )
54 | })}
55 |
56 |
57 |
58 | )
59 | } else {
60 | return Loading...
61 | }
62 | }
63 | }
64 |
65 | export default TableList
--------------------------------------------------------------------------------
/server/routes/hands.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/models');
2 | const jwt = require('jsonwebtoken');
3 |
4 | module.exports = {
5 | handHistory(req, res) {
6 | jwt.verify(req.body.token, process.env.JWT_SECRET, (err, decoded) => {
7 | if (err) {
8 | return res.status(404).json({
9 | error: true,
10 | message: 'JWT invalid'
11 | })
12 | }
13 |
14 | db.User
15 | .find({ where: { id: decoded.id } })
16 | .then(user => {
17 | if (!user) {
18 | return res.status(404).json({
19 | error: true,
20 | message: 'JWT invalid'
21 | })
22 | }
23 |
24 | const limit = 20;
25 | let offset = 0;
26 |
27 | db.Hand.findAndCountAll({
28 | include: [{
29 | model: db.UserHand,
30 | where: { user_id: user.id },
31 | }]
32 | })
33 | .then(data => {
34 | const page = req.params.page;
35 | const pages = Math.ceil(data.count / limit);
36 | offset = limit * (page - 1);
37 |
38 | db.Hand.findAll({
39 | include: [{
40 | model: db.UserHand,
41 | where: { user_id: user.id },
42 | }],
43 | limit,
44 | offset,
45 | order: [
46 | ['createdAt', 'desc'],
47 | ],
48 | })
49 | .then(hands => {
50 | res.send({
51 | hands: hands,
52 | count: data.count,
53 | pages: pages,
54 | })
55 | })
56 | .catch(err => {
57 | res.status(500).send(`Internal Server Error: ${err}`)
58 | })
59 | })
60 | .catch(err => {
61 | res.status(500).send(`Internal Server Error: ${err}`)
62 | })
63 | })
64 | })
65 | },
66 | }
--------------------------------------------------------------------------------
/client/app/components/Game/Pieces/Card.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { css } from 'emotion'
4 | import Paper from 'material-ui/Paper'
5 | import {
6 | red,
7 | grey,
8 | blue,
9 | green
10 | } from 'material-ui/styles/colors'
11 |
12 | const suits = {
13 | 'spades': '♠',
14 | 'diamonds': '♦',
15 | 'hearts': '♥',
16 | 'clubs': '♣'
17 | }
18 | const ranks = {
19 | 'ace': 'A',
20 | 'king': 'K',
21 | 'queen': 'Q',
22 | 'jack': 'J',
23 | '10': 'T'
24 | }
25 | const smallCard = css`padding: 4px 2px; margin: 0 4px; display: inline-block; font-size: 32px;`
26 | const smallCardInner = css`display: flex`
27 |
28 | const getSuitColor = suit => {
29 | switch(suit) {
30 | case 'spades':
31 | return { color: grey[900] }
32 | case 'diamonds':
33 | return { color: blue[500] }
34 | case 'hearts':
35 | return { color: red[500] }
36 | case 'clubs':
37 | return { color: green[500] }
38 | default:
39 | return { color: 'purple' }
40 | }
41 | }
42 |
43 | type Props = {
44 | card: {
45 | rank: string,
46 | suit: string,
47 | },
48 | small?: boolean,
49 | }
50 | function Card(props: Props) {
51 | const { card } = props
52 |
53 | if (card.rank === '0') {
54 | return
55 | } else if (card.rank === 'hidden') {
56 | return
57 | } else {
58 | return (
59 |
60 |
61 |
{ranks[card.rank] ? ranks[card.rank] : card.rank}
62 |
{suits[card.suit]}
63 |
64 | {!props.small &&
65 |
66 | {ranks[card.rank] ? ranks[card.rank] : card.rank}
67 |
68 | }
69 |
70 | )
71 | }
72 | }
73 |
74 | export default Card
--------------------------------------------------------------------------------
/server/poker/deck.js:
--------------------------------------------------------------------------------
1 | var Deck = (function () {
2 | function Deck(opt) {
3 | if (opt === void 0) { opt = {}; }
4 | this._opt = {
5 | extend: opt['extend'] || [],
6 | suits: opt['suits'] || ['spades', 'hearts', 'diamonds', 'clubs'],
7 | ranks: opt['ranks'] || ['ace', 'king', 'queen', 'jack', '10', '9', '8', '7', '6', '5', '4', '3', '2'],
8 | multiply: opt['multiply'] || 1
9 | };
10 | if (this._opt.multiply < 1)
11 | this._opt.multiply = 1;
12 | this.shuffle();
13 | }
14 | Deck.prototype.shuffle = function () {
15 | this.cards = [];
16 | for (var i = 0; i < this._opt.multiply; i++) {
17 | for (var _i = 0, _a = this._opt.suits; _i < _a.length; _i++) {
18 | var suit = _a[_i];
19 | for (var _b = 0, _c = this._opt.ranks; _b < _c.length; _b++) {
20 | var rank = _c[_b];
21 | this.inlay({ suit: suit, rank: rank });
22 | }
23 | }
24 | for (var _d = 0, _e = this._opt.extend; _d < _e.length; _d++) {
25 | var card = _e[_d];
26 | if (!card.limit || i < card.limit)
27 | this.inlay({ suit: card.suit, rank: card.rank });
28 | }
29 | }
30 | };
31 | Deck.prototype.inlay = function (card) {
32 | if (card && card.suit && card.rank) {
33 | this.cards.push(card);
34 | return true;
35 | }
36 | else
37 | return false;
38 | };
39 | Deck.prototype.count = function () {
40 | return this.cards.length;
41 | };
42 | Deck.prototype.draw = function () {
43 | var count = this.count();
44 | if (count > 0)
45 | return this.cards.splice(Math.floor(Math.random() * count), 1)[0] || null;
46 | else
47 | return null;
48 | };
49 | return Deck;
50 | }());
51 | var module = module;
52 | if (typeof module == "object" && typeof module.exports == "object")
53 | module.exports = Deck;
--------------------------------------------------------------------------------
/client/app/components/Game/Pieces/ChipPile.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import ChipStack from './ChipStack'
4 | import { withStyles, createStyleSheet } from 'material-ui/styles'
5 | import { red, green, purple, orange } from 'material-ui/styles/colors'
6 |
7 | const styleSheet = createStyleSheet('ChipPile', () => ({
8 | chipPile: {
9 | display: 'flex',
10 | alignItems: 'flex-end',
11 | justifyContent: 'center',
12 | }
13 | }))
14 |
15 | type Props = {
16 | amount: number,
17 | classes: Object,
18 | }
19 | const ChipPile = ({ amount, classes }: Props) => {
20 | // deal with floating point numbers
21 | let cents = amount * 100
22 |
23 | let tenDollarChips,
24 | oneDollarChips,
25 | fiftyCentChips,
26 | tenCentChips,
27 | fiveCentChips,
28 | oneCentChips
29 |
30 | tenDollarChips = Math.floor(cents / 1000)
31 | cents -= tenDollarChips * 1000
32 |
33 | oneDollarChips = Math.floor(cents / 100)
34 | cents -= oneDollarChips * 100
35 |
36 | fiftyCentChips = Math.floor(cents / 50)
37 | cents -= fiftyCentChips * 50
38 |
39 | tenCentChips = Math.floor(cents / 10)
40 | cents -= tenCentChips * 10
41 |
42 | fiveCentChips = Math.floor(cents / 5)
43 | cents -= fiveCentChips * 5
44 |
45 | oneCentChips = Math.round(cents)
46 |
47 | return (
48 |
49 | {tenDollarChips > 0 &&
50 |
51 | }
52 | {oneDollarChips > 0 &&
53 |
54 | }
55 | {fiftyCentChips > 0 &&
56 |
57 | }
58 | {tenCentChips > 0 &&
59 |
60 | }
61 | {fiveCentChips > 0 &&
62 |
63 | }
64 | {oneCentChips > 0 &&
65 |
66 | }
67 |
68 | )
69 | }
70 |
71 | export default withStyles(styleSheet)(ChipPile)
--------------------------------------------------------------------------------
/client/app/reducers/groups.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_GROUPS,
3 | REQUEST_GROUPS_SUCCESS,
4 | REQUEST_GROUPS_FAILURE,
5 | CREATE_GROUP_REQUEST,
6 | CREATE_GROUP_SUCCESS,
7 | CREATE_GROUP_FAILURE,
8 | DELETE_GROUP_REQUEST,
9 | DELETE_GROUP_SUCCESS,
10 | DELETE_GROUP_FAILURE,
11 | } from '../actions/groups'
12 |
13 | const initialState = {
14 | groups: [],
15 | isLoading: false,
16 | errorMessage: '',
17 | }
18 |
19 | function groups(state = initialState, action) {
20 | switch (action.type) {
21 | case REQUEST_GROUPS:
22 | return {
23 | ...state,
24 | isLoading: true,
25 | errorMessage: '',
26 | }
27 | case REQUEST_GROUPS_SUCCESS:
28 | return {
29 | ...state,
30 | groups: action.groups,
31 | isLoading: false,
32 | errorMessage: '',
33 | }
34 | case REQUEST_GROUPS_FAILURE:
35 | return {
36 | ...state,
37 | isLoading: false,
38 | errorMessage: action.message.response.data.message,
39 | }
40 | case CREATE_GROUP_REQUEST:
41 | return {
42 | ...state,
43 | isLoading: true,
44 | errorMessage: '',
45 | }
46 | case CREATE_GROUP_SUCCESS:
47 | return {
48 | ...state,
49 | groups: [...state.groups, action.group],
50 | isLoading: false,
51 | errorMessage: '',
52 | }
53 | case CREATE_GROUP_FAILURE:
54 | return {
55 | ...state,
56 | isLoading: false,
57 | errorMessage: action.message.response.data.message,
58 | }
59 | case DELETE_GROUP_REQUEST:
60 | return {
61 | ...state,
62 | isLoading: true,
63 | errorMessage: '',
64 | }
65 | case DELETE_GROUP_SUCCESS:
66 | return {
67 | ...state,
68 | groups: state.groups.filter(group => group.id !== action.deletedGroupId),
69 | isLoading: false,
70 | errorMessage: '',
71 | }
72 | case DELETE_GROUP_FAILURE:
73 | return {
74 | ...state,
75 | isLoading: false,
76 | errorMessage: action.message.response.data.message,
77 | }
78 | default:
79 | return state
80 | }
81 | }
82 |
83 | export default groups
--------------------------------------------------------------------------------
/client/app/index.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'development') {
2 | require('!style-loader!css-loader!sass-loader!../scss/main.scss')
3 | }
4 | import React from 'react'
5 | import ReactDOM from 'react-dom'
6 | import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles'
7 | import createPalette from 'material-ui/styles/palette'
8 | import { blue, red } from 'material-ui/styles/colors'
9 |
10 | import { Provider } from 'react-redux'
11 | import { Router, Route, IndexRedirect, hashHistory } from 'react-router'
12 | import { syncHistoryWithStore, push } from 'react-router-redux'
13 | import store from './store/store'
14 |
15 | import App from './components/App'
16 | import NoMatch from './components/NoMatch'
17 | import Lobby from './components/Lobby'
18 | import Login from './components/Login'
19 | import Signup from './components/Signup'
20 | import Playground from './components/Playground'
21 | import HandHistory from './components/HandHistory'
22 | import Groups from './components/Groups'
23 |
24 | const history = syncHistoryWithStore(hashHistory, store)
25 |
26 | function requireLogin() {
27 | if (!store.getState().user.isAuthenticated) {
28 | store.dispatch(push('/login'))
29 | return
30 | }
31 | }
32 |
33 | const Root = () => (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | )
50 |
51 | const theme = createMuiTheme({
52 | palette: createPalette({
53 | type: 'light',
54 | primary: blue,
55 | accent: red
56 | }),
57 | })
58 |
59 | ReactDOM.render(
60 |
61 |
62 | ,
63 | document.getElementById('root')
64 | )
65 |
--------------------------------------------------------------------------------
/client/scss/main.scss:
--------------------------------------------------------------------------------
1 | $blue-lighten-5: #e3f2fd;
2 | $blue-lighten-4: #bbdefb;
3 | $blue-lighten-3: #90caf9;
4 | $blue-lighten-2: #64b5f6;
5 | $blue-lighten-1: #42a5f5;
6 | $blue: #2196f3;
7 | $blue-darken-1: #1e88e5;
8 | $blue-darken-2: #1976d2;
9 | $blue-darken-3: #1565c0;
10 | $blue-darken-4: #0d47a1;
11 |
12 | $amber: #ffc107;
13 | $amber-darken-1: #ffb300;
14 | $amber-darken-2: #ffa000;
15 | $amber-darken-3: #ff8f00;
16 | $amber-darken-4: #ff6f00;
17 |
18 | $orange: #ff9800;
19 | $orange-accent-1: #ffd180;
20 | $orange-accent-2: #ffab40;
21 | $orange-accent-3: #ff9100;
22 | $orange-accent-4: #ff6d00;
23 |
24 | $deep-orange-lighten-2: #ff8a65;
25 | $deep-orange-lighten-1: #ff7043;
26 | $deep-orange: #ff5722;
27 | $deep-orange-darken-1: #f4511e;
28 | $deep-orange-darken-2: #e64a19;
29 |
30 | $cyan-lighten-3: #80deea;
31 | $cyan-lighten-2: #4dd0e1;
32 | $cyan-lighten-1: #26c6da;
33 | $cyan: #00bcd4;
34 |
35 | $teal-lighten-3: #80cbc4;
36 | $teal-lighten-2: #4db6ac;
37 | $teal-lighten-1: #26a69a;
38 | $teal: #009688;
39 |
40 | $grey-lighten-5: #fafafa;
41 | $grey-lighten-4: #f5f5f5;
42 | $grey-lighten-3: #eeeeee;
43 |
44 | $blue-grey-lighten-5: #eceff1;
45 | $blue-grey-lighten-4: #cfd8dc;
46 | $blue-grey-lighten-3: #b0bec5;
47 | $blue-grey-lighten-2: #90a4ae;
48 | $blue-grey-lighten-1: #78909c;
49 | $blue-grey: #607d8b;
50 | $blue-grey-darken-1: #546e7a;
51 | $blue-grey-darken-2: #455a64;
52 | $blue-grey-darken-3: #37474f;
53 | $blue-grey-darken-4: #263238;
54 |
55 | // $box-shadow: 0px 1px 2px #222;
56 | $box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12);
57 |
58 | @import './card.scss';
59 | @import './seats.scss';
60 | @import './small_table.scss';
61 | @import './table.scss';
62 | @import url('https://fonts.googleapis.com/css?family=Oswald:400,700');
63 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400');
64 | @import url('https://fonts.googleapis.com/css?family=Lato');
65 | @import url('https://fonts.googleapis.com/css?family=Montserrat:400,600');
66 |
67 | body {
68 | margin: 0px;
69 | overflow: hidden;
70 | font-size: 16px;
71 | font-family: Montserrat, sans-serif;
72 | }
73 | button, input {
74 | font-size: inherit;
75 | font-family: inherit;
76 | }
77 | th {
78 | font-weight: normal;
79 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poker_friends",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server/index.js",
6 | "scripts": {
7 | "start": "node ./server/index",
8 | "test-poker": "mocha ./server/poker/__test__/*.js",
9 | "test-models": "mocha ./server/db/models/__test__/*.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "axios": "^0.16.1",
16 | "babel-core": "^6.21.0",
17 | "babel-loader": "^6.2.10",
18 | "babel-plugin-transform-class-properties": "^6.19.0",
19 | "babel-preset-es2015": "^6.18.0",
20 | "babel-preset-react": "^6.16.0",
21 | "babel-preset-stage-0": "^6.24.1",
22 | "bcrypt-nodejs": "0.0.3",
23 | "body-parser": "^1.16.0",
24 | "classnames": "^2.2.5",
25 | "cookie-parser": "^1.4.3",
26 | "css-loader": "^0.28.0",
27 | "emotion": "^8.0.12",
28 | "express": "^4.14.0",
29 | "extract-text-webpack-plugin": "^2.1.0",
30 | "foreman": "^2.0.0",
31 | "jsonwebtoken": "^7.4.0",
32 | "lodash": "^4.17.5",
33 | "material-ui": "^1.0.0-alpha.20",
34 | "material-ui-icons": "^1.0.0-alpha.19",
35 | "moment": "^2.18.1",
36 | "node-gyp": "^3.5.0",
37 | "node-sass": "^4.5.2",
38 | "path": "^0.12.7",
39 | "pg": "^6.1.5",
40 | "pg-hstore": "^2.3.2",
41 | "pokersolver": "^2.1.2",
42 | "random-id": "0.0.2",
43 | "react": "^15.4.2",
44 | "react-dom": "^15.4.2",
45 | "react-redux": "^5.0.4",
46 | "react-router": "^3.0.2",
47 | "react-router-redux": "^4.0.8",
48 | "redux": "^3.6.0",
49 | "redux-logger": "^3.0.1",
50 | "redux-thunk": "^2.2.0",
51 | "sass-loader": "^6.0.3",
52 | "sequelize": "^3.30.4",
53 | "socket.io": "^1.7.2",
54 | "style-loader": "^0.16.1",
55 | "underscore": "^1.8.3",
56 | "webpack": "^2.2.0-rc.4"
57 | },
58 | "devDependencies": {
59 | "babel-eslint": "^8.0.3",
60 | "chai": "^3.5.0",
61 | "eslint": "^4.13.1",
62 | "eslint-config-airbnb": "^16.1.0",
63 | "eslint-plugin-import": "^2.8.0",
64 | "eslint-plugin-jsx-a11y": "^6.0.3",
65 | "eslint-plugin-react": "^7.5.1",
66 | "flow-bin": "^0.64.0",
67 | "mocha": "^3.3.0",
68 | "react-hot-loader": "^1.3.1",
69 | "webpack-dev-middleware": "^1.10.2",
70 | "webpack-hot-middleware": "^2.18.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/client/scss/table.scss:
--------------------------------------------------------------------------------
1 | .poker-game {
2 | position: relative;
3 | max-width: 1200px;
4 | height: 100vh;
5 | box-sizing: border-box;
6 | z-index: 0;
7 | // font-family: 'Oswald', sans-serif;
8 | overflow: hidden;
9 | margin: 0px auto;
10 |
11 | .table-bg {
12 | width: 900px;
13 | height: 420px;
14 | position: absolute;
15 | left: 115px;
16 | top: 135px;
17 | box-sizing: border-box;
18 | z-index: -1;
19 |
20 | @media screen and (min-height: 1050px) {
21 | top: 205px;
22 | }
23 |
24 | .bg-outer {
25 | // background: $teal;
26 | border: 22px solid #2a2a2a;
27 | height: 100%;
28 | position: absolute;
29 | // box-shadow: 2px 2px 20px #000;
30 |
31 | .bg-inner {
32 | width: calc(100% - 10px);
33 | height: calc(100% - 10px);
34 | border: 5px solid #aaa;
35 | }
36 | }
37 | .bg-outer:first-child,
38 | .bg-outer:last-child {
39 | width: 400px;
40 | top: 0px;
41 | border-radius: 50%;
42 | z-index: -2;
43 |
44 | .bg-inner {
45 | border-radius: 50%;
46 | }
47 | }
48 | .bg-outer:first-child {
49 | left: 0px;
50 | }
51 | .bg-outer:last-child {
52 | right: 0px;
53 | }
54 | .bg-outer:nth-child(2) {
55 | width: 450px;
56 | left: 225px;
57 | border-left: none;
58 | border-right: none;
59 | z-index: -1;
60 |
61 | .bg-inner {
62 | width: 100%;
63 | border-left: none;
64 | border-right: none;
65 | }
66 | }
67 | }
68 | .board {
69 | position: relative;
70 | margin-top: 296px;
71 | width: 405px;
72 | left: 363px;
73 | text-align: center;
74 | color: white;
75 |
76 | @media screen and (min-height: 1050px) {
77 | margin-top: 371px;
78 | }
79 |
80 | .main-pot {
81 | position: absolute;
82 | width: 100%;
83 | bottom: 140px;
84 | }
85 | .card {
86 | margin: 8px 3px;
87 | }
88 | .chip-pile {
89 | position: absolute;
90 | top: -10%;
91 | left: 35%;
92 | }
93 | .card-silhouette {
94 | display: inline-block;
95 | border: 1px dashed #ccc;
96 | width: 75px;
97 | height: 105px;
98 | margin: 8px 3px;
99 | border-radius: 4px;
100 | vertical-align: top;
101 | box-sizing: border-box;
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/models');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcrypt-nodejs');
4 | const utils = require('../utils');
5 |
6 | module.exports = {
7 | signup(req, res) {
8 | const { body } = req;
9 | const hash = bcrypt.hashSync(body.password.trim())
10 |
11 | db.User
12 | .find({ where: { username: body.username } })
13 | .then(user => {
14 | if (!user) {
15 | db.User.create({
16 | username: body.username,
17 | password: hash
18 | })
19 | .then(user => {
20 | res.send({
21 | user: utils.getCleanUser(user),
22 | token: utils.generateToken(user)
23 | })
24 | })
25 | } else {
26 | return res.status(404).json({
27 | error: true,
28 | message: 'Username already exists'
29 | })
30 | }
31 | })
32 | },
33 | login(req, res) {
34 | const { body } = req;
35 |
36 | db.User
37 | .find({ where: { username: body.username } })
38 | .then(user => {
39 | if (!user) {
40 | return res.status(404).json({
41 | error: true,
42 | message: 'Username or password is wrong'
43 | })
44 | }
45 |
46 | bcrypt.compare(body.password, user.password, (err, valid) => {
47 | if (!valid) {
48 | return res.status(404).json({
49 | error: true,
50 | message: 'Username or password is wrong'
51 | })
52 | }
53 |
54 | res.send({
55 | user: utils.getCleanUser(user),
56 | token: utils.generateToken(user)
57 | })
58 | })
59 | })
60 | },
61 | verifyToken(req, res) {
62 | jwt.verify(req.body.token, process.env.JWT_SECRET, (err, decoded) => {
63 | if (err) {
64 | return res.status(404).json({
65 | error: true,
66 | message: 'JWT invalid'
67 | })
68 | }
69 |
70 | db.User
71 | .find({ where: { id: decoded.id } })
72 | .then(user => {
73 | if (!user) {
74 | return res.status(404).json({
75 | error: true,
76 | message: 'JWT invalid'
77 | })
78 | }
79 |
80 | res.send({
81 | user: utils.getCleanUser(user),
82 | token: utils.generateToken(user)
83 | })
84 | })
85 | })
86 | },
87 | }
--------------------------------------------------------------------------------
/server/poker/__test__/winner.test.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect
2 |
3 | var Table = require('../table')
4 | var Player = require('../player')
5 |
6 | describe('When the winner is determined', () => {
7 | let table
8 | let player1
9 | let player2
10 | const raiseAmount = 5
11 |
12 | beforeEach(() => {
13 | table = new Table(1, 'Test table', 6, 10)
14 | player1 = new Player('1', 1, 'Byron', 100)
15 | player2 = new Player('2', 2, 'Alice', 100)
16 |
17 | table.sitPlayer(player1, 1, 5)
18 | table.sitPlayer(player2, 2, 5)
19 | table.startHand()
20 | })
21 |
22 | describe('a straight vs two pair', () => {
23 | beforeEach(() => {
24 | table.deck.cards = [
25 | { rank: 'ace', suit: 'diamonds' },
26 | { rank: '5', suit: 'diamonds' },
27 | { rank: '2', suit: 'spades' },
28 | { rank: '8', suit: 'hearts' },
29 | { rank: '9', suit: 'hearts' },
30 | ]
31 | table.seats[1].hand = [
32 | { rank: 'ace', suit: 'spades' },
33 | { rank: '5', suit: 'spades' },
34 | ]
35 | table.seats[2].hand = [
36 | { rank: '3', suit: 'clubs' },
37 | { rank: '4', suit: 'clubs' },
38 | ]
39 | table.handleRaise('2', raiseAmount)
40 | table.changeTurn(2)
41 | table.handleCall('1')
42 | table.changeTurn(1)
43 | })
44 | it('the player with the straight wins', () => {
45 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(0)
46 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(10)
47 | })
48 | })
49 |
50 | describe('two pair vs two pair', () => {
51 | beforeEach(() => {
52 | table.deck.cards = [
53 | { rank: 'ace', suit: 'diamonds' },
54 | { rank: '5', suit: 'diamonds' },
55 | { rank: '3', suit: 'spades' },
56 | { rank: '4', suit: 'hearts' },
57 | { rank: 'jack', suit: 'hearts' },
58 | ]
59 | table.seats[1].hand = [
60 | { rank: 'ace', suit: 'spades' },
61 | { rank: '5', suit: 'spades' },
62 | ]
63 | table.seats[2].hand = [
64 | { rank: '3', suit: 'clubs' },
65 | { rank: '4', suit: 'clubs' },
66 | ]
67 | table.handleRaise('2', raiseAmount)
68 | table.changeTurn(2)
69 | table.handleCall('1')
70 | table.changeTurn(1)
71 | })
72 | it('the player with the higher two pair wins', () => {
73 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(10)
74 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(0)
75 | })
76 | })
77 | })
--------------------------------------------------------------------------------
/client/app/components/Signup.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import { css } from 'emotion'
5 |
6 | import { signUp } from '../actions/user'
7 |
8 | import Icon from 'material-ui/Icon'
9 | import Input from 'material-ui/Input';
10 | import { Panel, Button, Text, Link } from 'app/components'
11 |
12 | const container = css`
13 | max-width: 350px;
14 | padding-top: 200px;
15 | margin: 0 auto 0;
16 | `;
17 | const inputStyle = {
18 | fontFamily: 'Montserrat, sans-serif',
19 | width: '100%',
20 | marginBottom: '16px'
21 | };
22 | const linkStyle = {
23 | display: 'flex',
24 | alignItems: 'center',
25 | marginTop: '12px',
26 | color: '#2196f3',
27 | };
28 |
29 | type Props = {
30 | signUp: ({ username: string, password: string }) => void,
31 | isFetching: boolean,
32 | errorMessage: string,
33 | };
34 | class Signup extends React.Component {
35 | username: { value: ?string }
36 | password: { value: ?string }
37 |
38 | handleSubmit = () => {
39 | const username = this.username.value
40 | const password = this.password.value
41 |
42 | if (!username || !password) { return }
43 | this.props.signUp({ username, password })
44 | }
45 |
46 | render() {
47 | const { isFetching, errorMessage } = this.props
48 |
49 | return (
50 |
77 | )
78 | }
79 | }
80 |
81 | function mapStateToProps(state) {
82 | return {
83 | isFetching: state.user.isFetching,
84 | errorMessage: state.user.errorMessage
85 | }
86 | }
87 |
88 | const mapDispatchToProps = ({ signUp })
89 |
90 | export default connect(
91 | mapStateToProps,
92 | mapDispatchToProps
93 | )(Signup)
94 |
--------------------------------------------------------------------------------
/client/app/components/Login.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import { css } from 'emotion'
5 |
6 | import { login } from '../actions/user'
7 |
8 | import Icon from 'material-ui/Icon'
9 | import Input from 'material-ui/Input';
10 | import { Panel, Button, Text, Link } from 'app/components'
11 |
12 | const container = css`
13 | max-width: 350px;
14 | padding-top: 200px;
15 | margin: 0 auto 0;
16 | `
17 | const inputStyle = {
18 | fontFamily: 'Montserrat, sans-serif',
19 | width: '100%',
20 | marginBottom: '16px'
21 | };
22 | const linkStyle = {
23 | display: 'flex',
24 | alignItems: 'center',
25 | marginTop: '12px',
26 | color: '#2196f3'
27 | };
28 |
29 | type Props = {
30 | login: ({ username: string, password: string }) => void,
31 | isFetching: boolean,
32 | errorMessage: ?string,
33 | }
34 | class Login extends React.Component {
35 | username: { value: ?string }
36 | password: { value: ?string }
37 |
38 | handleSubmit = () => {
39 | const username = this.username.value
40 | const password = this.password.value
41 |
42 | if (!username || !password) { return }
43 | this.props.login({ username, password })
44 | }
45 |
46 | render() {
47 | const { isFetching, errorMessage } = this.props
48 |
49 | return (
50 |
77 | )
78 | }
79 | }
80 |
81 | function mapStateToProps(state) {
82 | return {
83 | isFetching: state.user.isFetching,
84 | errorMessage: state.user.errorMessage
85 | }
86 | }
87 |
88 | const mapDispatchToProps = ({ login })
89 |
90 | export default connect(
91 | mapStateToProps,
92 | mapDispatchToProps
93 | )(Login)
94 |
--------------------------------------------------------------------------------
/client/app/components/Game/Seats/index.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import SeatedPlayer from './SeatedPlayer'
4 | import EmptySeat from './EmptySeat'
5 |
6 | type Props = {
7 | user: {
8 | id: number,
9 | username: string,
10 | },
11 | table: {
12 | id: number,
13 | seats: {
14 | [seatId: any]: ?{
15 | id: number,
16 | player: {
17 | name: string,
18 | },
19 | hand: Array<{
20 | rank: string,
21 | suit: string,
22 | }>,
23 | bet: number,
24 | stack: number,
25 | },
26 | },
27 | maxPlayers: number,
28 | },
29 | onSeatClick: (tableId: number, seatId: number) => void,
30 | displayOffset: number,
31 | }
32 | class Seats extends React.Component {
33 | render() {
34 | const { user, table, onSeatClick, displayOffset } = this.props
35 | let seats = Object.keys(table.seats)
36 | const seatedPlayerIds = seats.map(seatId => table.seats[seatId] && table.seats[seatId].id);
37 | const seated = seatedPlayerIds.includes(user.id);
38 |
39 | return (
40 |
41 | {seats.map(seatId => {
42 | const seat = table.seats[seatId]
43 | const isButton = parseInt(seatId) === table.button ? true : false
44 |
45 | let displayOrder
46 | if (parseInt(seatId) + displayOffset > table.maxPlayers) {
47 | displayOrder = parseInt(seatId) + displayOffset - table.maxPlayers
48 | } else {
49 | displayOrder = parseInt(seatId) + displayOffset
50 | }
51 |
52 | let className = `seat-container seat${displayOrder}of${table.maxPlayers}`
53 | if (seat && seat.turn) {
54 | className += ' active'
55 | }
56 |
57 | if (seat) {
58 | return (
59 |
60 |
65 |
66 | )
67 | } else if (!seated) {
68 | return (
69 |
70 | {
73 | if (e) e.stopPropagation()
74 | onSeatClick(table.id, parseInt(seatId))
75 | }}/>
76 |
77 | )
78 | } else {
79 | return
80 | }
81 | })}
82 |
83 | )
84 | }
85 | }
86 |
87 | export default Seats
88 |
--------------------------------------------------------------------------------
/client/app/components/Groups/Group.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { css } from 'emotion'
4 |
5 | import { Modal, Panel, Button, Text, UserSearchModal } from 'app/components';
6 |
7 | const container = css`
8 | width: calc(100% - 470px);
9 | `;
10 |
11 | type Props = {
12 | group: any,
13 | deleteGroup: (groupId: number) => void,
14 | }
15 | type State = {
16 | confirmModalOpen: boolean,
17 | inviteModalOpen: boolean,
18 | }
19 | class Group extends React.Component {
20 | state = {
21 | confirmModalOpen: false,
22 | inviteModalOpen: false,
23 | }
24 |
25 | toggleConfirm = () => {
26 | this.setState({ confirmModalOpen: !this.state.confirmModalOpen })
27 | }
28 |
29 | toggleInvite = () => {
30 | this.setState({ inviteModalOpen: !this.state.inviteModalOpen })
31 | }
32 |
33 | handleDeleteClick = () => {
34 | this.props.deleteGroup(this.props.group.id);
35 | this.toggleConfirm();
36 | }
37 |
38 | createInvites = data => {
39 | console.log(data);
40 | }
41 |
42 | render() {
43 | const { group } = this.props;
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | Search code:
51 | {group.code}
52 |
53 |
54 |
55 |
58 |
61 |
62 |
63 |
64 |
65 | Are you sure you want to delete group: {group.name}?
66 |
67 |
68 |
74 |
77 |
78 |
79 |
85 |
86 | )
87 | }
88 | }
89 |
90 | export default Group
--------------------------------------------------------------------------------
/client/app/reducers/lobby.js:
--------------------------------------------------------------------------------
1 | import Moment from 'moment'
2 | import {
3 | RECEIVE_LOBBY_INFO,
4 | TABLES_UPDATED,
5 | PLAYERS_UPDATED,
6 | TABLE_JOINED,
7 | TABLE_LEFT,
8 | TABLE_UPDATED,
9 | } from '../actions/lobby'
10 |
11 | const initialState = {
12 | tables: {},
13 | players: {},
14 | openTables: {},
15 | messages: []
16 | }
17 |
18 | function lobby(state = initialState, action) {
19 | let newOpenTables
20 | switch (action.type) {
21 | case RECEIVE_LOBBY_INFO:
22 | return {
23 | ...state,
24 | tables: action.tables,
25 | players: action.players
26 | }
27 | case TABLES_UPDATED:
28 | newOpenTables = state.openTables
29 | for (let tableId of Object.keys(newOpenTables)) {
30 | newOpenTables[tableId].table = action.tables[tableId]
31 | }
32 |
33 | return {
34 | ...state,
35 | tables: action.tables,
36 | openTables: newOpenTables
37 | }
38 | case PLAYERS_UPDATED:
39 | return {
40 | ...state,
41 | players: action.players
42 | }
43 | case TABLE_JOINED:
44 | newOpenTables = state.openTables
45 | newOpenTables[action.tableId] = {
46 | table: action.tables[action.tableId],
47 | messages: []
48 | }
49 |
50 | return {
51 | ...state,
52 | tables: action.tables,
53 | openTables: newOpenTables
54 | }
55 | case TABLE_LEFT:
56 | newOpenTables = state.openTables
57 | delete newOpenTables[action.tableId]
58 |
59 | return {
60 | ...state,
61 | tables: action.tables,
62 | openTables: newOpenTables
63 | }
64 | case TABLE_UPDATED:
65 | newOpenTables = JSON.parse(JSON.stringify(state.openTables))
66 |
67 | if (newOpenTables[action.table.id]) {
68 | newOpenTables[action.table.id].table = action.table
69 |
70 | if (action.message) {
71 | const newMessage = {
72 | message: action.message,
73 | from: action.from,
74 | timestamp: Moment().format('LTS')
75 | }
76 | newOpenTables[action.table.id].messages.push(newMessage)
77 | }
78 |
79 | for (let winMessage of action.table.winMessages) {
80 | const newWinMessage = {
81 | message: winMessage,
82 | from: action.from,
83 | timestamp: Moment().format('LTS')
84 | }
85 | newOpenTables[action.table.id].messages.push(newWinMessage)
86 | }
87 | }
88 |
89 | return {
90 | ...state,
91 | openTables: newOpenTables
92 | }
93 | default:
94 | return state
95 | }
96 | }
97 |
98 | export default lobby
--------------------------------------------------------------------------------
/client/modules/components/UserSearchModal.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { connect } from 'react-redux'
3 | import { css } from 'emotion'
4 |
5 | import { searchUsers } from '../../app/actions/searchUsers'
6 |
7 | import Input from 'material-ui/Input';
8 | import Modal from './Modal'
9 | import Button from './Button'
10 |
11 | const buttons = css`
12 | text-align: right;
13 | `;
14 | const inputStyle = {
15 | width: '100%',
16 | fontSize: '20px',
17 | fontFamily: 'Montserrat, sans-serif',
18 | marginBottom: '32px',
19 | };
20 |
21 | type Props = {
22 | open: boolean,
23 | header: string,
24 | closeModal: () => void,
25 | onSubmit: () => void,
26 | token: string,
27 | searchUsers: (token: string, query: string) => void,
28 | searchResults: Array,
29 | }
30 | class UserSearchModal extends React.Component {
31 | handleSubmit = () => {
32 | const { searchResults, closeModal, token, searchUsers, onSubmit } = this.props
33 | searchUsers(token, '')
34 | onSubmit(searchResults)
35 | closeModal()
36 | }
37 |
38 | handleChange = (e: SyntheticEvent) => {
39 | e.preventDefault()
40 | const { token, searchUsers } = this.props
41 | searchUsers(token, e.target.value)
42 | }
43 |
44 | handleClose = () => {
45 | const { closeModal, token, searchUsers } = this.props
46 | searchUsers(token, '')
47 | closeModal()
48 | }
49 |
50 | render() {
51 | const { open, header, searchResults } = this.props;
52 |
53 | return (
54 |
55 | {header}
56 |
61 | {searchResults.length > 0 && (
62 |
63 | {searchResults.map((result, idx) =>
{result.username}
)}
64 |
65 | )}
66 |
67 |
74 |
77 |
78 |
79 | )
80 | }
81 | }
82 |
83 | function mapStateToProps(state) {
84 | return {
85 | searchResults: state.searchUsers.searchResults,
86 | isFetching: state.searchUsers.isFetching,
87 | errorMessage: state.searchUsers.errorMessage,
88 | token: state.user.token,
89 | }
90 | }
91 |
92 | const mapDispatchToProps = ({
93 | searchUsers,
94 | })
95 |
96 | export default connect(
97 | mapStateToProps,
98 | mapDispatchToProps
99 | )(UserSearchModal)
--------------------------------------------------------------------------------
/server/routes/groups.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/models')
2 | const jwt = require('jsonwebtoken');
3 |
4 | module.exports = {
5 | fetchGroups(req, res) {
6 | jwt.verify(req.query.access_token, process.env.JWT_SECRET, (err, decoded) => {
7 | if (err) {
8 | return res.status(404).json({
9 | error: true,
10 | message: 'Invalid access token'
11 | })
12 | }
13 |
14 | db.Group.findAll({
15 | include: [{
16 | model: db.GroupMember,
17 | where: { user_id: decoded.id },
18 | }],
19 | })
20 | .then(groups => {
21 | res.send({
22 | groups: groups,
23 | })
24 | })
25 | .catch(err => {
26 | res.status(500).send(`Internal Server Error: ${err}`)
27 | })
28 | })
29 | },
30 | createGroup(req, res) {
31 | jwt.verify(req.body.accessToken, process.env.JWT_SECRET, async (err, decoded) => {
32 | if (err) {
33 | return res.status(404).json({
34 | error: true,
35 | message: 'Invalid access token'
36 | })
37 | }
38 |
39 | const user = await db.User.findById(decoded.id)
40 | if (!user) throw new Error('User not found');
41 |
42 | let group;
43 | await db.sequelize.transaction(async transaction => {
44 | group = await db.Group.create({
45 | creator_id: user.id,
46 | name: req.body.groupAttrs.name,
47 | code: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
48 | }, { transaction })
49 | await db.GroupMember.create({
50 | group_id: group.id,
51 | user_id: user.id,
52 | is_admin: true,
53 | bankroll: 0,
54 | }, { transaction })
55 | })
56 |
57 | const createdGroup = await db.Group.findOne({
58 | where: { id: group.id },
59 | include: [{
60 | model: db.GroupMember,
61 | }]
62 | })
63 |
64 | if (!createdGroup) {
65 | res.status(500).send('Failed to create group');
66 | }
67 |
68 | res.send({ group: createdGroup });
69 | })
70 | },
71 | deleteGroup(req, res) {
72 | jwt.verify(req.query.accessToken, process.env.JWT_SECRET, async (err, decoded) => {
73 | if (err) {
74 | return res.status(404).json({
75 | error: true,
76 | message: 'Invalid access token'
77 | })
78 | }
79 |
80 | const group = await db.Group.findOne({
81 | where: {
82 | $and: {
83 | id: req.params.groupId,
84 | creator_id: decoded.id,
85 | }
86 | }
87 | })
88 |
89 | if (!group) {
90 | res.status(500).send('Could not find group to delete');
91 | }
92 |
93 | await group.destroy();
94 | res.send({ deletedGroupId: group.id })
95 | })
96 | }
97 | }
--------------------------------------------------------------------------------
/client/scss/seats.scss:
--------------------------------------------------------------------------------
1 | @media screen and (min-height: 1050px) {
2 | .seat1of6 {
3 | left: 475px;
4 | top: 95px;
5 |
6 | .bet {
7 | top: 155px;
8 | }
9 | }
10 | .seat2of6 {
11 | top: 195px;
12 | left: 895px;
13 |
14 | .bet {
15 | top: 145px;
16 | left: -150px;
17 | }
18 | }
19 | .seat3of6 {
20 | top: 460px;
21 | left: 895px;
22 |
23 | .bet {
24 | left: -150px;
25 | bottom: 85px;
26 | }
27 | }
28 | .seat4of6 {
29 | top: 560px;
30 | left: 475px;
31 |
32 | .bet {
33 | bottom: 130px;
34 | }
35 | }
36 | .seat5of6 {
37 | top: 460px;
38 | left: 55px;
39 |
40 | .bet {
41 | right: -150px;
42 | bottom: 85px;
43 | }
44 | }
45 | .seat6of6 {
46 | top: 195px;
47 | left: 55px;
48 |
49 | .bet {
50 | top: 145px;
51 | right: -150px;
52 | }
53 | }
54 | }
55 | @media screen and (max-height: 1049px) {
56 | .seat1of6 {
57 | left: 475px;
58 | top: 25px;
59 |
60 | .bet {
61 | top: 170px;
62 | }
63 | }
64 | .seat2of6 {
65 | top: 125px;
66 | left: 895px;
67 |
68 | .bet {
69 | top: 160px;
70 | left: -150px;
71 | }
72 | }
73 | .seat3of6 {
74 | top: 390px;
75 | left: 895px;
76 |
77 | .bet {
78 | left: -150px;
79 | bottom: 110px;
80 | }
81 | }
82 | .seat4of6 {
83 | top: 490px;
84 | left: 475px;
85 |
86 | .bet {
87 | bottom: 150px;
88 | }
89 | }
90 | .seat5of6 {
91 | top: 390px;
92 | left: 55px;
93 |
94 | .bet {
95 | right: -150px;
96 | bottom: 110px;
97 | }
98 | }
99 | .seat6of6 {
100 | top: 125px;
101 | left: 55px;
102 |
103 | .bet {
104 | top: 160px;
105 | right: -150px;
106 | }
107 | }
108 | }
109 | .seat-container {
110 | color: #fff;
111 | width: 180px;
112 | height: 150px;
113 | position: absolute;
114 | text-align: center;
115 | z-index: 5;
116 | transition: 0.2s all;
117 | }
118 | .bet {
119 | position: absolute;
120 | width: 100%;
121 | }
122 | .seat-info {
123 | position: absolute;
124 | width: 100%;
125 | bottom: 5px;
126 | }
127 | .seat-number {
128 | padding: 4px 0px;
129 | display: inline-block;
130 | width: 20%;
131 | border-radius: 0 0 0 4px;
132 | }
133 | .seat-player {
134 | position: relative;
135 | display: inline-block;
136 | padding: 4px 0px;
137 | width: 80%;
138 | border-radius: 0 0 4px 0;
139 | }
140 | .seat-container.active .seat-info .seat-player {
141 | color: #ffeb3b;
142 | }
143 | .seat-stack {
144 | font-size: 24px;
145 | padding: 8px 0px;
146 | border-radius: 4px 4px 0 0;
147 | }
148 | .button-chip {
149 | color: #fff;
150 | position: absolute;
151 | right: 5px;
152 | top: -42px;
153 | font-size: 10px;
154 | }
--------------------------------------------------------------------------------
/server/poker/__test__/betting.test.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect
2 |
3 | var Table = require('../table')
4 | var Player = require('../player')
5 |
6 | describe('Betting scenarios', () => {
7 | let table
8 | let player1
9 | let player2
10 |
11 | beforeEach(() => {
12 | table = new Table(1, 'Test table', 6, 10)
13 | player1 = new Player('socket1', 1, 'Byron', 100)
14 | player2 = new Player('socket2', 2, 'Alice', 100)
15 |
16 | table.sitPlayer(player1, 1, 10)
17 | table.sitPlayer(player2, 2, 5)
18 | table.startHand()
19 | })
20 |
21 | describe('after the hand has been started', () => {
22 | it('the first sat player is the big blind', () => {
23 | expect(table.seats[1].bet).to.be.equal(table.limit/100)
24 | })
25 |
26 | it('the second sat player is the small blind with the action', () => {
27 | expect(table.seats[2].bet).to.be.equal(table.limit/200)
28 | expect(table.seats[2].turn).to.be.true
29 | expect(table.turn).to.be.equal(2)
30 | })
31 | })
32 |
33 | describe('when the small blind folds preflop', () => {
34 | beforeEach(() => {
35 | table.handleFold('socket2')
36 | table.changeTurn(2)
37 | })
38 |
39 | it('the big blind wins $0.05', () => {
40 | expect(table.seats[1].stack).to.be.equal(10.05)
41 | })
42 |
43 | it('the small blind loses $0.05', () => {
44 | expect(table.seats[2].stack).to.be.equal(4.95)
45 | })
46 |
47 | it('the hand is over', () => {
48 | expect(table.handOver).to.be.true
49 | })
50 | })
51 |
52 | describe('when the small blind calls preflop', () => {
53 | beforeEach(() => {
54 | table.handleCall('socket2')
55 | table.changeTurn(2)
56 | })
57 |
58 | it('the small blind has a bet equal to the big blind', () => {
59 | expect(table.seats[2].bet).to.be.equal(table.seats[1].bet)
60 | })
61 |
62 | it('the small blind\'s stack decreases by the amount called', () => {
63 | expect(table.callAmount).to.be.equal(.1)
64 | expect(table.seats[2].stack).to.be.equal(4.9)
65 | })
66 |
67 | it('it is the big blind\'s turn', () => {
68 | expect(table.seats[1].turn).to.be.true
69 | expect(table.turn).to.be.equal(1)
70 | })
71 | })
72 |
73 | describe('when the small blind raises preflop', () => {
74 | beforeEach(() => {
75 | table.handleRaise('socket2', .3)
76 | table.changeTurn(2)
77 | })
78 |
79 | it('the small blind has a bet larger than the big blind', () => {
80 | expect(table.seats[2].bet).to.be.equal(.3)
81 | })
82 |
83 | it('the small blind\'s stack decreases by additional the amount raised', () => {
84 | expect(table.callAmount).to.be.equal(.3)
85 | expect(table.seats[2].stack).to.be.equal(4.7)
86 | })
87 |
88 | it('it is the big blind\'s turn', () => {
89 | expect(table.seats[1].turn).to.be.true
90 | expect(table.turn).to.be.equal(1)
91 | })
92 | })
93 | })
--------------------------------------------------------------------------------
/client/app/components/Game/Actions/RaiseSlider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Button from 'material-ui/Button'
3 | import AddIcon from 'material-ui-icons/Add'
4 | import Input from 'material-ui/Input/Input'
5 | import { withStyles, createStyleSheet } from 'material-ui/styles'
6 |
7 | const styleSheet = createStyleSheet('RaiseSlider', () => ({
8 | button: {
9 | width: '36px',
10 | height: '36px',
11 | margin: '0px 10px',
12 | },
13 | icon: {
14 | position: 'relative',
15 | color: '#fff'
16 | },
17 | container: {
18 | color: '#333',
19 | display: 'flex',
20 | alignItems: 'center',
21 | margin: '10px 0px',
22 | },
23 | sliderContainer: {
24 | display: 'flex',
25 | alignItems: 'center',
26 | flex: 4
27 | },
28 | slider: {
29 | flex: 5
30 | },
31 | input: {
32 | flex: 1,
33 | height: '65%',
34 | marginRight: '10px',
35 | fontFamily: 'Montserrat, sans-serif',
36 | }
37 | }))
38 |
39 | type Props = {
40 | raiseAmount: number,
41 | decreaseRaiseAmount: () => void,
42 | increaseRaiseAmount: () => void,
43 | onRaiseChange: () => void,
44 | table: {
45 | minRaise: number,
46 | maxBet: number,
47 | },
48 | seat: {
49 | stack: number,
50 | bet: number,
51 | },
52 | classes: Object,
53 | }
54 | const RaiseSlider = ({
55 | raiseAmount,
56 | decreaseRaiseAmount,
57 | increaseRaiseAmount,
58 | onRaiseChange,
59 | table,
60 | seat,
61 | classes
62 | }: Props) => {
63 | const maxBet = seat.stack + seat.bet
64 | const minRaise = table.minRaise > maxBet ? maxBet : table.minRaise
65 |
66 | return (
67 |
68 |
69 |
76 |
77 |
${minRaise.toFixed(2)}
78 |
79 |
89 |
90 |
91 |
${maxBet.toFixed(2)}
92 |
93 |
100 |
101 |
102 |
106 |
107 | )
108 | }
109 |
110 | export default withStyles(styleSheet)(RaiseSlider)
--------------------------------------------------------------------------------
/client/app/components/Game/Actions/ActionButtons.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react'
3 | import Button from 'material-ui/Button'
4 | import { withStyles, createStyleSheet } from 'material-ui/styles'
5 |
6 | const styleSheet = createStyleSheet('ActionButtons', () => ({
7 | container: {
8 | display: 'flex'
9 | },
10 | button: {
11 | flex: 1,
12 | color: '#fff',
13 | margin: '3px',
14 | height: '80px',
15 | padding: '0px 20px',
16 | fontSize: '16px',
17 | fontFamily: 'Montserrat, sans-serif',
18 | }
19 | }))
20 |
21 | type Seat = {
22 | id: number,
23 | bet: number,
24 | stack: number,
25 | }
26 | type Table = {
27 | id: number,
28 | seats: {
29 | [id: string]: Seat,
30 | },
31 | pot: number,
32 | callAmount: number,
33 | }
34 | type Props = {
35 | seat: Seat,
36 | table: Table,
37 | raiseAmount: number,
38 | totalCallAmount: number,
39 | handleFoldClick: () => void,
40 | handleCheckClick: () => void,
41 | handleCallClick: () => void,
42 | handleRaiseClick: () => void,
43 | classes: Object,
44 | }
45 | const ActionButtons = ({
46 | seat,
47 | table,
48 | raiseAmount,
49 | totalCallAmount,
50 | handleFoldClick,
51 | handleCheckClick,
52 | handleCallClick,
53 | handleRaiseClick,
54 | classes
55 | }: Props) => {
56 | const players = Object.keys(table.seats).map(id => table.seats[id])
57 | const notAllInPlayers = players.filter(tableSeat =>
58 | tableSeat && tableSeat.stack > 0
59 | )
60 | const othersAllIn = notAllInPlayers.length === 1
61 | && notAllInPlayers[0].id === seat.id
62 |
63 | return (
64 |
65 | {table.callAmount &&
66 |
73 | }
74 |
75 | {((!table.callAmount || seat.bet === table.callAmount) && seat.stack > 0) &&
76 |
83 | }
84 |
85 | {(table.callAmount > 0 && seat.bet !== table.callAmount) &&
86 |
93 | }
94 |
95 | {((seat.stack > table.callAmount) && !othersAllIn) &&
96 |
103 | }
104 |
105 | )
106 | }
107 |
108 | export default withStyles(styleSheet)(ActionButtons)
--------------------------------------------------------------------------------
/client/app/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOGIN_REQUEST,
3 | LOGIN_SUCCESS,
4 | LOGIN_FAILURE,
5 | SIGNUP_REQUEST,
6 | SIGNUP_SUCCESS,
7 | SIGNUP_FAILURE,
8 | LOGOUT,
9 | TOKEN_LOGIN_REQUEST,
10 | TOKEN_LOGIN_SUCCESS,
11 | TOKEN_LOGIN_FAILURE
12 | } from '../actions/user'
13 |
14 | const initialState = {
15 | isFetching: false,
16 | isAuthenticated: false,
17 | user: null,
18 | token: localStorage.getItem('client') || null,
19 | errorMessage: ''
20 | }
21 |
22 | function user(state = initialState, action) {
23 | switch (action.type) {
24 | case LOGIN_REQUEST:
25 | return {
26 | ...state,
27 | isFetching: true,
28 | isAuthenticated: false,
29 | errorMessage: ''
30 | }
31 | case LOGIN_SUCCESS:
32 | localStorage.setItem('client', action.token)
33 | return {
34 | isFetching: false,
35 | isAuthenticated: true,
36 | user: action.user,
37 | token: action.token,
38 | errorMessage: ''
39 | }
40 | case LOGIN_FAILURE:
41 | return {
42 | isFetching: false,
43 | isAuthenticated: false,
44 | user: null,
45 | token: null,
46 | errorMessage: action.message.response.data.message
47 | }
48 | case SIGNUP_REQUEST:
49 | return {
50 | ...state,
51 | isFetching: true,
52 | isAuthenticated: false,
53 | errorMessage: ''
54 | }
55 | case SIGNUP_SUCCESS:
56 | localStorage.setItem('client', action.token)
57 | return {
58 | isFetching: false,
59 | isAuthenticated: true,
60 | user: action.user,
61 | token: action.token,
62 | errorMessage: ''
63 | }
64 | case SIGNUP_FAILURE:
65 | return {
66 | isFetching: false,
67 | isAuthenticated: false,
68 | user: null,
69 | token: null,
70 | errorMessage: action.message.response.data.message
71 | }
72 | case LOGOUT:
73 | localStorage.removeItem('client')
74 | return {
75 | isFetching: false,
76 | isAuthenticated: false,
77 | user: null,
78 | token: null,
79 | errorMessage: ''
80 | }
81 | case TOKEN_LOGIN_REQUEST:
82 | return {
83 | ...state,
84 | isFetching: true,
85 | isAuthenticated: false,
86 | errorMessage: ''
87 | }
88 | case TOKEN_LOGIN_SUCCESS:
89 | localStorage.setItem('client', action.token)
90 | return {
91 | isFetching: false,
92 | isAuthenticated: true,
93 | user: action.user,
94 | token: action.token,
95 | errorMessage: ''
96 | }
97 | case TOKEN_LOGIN_FAILURE:
98 | localStorage.removeItem('client')
99 | return {
100 | isFetching: false,
101 | isAuthenticated: false,
102 | user: null,
103 | token: null,
104 | errorMessage: action.message.response.data.message
105 | }
106 | default:
107 | return state
108 | }
109 | }
110 |
111 | export default user
--------------------------------------------------------------------------------
/client/app/components/Game/ChatAndInfo/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { blueGrey } from 'material-ui/styles/colors'
4 |
5 | import TableControls from '../TableControls';
6 | import Chat from './Chat';
7 | import Spectators from './Spectators';
8 | import SitOutCheckbox from './SitOutCheckbox';
9 |
10 | const styles = {
11 | container: {
12 | position: 'absolute',
13 | width: 'calc(50vw - 10px)',
14 | height: '184px',
15 | padding: '5px',
16 | right: '0',
17 | bottom: '0',
18 | backgroundColor: blueGrey[100],
19 | border: `1px solid ${blueGrey[100]}`
20 | },
21 | tabs: {
22 | position: 'absolute',
23 | display: 'flex',
24 | height: '30px',
25 | top: '-30px',
26 | listStyleType: 'none',
27 | margin: '0',
28 | padding: '0',
29 | },
30 | tab: {
31 | color: '#999',
32 | backgroundColor: blueGrey[100],
33 | padding: '5px 10px',
34 | margin: '0 2px',
35 | borderRadius: '3px 3px 0 0',
36 | },
37 | activeTab: {
38 | color: '#333',
39 | backgroundColor: blueGrey[100],
40 | fontWeight: '600',
41 | padding: '5px 10px',
42 | margin: '0 2px',
43 | borderRadius: '3px 3px 0 0',
44 | },
45 | }
46 |
47 | type Props = {
48 | user: Object,
49 | table: Object,
50 | onLeaveClick: () => void,
51 | onStandClick: () => void,
52 | onRotateClockwise: () => void,
53 | onRotateCounterClockwise: () => void,
54 | }
55 | class ChatAndInfo extends React.Component {
56 | constructor() {
57 | super()
58 |
59 | this.state = {
60 | activeTab: 'Chat',
61 | }
62 | }
63 |
64 | setActiveTab = tabName => {
65 | this.setState({ activeTab: tabName })
66 | }
67 |
68 | render() {
69 | const tabs = ['Chat', 'Players', 'Table Info']
70 | const { user, table } = this.props;
71 |
72 | return (
73 |
74 |
75 | {tabs.map((tab, index) => (
76 | - this.setActiveTab(tab)}
80 | >
81 | {tab}
82 |
83 | ))}
84 |
85 |
86 |
92 |
93 | {this.state.activeTab === 'Chat' &&
94 |
95 | }
96 | {this.state.activeTab === 'Players' &&
97 |
101 | }
102 | {this.state.activeTab === 'Table Info' &&
103 |
104 | {table.name}, ${table.limit.toFixed(2)} NL Holdem, {table.maxPlayers} players
105 |
106 | }
107 |
108 |
109 |
110 | )
111 | }
112 | }
113 |
114 | export default ChatAndInfo
--------------------------------------------------------------------------------
/client/app/components/Game/Game.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Seats from './Seats'
3 | import Background from './Pieces/Background'
4 | import Board from './Pieces/Board'
5 | import Actions from './Actions'
6 | import ChatAndInfo from './ChatAndInfo';
7 |
8 | class Game extends React.Component {
9 | constructor() {
10 | super()
11 | this.state = {
12 | displayOffset: 0
13 | }
14 | this.rotateClockwiseClick = this.rotateClockwiseClick.bind(this)
15 | this.rotateCounterClockwiseClick = this.rotateCounterClockwiseClick.bind(this)
16 | }
17 |
18 | rotateClockwiseClick() {
19 | let currentOffset = this.state.displayOffset
20 | let maxOffset = this.props.table.maxPlayers - 1
21 | let newOffset = currentOffset === maxOffset ? 0 : currentOffset + 1
22 | this.setState({ displayOffset: newOffset })
23 | }
24 |
25 | rotateCounterClockwiseClick() {
26 | let currentOffset = this.state.displayOffset
27 | let maxOffset = this.props.table.maxPlayers - 1
28 | let newOffset = currentOffset === 0 ? maxOffset : currentOffset - 1
29 | this.setState({ displayOffset: newOffset })
30 | }
31 |
32 | isOwnTurn() {
33 | const { user, table } = this.props
34 | for (let seat of Object.values(table.seats)) {
35 | if (seat && seat.turn && seat.player.name === user.username) {
36 | return true
37 | }
38 | }
39 | return false
40 | }
41 |
42 | render() {
43 | const {
44 | classes,
45 | user,
46 | table,
47 | messages,
48 | onLeaveClick,
49 | onSeatClick,
50 | onStandClick,
51 | onRaiseClick,
52 | onCheckClick,
53 | onCallClick,
54 | onFoldClick,
55 | onTableMessage,
56 | gridViewOn,
57 | socket
58 | } = this.props
59 |
60 | const gameClass = gridViewOn ? 'poker-game poker-game-small' : 'poker-game'
61 | const seat = Object.values(table.seats).find(seat =>
62 | seat && seat.player.socketId === socket.id
63 | )
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
76 |
77 |
78 | {this.isOwnTurn() && (
79 |
87 | )}
88 |
onTableMessage(e, table.id)}
95 | onLeaveClick={() => onLeaveClick(table.id)}
96 | onStandClick={() => onStandClick(table.id)}
97 | onRotateClockwise={this.rotateClockwiseClick}
98 | onRotateCounterClockwise={this.rotateCounterClockwiseClick}
99 | />
100 |
101 | )
102 | }
103 | }
104 |
105 | export default Game
--------------------------------------------------------------------------------
/client/app/components/lobby/MainMenu/index.jsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { css } from 'emotion'
4 |
5 | import TableList from './TableList'
6 | import PlayerList from './PlayerList'
7 |
8 | const styles = {
9 | tab: css`
10 | font-size: 20px;
11 | margin-right: 30px;
12 | transition: 100ms ease;
13 |
14 | &:hover {
15 | cursor: pointer;
16 | color: #2196f3;
17 | border-bottom: 2px solid #2196f3;
18 | }
19 | `,
20 | activeTab: css`
21 | font-size: 20px;
22 | margin-right: 30px;
23 | color: #2196f3;
24 | border-bottom: 2px solid #2196f3;
25 | font-weight: 600;
26 | `,
27 | container: css`
28 | max-width: 525px;
29 | margin: 200px auto 0;
30 | `,
31 | }
32 |
33 | type Props = {
34 | user: {
35 | id: number,
36 | username: string,
37 | bankroll: number,
38 | },
39 | logout: () => void,
40 | openTables: {},
41 | tables: {},
42 | handleTableClick: (tableId: number) => void,
43 | players: {
44 | [socketId: string]: ?{
45 | id: number,
46 | name: string,
47 | bankroll: number,
48 | },
49 | },
50 | }
51 | type State = {
52 | activeTab: 'games' | 'players',
53 | }
54 | class MainMenu extends React.Component {
55 | state = {
56 | activeTab: 'games'
57 | }
58 |
59 | renderTabs() {
60 | const { activeTab } = this.state
61 | return (
62 |
68 |
69 | this.setState({ activeTab: 'games' })}>
72 | Games
73 |
74 | this.setState({ activeTab: 'players' })}>
77 | Players
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | render() {
85 | const {
86 | user,
87 | openTables,
88 | tables,
89 | handleTableClick,
90 | players,
91 | } = this.props
92 |
93 | if (!user) return null;
94 | const hasTableOpen = Object.keys(openTables).length > 0
95 | const userPlayer = Object.values(players).find(player =>
96 | player && player.id && player.id === user.id
97 | )
98 | if (!userPlayer) return null;
99 |
100 | return (
101 |
102 |
103 | {false && this.renderTabs()}
104 | {this.state.activeTab === 'games' &&
105 |
111 | }
112 | {this.state.activeTab === 'players' &&
113 |
117 | }
118 |
119 |
120 | )
121 | }
122 | }
123 |
124 | export default MainMenu;
--------------------------------------------------------------------------------
/client/app/actions/groups.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
3 |
4 | export const REQUEST_GROUPS = 'REQUEST_GROUPS'
5 | export const REQUEST_GROUPS_SUCCESS = 'REQUEST_GROUPS_SUCCESS'
6 | export const REQUEST_GROUPS_FAILURE = 'REQUEST_GROUPS_FAILURE'
7 |
8 | export const CREATE_GROUP_REQUEST = 'CREATE_GROUP_REQUEST'
9 | export const CREATE_GROUP_SUCCESS = 'CREATE_GROUP_SUCCESS'
10 | export const CREATE_GROUP_FAILURE = 'CREATE_GROUP_FAILURE'
11 |
12 | export const DELETE_GROUP_REQUEST = 'DELETE_GROUP_REQUEST'
13 | export const DELETE_GROUP_SUCCESS = 'DELETE_GROUP_SUCCESS'
14 | export const DELETE_GROUP_FAILURE = 'DELETE_GROUP_FAILURE'
15 |
16 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api'
17 |
18 | export function requestGroups() {
19 | return {
20 | type: REQUEST_GROUPS,
21 | }
22 | }
23 |
24 | export function requestGroupsSuccess(groups) {
25 | return {
26 | type: REQUEST_GROUPS_SUCCESS,
27 | groups,
28 | }
29 | }
30 |
31 | export function requestGroupsFailure(message) {
32 | return {
33 | type: REQUEST_GROUPS_FAILURE,
34 | message,
35 | }
36 | }
37 |
38 | export function fetchGroups(token) {
39 | return function (dispatch) {
40 | dispatch(requestGroups())
41 |
42 | return axios.get(`${ROOT_URL}/groups?access_token=${token}`)
43 | .then(res => {
44 | const { groups } = res.data
45 | dispatch(requestGroupsSuccess(groups))
46 | })
47 | .catch(err => {
48 | console.log(err)
49 | dispatch(requestGroupsFailure(err))
50 | })
51 | }
52 | }
53 |
54 | export function createGroupRequest() {
55 | return {
56 | type: CREATE_GROUP_REQUEST,
57 | }
58 | }
59 |
60 | export function createGroupSuccess(group) {
61 | return {
62 | type: CREATE_GROUP_SUCCESS,
63 | group,
64 | }
65 | }
66 |
67 | export function createGroupFailure(message) {
68 | return {
69 | type: CREATE_GROUP_FAILURE,
70 | message,
71 | }
72 | }
73 |
74 | export function createGroup(accessToken, groupAttrs) {
75 | return function (dispatch) {
76 | dispatch(createGroupRequest())
77 |
78 | return axios.post(`${ROOT_URL}/groups`, { accessToken, groupAttrs })
79 | .then(res => {
80 | const { group } = res.data
81 | dispatch(createGroupSuccess(group))
82 | })
83 | .catch(err => {
84 | console.log(err)
85 | dispatch(createGroupFailure(err))
86 | })
87 | }
88 | }
89 |
90 | export function deleteGroupRequest() {
91 | return {
92 | type: DELETE_GROUP_REQUEST,
93 | }
94 | }
95 |
96 | export function deleteGroupSuccess(deletedGroupId) {
97 | return {
98 | type: DELETE_GROUP_SUCCESS,
99 | deletedGroupId,
100 | }
101 | }
102 |
103 | export function deleteGroupFailure(message) {
104 | return {
105 | type: DELETE_GROUP_FAILURE,
106 | message,
107 | }
108 | }
109 |
110 | export function deleteGroup(accessToken, groupId) {
111 | return function (dispatch) {
112 | dispatch(deleteGroupRequest())
113 |
114 | return axios.delete(`${ROOT_URL}/groups/${groupId}?accessToken=${accessToken}`)
115 | .then(res => {
116 | const { deletedGroupId } = res.data
117 | dispatch(deleteGroupSuccess(deletedGroupId))
118 | })
119 | .catch(err => {
120 | console.log(err)
121 | dispatch(deleteGroupFailure(err))
122 | })
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/client/app/components/Game/BuyinModal.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React from 'react';
3 | import { Button } from 'app/components';
4 | import Dialog from 'material-ui/Dialog';
5 | import Input from 'material-ui/Input';
6 |
7 | const styles = {
8 | modal: {
9 | padding: '24px',
10 | },
11 | inputContainer: {
12 | display: 'flex',
13 | justifyContent: 'space-between',
14 | alignItems: 'center',
15 | marginBottom: '32px',
16 | fontFamily: 'Montserrat, sans-serif',
17 | },
18 | buttons: {
19 | textAlign: 'right',
20 | },
21 | error: {
22 | color: 'red',
23 | marginTop: '24px',
24 | fontSize: '14px',
25 | }
26 | }
27 |
28 | type Props = {
29 | open: boolean,
30 | tableId: ?number,
31 | seatId: ?number,
32 | table: ?{
33 | limit: number,
34 | seats: {
35 | [key: number]: ?{},
36 | },
37 | },
38 | seat: ?{
39 | stack: number,
40 | player: {},
41 | },
42 | closeModal: () => void,
43 | buyInAndSitDown: (tableId: number, seatId: number, amount: number) => void,
44 | handleRebuy: (tableId: number, seatId: number, amount: number) => void,
45 | }
46 | type State = {
47 | errorMessage: ?string,
48 | }
49 | class BuyinModal extends React.Component {
50 | buyinAmount: {
51 | value: null
52 | };
53 | state = {
54 | errorMessage: null,
55 | }
56 |
57 | handleClose = () => {
58 | this.setState({ errorMessage: null })
59 | this.props.closeModal();
60 | }
61 |
62 | handleBuyin = () => {
63 | const { tableId, seatId, table } = this.props
64 | if (!tableId || !seatId || !table) return null;
65 |
66 | const amount = parseFloat(this.buyinAmount.value)
67 | const minBuy = table.limit / 2
68 | const maxBuy = table.limit
69 |
70 | if (amount < minBuy || amount > maxBuy) {
71 | this.setState({ errorMessage: `Please enter an amount between ${minBuy} and ${maxBuy}` })
72 | return
73 | };
74 |
75 | const alreadySeated = table.seats[seatId]
76 | if (alreadySeated) {
77 | this.props.handleRebuy(tableId, seatId, amount)
78 | } else {
79 | this.props.buyInAndSitDown(tableId, seatId, amount)
80 | }
81 |
82 | this.props.closeModal()
83 | this.setState({ errorMessage: null })
84 | }
85 |
86 | render() {
87 | const { open, table, seat } = this.props
88 | if (!table) return null
89 | const mustBuyIn = table.handOver && seat && seat.stack == 0
90 |
91 | return (
92 |
124 | );
125 | }
126 | }
127 |
128 | export default BuyinModal
--------------------------------------------------------------------------------
/client/scss/small_table.scss:
--------------------------------------------------------------------------------
1 | .poker-game.poker-game-small {
2 | width: 50%;
3 | height: 50vh;
4 | font-size: 12px;
5 | float: left;
6 | box-sizing: border-box;
7 | outline: 1px solid #ccc;
8 |
9 | .seat1of6 {
10 | left: calc(50% - 50px);
11 | top: 5%;
12 |
13 | .bet {
14 | left: 85px;
15 | bottom: -40px;
16 | }
17 | }
18 | .seat2of6 {
19 | left: calc(94% - 100px);
20 |
21 | .bet {
22 | bottom: -20px;
23 | left: -85px;
24 | }
25 | }
26 | .seat3of6 {
27 | top: calc(63% - 80px);
28 | left: calc(94% - 100px);
29 |
30 | .bet {
31 | left: -85px;
32 | }
33 | }
34 | .seat4of6 {
35 | top: calc(76% - 80px);
36 | left: calc(50% - 50px);
37 |
38 | .bet {
39 | left: -85px;
40 | top: -15px;
41 | }
42 | }
43 | .seat5of6 {
44 | top: calc(63% - 80px);
45 |
46 | .bet {
47 | right: -85px;
48 | }
49 | }
50 | .seat6of6 {
51 | .bet {
52 | bottom: -20px;
53 | right: -85px;
54 | }
55 | }
56 | .seat-container {
57 | width: 100px;
58 | height: 80px;
59 |
60 | .seat-stack {
61 | font-size: 2em;
62 | padding: 0px;
63 | }
64 | .hand {
65 | bottom: 96px;
66 |
67 | .card:first-child {
68 | left: 21px;
69 | }
70 | .card:last-child {
71 | right: 21px;
72 | }
73 | }
74 | .hand:hover {
75 | .card:first-child {
76 | top: -25px;
77 | left: 7px;
78 | }
79 | .card:last-child {
80 | top: -25px;
81 | right: 7px;
82 | }
83 | }
84 | .seat-info > div:nth-child(2) {
85 | font-size: 0.9em;
86 | }
87 | .seat-last-action {
88 | font-size: 0.8em;
89 | bottom: -9px;
90 | }
91 | .button-chip {
92 | width: 15px;
93 | height: 15px;
94 | top: 25px;
95 | right: -20px;
96 | }
97 | .bet span {
98 | font-size: 0.9em;
99 | }
100 | }
101 | .card {
102 | width: 40px;
103 | height: 56px;
104 | margin: 2px;
105 | border-radius: 2px;
106 |
107 | .card-back {
108 | width: 36px;
109 | height: 52px;
110 | margin: 2px;
111 | }
112 | .small-picture {
113 | padding-left: 2px;
114 |
115 | div:last-child {
116 | margin-top: -10px;
117 | }
118 | }
119 | .big-picture {
120 | font-size: 3.5em;
121 | padding-right: 2px;
122 | }
123 | }
124 | .board {
125 | top: 30%;
126 |
127 | .card {
128 | margin: 5px 2px;
129 | }
130 | .chip-pile {
131 | top: -5%;
132 | left: 33%;
133 | }
134 | .card-silhouette {
135 | width: 40px;
136 | height: 56px;
137 | margin: 5px 2px;
138 | border-radius: 2px;
139 | }
140 | }
141 | .table-bg {
142 | .bg-outer {
143 | border-width: 12px;
144 |
145 | .bg-inner {
146 | width: calc(100% - 6px);
147 | height: calc(100% - 6px);
148 | border-width: 3px;
149 | }
150 | }
151 | }
152 | .chips {
153 | width: 17px;
154 |
155 | > div {
156 | height: 3px;
157 | width: 15px;
158 | }
159 | }
160 | .game-chat {
161 | font-size: 1em;
162 |
163 | .messages {
164 | height: 6.5vh;
165 | margin: 5px 5px 0px 5px;
166 | }
167 | .chat-input {
168 | padding: 3px 27px 3px 5px;
169 | }
170 | }
171 | .actions-container {
172 | .actions {
173 | margin: 5px 2px 3px 2px;
174 |
175 | button {
176 | height: 25px;
177 | }
178 | .raise-slider-container {
179 | padding: 5px 3px;
180 | font-size: 1em;
181 |
182 | .raise-slider {
183 | input {
184 | width: 50%;
185 | }
186 | span {
187 | top: -6px;
188 | }
189 | button {
190 | font-size: 0.75em;
191 | width: 20px;
192 | height: 20px;
193 | margin-top: 3px;
194 | }
195 | }
196 | }
197 | .raise-size-buttons {
198 | button {
199 | height: 20px;
200 | font-size: 0.9em;
201 | }
202 | }
203 | }
204 | }
205 | }
--------------------------------------------------------------------------------
/client/app/actions/user.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
3 | import { hashHistory } from 'react-router'
4 | import { push } from 'react-router-redux'
5 |
6 | export const LOGIN_REQUEST = 'LOGIN_REQUEST'
7 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
8 | export const LOGIN_FAILURE = 'LOGIN_FAILURE'
9 |
10 | export const SIGNUP_REQUEST = 'SIGNUP_REQUEST'
11 | export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'
12 | export const SIGNUP_FAILURE = 'SIGNUP_FAILURE'
13 |
14 | export const LOGOUT = 'LOGOUT'
15 |
16 | export const TOKEN_LOGIN_REQUEST = 'TOKEN_LOGIN_REQUEST'
17 | export const TOKEN_LOGIN_SUCCESS = 'TOKEN_LOGIN_SUCCESS'
18 | export const TOKEN_LOGIN_FAILURE = 'TOKEN_LOGIN_FAILURE'
19 |
20 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api'
21 |
22 | // login
23 | export function requestLogin(params) {
24 | return {
25 | type: LOGIN_REQUEST,
26 | params
27 | }
28 | }
29 |
30 | export function loginSuccess(user, token) {
31 | return {
32 | type: LOGIN_SUCCESS,
33 | user,
34 | token
35 | }
36 | }
37 |
38 | export function loginFailure(message) {
39 | return {
40 | type: LOGIN_FAILURE,
41 | message
42 | }
43 | }
44 |
45 | export function login(params) {
46 | return function(dispatch) {
47 | dispatch(requestLogin(params))
48 |
49 | return axios.post(`${ROOT_URL}/login`, params)
50 | .then(res => {
51 | const user = res.data.user
52 | const token = res.data.token
53 |
54 | if (user) {
55 | dispatch(loginSuccess(user, token))
56 | dispatch(push('/lobby'))
57 | } else {
58 | console.log(res)
59 | }
60 | })
61 | .catch(err => {
62 | console.log(err)
63 | dispatch(loginFailure(err))
64 | })
65 | }
66 | }
67 |
68 | // signup
69 | export function requestSignUp(params) {
70 | return {
71 | type: SIGNUP_REQUEST,
72 | params
73 | }
74 | }
75 |
76 | export function signUpSuccess(user, token) {
77 | return {
78 | type: SIGNUP_SUCCESS,
79 | user,
80 | token
81 | }
82 | }
83 |
84 | export function signUpFailure(message) {
85 | return {
86 | type: SIGNUP_FAILURE,
87 | message
88 | }
89 | }
90 |
91 | export function signUp(params) {
92 | return function(dispatch) {
93 | dispatch(requestSignUp(params))
94 |
95 | return axios.post(`${ROOT_URL}/signup`, params)
96 | .then(res => {
97 | const user = res.data.user
98 | const token = res.data.token
99 |
100 | if (user) {
101 | dispatch(signUpSuccess(user, token))
102 | dispatch(push('/lobby'))
103 | } else {
104 | console.log(res)
105 | }
106 | })
107 | .catch(err => {
108 | console.log(err)
109 | dispatch(signUpFailure(err))
110 | })
111 | }
112 | }
113 |
114 | // logout
115 | export function logout() {
116 | return function(dispatch) {
117 | dispatch({ type: LOGOUT })
118 | dispatch(push('/login'))
119 | }
120 | }
121 |
122 | // jwt token verification
123 | export function requestTokenLogin() {
124 | return {
125 | type: TOKEN_LOGIN_REQUEST
126 | }
127 | }
128 |
129 | export function tokenLoginSuccess(user, token) {
130 | return {
131 | type: TOKEN_LOGIN_SUCCESS,
132 | user,
133 | token
134 | }
135 | }
136 |
137 | export function tokenLoginFailure(message) {
138 | return {
139 | type: TOKEN_LOGIN_FAILURE,
140 | message
141 | }
142 | }
143 |
144 | export function tokenLogin(token) {
145 | return function(dispatch) {
146 | dispatch(requestTokenLogin())
147 |
148 | return axios.post(`${ROOT_URL}/verify_jwt`, { token })
149 | .then(res => {
150 | const user = res.data.user
151 | const token = res.data.token
152 |
153 | if (user) {
154 | dispatch(tokenLoginSuccess(user, token))
155 | if (hashHistory.getCurrentLocation().pathname !== '/lobby') {
156 | dispatch(push('/lobby'))
157 | }
158 | } else {
159 | console.log(res)
160 | }
161 | })
162 | .catch(err => {
163 | console.log(err)
164 | dispatch(tokenLoginFailure(err))
165 | })
166 | }
167 | }
--------------------------------------------------------------------------------
/client/app/components/Game/Actions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RaiseSlider from './RaiseSlider'
3 | import PotSizeButton from './PotSizeButton'
4 | import ActionButtons from './ActionButtons'
5 |
6 | import { blueGrey } from 'material-ui/styles/colors'
7 |
8 | const styles = {
9 | container: {
10 | position: 'absolute',
11 | width: 'calc(50vw - 10px)',
12 | height: '184px',
13 | padding: '5px',
14 | left: '0',
15 | bottom: '0',
16 | backgroundColor: blueGrey[100],
17 | border: `1px solid ${blueGrey[100]}`
18 | },
19 | }
20 |
21 | class Actions extends React.Component {
22 | constructor(props) {
23 | super(props)
24 |
25 | const { user, table } = this.props
26 | const seat = Object.values(table.seats).filter(seat =>
27 | seat !== null && seat.player.name === user.username
28 | )[0]
29 |
30 | const raiseAmount = table.minRaise > seat.stack + seat.bet ?
31 | seat.stack + seat.bet : table.minRaise
32 |
33 | this.state = {
34 | raiseAmount: raiseAmount
35 | }
36 |
37 | this.handleRaiseChange = this.handleRaiseChange.bind(this)
38 | }
39 |
40 | handleRaiseChange(e) {
41 | const { table } = this.props
42 | if (e.target.value < table.minRaise || e.target.value > table.maxBet) {
43 | return
44 | }
45 | this.setState({ raiseAmount: e.target.value })
46 | }
47 |
48 | handleRaiseUpdate(amount) {
49 | const { table } = this.props
50 | const seat = this.findOwnSeat()
51 | let newRaiseAmount = parseFloat(amount)
52 |
53 | if (amount < table.minRaise) {
54 | newRaiseAmount = table.minRaise
55 | } else if (amount > seat.stack + seat.bet) {
56 | newRaiseAmount = seat.stack + seat.bet
57 | }
58 |
59 | this.setState({ raiseAmount: newRaiseAmount })
60 | }
61 |
62 | findOwnSeat() {
63 | const { user, table } = this.props
64 | const seat = Object.values(table.seats).filter(seat =>
65 | seat !== null && seat.player.name === user.username
66 | )[0]
67 |
68 | return seat
69 | }
70 |
71 | render() {
72 | const {
73 | table,
74 | onRaiseClick,
75 | onCheckClick,
76 | onCallClick,
77 | onFoldClick
78 | } = this.props
79 |
80 | const seat = this.findOwnSeat()
81 | if (seat.sittingOut || table.handOver) return null;
82 |
83 | const totalCallAmount = table.callAmount - seat.bet > seat.stack ? seat.stack : table.callAmount - seat.bet
84 |
85 | let pot = table.pot
86 | if (table.callAmount) {
87 | pot = table.pot * 2 + totalCallAmount
88 | }
89 |
90 | const potSizes = [
91 | ['Min', table.minRaise],
92 | ['½ pot', pot * 1/2],
93 | ['⅔ pot', pot * 2/3],
94 | ['¾ pot', pot * 3/4],
95 | ['Pot', pot],
96 | ['All in', seat.bet + seat.stack]
97 | ]
98 |
99 | return (
100 |
101 |
onFoldClick(table.id)}
107 | handleCheckClick={() => onCheckClick(table.id)}
108 | handleCallClick={() => onCallClick(table.id)}
109 | handleRaiseClick={() => onRaiseClick(table.id, parseFloat(this.state.raiseAmount))}
110 | />
111 |
112 | {seat.stack > table.callAmount &&
113 | this.handleRaiseUpdate(this.state.raiseAmount - table.minBet)}
116 | increaseRaiseAmount={() => this.handleRaiseUpdate(this.state.raiseAmount + table.minBet)}
117 | onRaiseChange={this.handleRaiseChange}
118 | table={table}
119 | seat={seat}
120 | />
121 | }
122 |
123 | {seat.stack > table.callAmount &&
124 |
125 | {potSizes.map(potSize =>
126 |
this.handleRaiseUpdate(potSize[1])}
130 | />
131 | )}
132 |
133 | }
134 |
135 | )
136 | }
137 | }
138 |
139 | export default Actions
--------------------------------------------------------------------------------
/client/app/components/HandHistory/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react'
3 | import { connect } from 'react-redux'
4 | import { css } from 'emotion'
5 |
6 | import { fetchHandHistory } from '../../actions/hands'
7 | import Icon from 'material-ui/Icon'
8 | import Hand from './Hand'
9 | import { Panel, Button } from 'app/components';
10 | import theme from 'app/utils/theme';
11 |
12 | const container = css`
13 | height: calc(100% - 170px);
14 | padding: 80px 20px;
15 | `;
16 | const link = css`
17 | display: flex;
18 | justify-content: space-between;
19 | margin-bottom: 10px;
20 | &:hover {
21 | cursor: pointer;
22 | color: ${theme.colors.blue};
23 | }
24 | `;
25 | const handList = css`
26 | position: relative;
27 | width: 450px;
28 | height: 100%;
29 | `
30 | const pagination = css`
31 | width: calc(100% - 48px);
32 | position: absolute;
33 | bottom: 16px;
34 | font-size: 12px;
35 | display: flex;
36 | justify-content: space-between;
37 | align-items: center;
38 | `;
39 | const paginationButton = {
40 | padding: '2px 4px',
41 | }
42 |
43 | type Props = {
44 | hands: Array