Compare commits

..

3 Commits

Author SHA1 Message Date
ronreg-ribdev
db9e8acb5b Update README, add license terms for map 2020-05-11 23:33:37 -07:00
ronreg-ribdev
b3802eba76 Image assets, basic layout 2020-05-11 23:33:37 -07:00
ronreg-ribdev
ceed2be120 Initial bart-app commit 2020-05-11 23:33:37 -07:00
39 changed files with 77150 additions and 695 deletions

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
URBIT_PIERS: [ URBIT_PIERS: [
"../zod/home", "/home/greg/code/zod/home",
] ]
}; };

View File

@ -1,38 +1,5 @@
BART (Bay Area Rapid Transit) landscape app for [Urbit](http://urbit.org). BART runner app for [Urbit](http://urbit.org).
The Bart App Map was created by Trucy Phan (https://github.com/trucy/bart-map) and is used under The Bart App Map was created by Trucy Phan (https://github.com/trucy/bart-map) and is used under
the terms of the Creative Commons Attribution 3.0 Unported License. the terms of the Creative Commons Attribution 3.0 Unported License.
# Installation
This app is based off of the [create-landscape-app](https://github.com/urbit/create-landscape-app) scaffolding.
To install, first boot your ship, and mount its pier using `|mount %` in the Dojo.
Then clone this repo, and create a file called `.urbitrc` at the root of the repo directory
with the following contents:
```
module.exports = {
URBIT_PIERS: [
"/path/to/ship/home",
]
};
```
For instance, if the repo was cloned into the same directory as a planet with a pier
`zod`, you might make the path `../zod/home`.
Then run the following Unix commands from the root of the repo:
```
$ yarn
$ yarn run build
```
This will build and package the javascript files, and move them into the directory
specified in the `.urbitrc` file.
Finally, run `|commit %home` in your ship's Dojo to make Urbit aware of those files,
and then run `|start %barttile` to start the app. You should then see a `BART info`
tile on your Landscape home screen.

16
dist/index.js vendored Normal file
View File

@ -0,0 +1,16 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/index.js";import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
api.setAuthTokens({
ship: window.ship
});
subscription.start();
ReactDOM.render((
React.createElement(Root, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 15}} )
), document.querySelectorAll("#root")[0]);

49
dist/js/api.js vendored Normal file
View File

@ -0,0 +1,49 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
}
bind(path, method, ship = this.authTokens.ship, appl = "barttile", success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
barttile(data) {
this.action("barttile", "json", data);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}
export let api = new UrbitApi();
window.api = api;

48
dist/js/components/lib/header-bar.js vendored Normal file
View File

@ -0,0 +1,48 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/js/components/lib/header-bar.js";import React, { Component } from "react";
import { cite } from '../../lib/util';
import { IconHome } from "/components/lib/icons/icon-home";
import { Sigil } from "/components/lib/icons/sigil";
export class HeaderBar extends Component {
render() {
let title = document.title === "Home" ? "" : document.title;
return (
React.createElement('div', {
className:
"bg-white bg-gray0-d w-100 justify-between relative tc pt3 db"
,
style: { height: 40 }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 12}}
, React.createElement('a', {
className: "dib gray2 f9 inter absolute left-0" ,
href: "/",
style: { top: 14 }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 17}}
, React.createElement(IconHome, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 21}})
, React.createElement('span', {
className: "ml2 white-d v-top lh-title" ,
style: { paddingTop: 3 }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 22}}, "Home"
)
)
, React.createElement('span', {
className: "f9 white-d inter dib" ,
style: {
verticalAlign: "text-top",
paddingTop: 3
}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 28}}
, title
)
, React.createElement('div', { className: "absolute right-0 lh-copy" , style: { top: 8 }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 36}}
, React.createElement(Sigil, {
ship: "~" + window.ship,
classes: "v-mid mix-blend-diff" ,
size: 16,
color: "#000000", __self: this, __source: {fileName: _jsxFileName, lineNumber: 37}}
)
, React.createElement('span', { className: "mono white-d f9 ml2 c-default" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 43}}, cite(window.ship))
)
)
);
}
}

View File

@ -0,0 +1,15 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/js/components/lib/icons/icon-home.js";import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
React.createElement('img', {
className: "invert-d " + classes,
src: "/~barttile/img/Home.png",
width: 16,
height: 16, __self: this, __source: {fileName: _jsxFileName, lineNumber: 7}}
)
);
}
}

View File

@ -0,0 +1,9 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/js/components/lib/icons/icon-spinner.js";import React, { Component } from 'react';
export class IconSpinner extends Component {
render() {
return (
React.createElement('div', { className: "spinner-pending", __self: this, __source: {fileName: _jsxFileName, lineNumber: 6}})
);
}
}

32
dist/js/components/lib/icons/sigil.js vendored Normal file
View File

