Skip to content
Tic Tac Toe made with React and ActionCable
Ruby JavaScript HTML CSS CoffeeScript
Branch: master
Clone or download
Latest commit eca05d1 May 7, 2017
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
app Highlight the next player or winner in the StatusLabel May 7, 2017
bin Initial commit Apr 21, 2017
client Highlight the next player or winner in the StatusLabel May 7, 2017
config ActionCable setup for Heroku Apr 22, 2017
db Add tic tac toe React and game model Apr 22, 2017
lib
log Initial commit Apr 21, 2017
public Initial commit Apr 21, 2017
test Add tic tac toe React and game model Apr 22, 2017
tmp Initial commit Apr 21, 2017
vendor/assets Initial commit Apr 21, 2017
.gitignore
.ruby-version Initial commit Apr 21, 2017
Gemfile Use font awesome icon for restart button Apr 24, 2017
Gemfile.lock Use font awesome icon for restart button Apr 24, 2017
Procfile
Procfile.dev Actual react_on_rails install Apr 21, 2017
README.md Update README May 7, 2017
Rakefile Initial commit Apr 21, 2017
config.ru Initial commit Apr 21, 2017
package.json
yarn.lock ActionCable working connection Apr 22, 2017

README.md

Tic Tac Toe

React Tic Tac Toe gif

Demo

Built With

  • React - Used for the views
  • Ruby on Rails - Mainly used as an API to communicate with React containers
  • Bootstrap 4 - Used Reactstrap to style React components
  • D3 - Used for the donut chart on stats page
  • React Motion - Used to animate the donut chart updates

ActionCable and React Code

When someone wins a game, a POST request is sent to games#create

// client/app/bundles/TicTacToe/containers/GameContainer.jsx

componentDidUpdate(prevProps, prevState) {
  const current = this.state.history[this.state.stepNumber];
  const winner = this.calculateWinner(current.squares);
  if (winner.name) {
    $.ajax({
      url:'/games',
      type:'POST',
      dataType:'json',
      data:{
          game: {winner: winner.name,  history: this.state.history}
      }
    });
  }
}

On successful save, the create action broadcasts updated stats to the games ActionCable channel

# app/controllers/games_controller.rb

def create
  @game = Game.new(game_params)

  if @game.save
    ActionCable.server.broadcast 'games',
      winner: @game.winner,
      stats: Game.stats
    render json: @game, status: :created, location: @game
  else
    render json: @game.errors, status: :unprocessable_entity
  end
end

The StatsContainer receives the updated stats and passes them to the Stats component

// client/app/bundles/Stats/containers/StatsContainer.jsx

componentWillMount() {
  var self = this;
  if (typeof App !== 'undefined'){
    App.messages = App.cable.subscriptions.create('GamesChannel', {  
      received: function(data) {
        return self.setState({ data: data.stats });
      }
    });
  }
}

render() {        
  return <Stats data={this.state.data} />
}

The Stats component passes the new data to the D3DonutChart component

// client/app/bundles/Stats/components/Stats.jsx

<Container>
  <Row>
    <Col lg={{ size: 4, offset: 4 }} className="align-self-center">
      <h3 className="stats-label text-center">Wins</h3>
      <div className="chart-wrapper text-center">
        <D3DonutChart data={this.props.data} />
      </div>
      <ChartLabel label="X" data={this.props.data[0]} />
      <ChartLabel label="O" data={this.props.data[1]} />
      <NavLink name="game" link="/" />
    </Col>
  </Row>
</Container>

Where it is finally rendered using D3 and React Motion

// client/app/bundles/Stats/components/D3DonutChart.jsx

render() {
  var width = 290,
      height = 290,
      radius = Math.min(width, height) / 2;

  var color = d3.scale.ordinal()
      .range(["#3299BB", "#FF9900"]);

  var pie = d3.layout.pie()
      .value(d => d)
      .sort(null);

  var arc = d3.svg.arc()
      .innerRadius(radius - 100)
      .outerRadius(radius - 20);

  var displayedData = pie(this.props.data);

  return (
    <svg width={width} height={height}>
      <g transform={"translate(" + width / 2 + "," + height / 2 + ")"}>
        {displayedData.map((slice, i) =>
          <Motion
            key={i}
            defaultStyle={{
              startAngle: slice.startAngle,
              endAngle: slice.endAngle,
              padAngle: slice.padAngle,
            }}
            style={{
              startAngle: spring(slice.startAngle),
              endAngle: spring(slice.endAngle),
              padAngle: spring(slice.padAngle)
            }}>{value => <path
              fill={color(i)}
              d={arc(value)} />
            }</Motion>
        )}
      </g>
    </svg>
  );
}

Getting Started

Prerequisites

ruby -v             # 2.3.4
node -v             # 7.9.0
brew install yarn
rails -v            # 5.0.2
gem install foreman

Installing

Install dependencies

bundle && yarn

Setup database

rails db:setup

Run server

foreman start -f Procfile.dev

Play some tic tac toe

You can’t perform that action at this time.