Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
In-browser prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
i-like-robots committed Mar 6, 2014
1 parent 48dde9d commit d950ce3
Show file tree
Hide file tree
Showing 27 changed files with 48,922 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"directory": "vendor"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ Thumbs.db
.grunt
node_modules
bower_components

heroku
heroku.pub
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node server.js
20 changes: 20 additions & 0 deletions app/bootstrap.jsx
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);

})();
88 changes: 88 additions & 0 deletions app/component/network.jsx
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>
);
}

});
173 changes: 173 additions & 0 deletions app/component/predictions.jsx
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>
);
}

});
64 changes: 64 additions & 0 deletions app/component/tube-tracker.jsx
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>
);
}

});
Loading

0 comments on commit d950ce3

Please sign in to comment.