Skip to content

Instantly share code, notes, and snippets.

@awatson1978
Created April 28, 2016 13:48
Show Gist options
  • Save awatson1978/34b00e263824ac53171e249266e34662 to your computer and use it in GitHub Desktop.
Save awatson1978/34b00e263824ac53171e249266e34662 to your computer and use it in GitHub Desktop.
React Swipe Card Component
import React from 'react';
import ProfileInfo from './ProfileInfo.jsx';
const DRAG_THRESHOLD_PERCENTAGE = 30;
class VoteProfile extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.state = {
// indicates, if the user is moving his finger on the screen
isDragging: false,
// indicates, where the user put a finger on the screen, to compute the offset while he moves it
startPosition: [0, 0],
// relative position of the finger from the original position, where it was placed on the screen
offset: {x: 0, y: 0},
// while swiping to the side, the card can either rotate up or down, depending on the direction that
// the user moved his finger first
dragDirectionLocked: false,
// 1 = "up", -1 = "down"
dragDirection: 1
};
}
componentDidMount() {
const {user} = this.props;
const voteProfile = document.getElementById('vote-profile-' + user._id);
voteProfile.addEventListener('touchstart', this.handleMouseDown);
window.addEventListener('touchmove', this.handleMouseMove);
window.addEventListener('touchend', this.handleMouseUp);
voteProfile.addEventListener('mousedown', this.handleMouseDown);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
}
componentWillUnmount() {
const {user} = this.props;
const voteProfile = document.getElementById('vote-profile-' + user._id);
voteProfile.removeEventListener('touchstart', this.handleMouseDown);
window.removeEventListener('touchmove', this.handleMouseMove);
window.removeEventListener('touchend', this.handleMouseUp);
voteProfile.removeEventListener('mousedown', this.handleMouseDown);
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
}
/**
* On mouse down (touch screen), save the absolute position of the finger as a starting position.
* Also save the time to compute the duration, that the finger was placed on the screen.
*/
handleMouseDown(e) {
e.preventDefault();
if (e.type === 'touchstart') {
e = e.touches[0];
}
this.setState({
clickAt: new Date().getTime(),
isDragging: true,
startPosition: [e.pageX, e.pageY]
});
}
/**
* Whenever the finger moves, compute and save the relative position of the finger to the starting position (offset).
* If it's the first time user moved his finger since he touched the screen,
* determine if the direction is up or down and save it.
*/
handleMouseMove(e) {
e.preventDefault();
if (e.type === 'touchmove') {
e = e.touches[0];
}
const {isDragging, startPosition: [dx, dy], dragDirectionLocked} = this.state;
if (isDragging) {
const offset = {x: e.pageX - dx, y: e.pageY - dy};
// if direction of the rotation wasn't set and locked yet, set and lock it
if (dragDirectionLocked) {
this.setState({offset});
} else {
this.setState({
offset: offset,
dragDirection: offset.y < 0 ? -1 : 1,
dragDirectionLocked: true
});
}
}
}
/**
* When the finger leaves the screen, compute the duration of the finger touching the screen.
* If the finger left the screen after less than 100 ms, consider it a click, not a swipe.
*/
handleMouseUp() {
const {clickAt} = this.state;
const now = new Date().getTime();
if (clickAt) {
if (now - clickAt > 100) {
this.handleVote();
} else {
this.handleClick();
}
}
}
/**
* Compute, how many percent of the screen width the finger moved from it's starting position.
* If it covered at least DRAG_THRESHOLD_PERCENTAGE percent, consider it a valid vote.
*/
handleVote() {
const {offset} = this.props;
const windowWidth = window.innerWidth;
const percentage = (Math.abs(offset.x) / windowWidth) * 100;
if (percentage >= DRAG_THRESHOLD_PERCENTAGE) {
const {user, actions} = this.props;
const like = offset.x > 0;
this.resetState();
actions.vote(user._id, like);
} else {
this.resetState();
}
}
/**
* In case of a click, redirect to a user's profile detail.
*/
handleClick() {
const {user} = this.props;
const {push} = this.props.actions;
this.resetState();
push('/profile/detail/' + user._id);
}
/**
* Helper method to reset the state after user's finger leaves the screen.
*/
resetState() {
this.setState({
clickAt: null,
isDragging: false,
startPosition: [0, 0],
offset: {x: 0, y: 0},
dragDirection: 1,
dragDirectionLocked: false
});
}
/**
* Compute the current styles of the card, depending on if the finger is touching the screen and the offset.
* If the finger is touching the screen, relative position is set to the card to simulate th effect of
* the card following the finger.
* Also an arbitrary rotation effect is computed for the swiped card.
*/
getStyleProfile() {
const {index} = this.props;
const {isDragging, dragDirection, offset: {x, y}} = this.state;
if (index === 0 && isDragging) {
const rotation = dragDirection * (x / 20);
return {
position: 'relative',
left: x + 'px',
top: y + 'px',
transform: 'rotate(' + rotation + 'deg)',
transformStyle: 'flat'
};
}
return {};
}
/**
* This is just to set the profile picture of the user as a background image.
*/
getStylePicture() {
const {user} = this.props;
let profilePicture;
if (user.profile.pictures.length > 0) {
profilePicture = user.profile.pictures[0].link;
return {'backgroundImage': `url(${profilePicture})`};
}
return {};
}
/**
* When the user swipes the card to the side, a stamp appears on it, that says "LIKE" or "NOPE",
* depending on the direction of the swipe.
* Depending on how far to the side user swipes, the opacity of the stamp changes,
* starting from 0 and going all the way to 1.
* This method computes the opacity.
*/
getStyleStamp(like) {
const {index, offset: {x}} = this.props;
const {isDragging} = this.state;
if (index === 0 && isDragging) {
const windowWidth = window.innerWidth;
const opacityThreshold = (windowWidth / 100) * DRAG_THRESHOLD_PERCENTAGE;
// either like and x > 0 or !like and x < 0
if (like === (x > 0)) {
// if x is in the opacity threshold range, the opacity will be lower than 1
return {
opacity: Math.min(Math.abs(x) / opacityThreshold, 1)
};
}
}
return {};
}
render() {
const {user} = this.props;
return (
<div id={'vote-profile-' + user._id} className="vote-profile" style={this.getStyleProfile()}>
<div className="vote-profile-picture">
<div className="vote-profile-picture-content" style={this.getStylePicture()}>
<div className="vote-profile-picture-stamp vote-profile-picture-stamp-like"
style={this.getStyleStamp(true)}
>
<div className="vote-profile-picture-stamp-wrapper">LIKE</div>
</div>
<div className="vote-profile-picture-stamp vote-profile-picture-stamp-dislike"
style={this.getStyleStamp(false)}
>
<div className="vote-profile-picture-stamp-wrapper">NOPE</div>
</div>
</div>
</div>
<ProfileInfo user={user}/>
</div>
);
}
}
export default VoteProfile;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment