This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
48dde9d
commit d950ce3
Showing
27 changed files
with
48,922 additions
and
3 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"directory": "vendor" | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,6 @@ Thumbs.db | |
.grunt | ||
node_modules | ||
bower_components | ||
|
||
heroku | ||
heroku.pub |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: node server.js |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** @jsx React.DOM */ | ||
(function() { | ||
var feature; | ||
|
||
var requiredFeatures = { | ||
"the selectors API": document.querySelector, | ||
"ES5 array methods": Array.prototype.forEach, | ||
"DOM level 2 events": window.addEventListener, | ||
"the HTML5 history API": window.history.pushState | ||
}; | ||
|
||
for (feature in requiredFeatures) { | ||
if (!requiredFeatures[feature]) { | ||
return alert("Sorry, but your browser does not support " + feature + " so this app won't work properly."); | ||
} | ||
} | ||
|
||
window.app = React.renderComponent(<TubeTracker networkData={data} />, document.body); | ||
|
||
})(); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/** @jsx React.DOM */ | ||
var Network = React.createClass({ | ||
|
||
getInitialState: function() { | ||
return { | ||
collapsible: window.innerWidth <= 800, | ||
open: false | ||
}; | ||
}, | ||
|
||
handleToggle: function() { | ||
this.setState({ open: !this.state.open }); | ||
}, | ||
|
||
handleResize: function() { | ||
this.setState({ collapsible: window.innerWidth <= 800 }); | ||
}, | ||
|
||
componentWillMount: function() { | ||
// Simple event debouncing to avoid multiple recalculations | ||
this.debounce = utils.debounceEvent(this.handleResize, 250); | ||
window.addEventListener("resize", this.debounce, false); | ||
}, | ||
|
||
componentWillUnmount: function() { | ||
window.removeEventListener("resize", this.debounce, false); | ||
}, | ||
|
||
render: function() { | ||
var networkData = this.props.networkData; | ||
var networkLineCodes = Object.keys(networkData.lines); | ||
|
||
var toggleText = this.state.open ? "Close" : "Open"; | ||
var toggleClass = this.state.collapsible ? (this.state.open ? "is-open" : "is-closed") : "is-static"; | ||
|
||
var generatedForms = networkLineCodes.map(function(lineCode, i) { | ||
return <Line networkData={networkData} lineCode={lineCode} key={i} />; | ||
}, this); | ||
|
||
return ( | ||
<div className={"network " + toggleClass}> | ||
{generatedForms} | ||
<button type="button" className="network__toggle" onClick={this.handleToggle}>{toggleText}</button> | ||
</div> | ||
); | ||
} | ||
|
||
}); | ||
|
||
var Line = React.createClass({ | ||
|
||
handleSubmit: function(event) { | ||
event.preventDefault(); | ||
|
||
// Dispatch an event for other components to capture | ||
var updateEvent = new CustomEvent("tt:update", { | ||
detail: { | ||
station: this.refs.station.getDOMNode().value, | ||
line: this.props.lineCode | ||
}, | ||
bubbles: true | ||
}); | ||
|
||
this.refs.form.getDOMNode().dispatchEvent(updateEvent); | ||
}, | ||
|
||
render: function() { | ||
var lineCode = this.props.lineCode; | ||
var networkData = this.props.networkData; | ||
var stationsOnThisLine = networkData.stationsOnLines[lineCode]; | ||
|
||
var generatedOptions = stationsOnThisLine.map(function(stationCode, i) { | ||
return <option value={stationCode} key={i}>{networkData.stations[stationCode]}</option>; | ||
}); | ||
|
||
return ( | ||
<form ref="form" onSubmit={this.handleSubmit}> | ||
<fieldset className={"network__line network__line--" + lineCode.toLowerCase()}> | ||
<legend>{networkData.lines[lineCode]}</legend> | ||
<input type="hidden" name="line" value={lineCode} /> | ||
<select name="station" ref="station">{generatedOptions}</select> | ||
<button type="submit" title="View train times">Go</button> | ||
</fieldset> | ||
</form> | ||
); | ||
} | ||
|
||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/** @jsx React.DOM */ | ||
var Predictions = React.createClass({ | ||
|
||
getInitialState: function() { | ||
return { status: this.props.line && this.props.station ? "loading" : "welcome" }; | ||
}, | ||
|
||
fetchPredictions: function(line, station) { | ||
// The circle line isn't defined in the API but shares platforms with other lines | ||
if (line === "O") { | ||
line = utils.mapCircleLineStation(station, this.props.networkData); | ||
} | ||
|
||
var api = "http://cloud.tfl.gov.uk/TrackerNet/PredictionDetailed/" + line + "/" + station; | ||
|
||
this.setState({ status: "loading" }); | ||
|
||
// The TrackerNet API does not support cross-origin requests so we must use a proxy | ||
utils.httpRequest(utils.proxyRequestURL(api), this.predictionsSuccess, this.predictionsError); | ||
}, | ||
|
||
predictionsError: function(error) { | ||
this.setState({ | ||
status: "error", | ||
predictionData: null | ||
}); | ||
|
||
// Pipe the error into your error logging setup | ||
// Airbrake.push({ error: error }); | ||
}, | ||
|
||
predictionsSuccess: function(responseDoc) { | ||
// Because we're using a proxy it will return a 200 and XML even if the | ||
// TrackerNet API is unavailble or request was invalid. | ||
if (!utils.validateResponse(responseDoc)) { | ||
return this.predictionError(new Error("Invalid API response")); | ||
} | ||
|
||
this.setState({ | ||
status: "success", | ||
|
||
// Dealing with XML in the browser is so ugly that I've | ||
// used 'models' to abstract it away. | ||
predictionData: new Prediction(responseDoc) | ||
}); | ||
}, | ||
|
||
resetPoll: function(line, station) { | ||
this.fetchPredictions(line, station); | ||
|
||
if (this.poll) { | ||
clearInterval(this.poll); | ||
} | ||
|
||
this.poll = setInterval(this.fetchPredictions.bind(this, line, station), 1000 * 30); | ||
}, | ||
|
||
componentDidMount: function() { | ||
if (this.props.line && this.props.station) { | ||
this.resetPoll(this.props.line, this.props.station); | ||
} | ||
}, | ||
|
||
componentWillUnmount: function() { | ||
clearInterval(this.poll); | ||
}, | ||
|
||
componentWillReceiveProps: function(newProps) { | ||
this.resetPoll(newProps.line, newProps.station); | ||
}, | ||
|
||
shouldComponentUpdate: function(newProps, newState) { | ||
// Only update when line/station changes or new predictions load otherwise the | ||
// loading notice will be displayed when refreshing current predictions. | ||
return this.props !== newProps || this.state.predictionData !== newState.predictionData; | ||
}, | ||
|
||
render: function() { | ||
if (this.state.status === "success") { | ||
return <DepartureBoard predictionData={this.state.predictionData} />; | ||
} | ||
|
||
return <Notice type={this.state.status} />; | ||
} | ||
|
||
}); | ||
|
||
var DepartureBoard = React.createClass({ | ||
|
||
render: function() { | ||
var predictionData = this.props.predictionData; | ||
var station = predictionData.station(); | ||
|
||
var generatedPlatforms = station.platforms().map(function(platform) { | ||
return ( | ||
<div className="platform" key={"platform-" + platform.number()}> | ||
<h2 className="platform__heading">{platform.name()}</h2> | ||
<Trains trains={platform.trains()} /> | ||
</div> | ||
); | ||
}); | ||
|
||
// Heading does not account for circle line mapping, meh | ||
return ( | ||
<div className="departures"> | ||
<h1 className="departures__heading">{station.name() + " " + predictionData.line()}</h1> | ||
{generatedPlatforms} | ||
</div> | ||
); | ||
} | ||
|
||
}); | ||
|
||
var Trains = React.createClass({ | ||
|
||
render: function() { | ||
var generatedTrains = this.props.trains.map(function(train) { | ||
return ( | ||
<tr key={"train-" + train.id()}> | ||
<td>{train.timeTo()}</td> | ||
<td>{train.destination()}</td> | ||
<td>{train.location()}</td> | ||
</tr> | ||
); | ||
}); | ||
|
||
return ( | ||
<table className="trains"> | ||
<thead> | ||
<tr> | ||
<th>Time</th> | ||
<th>Destination</th> | ||
<th>Current location</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{generatedTrains} | ||
</tbody> | ||
</table> | ||
); | ||
} | ||
|
||
}); | ||
|
||
var Notice = React.createClass({ | ||
|
||
statusText: function(status) { | ||
var text; | ||
|
||
switch (status) { | ||
case "error": | ||
text = "Sorry an error occured, please try again."; | ||
break; | ||
case "loading": | ||
text = "Loading predictions…" | ||
break; | ||
case "welcome": | ||
text = "Please choose a station."; | ||
break; | ||
} | ||
|
||
return text; | ||
}, | ||
|
||
render: function() { | ||
return ( | ||
<div className={"notice notice--" + this.props.type}> | ||
<p>{this.statusText(this.props.type)}</p> | ||
</div> | ||
); | ||
} | ||
|
||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** @jsx React.DOM */ | ||
var TubeTracker = React.createClass({ | ||
|
||
validateUserInput: function(line, station) { | ||
return /^[A-Z]$/.test(line) && /^[A-Z]{3}$/.test(station) && | ||
utils.isStationOnLine(line, station, this.props.networkData); | ||
}, | ||
|
||
formatAndValidateUserInput: function(userLine, userStation) { | ||
var line = null; | ||
var station = null; | ||
|
||
// We could have added extra states for invalid data | ||
// but it's easier simply to ignore it. | ||
if (this.validateUserInput(userLine, userStation)) { | ||
line = userLine; | ||
station = userStation; | ||
} | ||
|
||
return { | ||
line: line, | ||
station: station | ||
}; | ||
}, | ||
|
||
getInitialState: function() { | ||
return this.formatAndValidateUserInput( | ||
utils.queryStringProperty(utils.getQueryString(), "line"), | ||
utils.queryStringProperty(utils.getQueryString(), "station") | ||
); | ||
}, | ||
|
||
handleUpdate: function(e) { | ||
this.setState(this.formatAndValidateUserInput(e.detail.line, e.detail.station)); | ||
}, | ||
|
||
componentWillUpdate: function(newProps, newState) { | ||
// When the state changes push a query string so users can bookmark | ||
// or share the link to a chosen departure board. | ||
window.history.pushState(null, null, utils.formatQueryString(newState)); | ||
}, | ||
|
||
componentWillMount: function() { | ||
window.addEventListener("tt:update", this.handleUpdate, false); | ||
}, | ||
|
||
componentWillUnmount: function() { | ||
window.removeEventListener("tt:update", this.handleUpdate, false); | ||
}, | ||
|
||
render: function() { | ||
return ( | ||
<div className="layout"> | ||
<div className="layout__sidebar"> | ||
<Network networkData={this.props.networkData} /> | ||
</div> | ||
<div className="layout__content"> | ||
<Predictions line={this.state.line} station={this.state.station} networkData={this.props.networkData} /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
}); |
Oops, something went wrong.