React.js - Change image based on door puzzle answer

Viewed 315

I've made this little game in React.js:

Demo: https://door-game.netlify.com/ App.js file: https://github.com/Blazej6/Door-game/blob/master/src/App.js

I want to render a picture in the center button that matches the choosen framework. 3 Vue renders vue, 3 react - react etc.

How do I make the logic to do that?

Did some experimental approches, like placing a color class anchor inside app and circle components but it seems to not reading current state at all, at least not from current angle, also tried to actualy use react router and encolse circle component in a link, but that really screws up the css for whatever reason

Is there really no one up to the task?

2 Answers

For a simple app like this, there is no need to integrate redux/mobx yet. What I recommend is something that is very common in React, and that is to lift your state up.

We can accomplish this through three steps:

  1. Dumb down the Circleone, Circletwo, Circlethree components. They only need to know what the current angle is in order to render

  2. ClawCircle should be told what image to render (or otherwise blank)

  3. App needs to hold the state for all this information (and thus we've "lifted" the state up from CircleX to its parent, App).

Step 1

Instead of holding the currentAngle in the state, let's assume that information is given to us through the prop currentAngle. When a circle gets clicked, we'll just tell whoever created the circle that we were clicked on, because they will pass us a prop called onClick.

Since we now don't need to keep track of our state, we can make the component stateless and just turn it into a functional component.

For example, CircleOne might turn out to look more like this:

const CircleOne = ({ currentAngle, onClick }) => (
  <div
    className="App-logo small-logo"
    alt="logo"
    style={{ transform: `rotateZ(${currentAngle}deg)` }}
    onClick={onClick}
  >
    <div className="little-circle one react">
    {/* ... rest of your divs */}
  </div>
);

Step 2

Next, let's change ClawCircle, we'll give it an optional imageClass prop that might be claw-react, claw-vue etc, or it might just be an empty string (update css accordingly to render the image too!). So the render method might change into this:

render() {
  const circleStyle = { transform: `rotateZ(${this.props.currentAngle}deg)` };
  return (
    <div
      className={`App-logo claw-circle ${this.props.imageClass}`}
      alt="logo"
      style={circleStyle}
      onClick={this.rotateCircle.bind(this)}
    />
  );
}

By the way, the bind call can be done in the constructor instead of the render method, this way we don't have to re-bind every time the component re-renders.

constructor(props) {
  super(props);
  // constructor code
  this.rotateCircle = this.rotateCircle.bind(this);
}

// later: onClick={this.rotateCircle}

Step 3

This is the more complicated step, as we now have to delegate the heavy work to App instead of the individual Circles.

So App needs to know the angles of each individual circle, and handle what happens when each circle is clicked. Furthermore, when angles change, we want to check if all three of them are equal. If they are equal, we need to tell ClawCircle what image to render.

All in all, it would probably look something like this:

EDIT: I should have probably tried running this code before writing it on the fly here. Here's the full version (tested!) Just make sure you have claw-react claw-vue and claw-angular rules in your CSS

import React, { Component } from 'react';
import './App.css';
import { CSSTransitionGroup } from 'react-transition-group';

class HalfCircle extends Component {      
  render() {
    return (
          <div className="App-logo half-circle" alt="logo">
          </div>
    );
  }
}

const Circleone = ({ currentAngle, onClick }) => (
  <div
    className="App-logo small-logo"
    alt="logo"
    style={{ transform: `rotateZ(${currentAngle}deg` }}
    onClick={onClick}
  >
    <div className="little-circle one react"></div>
    <div className="little-circle two angular"></div>
    <div className="little-circle three vue"></div>
  </div>
);


const Circletwo = ({ currentAngle, onClick }) => (
  <div
    className="App-logo big-logo"
    alt="logo"
    style={{ transform: `rotateZ(${currentAngle}deg` }}
    onClick={onClick}
  >
    <div className="little-circle un react"></div>
    <div className="little-circle dos angular"></div>
    <div className="little-circle tres vue"></div>
  </div>
);


const Circlethree = ({ currentAngle, onClick }) => (
  <div
    className="App-logo biggest-logo"
    alt="logo"
    style={{ transform: `rotateZ(${currentAngle}deg` }}
    onClick={onClick}
  >
    <div className="little-circle ein react"></div>
    <div className="little-circle zwei angular"></div>
    <div className="little-circle drei vue"></div>
  </div>
);

class ClawCircle extends Component {      
  constructor(props){
    super(props)
    this.state = {
      currentAngle: 45,
      anglePerClick: 360,
    }
  }  
  rotateCircle() {
    const { currentAngle, anglePerClick } = this.state;
    this.setState({ 
      currentAngle: currentAngle + anglePerClick 
    })
  }
  render() {
    const circleStyle = {
      transform: `rotateZ(${this.state.currentAngle}deg)`
    }

    return (
      <div
        className={`App-logo claw-circle ${this.props.imageName}`}
        alt="logo"
        style={circleStyle}
        onClick={this.rotateCircle.bind(this)}
      />
    );
  }
}

const getNameForAngle = (one, two, three) => {
  if (one === two && one === three) {
    switch(one) {
      case 120:
        return 'claw-react';
      case 240:
        return 'claw-vue';
      case 360:
        return 'claw-angular';
      default:
        return '';
    }
  }

  return '';
};

class App extends Component {  

  constructor(props) {
    super(props);

    this.state = {
      oneAngle: 120,
      twoAngle: 120,
      threeAngle: 120,
    };

    this.handleOneClick = this.handleOneClick.bind(this);
    this.handleTwoClick = this.handleTwoClick.bind(this);
    this.handleThreeClick = this.handleThreeClick.bind(this);
  }

  handleClick(circle) {
    const nextAngle = this.state[circle] + 120;
    this.setState ({
      [circle]: nextAngle
    });
  }

  handleOneClick() {
    this.handleClick('oneAngle');
  }

  handleTwoClick() {
    this.handleClick('twoAngle');
  }

  handleThreeClick() {
    this.handleClick('threeAngle');
  }

  render() {  
    const { oneAngle, twoAngle, threeAngle } = this.state;

    const imageName = getNameForAngle(oneAngle, twoAngle, threeAngle);

    return (
      <div className="App">
        <header className="App-header">
          <Circleone
            currentAngle={oneAngle}
            onClick={this.handleOneClick}
          />
          <Circletwo
            currentAngle={twoAngle}
            onClick={this.handleTwoClick}
          />
          <Circlethree
            currentAngle={threeAngle}
            onClick={this.handleThreeClick}
          />    
          <ClawCircle imageName={imageName} />
          <HalfCircle/>
        </header>
      </div>
    );
  }
}

export default App;

Ok it seems that components encapsulation really disfavors this king of fun in here, anyway I got the app working with pure js, all hail global variables!

Here is the codepen if anyone needs it: https://codepen.io/Raitar/pen/OOWRzb

And of course the JS code:

var angle=0;
var angle2=0;
var angle3=0;

count = 0;
count2 = 0;
count3 = 0;

document.getElementById("small-logo").addEventListener("click", rotateCircle)
document.getElementById("big-logo").addEventListener("click", rotateCircle2)
document.getElementById("biggest-logo").addEventListener("click", rotateCircle3)

function rotateCircle(){
angle+=120;
this.style.webkitTransform="rotate("+angle+"deg)";
count += 1;
if (count > 2) {
  count = 0;
}
}  
function rotateCircle2(){
angle2+=120;
this.style.webkitTransform="rotate("+angle2+"deg)";
count2 += 1;
if (count2 > 2) {
  count2 = 0;
}
}
function rotateCircle3(){
angle3+=120;
this.style.webkitTransform="rotate("+angle3+"deg)";
count3 += 1;
if (count3 > 2) {
  count3 = 0;
}
}

angular = "background-image: 
url(https://raw.githubusercontent.com/Blazej6/Door-
game/master/src/img/angular.png);"
react = "background-image: 
url(https://raw.githubusercontent.com/Blazej6/Door-
game/master/src/img/react.png);"
vue = "background-image: url(https://raw.githubusercontent.com/Blazej6/Door-
game/master/src/img/vue.png);"

document.getElementById("claw-circle").addEventListener("click", 
changeCenter)
var x = document.getElementById("claw-circle")


function changeCenter() {
if (count == 0 && count2 == 0 && count3 == 0) {
x.style.cssText = angular;
} else if(count == 1 && count2 == 1 && count3 == 1) {
x.style.cssText = react;  
} else if(count == 2 && count2 == 2 && count3 == 2) {
x.style.cssText = vue;  
}
}
Related