やったこと
- Reactでポスト詳細画面を実装した。
- rechartsを用いて円グラフを表示させた。
- ログイン中のユーザー情報によって、表示をコントロールした。
- 詳細画面の中で、投票の変更を行えるようにした。
成果物
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)
- 難しいことはやっていないけど、コードを書き切るのはなかなか大変でした。
- ボタンを押したときに呼び出す関数に引数を渡したい。記述例:
<Button onClick={() => this.ChangeLike(1)}>
(【React】イベントハンドラで引数を使いたい【備忘録】)
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
- 円グラフを表示させるモジュールとして、rechartsを用いました。
- rechartsのコードはこちらを参考にしました。http://recharts.org/en-US/examples/PieChartWithCustomizedLabel
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>
);
}
}