Forked from Daniel Williams's Pen React JS Weather App.
Forked from Daniel Williams's Pen React JS Weather App.
Forked from Daniel Williams's Pen React JS Weather App.
A Pen by Captain Anonymous on CodePen.
<div id="mount-point"></div> |
Forked from Daniel Williams's Pen React JS Weather App.
Forked from Daniel Williams's Pen React JS Weather App.
Forked from Daniel Williams's Pen React JS Weather App.
A Pen by Captain Anonymous on CodePen.
/** @jsx React.DOM */ | |
/// So secure | |
var key = "98c355d73f22c6eb33c4bc0bd22031fe"; | |
/// Google Map Vars | |
var map; | |
var marker; | |
var mapZoomLevel; | |
/// Config for the app setup | |
config = { | |
initialLat: 51.75, | |
initialLon: -3.38, | |
mapZoomLevel: 10 | |
} | |
/// App | |
var WeatherApp = React.createClass({ | |
/** | |
* Set an initial state | |
*/ | |
getInitialState: function () { | |
/** | |
* Thinking in React | |
* | |
* 1. Is it passed in from a parent via props? If so, it probably isn't state. | |
* 2. Does it change over time? If not, it probably isn't state. | |
* 3. Can you compute it based on any other state or props in your component? If so, it's not state. | |
*/ | |
return { | |
/// Passed as props on render | |
lat: this.props.initialLat, | |
lon: this.props.initialLon, | |
/// These will be updated with data from the API, so are subject to change (point 2) | |
location: '', | |
weather: '', | |
icon: '' | |
}; | |
}, | |
/** | |
* Capitalize the first letter of a string | |
*/ | |
capitalizeFirstLetter: function(string) { | |
return string.charAt(0).toUpperCase() + string.slice(1); | |
}, | |
/** | |
* Request data from the API | |
* No fail checks! >.< | |
*/ | |
getData: function (location, lat, lon) { | |
/// Variable to return | |
var data; | |
/// If it's a search | |
if (location !== null) | |
{ | |
data = $.get('http://api.openweathermap.org/data/2.5/weather?q=' + location + '&APPID=' + key) | |
} | |
else /// It's a pin drop | |
{ | |
data = $.get('http://api.openweathermap.org/data/2.5/weather?lat=' + lat + '&lon=' + lon + '&APPID=' + key) | |
} | |
return data; | |
}, | |
/** | |
* Update state | |
*/ | |
updateState: function (locationName, lat, lon) { | |
/// Get data from the API, then set state with it | |
this.getData(locationName, lat, lon) | |
.then(function(data) { | |
/// Update the state, pass updateMap as a callback | |
this.setState({ | |
lat: data.coord.lat, | |
lon: data.coord.lon, | |
weather: this.capitalizeFirstLetter( data.weather[0].description ), | |
location: data.name, | |
icon: 'http://openweathermap.org/img/w/' + data.weather[0].icon + '.png' /// Messy | |
}, this.updateMap ) /// Pass updateMap as a callback | |
}.bind(this)); | |
}, | |
/** | |
* Run when 'search' button is pressed | |
*/ | |
locationSearch: function () { | |
/// Get the value from the search field | |
var location = this.refs.newLocation.getDOMNode().value; | |
if ( location !== '' ) | |
{ | |
/// Update state with new API Data based on location name | |
this.updateState(location, null, null); | |
} | |
}, | |
/** | |
* Run when 'location' button is used | |
*/ | |
geolocationSearch: function () { | |
/// Successful geolocation | |
var success = function (position) { | |
var lat = position.coords.latitude; | |
var lon = position.coords.longitude; | |
/// Update state with new API Data based on lat lon | |
this.updateState(null, lat, lon); | |
}.bind(this); | |
/// Error'd geolocation | |
var error = function (error) { | |
if (error.message == 'User denied Geolocation') | |
{ | |
alert('Please enable location services'); | |
} | |
}; | |
/// Get the position | |
navigator.geolocation.getCurrentPosition(success, error); | |
}, | |
/** | |
* Run when the form is submitted | |
*/ | |
formSubmit: function (e) { | |
e.preventDefault(); | |
/// Clear the input | |
this.refs.newLocation.getDOMNode().value = ''; | |
}, | |
/** | |
* Render the map on the page | |
*/ | |
renderMap: function(lat, lng) { | |
/** | |
* Map coordinates and pin coordinates are added in updateMap(), | |
* which is run by updateStateWithData() | |
*/ | |
/// Create a new map | |
map = new google.maps.Map(document.getElementById('map'), { | |
zoom: config.mapZoomLevel, | |
disableDefaultUI: true, | |
zoomControl: true | |
}); | |
/// Create a new marker | |
marker = new google.maps.Marker({ | |
map: map, | |
draggable: true | |
}); | |
/// Set the initial pin drop animation | |
marker.setAnimation(google.maps.Animation.DROP); | |
/// Add an event listener for click | |
google.maps.event.addListener(map, 'click', function(event) { | |
var latLng = event.latLng; | |
var lat = latLng.lat(); | |
var lng = latLng.lng(); | |
/// Update state based on lat lon | |
this.updateState(null, lat, lng) | |
}.bind(this)); | |
/// Add an event listener for drag end | |
google.maps.event.addListener(marker, 'dragend', function(event) { | |
var latLng = event.latLng; | |
var lat = latLng.lat(); | |
var lng = latLng.lng(); | |
/// Update state based on lat lon | |
this.updateState(null, lat, lng) | |
}.bind(this)); | |
/// Update variable on map change | |
map.addListener('zoom_changed', function() { | |
mapZoomLevel = map.getZoom(); | |
}); | |
}, | |
/** | |
* Set map marker position and pan settings | |
*/ | |
updateMap: function (lat, lon) { | |
var latLng = new google.maps.LatLng(this.state.lat, this.state.lon); | |
/// Set a timeout before doing map stuff | |
window.setTimeout( function() { | |
/// Set the marker position | |
marker.setPosition(latLng); | |
/// Pan map to that position | |
map.panTo(latLng); | |
}.bind(this), 300); | |
}, | |
/** | |
* After initial render | |
*/ | |
componentDidMount: function () { | |
/// Render a new map | |
this.renderMap(); | |
/// Run update state, passing in the setup | |
this.updateState(null, this.state.lat, this.state.lon); | |
}, | |
/** | |
* Render the app | |
*/ | |
render: function() { | |
return ( | |
<div id="app"> | |
<div id="app__interface"> | |
<div className="panel panel-default"> | |
<div className="panel-heading text-center"><span className="text-muted">Enter a place name below, drag the marker <em>or</em> click directly on the map</span></div> | |
<div className="panel-body"> | |
{ /* Search Form - Ideally this should be moved out */ } | |
<form onSubmit={this.formSubmit}> | |
<div className="input-group pull-left"> | |
<input type="text" className="form-control" placeholder="Enter a town/city name" ref="newLocation"/> | |
<span className="input-group-btn"> | |
<button type="submit" className="btn btn-default" onClick={this.locationSearch}>Search</button> | |
</span> | |
</div> | |
<button className="btn btn-default pull-right" onClick={this.geolocationSearch}><span className="glyphicon glyphicon-map-marker" aria-hidden="true"></span> Use my location</button> | |
</form> | |
</div> | |
<WeatherDetails location={this.state.location} weather={this.state.weather} icon={this.state.icon} /> | |
</div> | |
</div> | |
<div id="map"></div> | |
</div> | |
); | |
} | |
}); | |
var WeatherDetails = React.createClass({ | |
render: function() { | |
return ( | |
<div className="panel-heading weather"> | |
<p className="text-muted"><strong>{this.props.location}</strong></p> | |
<span className="text-muted">{this.props.weather}</span> | |
<div className="weather__icon"> | |
<img src={this.props.icon} /> | |
</div> | |
</div> | |
) | |
} | |
}); | |
/** | |
* Slap it on the page | |
*/ | |
React.renderComponent( | |
<WeatherApp initialLat={config.initialLat} initialLon={config.initialLon}/>, | |
document.getElementById('mount-point') | |
); |
<script src="https://codepen.io/chriscoyier/pen/yIgqi.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.1/react.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.0/JSXTransformer.js"></script> | |
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script> | |
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script> |
.panel > div { | |
border-radius: 0 !important; | |
} | |
$panel-width: 500px; | |
html, body { | |
width: 100%; | |
height: 100%; | |
} | |
#mount-point, | |
#app { | |
width: 100%; | |
height: 100%; | |
} | |
#app__interface { | |
position: absolute; | |
top: 10px; | |
left: 50%; | |
width: $panel-width; | |
z-index: 99; | |
margin-left: -($panel-width / 2); | |
} | |
#map { | |
width: 100%; | |
height: 100%; | |
} | |
form .input-group { | |
width: 310px; | |
} | |
.panel { | |
width: $panel-width; | |
margin-left: auto; | |
margin-right: auto; | |
} | |
.weather { | |
position: relative; | |
border-top: 1px solid #ddd; | |
padding-top: 20px; | |
padding-bottom: 20px; | |
p { | |
max-width: 80%; | |
} | |
} | |
.weather__icon { | |
position: absolute; | |
right: 15px; | |
bottom: 20px; | |
width: 50px; | |
height: 50px; | |
margin-left: -25px; | |
border: 1px solid #ddd; | |
border-radius: 50%; | |
background: #fff; | |
box-shadow: 0px 1px 0px #ddd; | |
& > img { | |
width: 40px; | |
height: 40px; | |
margin-top: 5px; | |
margin-left: 5px; | |
opacity: 0.4; | |
} | |
} |
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" /> |