@ -0,0 +1,32 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/js/components/lib/icons/sigil.js";import React, { Component } from 'react';
import { sigil, reactRenderer } from 'urbit-sigil-js';
export class Sigil extends Component {
render() {
const { props } = this;
let classes = props.classes || "";
if (props.ship.length > 14) {
return (
React.createElement('div', {
className: "bg-black dib " + classes,
style: { width: props.size, height: props.size }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 13}}
)
);
} else {
return (
React.createElement('div', { className: "dib " + classes, style: { flexBasis: 32, backgroundColor: props.color }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 20}}
, sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: [props.color, "white"]
})
)
);
}
}
}

42
dist/js/components/root.js vendored Normal file
View File

@ -0,0 +1,42 @@
const _jsxFileName = "/home/greg/code/bart-tile/src/js/components/root.js";import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash';
import { HeaderBar } from "./lib/header-bar.js"
export class Root extends Component {
constructor(props) {
super(props);
}
render() {
return (
React.createElement(BrowserRouter, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 15}}
, React.createElement('div', { className: "absolute h-100 w-100 bg-gray0-d ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 16}}
, React.createElement(HeaderBar, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 17}})
, React.createElement(Route, { exact: true, path: "/~barttile", render: () => {
return (
React.createElement('div', { className: "cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 20}}
, React.createElement('h1', { className: "mt0 f8 fw4" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 21}}, "BART Info" )
, React.createElement('div', { style: {display: "grid", gridTemplateColumns: "66% 34%", gridGap: "10px" }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 22}}
, React.createElement('div', { className: "topinfo", style: {gridColumn: "1 / 2"}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 23}}
, React.createElement('p', { className: "lh-copy measure pt3" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 24}}, "Current time is (current time)" )
, React.createElement('p', { className: "lh-copy measure pt3" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 25}}, "Today's bart map:" )
)
, React.createElement('div', { className: "map", style: {gridColumn: "1", gridRow: "2"}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 27}}
, React.createElement('img', { src: "/~barttile/img/BART-Map-Weekday-Saturday.png", __self: this, __source: {fileName: _jsxFileName, lineNumber: 28}} )
)
, React.createElement('div', { className: "searchsidebar", style: {gridColumn: "2", gridRow: "2"}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 30}}, "Search stuff here"
)
)
)
)}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 18}}
)
)
)
)
}
}

82
dist/js/lib/util.js vendored Normal file
View File

