やったこと
- Tinder風UIで投稿に対して、「好き」か「嫌い」が投票できる画面を実装した。
- モジュールはreact-swipe-card-chsstmを使って、Tinder風UIを実装した。
今回の成果
できなかったこと
- react-swipe-card-chsstmのコードを修正して使いたかったが、どうしても修正後のモジュールが上手くインストールできなかった。
- Github上でForkしたあとにコード修正、再度インストールまでは良かったが、トランスパイル(コンパイル)が上手くいってないのかな〜...
実装手順
モジュールインストール
ここでインストール済み。npm install react-swipe-card-chsstmで行けるはず。
Rails APIの調整
- ログイン中ユーザーが「好き」か「嫌い」か投票していないポストをランダムで10個ずつ返すAPIを作る
- answered_posts_idでログイン中ユーザーが「投票した」ポストのidを呼び出している。
- Posw.where.not("id ~ で、投票したpost_id以外のidのポストの中から10個をランダムで返している。
users_controllerにnot_answered_postsを追加する
users_controller.rb
def not_answered_posts
@user = current_api_v1_user
answered_posts_id = "SELECT post_id from likes WHERE user_id = :user_id"
not_answered_posts = Post.where.not("id IN (#{answered_posts_id})", user_id: @user.id).order('RANDOM()').limit(10)
json_data = {
'posts': not_answered_posts,
}
render json: { status: 'SUCCESS', message: 'Loaded the not_answered_posts', data: json_data}
end
React
Home.js
- CardのonSwipeLeft、onSwipeRightで左右にスワイプ時に呼び出す関数を指定している。
- submitSuki, submitKiraiで「好き」「嫌い」を投票している。
- 再読み込みボタンで、リロードし、追加のnot_answered_posts(ログイン中のユーザーがまだ投票していないポスト)を10個ずつ読み込んでいる。
Home.js
import React from 'react';
import PropTypes from 'prop-types';
import './HomeStyles.css'
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import queryString from 'query-string';
import _ from 'lodash';
import axios from 'axios';
import Cards, { Card } from 'react-swipe-card-chsstm';
import "normalize.css";
//import "./styles.css";
const styles = theme => ({
// ヘッダーロゴ
homeimg: {
height: '20%',
width: '60%',
display: 'block',
margin: 'auto',
},
conceptimg: {
display: 'flex',
width: '80%',
display: 'block',
margin: '10px auto',
},
button: {
margin: '0px 5px',
},
sukibutton: {
margin: '0px 15px',
backgroudColor: '#000000',
},
kiraibutton: {
margin: '0px 15px',
backgroudColor: '#ffffff'
},
});
class Home extends React.Component {
constructor(props) {
super(props)
this.state = {
not_answered_posts: []
}
const auth_token = localStorage.auth_token
const client_id = localStorage.client_id
const uid = localStorage.uid
//新着順
axios.get(process.env.REACT_APP_API_URL + `/api/v1/not_answered_posts`, {
headers: {
'access-token': auth_token,
'client': client_id,
'uid': uid
}
})
.then((response) => {
const data = response.data.data;
this.setState({
not_answered_posts: data.posts,
});
})
.catch(() => {
});
this.reload = this.reload.bind(this);
}
reset = () => {
this.setState(state => ({
id: state.id + 1
}));
};
componentDidMount() {
}
submitSuki(post) {
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: post.id,
suki: 1,
}
axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, {
headers: {
'access-token': auth_token,
'client': client_id,
'uid': uid
}
})
.then(response => { })
.catch(error => { })
}
submitKirai(post) {
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: post.id,
suki: 0,
}
axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, {
headers: {
'access-token': auth_token,
'client': client_id,
'uid': uid
}
})
.then(response => { })
.catch(error => { })
}
reload() {
const auth_token = localStorage.auth_token
const client_id = localStorage.client_id
const uid = localStorage.uid
//新着順
axios.get(process.env.REACT_APP_API_URL + `/api/v1/not_answered_posts`, {
headers: {
'access-token': auth_token,
'client': client_id,
'uid': uid
}
})
.then((response) => {
const data = response.data.data;
var not_answered_posts = this.state.not_answered_posts
data.posts.forEach(post => {
not_answered_posts.push(post);
});
this.setState({
not_answered_posts: not_answered_posts,
});
})
.catch(() => {
});
}
render() {
const { CurrentUserReducer } = this.props;
const { classes } = this.props;
let cards;
return (
<div className="home">
<div className="background">
<Button size="large" variant="contained" color="blue" onClick={this.reload} className={classes.sukibutton}>
再読み込み
</Button>
</div>
<Cards
onEnd={this.endSwipe}
className="master-root"
likeOverlay={<h1>スキ</h1>}
dislikeOverlay={<h1>キライ</h1>}
ref={(ref) => cards = ref}
>
{this.state.not_answered_posts.map(item =>
<Card
onSwipeLeft={() => this.submitKirai(item)}
onSwipeRight={() => this.submitSuki(item)}>
<h2>{item.content}</h2>
</Card>
)}
</Cards>
<div className="buttonArea">
<Button size="large" variant="contained" color="blue" onClick={() => { cards.dislike(); }} className={classes.sukibutton}>
キライ
</Button>
<Button size="large" variant="contained" color="red" onClick={() => { cards.like(); }} className={classes.kiraibutton}>
スキ
</Button>
</div>
</div >
)
}
}
Home.propTypes = {
classes: PropTypes.object.isRequired,
post: PropTypes.object.isRequired,
};
const mapState = (state, ownProps) => ({
CurrentUserReducer: state.CurrentUserReducer,
});
function mapDispatch(dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapState, mapDispatch)(
withStyles(styles, { withTheme: true })(Home)
);
HomeStyles.css
- Home.jsのスタイルを別途HomeStyles.cssに記述しています。
- これ、結構うまくやらないとちゃんと配置してくれなくて苦労しました。Cardを10枚ずつ重ねて表示していて、z-indexを使っているから、ここで配置がずれたりする。
- ここを参考にしました。https://github.com/chsstm/react-swipe-card/blob/master/stories/style.css
HomeStyles.css
html {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
body {
margin: 0 auto;
padding: 0;
font-family: sans-serif;
text-align: center;
}
img {
width: 100%;
}
li {
list-style: none;
}
h2 {
text-align: left;
word-wrap: break-word;
margin: 20px;
font-size: 20pt;
}
.home {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
min-height: 500px;
}
.master-root {
margin: 10px 0px;
position: absolute;
height: 50%;
width: 100%;
/* z-index:2; */
}
.card {
background-color: white;
background-size: cover;
position: absolute;
left: 0;
right:0;
top:0;
bottom:0;
background: #f8f3f3;
height: 80%;
width: 80%;
margin: auto;
transition: box-shadow 0.3s;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
border: 10px solid #212121;
cursor: pointer;
}
.buttonArea{
position: absolute;
bottom: 20%;
width: 100%;
margin: 0 auto;
}
.animate {
transition: transform 0.3s;
box-shadow: none;
}
.inactive {
box-shadow: none;
}
.alert {
width: 45%;
min-height: 10%;
position: absolute;
z-index: 9999;
opacity: 0;
transition: opacity 0.5s;
color: white;
vertical-align: middle;
line-height: 3rem;
}
.alert-visible {
opacity: 1;
}
.alert-right {
top: 0;
right: 0;
background: red;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
}
.alert-left {
top: 0;
left: 0;
background: blue;
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
}
.alert-top {
background: black;
border-radius: 50px;
transform: translate(-50%, 0);
margin-left: 50%;
}
.alert-bottom {
bottom: 0;
background: black;
border-top-left-radius: 50px;
border-radius: 50px;
transform: translate(-50%, 0);
margin-left: 50%;
}
.action-button {
cursor: pointer;
padding: 10px 50px;
border: none;
outline: none;
background-color: lightgrey;
margin: 0px 10px;
}
.action-button:active {
background-color: black;
color: white;
}