-
-
Save awatson1978/34b00e263824ac53171e249266e34662 to your computer and use it in GitHub Desktop.
React Swipe Card Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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