@ -0,0 +1,82 @@
import _ from 'lodash';
import classnames from 'classnames';
export function uuid() {
let str = "0v"
str += Math.ceil(Math.random()*8)+"."
for (var i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random()*10000000).toString(32);
_str = ("00000"+_str).substr(-5,5);
str += _str+".";
}
return str.slice(0,-1);
}
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
return !!r;
}
/*
Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da
To:
(javascript Date object)
*/
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
/*
Goes from:
(javascript Date object)
To:
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d, mil) {
  var fil = function(n) {
    return n >= 10 ? n : "0" + n;
  };
  return (
    `~${d.getUTCFullYear()}.` +
    `${(d.getUTCMonth() + 1)}.` +
    `${fil(d.getUTCDate())}..` +
    `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
  );
}
export function deSig(ship) {
return ship.replace('~', '');
}
// trim patps to match dojo, chat-cli
export function cite(ship) {
let patp = ship, shortened = "";
if (patp.startsWith("~")) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
}

11
dist/js/reducers/config.js vendored Normal file
View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class ConfigReducer {
reduce(json, state) {
let data = _.get(json, 'barttile', false);
if (data) {
state.inbox = data.inbox;
}
}
}

11
dist/js/reducers/initial.js vendored Normal file
View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
let data = _.get(json, 'initial', false);
if (data) {
state.inbox = data.inbox;
}
}
}

17
dist/js/reducers/update.js vendored Normal file
View File

@ -0,0 +1,17 @@
import _ from 'lodash';
export class UpdateReducer {
reduce(json, state) {
let data = _.get(json, 'update', false);
if (data) {
this.reduceInbox(_.get(data, 'inbox', false), state);
}
}
reduceInbox(inbox, state) {
if (inbox) {
state.inbox = inbox;
}
}
}

35
dist/js/store.js vendored Normal file
View File

@ -0,0 +1,35 @@
import { InitialReducer } from '/reducers/initial';
import { ConfigReducer } from '/reducers/config';
import { UpdateReducer } from '/reducers/update';
class Store {
constructor() {
this.state = {
inbox: {}
};
this.initialReducer = new InitialReducer();
this.configReducer = new ConfigReducer();
this.updateReducer = new UpdateReducer();
this.setState = () => { };
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
console.log(json);
this.initialReducer.reduce(json, this.state);
this.configReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

34
dist/js/subscription.js vendored Normal file
View File

@ -0,0 +1,34 @@
import { api } from '/api';
import { store } from '/store';
import urbitOb from 'urbit-ob';
export class Subscription {
start() {
if (api.authTokens) {
// this.initializebarttile();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
// initializebarttile() {
// api.bind('/primary', 'PUT', api.authTokens.ship, 'barttile',
// this.handleEvent.bind(this),
// this.handleError.bind(this));
// }
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
api.bind('/primary', 'PUT', api.authTokens.ship, 'barttile',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
}
export let subscription = new Subscription();

1
dist/js/vendor/sigils-1.2.5.js vendored Normal file

File diff suppressed because one or more lines are too long

20
dist/tile.js vendored Normal file
View File

@ -0,0 +1,20 @@
const _jsxFileName = "/home/greg/code/bart-tile/tile/tile.js";import React, { Component } from 'react';
import _ from 'lodash';
export default class barttileTile extends Component {
render() {
return (
React.createElement('div', { className: "w-100 h-100 relative bg-white bg-gray0-d ba b--black b--gray1-d" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 9}}
, React.createElement('a', { className: "w-100 h-100 db pa2 no-underline" , href: "/~barttile", __self: this, __source: {fileName: _jsxFileName, lineNumber: 10}}
, React.createElement('p', { className: "black white-d absolute f9" , style: { left: 8, top: 8 }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 11}}, "BART")
, React.createElement('img', { className: "absolute", src: "/~barttile/img/Temp-Bart-Icon.png", style: {top: 40, left: 20, width: "50%"}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 12}})
)
)
);
}
}
window.barttileTile = barttileTile;

View File

@ -29,7 +29,7 @@ gulp.task('css-bundle', function() {
.src('src/index.css') .src('src/index.css')
.pipe(cssimport()) .pipe(cssimport())
.pipe(postcss(plugins)) .pipe(postcss(plugins))
.pipe(gulp.dest('./urbit/app/bartinfo/css')); .pipe(gulp.dest('./urbit/app/barttile/css'));
}); });
gulp.task('jsx-transform', function(cb) { gulp.task('jsx-transform', function(cb) {
@ -71,7 +71,7 @@ gulp.task('js-imports', function(cb) {
console.log(e); console.log(e);
cb(); cb();
}) })
.pipe(gulp.dest('./urbit/app/bartinfo/js/')) .pipe(gulp.dest('./urbit/app/barttile/js/'))
.on('end', cb); .on('end', cb);
}); });
@ -97,21 +97,21 @@ gulp.task('tile-js-imports', function(cb) {
console.log(e); console.log(e);
cb(); cb();
}) })
.pipe(gulp.dest('./urbit/app/bartinfo/js/')) .pipe(gulp.dest('./urbit/app/barttile/js/'))
.on('end', cb); .on('end', cb);
}); });
gulp.task('js-minify', function () { gulp.task('js-minify', function () {
return gulp.src('./urbit/app/bartinfo/js/index.js') return gulp.src('./urbit/app/barttile/js/index.js')
.pipe(minify()) .pipe(minify())
.pipe(gulp.dest('./urbit/app/bartinfo/js/')); .pipe(gulp.dest('./urbit/app/barttile/js/'));
}); });
gulp.task('tile-js-minify', function () { gulp.task('tile-js-minify', function () {
return gulp.src('./urbit/app/bartinfo/js/tile.js') return gulp.src('./urbit/app/barttile/js/tile.js')
.pipe(minify()) .pipe(minify())
.pipe(gulp.dest('./urbit/app/bartinfo/js/')); .pipe(gulp.dest('./urbit/app/barttile/js/'));
}); });
gulp.task('urbit-copy', function () { gulp.task('urbit-copy', function () {

View File

@ -8,8 +8,7 @@ class UrbitApi {
this.bindPaths = []; this.bindPaths = [];
} }
bind(path, method, ship = this.authTokens.ship, appl = "bartinfo", success, fail) { bind(path, method, ship = this.authTokens.ship, appl = "barttile", success, fail) {
console.log(`Calling api.bind() with path ${path} ship ${ship} method ${method}`);
this.bindPaths = _.uniq([...this.bindPaths, path]); this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, appl, path, window.subscriptionId = window.urb.subscribe(ship, appl, path,
@ -30,8 +29,8 @@ class UrbitApi {
}); });
} }
bartinfo(data) { barttile(data) {
this.action("bartinfo", "json", data); this.action("barttile", "json", data);
} }
action(appl, mark, data) { action(appl, mark, data) {

View File

@ -6,7 +6,7 @@ export class IconHome extends Component {
return ( return (
<img <img
className={"invert-d " + classes} className={"invert-d " + classes}
src="/~bartinfo/img/Home.png" src="/~barttile/img/Home.png"
width={16} width={16}
height={16} height={16}
/> />

View File

@ -1,369 +1,39 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from "react-router-dom"; import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash'; import _ from 'lodash';
import { HeaderBar } from "./lib/header-bar.js" import { HeaderBar } from "./lib/header-bar.js"
function padNumber(number) {
if (number == 0) {
return "00";
}
if (number <= 9) {
return `0${number}`
}
return number.toString();
}
function isSundaySchedule(curTime) {
// Deliberately switch over the effective day in the middle of the
// night.
const dayOfWeek = curTime.getDay();
const hour = curTime.getHours();
const isSunday = (dayOfWeek === 0 && hour > 4) || (dayOfWeek === 1 && hour < 4);
return isSunday;
}
function getOptionsFromStations(stations) {
return _.map(stations, (station) => {
const abbr = station.abbr;
const name = station.name;
return <option key={abbr} value={abbr}>{name}</option>;
});
}
class ScheduleWidget extends Component {
constructor(props) {
super(props);
this.state = { station: "" };
}
static getDerivedStateFromProps(props, state) {
if (state.station === "" && props.stations && props.stations[0]) {
const abbr = props.stations[0].abbr;
return { station: abbr }
}
return null;
}
getSchedule(evt) {
// Needs to make a request at https://www.bart.gov/schedules/bystationresults?station=12TH&date=06/03/2020&time=6%3A30%20PM
const station = this.state.station;
const t = new Date();
const date = `${t.getMonth()}/${t.getDay()}/${t.getYear()}`
const hours = t.getHours();
const h = hours === 0 ? 12 : hours % 12;
const m = padNumber(t.getMinutes());
const meridian = hours >= 12 ? "PM": "AM"
const timeStr = `${h}:${m} ${meridian}`;
const url = `https://www.bart.gov/schedules/bystationresults?station=${station}&date=${date}&time=${timeStr}`;
window.open(url, '_blank');
evt.preventDefault();
}
changeStation(evt) {
const value = evt.target.value;
this.setState({station: value});
}
render() {
const stations = this.props.stations;
return (<div>
<form name="getSchedule" onSubmit={this.getSchedule.bind(this)}>
<select disabled={!stations} name="stations" value={this.state.fromStation} onChange={this.changeStation.bind(this)}>
{ getOptionsFromStations(stations) }
</select>
<input type="submit" value="Get schedule"/>
</form>
</div>);
}
}
class ElevatorWidget extends Component {
statuses() {
const elevatorStatuses = this.props.elevatorStatuses;
if (elevatorStatuses.length === 0) {
return <p>No elevators are known to be out of service.</p>;
}
return (<div>
{ _.map(elevatorStatuses, (st, idx) => {
const desc = st.description['#cdata-section'];
return <p key={idx}>{desc}</p>;
})
}
</div>);
}
render() {
return (<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
<h1 className="mt0 f8 fw4">BART Info - Elevator status</h1>
<Link to="/~bartinfo">Route planner</Link>
{ this.statuses() }
</div>);
}
}
class TimeScheduleWidget extends Component {
constructor(props) {
super(props)
this.state = { curTime: new Date() };
}
componentDidMount() {
this.timerId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({curTime: new Date()});
}
render() {
const curTime = this.state.curTime;
const timeStr = curTime.toLocaleTimeString();
const serviceStr = isSundaySchedule(curTime) ? "Sunday service" : "Weekday / Saturday service";
return (<div style={{textAlign: "center"}}>
<p className="lh-copy measure pt3">Current time: <b>{timeStr}</b></p>
<p className="lh-copy measure pt3">{serviceStr}</p>
</div>);
}
}
class RouteSearch extends Component {
constructor(props) {
super(props);
const now = new Date();
const hours = now.getHours();
this.state = {
fromStation: "",
toStation: "",
depart: 'now',
min: now.getMinutes(),
hour: hours === 0 ? 12 : hours % 12,
isPM: hours >= 12
};
}
static getDerivedStateFromProps(props, state) {
if (state.fromStation === "" && props.stations && props.stations[0]) {
const abbr = props.stations[0].abbr;
return { ...state, fromStation: abbr, toStation: abbr};
}
return null;
}
stationSearch(evt) {
evt.preventDefault();
api.action("bartinfo", "json", {
from: this.state.fromStation,
to: this.state.toStation,
min: this.state.min,
hour: this.state.hour,
isPM: this.state.isPM,
});
}
changeStation(evt) {
const target = evt.target.name;
const value = evt.target.value;
if (target === "fromStation") {
this.setState({fromStation: value});
} else if (target === "toStation") {
this.setState({toStation: value});
}
}
setDepartNow(evt) {
evt.preventDefault();
this.setState({depart: "now"});
}
setDepartAt(evt) {
evt.preventDefault();
const now = new Date();
const hours = now.getHours();
this.setState({
depart: "givenTime",
min: now.getMinutes(),
hour: hours === 0 ? 12 : hours % 12,
isPM: hours >= 12
});
}
renderTimePicker() {
const state = this.state;
const departNow = this.state.depart === 'now';
return (<div style={{display: "flex"}}>
<div>
<a href="" onClick={ this.setDepartNow.bind(this) }>
<div>
<p>{ departNow ? <b>Now</b> : "Now" }</p>
</div>
</a>
<a href="" onClick={ this.setDepartAt.bind(this)}>
<div>
<p>{ departNow ? "At..." : <b>At...</b>}</p>
</div>
</a>
</div>
<div>
<div></div>
<div>
</div>
<select
name="hour"
value={this.state.hour}
onChange={(evt) => this.setState({hour: parseInt(evt.target.value)}) } disabled={departNow}
>
{ _.map(_.range(1, 13), (hour) => { return <option key={`h-${hour}`} value={hour}>{padNumber(hour)}</option>;}) }
</select>
<span>:</span>
<select
name="min"
value={this.state.min}
onChange={(evt) => this.setState({min: parseInt(evt.target.value)}) } disabled={departNow}
>
{ _.map(_.range(0, 60), (min) => { return <option key={`m-${min}`} value={min}>{padNumber(min)}</option>;}) }
</select>
<select
name="isPM"
value={this.state.isPM ? "PM" : "AM"}
disabled={departNow}
onChange={(evt) => this.setState({isPM: evt.target.value === "PM"})}
>
<option value="AM">AM</option>
<option value="PM">PM</option>
</select>
</div>
</div>);
}
render() {
const receivedStations = this.props.stations;
return (<form name="bartSearch" onSubmit={this.stationSearch.bind(this)}>
From:
<select disabled={!receivedStations} name="fromStation" value={this.state.fromStation} onChange={this.changeStation.bind(this)}>
{ getOptionsFromStations(receivedStations) }
</select>
<br/>
To:
<select disabled={!receivedStations} name="toStation" value={this.state.toStation} onChange={this.changeStation.bind(this)}>
{ getOptionsFromStations(receivedStations) }
</select>
<div>
Depart at:
{ this.renderTimePicker() }
</div>
<div style={{padding: '5px'}}>
<input type="submit" value="Search"/>
</div>
</form>);
}
}
class IndividualRouteResult extends Component {
render() {
const trip = this.props.trip;
return (<div>
Depart: {trip.depart} Arrive: {trip.arrive} ({trip.time})
<br/>
Cost: {trip.fare}
<br/>
Legs:
{ _.map(trip.legs, (leg) => `${leg.line} line`) }
</div>);
}
}
class RouteResults extends Component {
render() {
const routes = this.props.routes;
console.log(this.props.routes);
if (!routes) {
return (<div></div>);
}
const request = routes.request;
const trip = request.trip;
const trips = _.map(trip, (t) => {
return {
fare: t['@fare'],
depart: t['@origTimeMin'],
arrive: t['@destTimeMin'],
time: t['@tripTime'],
legs: _.map(t.leg, (leg) => {
return {line: leg['@trainHeadStation'] };
})
};
});
return (<div>
Trains:
<br/>
{ _.map(trips, (trip, idx) => <IndividualRouteResult key={idx} trip={trip} />) }
</div>);
}
}
class RoutePlanner extends Component {
render() {
const curTime = this.props.curTime;
const mapFilename = "BART-system-map.png";
const mapPath=`/~bartinfo/img/${mapFilename}`;
return (
<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
<h1 className="mt0 f8 fw4">BART Info</h1>
<Link to="/~bartinfo/elevators">Elevator information</Link>
<div style={{display: "grid", gridTemplateColumns: "66% 34%", gridGap: "10px" }}>
<div className="topinfo" style={{gridColumn: "1 / 2"}}>
<TimeScheduleWidget/>
</div>
<div className="map" style={{gridColumn: "1", gridRow: "2"}}>
<img src={mapPath} />
</div>
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
Search scheduled trains:
<RouteSearch stations={this.props.stations} curTime={curTime} />
<br/>
<RouteResults routes={this.props.routes} />
or see the official bart scheduler for a given station, date and time:
<ScheduleWidget stations={this.props.stations}/>
</div>
</div>
</div>
);
}
}
export class Root extends Component { export class Root extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = store.state;
store.setStateHandler((newState) => {
this.setState(newState);
});
} }
render() { render() {
return ( return (
<BrowserRouter> <BrowserRouter>
<div className="absolute h-100 w-100 bg-gray0-d ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl"> <div className="absolute h-100 w-100 bg-gray0-d ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
<HeaderBar/> <HeaderBar/>
<Route exact path="/~bartinfo/elevators" render={ () => <Route exact path="/~barttile" render={ () => {
<ElevatorWidget return (
elevatorStatuses={this.state.elevators || []} <div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
/> }/> <h1 className="mt0 f8 fw4">BART Info</h1>
<Route exact path="/~bartinfo" render={ () => <div style={{display: "grid", gridTemplateColumns: "66% 34%", gridGap: "10px" }}>
<RoutePlanner <div className="topinfo" style={{gridColumn: "1 / 2"}}>
curTime={new Date() } <p className="lh-copy measure pt3">Current time is (current time)</p>
stations={this.state.stations || []} <p className="lh-copy measure pt3">Today's bart map:</p>
routes={this.state.routes} </div>
/> } /> <div className="map" style={{gridColumn: "1", gridRow: "2"}}>
<img src="/~barttile/img/BART-Map-Weekday-Saturday.png" />
</div>
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
Search stuff here
</div>
</div>
</div>
)}}
/>
</div> </div>
</BrowserRouter> </BrowserRouter>
) )

11
src/js/reducers/config.js Normal file
View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class ConfigReducer {
reduce(json, state) {
let data = _.get(json, 'barttile', false);
if (data) {
state.inbox = data.inbox;
}
}
}

View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
let data = _.get(json, 'initial', false);
if (data) {
state.inbox = data.inbox;
}
}
}

View File

@ -5,20 +5,13 @@ export class UpdateReducer {
reduce(json, state) { reduce(json, state) {
let data = _.get(json, 'update', false); let data = _.get(json, 'update', false);
if (data) { if (data) {
const stations = _.get(data, 'stations', false); this.reduceInbox(_.get(data, 'inbox', false), state);
if (stations) { }
state.stations = stations;
} }
const elevators = _.get(data, "elevators", false); reduceInbox(inbox, state) {
if (elevators) { if (inbox) {
state.elevators = elevators; state.inbox = inbox;
}
const routes = _.get(data, "routes", false);
if (routes) {
state.routes = routes;
}
} }
} }
} }

View File

@ -1,25 +1,32 @@
import { InitialReducer } from '/reducers/initial';
import { ConfigReducer } from '/reducers/config';
import { UpdateReducer } from '/reducers/update'; import { UpdateReducer } from '/reducers/update';
class Store { class Store {
constructor() { constructor() {
this.state = {} this.state = {
inbox: {}
};
this.initialReducer = new InitialReducer();
this.configReducer = new ConfigReducer();
this.updateReducer = new UpdateReducer(); this.updateReducer = new UpdateReducer();
this.setState = () => { }; this.setState = () => { };
} }
setStateHandler(setState) { setStateHandler(setState) {
console.log("Calling setStateHandler");
this.setState = setState; this.setState = setState;
} }
handleEvent(data) { handleEvent(data) {
console.log("Handling event");
console.log(data);
let json = data.data; let json = data.data;
console.log(json);
this.initialReducer.reduce(json, this.state);
this.configReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state); this.updateReducer.reduce(json, this.state);
this.setState(this.state); this.setState(this.state);
} }
} }

View File

@ -7,26 +7,17 @@ import urbitOb from 'urbit-ob';
export class Subscription { export class Subscription {
start() { start() {
if (api.authTokens) { if (api.authTokens) {
this.initializebartinfo(); // this.initializebarttile();
} else { } else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~"); console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
} }
} }
initializebartinfo() { // initializebarttile() {
api.bind("/routes", "PUT", api.authTokens.ship, "bartinfo", // api.bind('/primary', 'PUT', api.authTokens.ship, 'barttile',
this.handleEvent.bind(this), // this.handleEvent.bind(this),
this.handleError.bind(this)); // this.handleError.bind(this));
// }
api.bind("/elevators", "PUT", api.authTokens.ship, "bartinfo",
this.handleEvent.bind(this),
this.handleError.bind(this));
api.bind("/bartstations", "PUT", api.authTokens.ship, 'bartinfo',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
handleEvent(diff) { handleEvent(diff) {
store.handleEvent(diff); store.handleEvent(diff);
@ -34,6 +25,9 @@ export class Subscription {
handleError(err) { handleError(err) {
console.error(err); console.error(err);
api.bind('/primary', 'PUT', api.authTokens.ship, 'barttile',
this.handleEvent.bind(this),
this.handleError.bind(this));
} }
} }

View File

@ -2,14 +2,14 @@ import React, { Component } from 'react';
import _ from 'lodash'; import _ from 'lodash';
export default class bartinfoTile extends Component { export default class barttileTile extends Component {
render() { render() {
return ( return (
<div className="w-100 h-100 relative bg-white bg-gray0-d ba b--black b--gray1-d"> <div className="w-100 h-100 relative bg-white bg-gray0-d ba b--black b--gray1-d">
<a className="w-100 h-100 db pa2 no-underline" href="/~bartinfo"> <a className="w-100 h-100 db pa2 no-underline" href="/~barttile">
<p className="black white-d absolute f9" style={{ left: 8, top: 8 }}>BART Info</p> <p className="black white-d absolute f9" style={{ left: 8, top: 8 }}>BART</p>
<img className="absolute" src="/~bartinfo/img/Temp-Bart-Icon.png" style={{top: 40, left: 20, width: "50%"}}/> <img className="absolute" src="/~barttile/img/Temp-Bart-Icon.png" style={{top: 40, left: 20, width: "50%"}}/>
</a> </a>
</div> </div>
); );
@ -17,4 +17,4 @@ export default class bartinfoTile extends Component {
} }
window.bartinfoTile = bartinfoTile; window.barttileTile = barttileTile;

View File

@ -1,255 +0,0 @@
/+ *server, default-agent
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/bartinfo/index
/| /html/
/~ ~
==
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/bartinfo/js/tile
/| /js/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/bartinfo/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/bartinfo/css/index
/| /css/
/~ ~
==
/= bartinfo-png
/^ (map knot @)
/: /===/app/bartinfo/img /_ /png/
::
|%
+$ card card:agent:gall
--
^- agent:gall
=<
|_ bol=bowl:gall
+* this .
bartinfo-core +>
cc ~(. bartinfo-core bol)
def ~(. (default-agent this %|) bol)
::
++ on-init
^- (quip card _this)
=/ launcha [%launch-action !>([%add %bartinfo / '/~bartinfo/js/tile.js'])]
:_ this
:~ [%pass / %arvo %e %connect [~ /'~bartinfo'] %bartinfo]
[%pass /bartinfo %agent [our.bol %launch] %poke launcha]
==
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bol src.bol)
?+ mark (on-poke:def mark vase)
%handle-http-request
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
poke-handle-http-request:cc
%json
=+ !<(jon=json vase)
:_ this
(poke-handle-json:cc jon)
::
==
::
++ on-watch
|= =path
^- (quip card _this)
~& "on-watch path: {<path>}"
?: ?=([%bartstations *] path)
=/ bart-station-request
=/ out *outbound-config:iris
=/ req bart-api-request-stations:cc
[%pass /bartstationrequest %arvo %i %request req out]
[~[bart-station-request] this]
?: ?=([%elevators *] path)
=/ elevator-status-request
=/ out *outbound-config:iris
=/ req bart-api-elevator-status:cc
[%pass /elevators %arvo %i %request req out]
[~[elevator-status-request] this]
?: ?=([%routes *] path)
[[~] this]
?: ?=([%http-response *] path)
`this
?. =(/ path)
(on-watch:def path)
[[%give %fact ~ %json !>(*json)]~ this]
::
++ on-agent on-agent:def
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
~& "The on-arvo wire: {<wire>}"
?: ?=(%http-response +<.sign-arvo)
=/ http-moves=(list card)
?+ wire ~
[%bartstationrequest *]
=/ value=json (parse-request-stations-response:cc client-response.sign-arvo)
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/bartstations] %json !>(update)]~
::
[%elevators *]
=/ value=json (parse-elevator-status-response:cc client-response.sign-arvo)
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/elevators] %json !>(update)]~
::
[%routeplan *]
=/ value=json (parse-routeplan-response:cc client-response.sign-arvo)
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/routes] %json !>(update)]~
==
[http-moves this]
?. ?=(%bound +<.sign-arvo)
(on-arvo:def wire sign-arvo)
[~ this]
::
++ on-save on-save:def
++ on-load on-load:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--
::
::
|_ bol=bowl:gall
::
:: request to http://api.bart.gov/api/stn.aspx?cmd=stns&key=Q5RQ-PUEB-999T-DWEI&json=y
:: get .root | .stations | .station for list of stations
++ bart-api-key "Q5RQ-PUEB-999T-DWEI"
++ bart-api-url-base "http://api.bart.gov/api"
++ with-json-handler
|= [response=client-response:iris jsonhandler=$-(json json)]
^- json
=, format
?. ?=(%finished -.response)
%- pairs:enjs [fulltext+s+'bart response error' ~]
=/ data=(unit mime-data:iris) full-file.response
?~ data %- pairs:enjs ~
=/ ujon=(unit json) (de-json:html q.data.u.data)
?~ ujon %- pairs:enjs ~
?> ?=(%o -.u.ujon)
=/ parsed-json=json u.ujon
(jsonhandler parsed-json)
++ bart-api-request-stations
^- request:http
=/ url (crip "{bart-api-url-base}/stn.aspx?cmd=stns&key={bart-api-key}&json=y")
=/ headers [['Accept' 'application/json']]~
[%'GET' url headers *(unit octs)]
::
++ parse-request-stations-response
|= response=client-response:iris
^- json
=, format
=/ handler |= jon=json
=/ root ((ot:dejs ~[['root' same]]) jon)
=/ stations ((ot:dejs ~[['stations' same]]) root)
=/ station ((ot:dejs ~[['station' (ar:dejs same)]]) stations)
=/ abbr-and-name %- turn :- station |= item=json
^- json
=/ [name=tape abbr=tape]
((ot:dejs ~[['name' sa:dejs] ['abbr' sa:dejs]]) item)
(pairs:enjs ~[name+(tape:enjs name) abbr+(tape:enjs abbr)])
(pairs:enjs [[%stations %a abbr-and-name] ~])
(with-json-handler response handler)
::
++ bart-api-elevator-status
^- request:http
=/ url (crip "{bart-api-url-base}/bsa.aspx?cmd=elev&key={bart-api-key}&json=y")
=/ headers [['Accept' 'application/json']]~
[%'GET' url headers *(unit octs)]
++ parse-elevator-status-response
|= response=client-response:iris
^- json
=, format
=/ handler |= jon=json
=/ root=json ((ot:dejs ~[['root' same]]) jon)
=/ bsa=(list json) ((ot:dejs ~[['bsa' (ar:dejs same)]]) root)
(pairs:enjs [[%elevators %a bsa] ~])
(with-json-handler response handler)
::
++ bart-api-routeplan
:: Documentation: http://api.bart.gov/docs/sched/depart.aspx
|= [from=tape to=tape hour=@ min=@ ispm=?]
^- request:http
=/ meridian ?:(ispm "pm" "am")
=/ minstr ?: =(min 0) "00"
?: (lte min 9) "0{<min>}"
"{<min>}"
=/ time "{<hour>}:{minstr}{meridian}"
=/ before 1
=/ after 3
=/ url (crip "{bart-api-url-base}/sched.aspx?cmd=depart&orig={from}&a={<after>}&b={<before>}&dest={to}&time={time}&key={bart-api-key}&json=y")
~& "Making BART API request to {<url>}"
=/ headers [['Accept' 'application/json']]~
[%'GET' url headers *(unit octs)]
++ parse-routeplan-response
|= response=client-response:iris
^- json
=, format
=/ handler
|= jon=json
=/ root=json ((ot:dejs [['root' same] ~]) jon)
=/ schedule=json ((ot:dejs [['schedule' same] ~]) root)
(pairs:enjs ~[[%routes schedule]])
(with-json-handler response handler)
++ poke-handle-json
|= jon=json
^- (list card)
~& jon
=, format
?. ?=(%o -.jon)
[~]
=/ [hour=@ min=@ ispm=? from-station=tape to-station=tape]
%.
jon
%: ot:dejs
['hour' ni:dejs]
['min' ni:dejs]
['isPM' bo:dejs]
['from' sa:dejs]
['to' sa:dejs]
~
==
=/ req (bart-api-routeplan from-station to-station hour min ispm)
=/ out *outbound-config:iris
[[%pass /routeplan %arvo %i %request req out] ~]
::
++ poke-handle-http-request
|= =inbound-request:eyre
^- simple-payload:http
=+ url=(parse-request-line url.request.inbound-request)
?+ site.url not-found:gen
[%'~bartinfo' %css %index ~] (css-response:gen style)
[%'~bartinfo' %js %tile ~] (js-response:gen tile-js)
[%'~bartinfo' %js %index ~] (js-response:gen script)
::
[%'~bartinfo' %img @t *]
=/ name=@t i.t.t.site.url
=/ img (~(get by bartinfo-png) name)
?~ img
not-found:gen
(png-response:gen (as-octs:mimes:html u.img))
::
[%'~bartinfo' *] (html-response:gen index)
==
--

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

113
urbit/app/barttile.hoon Normal file
View File

@ -0,0 +1,113 @@
/+ *server, default-agent
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/barttile/index
/| /html/
/~ ~
==
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/barttile/js/tile
/| /js/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/barttile/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/barttile/css/index
/| /css/
/~ ~
==
/= barttile-png
/^ (map knot @)
/: /===/app/barttile/img /_ /png/
::
|%
+$ card card:agent:gall
--
^- agent:gall
=<
|_ bol=bowl:gall
+* this .
barttile-core +>
cc ~(. barttile-core bol)
def ~(. (default-agent this %|) bol)
::
++ on-init
^- (quip card _this)
=/ launcha [%launch-action !>([%add %barttile / '/~barttile/js/tile.js'])]
:_ this
:~ [%pass / %arvo %e %connect [~ /'~barttile'] %barttile]
[%pass /barttile %agent [our.bol %launch] %poke launcha]
==
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bol src.bol)
?+ mark (on-poke:def mark vase)
%handle-http-request
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
poke-handle-http-request:cc
::
==
::
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?: ?=([%http-response *] path)
`this
?. =(/ path)
(on-watch:def path)
[[%give %fact ~ %json !>(*json)]~ this]
::
++ on-agent on-agent:def
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=(%bound +<.sign-arvo)
(on-arvo:def wire sign-arvo)
[~ this]
::
++ on-save on-save:def
++ on-load on-load:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--
::
::
|_ bol=bowl:gall
::
++ poke-handle-http-request
|= =inbound-request:eyre
^- simple-payload:http
=+ url=(parse-request-line url.request.inbound-request)
?+ site.url not-found:gen
[%'~barttile' %css %index ~] (css-response:gen style)
[%'~barttile' %js %tile ~] (js-response:gen tile-js)
[%'~barttile' %js %index ~] (js-response:gen script)
::
[%'~barttile' %img @t *]
=/ name=@t i.t.t.site.url
=/ img (~(get by barttile-png) name)
?~ img
not-found:gen
(png-response:gen (as-octs:mimes:html u.img))
::
[%'~barttile' *] (html-response:gen index)
==
::
--

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,16 +1,16 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>bartinfo</title> <title>barttile</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/> content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<link rel="stylesheet" href="/~bartinfo/css/index.css" /> <link rel="stylesheet" href="/~barttile/css/index.css" />
</head> </head>
<body> <body>
<div id="root" /> <div id="root" />
<script src="/~/channel/channel.js"></script> <script src="/~/channel/channel.js"></script>
<script src="/~modulo/session.js"></script> <script src="/~modulo/session.js"></script>
<script src="/~bartinfo/js/index.js"></script> <script src="/~barttile/js/index.js"></script>
</body> </body>
</html> </html>

57329
urbit/app/barttile/js/index.js Normal file

File diff suppressed because it is too large Load Diff

19193
urbit/app/barttile/js/tile.js Normal file

File diff suppressed because it is too large Load Diff