LoginSignup
2
1

More than 3 years have passed since last update.

Rechartsを使って円グラフを表示させるポスト詳細画面の実装【初学者のReact×Railsアプリ開発 第11回】

Posted at

やったこと

  • Reactでポスト詳細画面を実装した。
  • rechartsを用いて円グラフを表示させた。
  • ログイン中のユーザー情報によって、表示をコントロールした。
  • 詳細画面の中で、投票の変更を行えるようにした。

成果物

jqe0k-3qqon.gif

Rails実装手順

route.rb

route.rb
Rails.application.routes.draw do
 namespace :api, defaults: { format: :json } do
    namespace :v1 do

      delete 'posts/:id', to: 'posts#destroy'

    end
 end
 root 'home#about'
end

likes_controller, posts_controller

すでに実装済み。
Ruby on Rails APIモードのCRUD実装 【初学者のReact✗Railsアプリ開発 第5回】
Ruby on Rails APIモードでいいね機能を実装する【初学者のReact×Railsアプリ開発 第6回】

React実装手順

App.js(ルーティング)

App.js
import PostsDetail from './containers/PostDetail';
            <Auth>
              <Switch>
                <Route exact path="/" component={Home} />
                <Route path='/create' component={Create} />
                <Route path='/postslist' component={PostsList} />
                <Route exact path="/posts/:id" component={PostsDetail} />
              </Switch>
            </Auth>

containers/PostDetail.js(render)

  • 条件によって何を表示するかを分けています。
  • renderGraphWithConditionでは、投票数が1票以上あるときと0票のときで表示内容を分けています。
  • renderButtonWithConditionでは、ログイン中ユーザーのその投稿に対する投票情報で表示を分けています。
  • renderDeleteButtonでは、自分が作成した投稿のとき削除ボタンを表示します。
  • Scrollbars: react-custom-scrollbarsモジュールはめちゃ便利。
PostDetail.js
  render() {
    const { CurrentUserReducer } = this.props;
    const isloggedin = CurrentUserReducer.isLoggedin;

    const { classes } = this.props;
    return (
      <Scrollbars>
        <div className={classes.textLeft}>

          {this.renderGraphWithCondition(this.state.all_count)}
          {this.renderButtonWithCondition(this.state.user_answer_suki)}
          {this.renderDeleteButton()}

        </div>
      </Scrollbars>
    );
  }

containers/PostDetail.js(function)

PostDetail.js
  constructor(props) {
    super(props);
    this.state = {
      user_answer_suki: []
    };
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid
    const { CurrentUserReducer } = this.props;
    axios.get(process.env.REACT_APP_API_URL + `/api/v1/posts/${this.props.match.params.id}`, {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then((response) => {
        const postdata = response.data.data;
        this.setState({
          suki_percent: postdata.post.suki_percent,
          kirai_percent: 100 - postdata.post.suki_percent,
          suki_count: postdata.post.suki_count,
          kirai_count: postdata.post.kirai_count,
          content: postdata.post.content,
          created_at: postdata.post.created_at,
          all_count: postdata.post.all_count,
          username: postdata.user.name
        });
      })
      .catch(() => {
        this.props.history.push('/')
      });

    axios.get(process.env.REACT_APP_API_URL + `/api/v1/likes/post/${this.props.match.params.id}/user/${uid}`, {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then((response) => {
        const answereddata = response.data.data;
        this.setState({
          user_answer_suki: answereddata.suki,
          user_answer_updatedat: answereddata.updated_at,
        })
      })

    this.ChangeLike = this.ChangeLike.bind(this);
    this.DeletePost = this.DeletePost.bind(this);
    this.submitLike = this.submitLike.bind(this);

  }

  renderGraphWithCondition(all_count) {
    const { classes } = this.props;
    if (all_count != 0) {
      return (
        <Paper className={classes.root} elevation={1}>
          <Typography variant="headline" component="h1" className={classes.content}>
            {this.state.content}
          </Typography>
          <Typography component="p" style={{ fontWeight: 'bold' }}>
            created by {this.state.username}
          </Typography>
          <PieChart suki_percent={this.state.suki_percent} kirai_percent={this.state.kirai_percent} />
          <Typography component="p" style={{ fontWeight: 'bold', fontSize: '1.2rem' }}>
            スキ: {this.state.suki_percent}% ({this.state.suki_count})
            </Typography>
          <Typography component="p" style={{ fontWeight: 'bold', fontSize: '1.2rem' }}>
            キライ: {this.state.kirai_percent}% ({this.state.kirai_count})
            </Typography>
          <Typography component="p" style={{ fontWeight: 'bold' }}>
            投票数: {this.state.all_count}
            </Typography>
        </Paper>
      )
    } else {
      return (
        <Paper className={classes.root} elevation={1}>
          <Typography variant="headline" component="h1" className={classes.content}>
            {this.state.content}
          </Typography>
          <Typography component="p" style={{ fontWeight: 'bold' }}>
            created by {this.state.username}
          </Typography>
          <Typography component="p" style={{ fontWeight: 'bold' }}>
            まだ誰も投票してません
          </Typography>
          <Typography component="p" style={{ fontWeight: 'bold' }}>
            投票数: {this.state.all_count}
            </Typography>
        </Paper>
      )
    }
  }

  renderButtonWithCondition(user_answer_suki) {
    const { classes } = this.props;
    if (user_answer_suki == 3) {
      return (
        <Paper className={classes.root}>
          <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={() => this.submitLike(1)}>
            スキ
          </Button>
          <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.submitLike(0)}>
            キライ
          </Button>
        </Paper>
      )
    } else if (user_answer_suki == 2) {
      return (
        <Paper className={classes.root}>
          <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={() => this.ChangeLike(1)}>
            スキ
          </Button>
          <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(0)}>
            キライ
          </Button>
        </Paper >
      )
    } else if (user_answer_suki == 1) {
      return (
        <Paper className={classes.root}>
          スキで回答済み
            <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(0)}>
            キライに変更する
            </Button>
        </Paper>
      )
    } else if (user_answer_suki == 0) {
      return (
        <Paper className={classes.root}>
          キライで回答済み
            <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(1)}>
            スキに変更する
            </Button>
        </Paper>

      )
    }
  }

  renderDeleteButton() {
    const { CurrentUserReducer } = this.props;
    const { classes } = this.props;
    if (CurrentUserReducer.items.name === this.state.username) {
      return (
        <Button variant="contained" size="large" color="primary" className={classes.button} onClick={this.DeletePost}>
          このテーマを削除する
        </Button>
      )
    } else {
    }
  }

  ChangeLike(suki) {
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid
    axios.put(process.env.REACT_APP_API_URL + `/api/v1/likes/post/${this.props.match.params.id}`,
      {
        'suki': suki
      },
      {
        headers: {
          'access-token': auth_token,
          'client': client_id,
          'uid': uid
        }
      })
      .then((response) => {
        const postdata = response.data.data;
        this.setState({
          suki_percent: postdata.post.suki_percent,
          kirai_percent: 100 - postdata.post.suki_percent,
          suki_count: postdata.post.suki_count,
          kirai_count: postdata.post.kirai_count,
          content: postdata.post.content,
          created_at: postdata.post.created_at,
          all_count: postdata.post.all_count,
          username: postdata.user.name
        });
        const answereddata = response.data.data.like;
        this.setState({
          user_answer_suki: answereddata.suki,
          user_answer_updatedat: answereddata.updated_at,
        })
      })
  }

  DeletePost() {
    const { CurrentUserReducer } = this.props;
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid
    axios.delete(process.env.REACT_APP_API_URL + `/api/v1/posts/${this.props.match.params.id}`,
      {
        headers: {
          'access-token': auth_token,
          'client': client_id,
          'uid': uid
        }
      })
    window.history.back(-2)
  }

  submitLike(suki) {
    const { CurrentUserReducer } = this.props;
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid
    const data = {
      user_id: CurrentUserReducer.items.id,
      post_id: this.props.match.params.id,
      suki: suki,
    }
    axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then((response) => {
        const postdata = response.data.data;
        this.setState({
          suki_percent: postdata.post.suki_percent,
          kirai_percent: 100 - postdata.post.suki_percent,
          suki_count: postdata.post.suki_count,
          kirai_count: postdata.post.kirai_count,
          content: postdata.post.content,
          created_at: postdata.post.created_at,
          all_count: postdata.post.all_count,
          username: postdata.user.name
        });
        const answereddata = response.data.data.like;
        this.setState({
          user_answer_suki: answereddata.suki,
          user_answer_updatedat: answereddata.updated_at,
        })
      })
  }

components/SimplePieChart.js

SimplePieChart.js
import React, { PureComponent } from 'react';
import {
  PieChart, Pie, Sector, Cell,
} from 'recharts';

const COLORS = ['#FF8042', '#0088FE',];

const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({
  cx, cy, midAngle, innerRadius, outerRadius, percent, index, name
}) => {
  const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
  const x = cx + radius * Math.cos(-midAngle * RADIAN);
  const y = cy + radius * Math.sin(-midAngle * RADIAN);

  return (
    <text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central" style={{ fontWeight: 'bold', whiteSpace: 'pre-line' }}>
      {`${(percent * 100).toFixed(0)}%`}
    </text>
  );
};

export default class SimplePieChart extends PureComponent {
  //static jsfiddleUrl = 'https://jsfiddle.net/alidingling/c9pL8k61/';

  constructor(props) {
    super(props)
  }

  render() {
    const { suki_percent, kirai_percent } = this.props;
    const data = [
      { name: 'スキ', value: suki_percent },
      { name: 'キライ', value: kirai_percent },
    ];
    return (
      <PieChart width={300} height={300}>
        <Pie
          startAngle={90}
          endAngle={-270}
          data={data}
          cx={120}
          cy={120}
          labelLine={false}
          label={renderCustomizedLabel}
          outerRadius={100}
          fill="#8884d8"
          dataKey="value"
        >
          {
            data.map((entry, index) => <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />)
          }
        </Pie>
      </PieChart>
    );
  }
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1