3
3

More than 3 years have passed since last update.

Tinder風UIで「好き」「嫌い」を投票できる画面を実装する【React×Railsアプリ開発 第8回】

Posted at

やったこと

  • Tinder風UIで投稿に対して、「好き」か「嫌い」が投票できる画面を実装した。
  • モジュールはreact-swipe-card-chsstmを使って、Tinder風UIを実装した。

今回の成果

v1n1g-k17jj.gif

できなかったこと

  • 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;
}
3
3
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
3
3