ChartMyCourse is a single-page web application and clone of MapMyRun; it enables users to create running routes, add other users as friends and comment on other users courses using:
MapMyRun allows users to create routes using Google Maps API and then decide what kind of workout they woud like to do using a side toolbar. I decided to simplify the interface as I found MapMyRuns too clunky and unintuitive. I did away with choosing workouts and decided to focus specifically on running/walking routes. I made Input to be two simple fields, a course name and a description, and also simplified the map toolbar with only three buttons: Undo Waypoint, Clear Waypoint, Return to Start. This is all done by implementing Google Maps API and Direction API, React/Redux state and props, and CSS in order to create a smooth, intuitive and clear course creating experience. Clicking on the map adds a waypoint and connects previously added waypoints to each other in real time, updating both the map, and the distance display toolbar.
// Create Map using Google Maps API with either Central Park or a previously saved routes first point as its center. Adds event listener (click) to enable creation of waypoints on map by extracting longitude + latitude from the point clicked.
createMap(){
const newYork = new google.maps.LatLng(40.785091, -73.968285);
const center = this.state.waypoints.length > 0 ? this.state.waypoints[0] : newYork;
const mapOptions = {center, zoom: 14}
this.map = new google.maps.Map(this.mapNode, mapOptions);
this.map.addListener('click', this.createWaypoint);
}
createWaypoint(e){
this.currentWaypoints.push({lat: e.latLng.lat(), lng: e.latLng.lng()});
this.renderRoutes();
}
// Course creation + distance updating using Directions API functions
renderRoutes(){
if (!this.directionsService) {this.directionsService = new google.maps.DirectionsService();}
if(!this.directionsDisplay) {this.directionsDisplay = new google.maps.DirectionsRenderer({map: this.map, preserveViewport: true});}
const midWaypoints = this.currentWaypoints.slice(1, this.currentWaypoints.length - 1);
const wpts = midWaypoints.map(way => ({
location: way,
stopover: false
}));
const initialWaypoint = {
origin: this.currentWaypoints[0],
destination: this.currentWaypoints[this.currentWaypoints.length - 1],
travelMode: 'WALKING',
waypoints: wpts
}
this.directionsService.route(initialWaypoint, (result, status) => {
if (status === 'OK') {
this.directionsDisplay.setDirections(result);
this.updateDistance(result);
}
});
this.setState({waypoints: this.currentWaypoints.slice()});
}
// Side Toolbar Buttons for returning to the start of the course, undoing the last waypoint creating and clearing all points. Also the distance updating function.
clearMarkers(){
if (this.currentWaypoints.length > 0){
this.currentWaypoints = [];
this.directionsDisplay.setDirections({routes: this.currentWaypoints});
this.setState({ distance: '0 MI', waypoints: [] });
}
}
returnToOrigin(){
if (this.state.waypoints.length > 1){
this.currentWaypoints.push(this.currentWaypoints[0]);
this.renderRoutes();
}
}
undoWaypoint(){
if (this.currentWaypoints.length < 2){
this.clearMarkers();
} else {
this.currentWaypoints.pop();
this.renderRoutes();
}
}
Comments are dynamically rendered using React/Redux changes of state and lifecycle methods. Conditionals are used in render functions to determine if a comment belongs to the current user and if so, a delete button is rendered for those comments belonging to the user.
// Rendering conditionally based on whether there are comments and whether the comments belongs to the user currently signed in
<div className='comments-container'>
{Object.values(comments).length < 1 ?
<div className='no-comments-message'>
<div className='fas fa-comment-slash'></div> No comments on this course, enter one below!</div>
: Object.values(comments).map((comment, idx) => (
<div className='comment-container' key={idx}>
<span className='comment-username'>{comment.username} says:</span>
<p className='comment-body'>{comment.body}</p>
<div className='comment-delete-container'>
{comment.author_id === currentUser.id ?
<button onClick={() => this.removeComment(comment.id)} className='remove-comment-button'> DELETE</button>
: null}
</div>
</div>
))}
</div>
// Checking whether the comment props have updated using React lifecycle method
componentDidUpdate(prevProps){
if (Object.values(this.props.comments).length !== Object.values(prevProps.comments).length){
this.props.requestCourseComments(this.props.match.params.courseId);
this.props.requestCourse(this.props.match.params.courseId);
}
}