Compare commits

...

30 Commits
master ... dev

  1. 9
      .vscode/settings.json
  2. 15
      package.json
  3. BIN
      public/favicon.ico
  4. 12
      public/index.html
  5. 16
      public/manifest.json
  6. 22
      src/App.js
  7. 139
      src/actions/fare.js
  8. 67
      src/actions/fareattr.js
  9. 67
      src/actions/frequency.js
  10. 25
      src/actions/index.js
  11. 17
      src/actions/stop.js
  12. 7
      src/components/CalendarList.js
  13. 2
      src/components/FareAttrList.js
  14. 148
      src/components/FareAttributesDetail.js
  15. 222
      src/components/FareAttributesForm.js
  16. 81
      src/components/FareList.js
  17. 48
      src/components/FareRulesDetail.js
  18. 289
      src/components/FareRulesForm.js
  19. 27
      src/components/First.js
  20. 42
      src/components/FloatPane.js
  21. 6
      src/components/Footer.js
  22. 126
      src/components/FrequencyForm.js
  23. 73
      src/components/FrequencyOne.js
  24. 9
      src/components/Login.js
  25. 29
      src/components/Nav.js
  26. 38
      src/components/RouteDetail.js
  27. 60
      src/components/RouteForm.js
  28. 14
      src/components/RouteList.js
  29. 4
      src/components/Spinner.js
  30. 85
      src/components/StopForm.js
  31. 32
      src/components/StopList.js
  32. 84
      src/components/StopTimeForm.js
  33. 1
      src/components/StopTimeOne.js
  34. 112
      src/components/TripForm.js
  35. 5
      src/components/parts/HorizontalSelect.js
  36. 4
      src/components/parts/Select.js
  37. 41
      src/components/parts/SelectOptions.js
  38. 21
      src/constants/ActionTypes.js
  39. 2
      src/constants/Api.js
  40. 76
      src/constants/choices.js
  41. 122
      src/container/Geo.js
  42. 15
      src/container/Main.js
  43. 12
      src/index.js
  44. 11
      src/reducers/agency.js
  45. 19
      src/reducers/auth.js
  46. 4
      src/reducers/calendar.js
  47. 7
      src/reducers/fareattr.js
  48. 100
      src/reducers/farerule.js
  49. 88
      src/reducers/frequency.js
  50. 22
      src/reducers/geo.js
  51. 4
      src/reducers/index.js
  52. 4
      src/reducers/route.js
  53. 17
      src/reducers/stop.js
  54. 14
      src/reducers/stoptime.js
  55. 4
      src/reducers/trip.js
  56. 19
      src/sagas.js
  57. 16
      src/store.js
  58. 9
      src/utils/ApiClient.js
  59. 96
      src/utils/index.js

9
.vscode/settings.json vendored

@ -1,5 +1,8 @@
{ {
"todo-tree.flat": true, "todo-tree.flat": false,
"todo-tree.grouped": false, "todo-tree.grouped": false,
"todo-tree.expanded": true "todo-tree.expanded": true,
} "workbench.colorCustomizations": {
"titleBar.activeBackground": "#e2c34a"
}
}

15
package.json

@ -1,14 +1,21 @@
{ {
"name": "grunt-front", "name": "grunt-front",
"version": "0.1.0", "version": "0.3.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.6",
"@fortawesome/free-solid-svg-icons": "^5.4.1",
"@fortawesome/react-fontawesome": "^0.1.3",
"ajv": "^6.5.1", "ajv": "^6.5.1",
"axios": "^0.18.0", "axios": "^0.18.0",
"leaflet": "^1.3.1", "i": "^0.3.6",
"leaflet": "^1.3.2",
"leaflet-draw": "^1.0.2", "leaflet-draw": "^1.0.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"moment": "^2.22.2", "moment": "^2.22.2",
"npm": "^6.4.1",
"polyline-encoded": "0.0.8",
"raven-js": "^3.26.4",
"react": "^16.4.1", "react": "^16.4.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-flatpickr": "^3.6.4", "react-flatpickr": "^3.6.4",
@ -17,11 +24,13 @@
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-scripts": "1.1.4", "react-scripts": "1.1.4",
"react-select": "^1.2.1", "react-select": "^2.0.0-beta.7",
"react-visibility-sensor": "^5.0.1",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-api-middleware": "^2.3.0", "redux-api-middleware": "^2.3.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^5.10.0", "redux-persist": "^5.10.0",
"redux-saga": "^0.16.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"styled-components": "^3.3.2" "styled-components": "^3.3.2"
}, },

BIN
public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 483 B

12
public/index.html

@ -9,12 +9,20 @@
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="shortcut icon" href="https://static.10ninox.com/goth/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="https://static.10ninox.com/goth/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://static.10ninox.com/goth/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://static.10ninox.com/goth/favicon-16x16.png">
<link rel="mask-icon" href="https://static.10ninox.com/goth/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#000000">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" href="//static.traffy.xyz/lib/leaflet/dist/leaflet.css" /> <link rel="stylesheet" href="//static.traffy.xyz/lib/leaflet/dist/leaflet.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.2/leaflet.draw.css" /> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.2/leaflet.draw.css" />
<link rel="stylesheet" href="https://static.10ninox.com/css/bulma.min.css" /> <link rel="stylesheet" href="https://static.10ninox.com/css/bulma.min.css" />
<link rel="stylesheet" href="https://static.10ninox.com/css/bulma-checkradio.min.css" /> <link rel="stylesheet" href="https://static.10ninox.com/css/bulma-checkradio.min.css" />
<script defer="defer" src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script> <script src="https://cdn.ravenjs.com/3.26.4/raven.min.js" crossorigin="anonymous"></script>
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.

16
public/manifest.json

@ -1,11 +1,21 @@
{ {
"short_name": "React App", "short_name": "Grunt GoTH",
"name": "Create React App Sample", "name": "Grunt for GoTH GTFS",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "https://static.10ninox.com/goth/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
},
{
"src": "https://static.10ninox.com/goth/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "https://static.10ninox.com/goth/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
} }
], ],
"start_url": "./index.html", "start_url": "./index.html",

22
src/App.js

@ -1,6 +1,12 @@
import React from 'react' import React from 'react'
// import './App.css' // import './App.css'
import { Switch, Route } from 'react-router-dom' import { Switch, Route } from 'react-router-dom'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGhost, faSpinner, faPlus, faSignInAlt, faSignOutAlt, faEnvelope,
faCheck, faExclamationTriangle, faKey, faSearch, faAlignJustify,
faCodeBranch, faSquare, faCheckSquare, faCompass,
} from '@fortawesome/free-solid-svg-icons'
import Main from './container/Main' import Main from './container/Main'
import Geo from './container/Geo' import Geo from './container/Geo'
@ -8,6 +14,22 @@ import Public from './container/Public'
import { LOGIN_PATH } from './constants/path' import { LOGIN_PATH } from './constants/path'
library.add(faGhost)
library.add(faPlus)
library.add(faSignOutAlt)
library.add(faSignInAlt)
library.add(faSpinner)
library.add(faSearch)
library.add(faKey)
library.add(faCheck)
library.add(faEnvelope)
library.add(faExclamationTriangle)
library.add(faAlignJustify)
library.add(faCodeBranch)
library.add(faSquare)
library.add(faCheckSquare)
library.add(faCompass)
const App = () => ( const App = () => (
<Switch> <Switch>
{/* both /roster and /roster/:number begin with /roster */} {/* both /roster and /roster/:number begin with /roster */}

139
src/actions/fare.js

@ -0,0 +1,139 @@
import { RSAA } from 'redux-api-middleware'
import * as types from '../constants/ActionTypes'
import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api'
export const getFareAttr = (query) => ({
[RSAA]: {
endpoint: (state) => (
query === 'next' ?
state.next :
`${API_URL}/fare-attribute/?${query || ''}`
),
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.fareattr.fetching || (
state.fareattr.query !== undefined && state.fareattr.query === query),
types: [
{
type: types.FAREATTR_REQUEST,
meta: { query: query || '' },
},
types.FAREATTR_SUCCESS,
types.FAREATTR_FAILURE,
]
}
})
export const updateFareAttr = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
types.FAREATTR_UPDATE,
types.FAREATTR_FAILURE,
]
}
})
export const createFareAttr = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
types.FAREATTR_CREATE,
types.FAREATTR_FAILURE,
]
}
})
export const deleteFareAttr = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
{
type: types.FAREATTR_DELETE,
meta: { id }
},
types.FAREATTR_FAILURE,
]
}
})
export const getFareRule = (query) => ({
[RSAA]: {
endpoint: (state) => (
query === 'next' ?
state.farerule.next :
`${API_URL}/fare-rule/?${query || ''}`
),
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.farerule.fetching || (
state.farerule.query !== undefined && state.farerule.query === query),
types: [
{
type: types.FARERULE_REQUEST,
meta: { query: query || '' },
},
types.FARERULE_SUCCESS,
types.FARERULE_FAILURE,
]
}
})
export const updateFareRule = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-rule/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.FARERULE_REQUEST,
types.FARERULE_UPDATE,
types.FARERULE_FAILURE,
]
}
})
export const createFareRule = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-rule/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.FARERULE_REQUEST,
types.FARERULE_CREATE,
types.FARERULE_FAILURE,
]
}
})
export const deleteFareRule = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-rule/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.FARERULE_REQUEST,
{
type: types.FARERULE_DELETE,
meta: { id }
},
types.FARERULE_FAILURE,
]
}
})

67
src/actions/fareattr.js

@ -1,67 +0,0 @@
import { RSAA } from 'redux-api-middleware'
import * as types from '../constants/ActionTypes'
import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api'
export const getFareAttr = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.fareattr.fetching || state.fareattr.query === query,
types: [
{
type: types.FAREATTR_REQUEST,
meta: { query: query },
},
types.FAREATTR_SUCCESS,
types.FAREATTR_FAILURE,
]
}
})
export const updateFareAttr = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
types.FAREATTR_UPDATE,
types.FAREATTR_FAILURE,
]
}
})
export const createFareAttr = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
types.FAREATTR_CREATE,
types.FAREATTR_FAILURE,
]
}
})
export const deleteFareAttr = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/fare-attribute/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.FAREATTR_REQUEST,
{
type: types.FAREATTR_DELETE,
meta: { id }
},
types.FAREATTR_FAILURE,
]
}
})

67
src/actions/frequency.js

@ -0,0 +1,67 @@
import { RSAA } from 'redux-api-middleware'
import * as types from '../constants/ActionTypes'
import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api'
export const getFrequency = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.frequency.fetching || state.frequency.query === query,
types: [
{
type: types.FREQUENCY_REQUEST,
meta: { query: query },
},
types.FREQUENCY_SUCCESS,
types.FREQUENCY_FAILURE,
]
}
})
export const updateFrequency = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
types.FREQUENCY_UPDATE,
types.FREQUENCY_FAILURE,
]
}
})
export const createFrequency = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
types.FREQUENCY_CREATE,
types.FREQUENCY_FAILURE,
]
}
})
export const deleteFrequency = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
{
type: types.FREQUENCY_DELETE,
meta: { id }
},
types.FREQUENCY_FAILURE,
]
}
})

25
src/actions/index.js

@ -65,6 +65,18 @@ export const geoLocationUpdate = (position) => {
} }
} }
export const geoStopToggler = () => {
return {
type: types.GEO_STOPMARKER_TOGGLE,
}
}
export const geoStopAuraToggler = () => {
return {
type: types.GEO_STOP_AURA_TOGGLE,
}
}
export const geoLocationFailed = (positionErr) => { export const geoLocationFailed = (positionErr) => {
// PositionError {code: 3, message: "Timeout expired"} // PositionError {code: 3, message: "Timeout expired"}
return { return {
@ -91,6 +103,19 @@ export const polygonReset = () => {
} }
} }
export const lastCenterUpdate = (lat, lon, zoom) => {
// this only update current map center from map movement alone
let payload = {
lastCenter: [lat, lon]
}
if (zoom !== undefined)
payload['zoom'] = zoom
return {
type: types.GEO_LASTCENTER_UPDATE,
payload,
}
}
export const mapCenterUpdate = (lat, lon) => { export const mapCenterUpdate = (lat, lon) => {
return { return {
type: types.GEO_MAPCENTER_UPDATE, type: types.GEO_MAPCENTER_UPDATE,

17
src/actions/stop.js

@ -33,6 +33,23 @@ export const updateStop = (id, body) => ({
} }
}) })
export const mergeStop = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/stop/${id}/merge/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.STOP_REQUEST,
{
type: types.STOP_UPDATE,
meta: { id, opt: 'merge', mergeWith: body }
},
types.STOP_FAILURE,
]
}
})
export const createStop = (body) => ({ export const createStop = (body) => ({
[RSAA]: { [RSAA]: {
endpoint: `${API_URL}/stop/`, endpoint: `${API_URL}/stop/`,

7
src/components/CalendarList.js

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { getCalendar } from '../actions/calendar' import { getCalendar } from '../actions/calendar'
import store from '../store' import store from '../store'
@ -27,8 +28,8 @@ margin-bottom: 1rem;
const CheckboxIcon = (props) => ( const CheckboxIcon = (props) => (
<GapBox checked={props.checked}> <GapBox checked={props.checked}>
{props.checked === true && <span><i className="fas fa-check-square"></i></span>} {props.checked === true && <span><FontAwesomeIcon icon="check-square" /></span>}
{props.checked !== true && <span><i className="fas fa-square"></i></span>} {props.checked !== true && <span><FontAwesomeIcon icon="square" /></span>}
</GapBox> </GapBox>
) )
@ -51,7 +52,7 @@ class CalendarList extends Component {
<nav className="level is-mobile"> <nav className="level is-mobile">
<p className="level-item has-text-centered"> <p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/new`}> <Link className="link is-info" to={`${match.url}/new`}>
<i className="fas fa-plus" /> New service <FontAwesomeIcon icon="plus" /> New service
</Link> </Link>
</p> </p>
</nav> </nav>

2
src/components/FareAttrList.js

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Spinner from './Spinner' import Spinner from './Spinner'
import { getFareAttr } from '../actions/fareattr' import { getFareAttr } from '../actions/fare'
import store from '../store' import store from '../store'

148
src/components/FareAttributesDetail.js

@ -0,0 +1,148 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import VisibilitySensor from 'react-visibility-sensor'
import {
PaymentMethodChoices, TransferChoices
} from '../constants/choices'
import { getItemFromList } from '../utils'
import { getFareAttr, getFareRule } from '../actions/fare'
import store from '../store'
import { FareRulesOne } from './FareRulesDetail'
import Spinner from './Spinner'
const StyledFareAttributesDetail = styled.div`
padding: 1rem;
background: #fafafa;
`
const FakeRow = styled.nav`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
export const FareAttributesOne = (props) => {
const { item, edit } = props
const payTxt = getItemFromList(item.payment_method, PaymentMethodChoices, '')
const transferTxt = getItemFromList(item.transfer, TransferChoices, '')
return (
<FakeRow className="level panel" key={item.fare_id}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Fare ID</p>
<Link to={`/fare/attributes/${item.id}${edit ? '/edit' : ''}`}>{item.fare_id}</Link>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Price</p>
<p key={item.id}>{item.price} <small>{item.currency_type}</small>
<br />
{payTxt && payTxt.label}
</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Transfer / sec</p>
<p>{transferTxt && transferTxt.label} / {item.transfer_duration}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Agency</p>
<Link to={`/agency/${item.agency.agency_id}`} >{item.agency.name}</Link>
</div>
</div>
</FakeRow>
)
}
class FareAttributesDetail extends Component {
state = {
fetching: true
}
constructor() {
super()
this.renderItem = this.renderItem.bind(this)
}
componentWillMount() {
const { props } = this
const { attribID } = props.match.params
const { results } = props.fareattr
const ones = results.filter(ele => ele.id === +attribID)
store.dispatch(getFareRule(`fareattr=${attribID}`))
if (ones.length > 0) {
this.setState({fetching: false})
} else {
store.dispatch(getFareAttr())
this.setState({fetching: true})
}
}
componentWillReceiveProps(nextProps) {
if (this.props.fareattr.fetching && !nextProps.fareattr.fetching) {
this.setState({fetching: false})
}
}
renderItem(one) {
return (
<StyledFareAttributesDetail>
<FareAttributesOne item={one} edit={true} />
</StyledFareAttributesDetail>
)
}
handleFetchMoreRules() {
store.dispatch(getFareRule('next'))
}
render () {
const { fetching, results } = this.props.fareattr
const { farerule } = this.props
// redirect to view page if no data
const { attribID } = this.props.match.params
const ones = results.filter(ele => ele.id === +attribID)
if (this.state.fetching || fetching)
return <Spinner show />
if (ones.length < 1)
return <Redirect to='/fare' />
return (
<div>
{this.renderItem(ones[0])}
<Link className="link is-info" to={`/fare/rules/new?fareattr=${ones[0].id}`}>
<FontAwesomeIcon icon="plus" /> New fare rule
</Link>
{farerule.results && farerule.results.map(ele => <FareRulesOne key={`ff-${ele.id}`} item={ele} />)}
<Spinner show={farerule.fetching} />
{(!farerule.fetching && farerule.results.length > 10 && farerule.next) && <VisibilitySensor onChange={this.handleFetchMoreRules}>
<div>More</div>
</VisibilitySensor>}
</div>
)
}
}
const mapStateToProps = state => ({
fareattr: state.fareattr,
farerule: state.farerule,
})
const connectFareAttributesDetail = connect(
mapStateToProps,
)(FareAttributesDetail)
export default connectFareAttributesDetail

222
src/components/FareAttributesForm.js

@ -0,0 +1,222 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import {
updateFareAttr,
createFareAttr,
deleteFareAttr
} from '../actions/fare'
import store from '../store'
import Input from './parts/Input'
import OurSelect from './parts/Select'
import AsyncSelect from 'react-select/lib/Async'
import {
PaymentMethodChoices, TransferChoices
} from '../constants/choices'
import { getItemFromList, getAgencyAsyncSelect } from '../utils'
import { AgencyOption } from './parts/SelectOptions'
const StyledFareAttributesForm = styled.div`
padding: 1rem;
background: #fafafa;
`
class FareAttributesForm extends Component {
state = {
id: null,
fare_id: "",
price: 0,
currency_type: "THB", // ISO 4217
payment_method: "0", // 0 - paid on board or 1 - paid before boarding
transfer: "0", // 0 no transfer, 1 - transfer once, 2 tranfer twice,
// '' - unlimited transfer
agency: null, // optional
transfer_duration: "0", // optional 0 - no transfer allowed,
// otherwise it's length of time in seconds before
// transfer expires
}
constructor() {
super()
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleDelete = this.handleDelete.bind(this)
this.renderForm = this.renderForm.bind(this)
}
handleChange(evt) {
let updated = {}
updated[evt.target.name] = evt.target.value
this.setState(updated)
}
handleSubmit() {
const { id } = this.state
let body = {...this.state}
delete body.id
if (id !== null) {
store.dispatch(updateFareAttr(id, body))
} else {
store.dispatch(createFareAttr(body))
}
this.setState({justSubmit: true})
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteFareAttr(id))
this.setState({justSubmit: true})
}
componentWillMount() {
const { props } = this
const { attribID } = props.match.params
const { results } = props.fareattr
const ones = results.filter(ele => ele.id === +attribID)
if (ones.length > 0) {
this.setState(ones[0])
}
}
renderForm() {
const one = this.state
const { fetching } = this.props.fareattr
return (
<StyledFareAttributesForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<Input
label="Fare ID"
type="text"
fieldName="fare_id"
value={one.fare_id || ''}
handleChange={this.handleChange} />
<Input
label="Price"
type="text"
fieldName="price"
value={one.price || ''}
handleChange={this.handleChange} />
<Input
key="currency_type"
label="Currency"
type="text"
fieldName="currency_type"
value={one.currency_type || 'THB'}
handleChange={this.handleChange} />
<OurSelect
label="Payment Method"
type="text"
fieldName="payment_method"
value={getItemFromList(one.payment_method, PaymentMethodChoices, '0')}
handleChange={this.handleChange}
choices={PaymentMethodChoices} />
<OurSelect
label="Transfer"
type="text"
fieldName="transfer"
value={getItemFromList(one.transfer, TransferChoices, '0')}
handleChange={this.handleChange}
choices={TransferChoices} />
<Input
key="transfer"
label="Transfer"
type="text"
fieldName="transfer"
value={one.transfer || '0'}
handleChange={this.handleChange} />
<div className="field">
<label className="label">Agency</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.agency && {...one.agency, label: one.agency.name}}
loadOptions={getAgencyAsyncSelect}
components={{ Option: AgencyOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'agency',
value: resp.value,
}}
this.handleChange(evt)
}
}}
/>
</div>
<Input
key="transfer_duration"
label="Transfer Duration (sec)"
type="text"
fieldName="transfer_duration"
value={one.transfer_duration || '0'}
handleChange={this.handleChange} />
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-link"
onClick={this.handleSubmit}
disabled={fetching}>
Save</button>
</div>
{one.id !== null && <div className="control">
<button className="button is-danger"
onClick={this.handleDelete}
disabled={fetching}>
DELETE</button>
</div>}
<div className="control">
<Link to={`/fare`} className="button is-text">Cancel</Link>
</div>
</div>
</StyledFareAttributesForm>
)
}
render () {
const one = this.state
const { fetching } = this.props.fareattr
// redirect to view page if no data
const { attribID } = this.props.match.params
// this is a create form
if (attribID === undefined) {
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/fare`} />
}
return this.renderForm()
}
if (one.id === null && attribID.length > 0)
return <Redirect to={`/fare`} />
// redirect to fare list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/fare`} />
}
return this.renderForm()
}
}
const mapStateToProps = state => ({
fareattr: state.fareattr
})
const connectFareAttributesForm = connect(
mapStateToProps,
)(FareAttributesForm)
export default connectFareAttributesForm

81
src/components/FareList.js

@ -0,0 +1,81 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { getFareAttr, getFareRule } from '../actions/fare'
import store from '../store'
import { FareAttributesOne } from './FareAttributesDetail'
import { FareRulesOne } from './FareRulesDetail'
const StyledBox = styled.div`
padding: 1rem;
background: #fafafa;
`
class FareList extends Component {
componentWillMount() {
const { fareattr, farerule } = this.props
if (fareattr.count === 0)
store.dispatch(getFareAttr())
if (farerule.count === 0)
store.dispatch(getFareRule())
}
render() {
const { fareattr, farerule } = this.props
const { match } = this.props
return (
<StyledBox>
<h1 className="title">Fare</h1>
<div className="columns">
<div className="column is-6">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/attributes/new`}>
<FontAwesomeIcon icon="plus" /> New fare attributes
</Link>
</p>
</nav>
{fareattr.results && Object.keys(fareattr.results).map(i => (
<FareAttributesOne item={fareattr.results[i]} />
))}
</div>
<div className="column is-6">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/rules/new`}>
<FontAwesomeIcon icon="plus" /> New fare rule
</Link>
</p>
</nav>
{farerule.results && Object.keys(farerule.results).map(i => (
<FareRulesOne key={`ff-${farerule.results[i].id}`} item={farerule.results[i]} />
))}
</div>
</div>
</StyledBox>
)
}
}
const mapStateToProps = state => ({
farerule: state.farerule,
fareattr: state.fareattr,
})
const connectFareList = connect(
mapStateToProps,
{},
)(FareList)
export default styled(connectFareList)`
color: palevioletred;
font-weight: bold;
`

48
src/components/FareRulesDetail.js

@ -0,0 +1,48 @@
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
const FakeRow = styled.nav`
padding-top: 3px;
padding-bottom: 3px;
background: white;
margin-bottom: 5px !important;
`
export const FareRulesOne = (props) => {
const { item } = props
return (
<FakeRow className="level panel" key={`fro-${item.fare_id}`}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Fare ID</p>
<Link to={`/fare/rules/${item.id}`}>{item.fare.fare_id}</Link>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Origin ID</p>
<p className="title">{item.origin_id}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Destination ID (or contains)</p>
<p className="title">
{item.destination_id}
{item.contains_id}
</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Route ID</p>
<p>
{item.route && (item.route.route_id || '-')}
</p>
</div>
</div>
</FakeRow>
)
}

289
src/components/FareRulesForm.js

@ -0,0 +1,289 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import {
updateFareRule,
createFareRule,
deleteFareRule
} from '../actions/fare'
import store from '../store'
import AsyncSelect from 'react-select/lib/Async'
import {
getStopsAsyncSelect,
getFareAttrAsyncSelect,
getRouteAsyncSelect
} from '../utils'
import {
StopOption, FareAttrOption, RouteOption
} from './parts/SelectOptions'
const StyledFareRulesForm = styled.div`
padding: 1rem;
background: #fafafa;
`
class FareRulesForm extends Component {
state = {
id: null,
fare: null,
route: null, // optinal
origin_id: "",
destination_id: "",
contains_id: "",
}
constructor() {
super()
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleDelete = this.handleDelete.bind(this)
this.renderForm = this.renderForm.bind(this)
}
handleChange(evt) {
let updated = {}
updated[evt.target.name] = evt.target.value
this.setState(updated)
}
handleSubmit() {
const { id } = this.state
let body = { ...this.state }
delete body.id
if (id !== null) {
store.dispatch(updateFareRule(id, body))
} else {
store.dispatch(createFareRule(body))
}
this.setState({ justSubmit: true })
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteFareRule(id))
this.setState({ justSubmit: true })
}
componentWillMount() {
const { props } = this
const { ruleID } = props.match.params
const { results } = props.farerule
const { search } = props.location
if (ruleID === undefined) {
// this is for create form
if (search.indexOf("?fareattr=") > -1) {
const fa = search.split("=")
const fat = props.fareattrItems.filter(ele => ele.id === +fa[1])
if (fat.length === 1)
this.setState({ fare: fat[0] })
}
} else {
// get state ready for edit form
const ones = results.filter(ele => ele.id === +ruleID)
if (ones.length > 0) {
this.setState(ones[0])
}
}
}
renderForm() {
const one = this.state
const { fetching } = this.props.farerule
return (
<StyledFareRulesForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<div className="field">
<label className="label">Fare ID</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.fare && { ...one.fare, label: one.fare.fare_id }}
loadOptions={getFareAttrAsyncSelect}
components={{ Option: FareAttrOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'fare',
value: resp.value,
}
}
this.handleChange(evt)
}
}}
/>
</div>
<div className="field">
<label className="label">Route <i>optional</i></label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.route && { value: one.route, label: one.route.route_id }}
loadOptions={getRouteAsyncSelect}
components={{ Option: RouteOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'route',
value: resp.value,
}
}
this.handleChange(evt)
}
}}
/>
</div>
<div className="field">
<label className="label">Origin ID</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.origin_id && { value: one.origin_id, label: one.origin_id }}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
let result
const fieldName = 'origin_id'
if (evt.action === 'select-option') {
result = {
target: {
name: fieldName,
value: resp.value.stop_id,
}
}
} else if (evt.action === 'clear') {
result = { target: { name: fieldName, value: '' } }
}
if (result !== undefined)
this.handleChange(result)
}}
/>
</div>
<div className="field">
<label className="label">Destination ID (or IDs)</label>
<AsyncSelect
isMulti
isClearable
cacheOptions
defaultOptions
defaultValue={one.destination_id && { value: one.destination_id, label: one.destination_id }}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
let result
const fieldName = 'destination_id'
if (evt.action === 'select-option') {
result = {
target: {
name: fieldName,
value: resp.map(el => el.value.stop_id).join(','),
}
}
} else if (evt.action === 'clear') {
result = { target: { name: fieldName, value: '' } }
}
if (result !== undefined)
this.handleChange(result)
}}
/>
</div>
<div className="field">
<label className="label">Contains ID (or IDs)</label>
<AsyncSelect
isMulti
isClearable
cacheOptions={true}
defaultOptions
defaultValue={one.contains_id && { value: one.contains_id, label: one.contains_id }}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
let result
const fieldName = 'contains_id'
if (evt.action === 'select-option') {
result = {
target: {
name: fieldName,
value: resp.map(el => el.value.stop_id).join(','),
}
}
} else if (evt.action === 'clear') {
result = { target: { name: fieldName, value: '' } }
}
if (result !== undefined)
this.handleChange(result)
}}
/>
</div>
<p>When having multiple destinations or contains, multiple fare rule records will be created automatically. Nothing differs from adding multiple records manually, it only saves time. !! only works with CREATE, UPDATE will throw 500</p>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-link"
onClick={this.handleSubmit}
disabled={fetching}>
Save</button>
</div>
{one.id !== null && <div className="control">
<button className="button is-danger"
onClick={this.handleDelete}
disabled={fetching}>
DELETE</button>
</div>}
<div className="control">
<Link to={`/fare${one.fare ? `/attributes/${one.fare.id}` : ''}`}
className="button is-text">Cancel</Link>
</div>
</div>
</StyledFareRulesForm>
)
}
render() {
const one = this.state
const { fetching } = this.props.farerule
// redirect to view page if no data
const { ruleID } = this.props.match.params
// this is a create form
if (ruleID === undefined) {
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/fare/attributes/${one.fare.id}`} />
}
return this.renderForm()
}
if (one.id === null && ruleID.length > 0)
return <Redirect to={`/fare`} />
// redirect to fare list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/fare/attributes/${one.fare.id}`} />
}
return this.renderForm()
}
}
const mapStateToProps = state => ({
farerule: state.farerule,
fareattrItems: state.fareattr.results,
})
const connectFareRulesForm = connect(
mapStateToProps,
)(FareRulesForm)
export default connectFareRulesForm

27
src/components/First.js

@ -0,0 +1,27 @@
import React, { Component } from 'react'
class First extends Component {
state = { inputValue: '' }
handleInputChange = (newValue) => {
console.log("inputChange", newValue)
const inputValue = newValue.replace(/\W/g, '')
this.setState({ inputValue })
return inputValue
}
render() {
return (
<div>
<br />
<br />
<h1>GoTH first page</h1>
<br />
<br />
</div>
)
}
}
export default First

42
src/components/FloatPane.js

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { Link, Route, Switch } from 'react-router-dom' import { Link, Route, Switch } from 'react-router-dom'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { logout } from '../utils/Auth' import { logout } from '../utils/Auth'
import RouteList from './RouteList' import RouteList from './RouteList'
@ -19,12 +20,6 @@ const StyledLink = styled(Link)`
} }
` `
const StyledLRPaddingNav = styled.nav`
padding-right: 10px;
padding-left: 0;
margin-bottom: 5px !important;
`
const StyledFloatPane = styled.div` const StyledFloatPane = styled.div`
min-width: 300px; min-width: 300px;
height: 100%; height: 100%;
@ -32,11 +27,20 @@ width: 25vw;
z-index: 30; z-index: 30;
background: #fefefefe; background: #fefefefe;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
display: flex;
flex-direction: column;
position: fixed; position: fixed;
left: ${props => props.hidePane ? '-500px' : 0}; left: ${props => props.hidePane ? '-500px' : 0};
top: 0px; top: 0px;
` `
const StyledLRPaddingNav = styled.nav`
padding-right: 10px;
padding-left: 0;
margin-bottom: 5px !important;
min-height: 44px;
`
const StyledPaneToggler = styled.div` const StyledPaneToggler = styled.div`
width: 30px width: 30px
height: 40px; height: 40px;
@ -55,11 +59,17 @@ border-radius: 0 5px 5px 0;
` `
const StyledScrollY = styled.div` const StyledScrollY = styled.div`
height: 100vh; flex-grow: 1;
overflow: auto; overflow: auto;
padding-bottom: 5rem; padding-bottom: 5rem;
` `
const SmallMuted = styled.p`
font-size: 0.85rem;
color: #888;
clear: both;
`
const SimpleAgencyList = (props) => ( const SimpleAgencyList = (props) => (
<nav className="panel"> <nav className="panel">
<p className="panel-heading"> <p className="panel-heading">
@ -67,12 +77,16 @@ const SimpleAgencyList = (props) => (
</p> </p>
<p className="panel-tabs"> <p className="panel-tabs">
<Link className="link is-info" to={`/agency/new`}> <Link className="link is-info" to={`/agency/new`}>
<i className="fas fa-plus" /> New agency <FontAwesomeIcon icon="plus"/> New agency
</Link> </Link>
</p> </p>
{props.agencies.map(ele => ( {props.agencies.map(ele => (
<Link key={ele.agency_id} className="panel-block" to={`/map/${ele.agency_id}`}> <Link key={ele.agency_id} className="panel-block" to={`/map/${ele.agency_id}`}>
{ele.name} <div>
{ele.name}
<br />
<SmallMuted>{ele.agency_id}</SmallMuted>
</div>
</Link>))} </Link>))}
</nav> </nav>
) )
@ -95,7 +109,7 @@ class FloatPane extends Component {
<a className="button is-small is-primary" <a className="button is-small is-primary"
onClick={() => store.dispatch(polygonReset())}> onClick={() => store.dispatch(polygonReset())}>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-out-alt"></i> <FontAwesomeIcon icon="sign-out-alt" />
</span> </span>
<span>Clear PG</span> <span>Clear PG</span>
</a> </a>
@ -103,7 +117,7 @@ class FloatPane extends Component {
<a className="button is-small is-outlined" <a className="button is-small is-outlined"
onClick={() => logout()}> onClick={() => logout()}>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-out-alt"></i> <FontAwesomeIcon icon="sign-out-alt" />
</span> </span>
<span>Logout</span> <span>Logout</span>
</a> </a>
@ -111,7 +125,7 @@ class FloatPane extends Component {
{!loggedIn && {!loggedIn &&
<Link className="button is-small is-outlined is-info" to='/login'> <Link className="button is-small is-outlined is-info" to='/login'>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-in-alt"></i> <FontAwesomeIcon icon="sign-in-alt" />
</span> </span>
<span>Sign in</span> <span>Sign in</span>
</Link> </Link>
@ -137,11 +151,11 @@ class FloatPane extends Component {
return ( return (
<StyledFloatPane hidePane={hidePane}> <StyledFloatPane hidePane={hidePane}>
<StyledPaneToggler hidePane={hidePane} onClick={this.togglePane.bind(this)}> <StyledPaneToggler hidePane={hidePane} onClick={this.togglePane.bind(this)}>
<i className='fas fa-align-justify' /> <FontAwesomeIcon icon='align-justify' />
</StyledPaneToggler> </StyledPaneToggler>
{this.renderTopLevel(loggedIn)} {this.renderTopLevel(loggedIn)}
<Breadcrumb {...this.state.breadcrumb} />
<StyledScrollY> <StyledScrollY>
<Breadcrumb {...this.state.breadcrumb} />
<Switch> <Switch>
<Route exact path={`/map/stop/new`} render={(props) => ( <Route exact path={`/map/stop/new`} render={(props) => (
<StopForm {...props} <StopForm {...props}

6
src/components/Footer.js

@ -1,13 +1,15 @@
import React from 'react' import React from 'react'
import { version } from '../../package.json'
const Footer = () => const Footer = () =>
<footer className="footer"> <footer className="footer">
<div className="content has-text-centered"> <div className="content has-text-centered">
<img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="168" height="42" /> <img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="168" height="42" />
<br/> <br />
<small>{ version }</small>
<br />
<a href="javascript:location.reload(true)"><i className="fas fa-sync"></i> reload</a> <a href="javascript:location.reload(true)"><i className="fas fa-sync"></i> reload</a>
</div> </div>
</footer> </footer>
export default Footer export default Footer

126
src/components/FrequencyForm.js

@ -0,0 +1,126 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import Input from './parts/Input'
import Select from './parts/Select'
import {
updateFrequency, createFrequency, deleteFrequency
} from '../actions/frequency'
import store from '../store'
import {
ExactTimeChoices
} from '../constants/choices'
import { getItemFromList } from '../utils'
const StyleBox = styled.div`
padding: 5px;
background: white;
margin-bottom: 1rem;
`
class FrequencyForm extends Component {
cancel = null
state = {
editMode: false,
trip: null,
id: null,
}
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleDelete = this.handleDelete.bind(this)
}
handleChange(evt) {
let updated = {}
updated[evt.target.name] = evt.target.value
this.setState(updated)
}
handleSubmit() {
const { id } = this.state
let body = {...this.state }
delete body.id
if (id !== null) {
store.dispatch(updateFrequency(id, body))
} else {
store.dispatch(createFrequency(body))
}
this.props.toggleEditMode()
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteFrequency(id))
this.props.toggleEditMode()
}
static getDerivedStateFromProps(props, state) {
if (props.item && props.item.id !== null && state.id === null) {
return props.item
} else if (props.trip !== undefined) {
return { trip: props.trip }
}
return null
}
render() {
const item = this.state
return (
<StyleBox>
<Input
label="Start"
type="text"
fieldName="start_time"
value={item.start_time || ''}
handleChange={this.handleChange} />
<Input
label="End"
type="text"
fieldName="end_time"
value={item.end_time || ''}
handleChange={this.handleChange} />
<Input
label="Headway secs"
type="text"
fieldName="headway_secs"
value={item.headway_secs || ''}
handleChange={this.handleChange} />
<Select
label="Exact Times"
type="text"
fieldName="exact_times"
value={getItemFromList(item.exact_times, ExactTimeChoices, '0')}
handleChange={this.handleChange}
choices={ExactTimeChoices} />
<div className="field is-grouped">
<div className="control">
<button className="button is-link"
onClick={this.handleSubmit}
disabled={false}>
Save</button>
</div>
{item.id !== null && <div className="control">
<button className="button is-danger"
onClick={this.handleDelete}
disabled={false}>
DELETE</button>
</div>}
{this.props.toggleEditMode &&
<div className="control">
<a onClick={() => this.props.toggleEditMode()} className="button is-text">Cancel</a>
</div>}
</div>
</StyleBox>
)
}
}
export default FrequencyForm

73
src/components/FrequencyOne.js

@ -0,0 +1,73 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import FrequencyForm from './FrequencyForm'
import { ExactTimeChoices } from '../constants/choices'
import { getItemFromList } from '../utils'
const StyledRow = styled.div`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
class FrequencyOne extends Component {
state = {
editMode: false
}
constructor(props) {
super(props)
this.toggleEditMode = this.toggleEditMode.bind(this)
}
toggleEditMode() {
this.setState({editMode: !this.state.editMode})
}
renderReadOnly = (item) => (
<StyledRow className="level panel" key={`st-item-${item.id}`}>
<div className="level-item has-text-centered">
<div>
<p className="heading">
<a onClick={() => this.toggleEditMode()}> EDIT</a>
&nbsp;Start Time</p>
<p className="title">{item.start_time}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">End Time</p>
<p className="title">{item.end_time}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Headway secs</p>
<p className="title">{item.headway_secs}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Exact Times</p>
<p className="title">{getItemFromList(item.exact_times, ExactTimeChoices).value}</p>
</div>
</div>
</StyledRow>
)
render() {
const { item, tripId } = this.props
if (this.state.editMode)
return <FrequencyForm
item={item}
trip={tripId}
toggleEditMode={this.toggleEditMode} />
return this.renderReadOnly(item)
}
}
export default FrequencyOne

9
src/components/Login.js

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
let invalidEmail = false let invalidEmail = false
let goodPassword = null let goodPassword = null
@ -16,11 +17,11 @@ const Login = (props) => (
onChange={props.updateField} onChange={props.updateField}
placeholder="yourname@example.com" /> placeholder="yourname@example.com" />
<span className="icon is-small is-left"> <span className="icon is-small is-left">
<i className="fas fa-envelope"></i> <FontAwesomeIcon icon="envelope"/>
</span> </span>
{ invalidEmail && { invalidEmail &&
<span className="icon is-small is-right"> <span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i> <FontAwesomeIcon icon="exclamation-triangle" />
</span>} </span>}
</div> </div>
{ invalidEmail && { invalidEmail &&
@ -36,11 +37,11 @@ const Login = (props) => (
onKeyPress={(e) => {(e.key === 'Enter') && props.fetchAuth()}} onKeyPress={(e) => {(e.key === 'Enter') && props.fetchAuth()}}
placeholder="Password" /> placeholder="Password" />
<span className="icon is-small is-left"> <span className="icon is-small is-left">
<i className="fas fa-key"></i> <FontAwesomeIcon icon="key"/>
</span> </span>
{ goodPassword === true && { goodPassword === true &&
<span className="icon is-small is-right"> <span className="icon is-small is-right">
<i className="fas fa-check"></i> <FontAwesomeIcon icon="check"/>
</span> } </span> }
</div> </div>
{ goodPassword === true && { goodPassword === true &&

29
src/components/Nav.js

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { logout } from '../utils/Auth' import { logout } from '../utils/Auth'
import { polygonReset } from '../actions' import { polygonReset } from '../actions'
@ -20,16 +21,28 @@ class Nav extends Component {
const { props } = this const { props } = this
return ( return (
<div className="navbar-start"> <div className="navbar-start">
<StyledLink to="/" className="navbar-item"> <div className="navbar-item has-dropdown is-hoverable">
Home <StyledLink to="/fare" className="navbar-link">
</StyledLink> Fare
</StyledLink>
<div className="navbar-dropdown is-boxed">
<StyledLink to={`/fare/rules/new`} className="navbar-item">
<FontAwesomeIcon icon="plus"/>
&nbsp; new fare rule
</StyledLink>
<StyledLink to={`/fare/attributes/new`} className="navbar-item">
<FontAwesomeIcon icon="plus"/>
&nbsp; new fare attributes
</StyledLink>
</div>
</div>
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
<StyledLink to="/calendar" className="navbar-link"> <StyledLink to="/calendar" className="navbar-link">
Service Service
</StyledLink> </StyledLink>
<div className="navbar-dropdown is-boxed"> <div className="navbar-dropdown is-boxed">
<StyledLink to={`/calendar/new`} className="navbar-item"> <StyledLink to={`/calendar/new`} className="navbar-item">
<i className="fas fa-plus" /> <FontAwesomeIcon icon="plus"/>
&nbsp; new service calendar &nbsp; new service calendar
</StyledLink> </StyledLink>
</div> </div>
@ -46,7 +59,7 @@ class Nav extends Component {
</StyledLink> </StyledLink>
))} ))}
<StyledLink to={`/agency/new`} className="navbar-item"> <StyledLink to={`/agency/new`} className="navbar-item">
<i className="fas fa-plus" /> <FontAwesomeIcon icon="plus"/>
&nbsp;Add new agency &nbsp;Add new agency
</StyledLink> </StyledLink>
</div> </div>
@ -84,7 +97,7 @@ class Nav extends Component {
<a className="button is-primary" <a className="button is-primary"
onClick={() => polygonReset()}> onClick={() => polygonReset()}>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-out-alt"></i> <FontAwesomeIcon icon="sign-out-alt"/>
</span> </span>
<span>Clear polygons</span> <span>Clear polygons</span>
</a> </a>
@ -94,7 +107,7 @@ class Nav extends Component {
<a className="button is-primary" <a className="button is-primary"
onClick={() => logout()}> onClick={() => logout()}>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-out-alt"></i> <FontAwesomeIcon icon="sign-out-alt"/>
</span> </span>
<span>Logout</span> <span>Logout</span>
</a> </a>
@ -102,7 +115,7 @@ class Nav extends Component {
{!loggedIn && {!loggedIn &&
<Link className="button is-primary" to='/login'> <Link className="button is-primary" to='/login'>
<span className="icon"> <span className="icon">
<i className="fas fa-sign-in-alt"></i> <FontAwesomeIcon icon="sign-in-alt"/>
</span> </span>
<span>Sign in</span> <span>Sign in</span>
</Link> </Link>

38
src/components/RouteDetail.js

@ -6,6 +6,8 @@ import { connect } from 'react-redux'
import Spinner from './Spinner' import Spinner from './Spinner'
import { getRoute, polygonUpdate } from '../actions' import { getRoute, polygonUpdate } from '../actions'
import { getStopTime } from '../actions/stoptime' import { getStopTime } from '../actions/stoptime'
import { getTrip } from '../actions/trip'
import { getFareAttr } from '../actions/fare'
import store from '../store' import store from '../store'
const StyledTripDesc = styled.div` const StyledTripDesc = styled.div`
@ -14,7 +16,6 @@ line-height: 0.85rem;
margin-left: 10px; margin-left: 10px;
` `
const RouteDesc = (props) => ( const RouteDesc = (props) => (
<div> <div>
<span className="panel-block"> <span className="panel-block">
@ -30,7 +31,7 @@ const RouteDesc = (props) => (
type: {props.route.route_type} type: {props.route.route_type}
</span> </span>
<span className="panel-block"> <span className="panel-block">
color: <br/>Text #{props.route.text_color || '-'} <br />BG #{props.route.route_text_color} color: <br/>Text #{props.route.route_color || '-'} <br />BG #{props.route.route_text_color}
</span> </span>
<span className="panel-block"> <span className="panel-block">
Sort order: {props.route.route_sort_order} Sort order: {props.route.route_sort_order}
@ -42,7 +43,7 @@ const RouteDesc = (props) => (
desc: {props.route.desc || '-'} desc: {props.route.desc || '-'}
</span> </span>
<span className="panel-block"> <span className="panel-block">
shapes: {props.route.geosjson !== null ? 'yes' : 'n/a'} shapes: {props.route.geojson !== null ? 'yes' : 'n/a'}
</span> </span>
</div> </div>
) )
@ -60,7 +61,15 @@ const TripList = (props) => (
{ele.stoptime.count > 0 && <div> {ele.stoptime.count > 0 && <div>
<b>Stop</b> #{ele.stoptime.count} <b>Stop</b> #{ele.stoptime.count}
<br /> <br />
<b>Time period</b> {ele.stoptime.period[0]} - {ele.stoptime.period[1]} <b>Time period</b>
<br />
&nbsp;&nbsp;{ele.stoptime.period[0]} - {ele.stoptime.period[1]}
<br />
{ele.frequency_set.length > 0 && <b>Frequency</b>}
{ele.frequency_set.map(freq => (
<span key={`freq-${freq.id}`}><br />&nbsp;&nbsp;{freq.start_time} -{freq.end_time}
<br />&nbsp;&nbsp;รอบละ {freq.headway_secs/60} นาที</span>
))}
</div>} </div>}
</StyledTripDesc> </StyledTripDesc>
</span> </span>
@ -75,14 +84,14 @@ const TripList = (props) => (
</div> </div>
) )
const FareRuleList = (props) => ( const FareAttrList = (props) => (
<div> <div>
{props.farerules.map(ele => ( {props.fareattrItems && props.fareattrItems.map(ele => (
<span key={ele.id} className="panel-block"> <span key={ele.id} className="panel-block">
{ele.fare.fare_id} - {ele.fare.price} {ele.fare_id} - {ele.price} {ele.currency_type}
</span> </span>
))} ))}
{props.farerules.length === 0 && <span key="empty-farerule" className="panel-block"> {props.fareattrItems && props.fareattrItems.length === 0 && <span key="empty-farerule" className="panel-block">
No fare rule set No fare rule set
</span>} </span>}
</div> </div>
@ -97,6 +106,8 @@ class RouteDetail extends Component {
componentDidMount() { componentDidMount() {
const { updateBreadcrumb, match } = this.props const { updateBreadcrumb, match } = this.props
updateBreadcrumb(match.params) updateBreadcrumb(match.params)
// fetch related trips
store.dispatch(getTrip(`route=${match.params.routeId}`))
} }
componentWillMount() { componentWillMount() {
@ -106,12 +117,12 @@ class RouteDetail extends Component {
} else { } else {
this.pushShapeToStore(match, route) this.pushShapeToStore(match, route)
} }
store.dispatch(getFareAttr(`agency=${match.params.agencyId}`))
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (this.props.route.count < newProps.route.count) { if (this.props.route.count < newProps.route.count) {
this.pushShapeToStore(this.props.match, newProps.route) this.pushShapeToStore(this.props.match, newProps.route)
} }
} }
@ -128,7 +139,7 @@ class RouteDetail extends Component {
} }
render() { render() {
const { route, match } = this.props const { route, match, trip, fareattr } = this.props
const { routeId, agencyId, routeParams } = match.params const { routeId, agencyId, routeParams } = match.params
const tRoute = route.results.filter(ele => ele.route_id === routeId) const tRoute = route.results.filter(ele => ele.route_id === routeId)
if (tRoute.length === 0) { if (tRoute.length === 0) {
@ -139,7 +150,6 @@ class RouteDetail extends Component {
} }
const item = tRoute[0] const item = tRoute[0]
const baseUrl = `/map/${agencyId}/route/${routeId}` const baseUrl = `/map/${agencyId}/route/${routeId}`
// TODO: change TripList to use from store.trip (when update will change automatically)
return ( return (
<nav className="panel"> <nav className="panel">
<p className="panel-heading"> <p className="panel-heading">
@ -154,9 +164,9 @@ class RouteDetail extends Component {
<Route exact path={`${baseUrl}`} render={(props) => ( <Route exact path={`${baseUrl}`} render={(props) => (
<RouteDesc route={item} {...props} />)} /> <RouteDesc route={item} {...props} />)} />
<Route exact path={`${baseUrl}/fare`} render={(props) => ( <Route exact path={`${baseUrl}/fare`} render={(props) => (
<FareRuleList farerules={item.farerule_set} {...props} />)} /> <FareAttrList fareattrItems={fareattr.results} {...props} />)} />
<Route exact path={`${baseUrl}/trip`} render={(props) => ( <Route exact path={`${baseUrl}/trip`} render={(props) => (
<TripList trips={item.trip_set} routeId={routeId} agencyId={agencyId} {...props} />)} /> <TripList trips={trip.results} routeId={routeId} agencyId={agencyId} {...props} />)} />
</nav> </nav>
) )
} }
@ -164,6 +174,8 @@ class RouteDetail extends Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
route: state.route, route: state.route,
trip: state.trip,
fareattr: state.fareattr,
}) })
export default connect( export default connect(
mapStateToProps mapStateToProps

60
src/components/RouteForm.js

@ -5,8 +5,13 @@ import { Redirect, Link } from 'react-router-dom'
import Input from './parts/Input' import Input from './parts/Input'
import Select from './parts/Select' import Select from './parts/Select'
import { updateRoute, createRoute, deleteRoute } from '../actions/route'; import {
getRoute, updateRoute,
createRoute, deleteRoute
} from '../actions/route';
import store from '../store' import store from '../store'
import { getItemFromList } from '../utils'
import { RouteTypeChoices } from '../constants/choices'
const StyledRouteForm = styled.div` const StyledRouteForm = styled.div`
padding: 1rem; padding: 1rem;
@ -66,23 +71,39 @@ class RouteForm extends Component {
this.setState({justSubmit: true}) this.setState({justSubmit: true})
} }
componentWillMount() { componentDidMount() {
const { props } = this const { routeId } = this.props.match.params
const { fetching, count } = this.props.route
if (routeId !== undefined && count === 0 && !fetching)
store.dispatch(getRoute(`route_id=${routeId}`))
}
static getDerivedStateFromProps(props, state) {
// if form is ready, then nothing else to do
if (state.agency !== null || state.id !== null)
return null
const { agencyId, routeId } = props.match.params const { agencyId, routeId } = props.match.params
const { results } = props.route if (routeId === undefined
const ones = results.filter(ele => ele.route_id === routeId) && state.agency === null
if (ones.length > 0) { && props.agency.count > 0) {
this.setState(ones[0]) // this is for new route
props.updateBreadcrumb({ agencyId, routeId }) const { results } = props.agency
} else { const agencies = results.filter(ele => ele.agency_id === agencyId)
const agencies = props.agency.results.filter(ele => ele.agency_id === agencyId)
if (agencies.length > 0) { if (agencies.length > 0) {
let d = {}
d["agency_id"] = agencies[0].id
this.setState(d)
props.updateBreadcrumb({ agencyId, routeId: 'new' }) props.updateBreadcrumb({ agencyId, routeId: 'new' })
return { agency: agencies[0].id }
}
} else if (routeId !== undefined && state.id === null) {
// this is for editing route
const ones = props.route.results.filter(ele => ele.route_id === routeId)
if (ones.length > 0) {
props.updateBreadcrumb({ agencyId, routeId })
return ones[0]
} }
return null
} }
return null
} }
renderForm() { renderForm() {
@ -126,18 +147,9 @@ class RouteForm extends Component {
<Select <Select
label="Route Type" label="Route Type"
fieldName="route_type" fieldName="route_type"
value={one.route_type || ''} value={getItemFromList(one.route_type, RouteTypeChoices)}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={RouteTypeChoices} />
{ value: '0', label: 'Tram, Light rail' },
{ value: '1', label: 'Subway (within metro area)' },
{ value: '2', label: 'Rail' },
{ value: '3', label: 'Bus' },
{ value: '4', label: 'Ferry' },
{ value: '5', label: 'Cable car' },
{ value: '6', label: 'Gondola' },
{ value: '7', label: 'Funicular (steep inclines)' },
]} />
<Input <Input
label="Route URL" label="Route URL"

14
src/components/RouteList.js

@ -2,16 +2,15 @@ import React, { Component } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Spinner from './Spinner' import Spinner from './Spinner'
import { getRoute } from '../actions' import { getRoute } from '../actions'
import store from '../store' import store from '../store'
const StyledLongName = styled.div` const StyledLongName = styled.div`
margin-left: 1rem;
font-size: 0.9rem; font-size: 0.9rem;
font-style: italic; color: #888;
color: #444;
` `
class RouteList extends Component { class RouteList extends Component {
@ -54,10 +53,13 @@ class RouteList extends Component {
className="panel-block" className="panel-block"
to={`/map/${agencyId}/route/${ele.route_id}`}> to={`/map/${agencyId}/route/${ele.route_id}`}>
<span className="panel-icon"> <span className="panel-icon">
<i className="fas fa-code-branch" aria-hidden="true"></i> <FontAwesomeIcon icon="code-branch" ariaHidden="true"/>
</span> </span>
{ele.short_name} <div>
{ele.long_name.length > 0 && <StyledLongName>{ ele.long_name }</StyledLongName>} {ele.short_name}
<br/>
{ele.long_name.length > 0 && <StyledLongName>{ele.long_name}</StyledLongName>}
</div>
</Link>) </Link>)
)} )}
</nav> </nav>

4
src/components/Spinner.js

@ -1,8 +1,10 @@
import React from 'react' import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const Spinner = (props) => ( const Spinner = (props) => (
<span> <span>
{props.show && <span className={props.className}><i className="fas fa-spinner" /></span>} {props.show && <span className={props.className}>
<FontAwesomeIcon icon="compass" pulse /></span>}
</span> </span>
) )

85
src/components/StopForm.js

@ -2,20 +2,27 @@ import React, { Component } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom' import { Redirect, Link } from 'react-router-dom'
import AsyncSelect from 'react-select/lib/Async'
import Input from './parts/Input' import Input from './parts/Input'
import Select from './parts/Select' import Select from './parts/Select'
import { import {
mapCenterUpdate, draggableMarkerEnable, draggableMarkerDisable mapCenterUpdate, draggableMarkerEnable, draggableMarkerDisable
} from '../actions' } from '../actions'
import { updateStop, createStop, deleteStop, getStop } from '../actions/stop' import { updateStop, createStop, deleteStop, getStop, mergeStop } from '../actions/stop'
import store from '../store' import store from '../store'
import {
StopLocationTypes, StopWheelChairInfo
} from '../constants/choices'
import { getItemFromList, getStopsAsyncSelect } from '../utils'
import { StopOption } from './parts/SelectOptions'
const StyledStopForm = styled.div` const StyledStopForm = styled.div`
padding: 1rem; padding: 1rem;
background: #fafafa; background: #fafafa;
` `
// TODO: need to deal with shapes // TODO: need to deal with shapes
class StopForm extends Component { class StopForm extends Component {
@ -31,6 +38,7 @@ class StopForm extends Component {
stop_timezone: 'Asia/Bangkok', stop_timezone: 'Asia/Bangkok',
wheelchair_boarding: '0', wheelchair_boarding: '0',
latlon: [], latlon: [],
mergeWith: null,
// parent_station: null, // parent_station: null,
} }
@ -38,6 +46,7 @@ class StopForm extends Component {
super() super()
this.handleChange = this.handleChange.bind(this) this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this) this.handleSubmit = this.handleSubmit.bind(this)
this.handleMerge = this.handleMerge.bind(this)
this.handleDelete = this.handleDelete.bind(this) this.handleDelete = this.handleDelete.bind(this)
this.renderForm = this.renderForm.bind(this) this.renderForm = this.renderForm.bind(this)
this.setToCurrentLocation = this.setToCurrentLocation.bind(this) this.setToCurrentLocation = this.setToCurrentLocation.bind(this)
@ -58,6 +67,8 @@ class StopForm extends Component {
delete body.id delete body.id
delete body.geojson delete body.geojson
delete body.latlon delete body.latlon
// this is for merger only
delete body.mergeWith
if (id !== null) { if (id !== null) {
store.dispatch(updateStop(id, body)) store.dispatch(updateStop(id, body))
} else { } else {
@ -66,6 +77,15 @@ class StopForm extends Component {
this.setState({justSubmit: true}) this.setState({justSubmit: true})
} }
handleMerge() {
const { id } = this.state
let body = {
stop: this.state.mergeWith,
}
store.dispatch(mergeStop(id, body))
this.setState({justSubmit: true})
}
handleDelete() { handleDelete() {
const { id } = this.state const { id } = this.state
store.dispatch(deleteStop(id)) store.dispatch(deleteStop(id))
@ -133,9 +153,14 @@ class StopForm extends Component {
store.dispatch(draggableMarkerEnable(coords.latitude, coords.longitude)) store.dispatch(draggableMarkerEnable(coords.latitude, coords.longitude))
} }
setToMapCenter() {
const { lastCenter } = this.props
store.dispatch(draggableMarkerEnable(lastCenter[0], lastCenter[1]))
}
renderForm() { renderForm() {
const one = this.state const one = this.state
const { coords } = this.props const { coords, lastCenter } = this.props
const { results, fetching } = this.props.stop const { results, fetching } = this.props.stop
const { latlon } = one const { latlon } = one
return ( return (
@ -156,8 +181,11 @@ class StopForm extends Component {
value={one.name || ''} value={one.name || ''}
handleChange={this.handleChange} /> handleChange={this.handleChange} />
<p><b>Lat, Lon</b> <p><b>Lat, Lon</b>&nbsp;
{coords && <a onClick={this.setToCurrentLocation.bind(this)}> -> current location</a>} {coords && <a onClick={this.setToCurrentLocation.bind(this)}>[current location]</a>}
&nbsp;
{lastCenter.length > 0 && <a onClick={this.setToMapCenter.bind(this)}>[center]</a>}
&nbsp;
<br /> <br />
{latlon[0] && latlon[0].toFixed(4)}, {latlon[1] && latlon[1].toFixed(4)} {latlon[0] && latlon[0].toFixed(4)}, {latlon[1] && latlon[1].toFixed(4)}
</p> </p>
@ -186,13 +214,9 @@ class StopForm extends Component {
<Select <Select
label="Location Type" label="Location Type"
fieldName="location_type" fieldName="location_type"
value={one.location_type || '0'} value={getItemFromList(one.location_type, StopLocationTypes)}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={StopLocationTypes} />
{ value: '0', label: 'Stop' },
{ value: '1', label: 'Station' },
{ value: '2', label: 'Station Entrance/Exit' },
]} />
<Input <Input
label="Timezone" label="Timezone"
@ -204,13 +228,9 @@ class StopForm extends Component {
<Select <Select
label="Wheelchair" label="Wheelchair"
fieldName="wheelchair_boarding" fieldName="wheelchair_boarding"
value={one.wheelchair_boarding || '0'} value={getItemFromList(one.wheelchair_boarding, StopWheelChairInfo)}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={StopWheelChairInfo} />
{ value: '0', label: 'No information' },
{ value: '1', label: 'Possible (partially or fully)' },
{ value: '2', label: 'Not possible' },
]} />
{/* <Input {/* <Input
label="Parent Station" label="Parent Station"
@ -240,6 +260,38 @@ class StopForm extends Component {
className="button is-text">Cancel</Link>} className="button is-text">Cancel</Link>}
</div> </div>
</div> </div>
<div className="content">
<div className="field">
<label className="label">Merge with stop</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={null}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'mergeWith',
value: resp.value,
}}
this.handleChange(evt)
}
}}
/>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-warning"
onClick={this.handleMerge}
disabled={fetching || this.state.mergeWith === null}>
Merge</button>
</div>
</div>
</div>
</StyledStopForm> </StyledStopForm>
) )
} }
@ -264,6 +316,7 @@ const mapStateToProps = state => ({
stop: state.stop, stop: state.stop,
draggableMarkerLatlon: state.geo.draggableMarkerLatlon, draggableMarkerLatlon: state.geo.draggableMarkerLatlon,
coords: state.geo.coords, coords: state.geo.coords,
lastCenter: state.geo.lastCenter,
}) })
const connectStopForm = connect( const connectStopForm = connect(

32
src/components/StopList.js

@ -2,25 +2,39 @@ import React, { Component } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { getStop } from '../actions/stop'
import store from '../store' import store from '../store'
import { STOP_REQUEST } from '../constants/ActionTypes'
const StyledBox = styled.div` const StyledBox = styled.div`
background: #fafafa; background: #fafafa;
` `
class StopList extends Component { class StopList extends Component {
componentWillMount() { componentWillMount() {
const { count } = this.props.stop const { count } = this.props.stop
if (count === 0) if (count === 0)
store.dispatch(getStop()) store.dispatch({
type: STOP_REQUEST,
payload: { query: `` }
})
} }
handleStopSearch(evt) { handleStopSearch(evt) {
store.dispatch(getStop(`search=${evt.target.value}`)) store.dispatch({
type: STOP_REQUEST,
payload: { query: `search=${evt.target.value}` }
})
}
fetchStopNearby(evt) {
const { lastCenter } = this.props
store.dispatch({
type: STOP_REQUEST,
payload: { query: `near=${lastCenter[0]},${lastCenter[1]}&range=3&limit=90` }
})
} }
render() { render() {
@ -33,8 +47,9 @@ class StopList extends Component {
</p> </p>
<p className="panel-tabs"> <p className="panel-tabs">
<Link className="link is-info" to={`/map/stop/new`}> <Link className="link is-info" to={`/map/stop/new`}>
<i className="fas fa-plus" /> New stop <FontAwesomeIcon icon="plus" /> New stop
</Link> </Link>
<a className="link is-info" onClick={this.fetchStopNearby.bind(this)}>Near</a>
</p> </p>
<div className="panel-block"> <div className="panel-block">
<p className="control has-icons-left"> <p className="control has-icons-left">
@ -45,7 +60,7 @@ class StopList extends Component {
onChange={(e) => { this.handleStopSearch(e) } } onChange={(e) => { this.handleStopSearch(e) } }
onKeyPress={(e) => { (e.key === 'Enter') && this.handleStopSearch(e) }} /> onKeyPress={(e) => { (e.key === 'Enter') && this.handleStopSearch(e) }} />
<span className="icon is-small is-left"> <span className="icon is-small is-left">
<i className="fas fa-search" aria-hidden="true"></i> <FontAwesomeIcon icon="search" ariaHidden="true"/>
</span> </span>
</p> </p>
</div> </div>
@ -54,7 +69,7 @@ class StopList extends Component {
<Link to={`/map/stop/${ele.stop_id}`}>{ele.stop_id} - {ele.name}</Link> <Link to={`/map/stop/${ele.stop_id}`}>{ele.stop_id} - {ele.name}</Link>
</span> </span>
))} ))}
{results.length === 0 && <span key="empty-farerule" className="panel-block"> {results.length === 0 && <span key="empty-stop" className="panel-block">
No stop found</span>} No stop found</span>}
</nav> </nav>
</StyledBox> </StyledBox>
@ -63,7 +78,8 @@ class StopList extends Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
stop: state.stop stop: state.stop,
lastCenter: state.geo.lastCenter,
}) })
const connectStopList = connect( const connectStopList = connect(
mapStateToProps, mapStateToProps,

84
src/components/StopTimeForm.js

@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Select from 'react-select' import AsyncSelect from 'react-select/lib/Async'
import { components } from 'react-select'
import Input from './parts/Input' import Input from './parts/Input'
import OurSelect from './parts/Select' import OurSelect from './parts/Select'
@ -8,7 +9,10 @@ import {
updateStopTime, createStopTime, deleteStopTime updateStopTime, createStopTime, deleteStopTime
} from '../actions/stoptime' } from '../actions/stoptime'
import store from '../store' import store from '../store'
import { API_URL } from '../constants/Api' import {
PickUpTypes, DropOffTypes, TimePointChoices
} from '../constants/choices'
import { getItemFromList, getStopsAsyncSelect } from '../utils'
const StyleBox = styled.div` const StyleBox = styled.div`
padding: 5px; padding: 5px;
@ -16,8 +20,20 @@ background: white;
margin-bottom: 1rem; margin-bottom: 1rem;
` `
const Option = (props) => {
const { stop_id, name, stop_desc } = props.data
return (
<components.Option {...props}>
<code>{stop_id}</code> {name}
{stop_desc.length > 0 && <small><br />{stop_desc}</small>}
</components.Option>
)
}
class StopTimeForm extends Component { class StopTimeForm extends Component {
cancel = null
state = { state = {
editMode: false, editMode: false,
trip: null, trip: null,
@ -64,15 +80,6 @@ class StopTimeForm extends Component {
return null return null
} }
getStops = (inputValue) => {
if (!inputValue) {
return Promise.resolve({ options: [] });
}
return fetch(`${API_URL}/stop/?search=${inputValue}`)
.then((response) => response.json())
.then((json) => ({ options: json.results }))
}
render() { render() {
const item = this.state const item = this.state
return ( return (
@ -86,19 +93,21 @@ class StopTimeForm extends Component {
<div className="field"> <div className="field">
<label className="label">Stop</label> <label className="label">Stop</label>
<Select.Async <AsyncSelect
value={this.state.stop} cacheOptions={true}
labelKey="stop_id" defaultOptions
valueKey="id" defaultValue={item.stop && {...item.stop, label: item.stop.name}}
filterOptionunion={false} loadOptions={getStopsAsyncSelect}
loadOptions={this.getStops} components={{ Option }}
onChange={(data) => { onChange={(item, evt) => {
let evt = { if (evt.action === 'select-option') {
target: { let evt = {
name: 'stop', target: {
value: data, name: 'stop',
}} value: item,
return this.handleChange(evt) }}
this.handleChange(evt)
}
}} }}
/> />
</div> </div>
@ -126,38 +135,25 @@ class StopTimeForm extends Component {
label="Pickup Type" label="Pickup Type"
type="text" type="text"
fieldName="pickup_type" fieldName="pickup_type"
value={item.pickup_type || '0'} value={getItemFromList(item.pickup_type, PickUpTypes, '0')}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={PickUpTypes} />
{ value: '0', label: 'Regularly scheduled pickup' },
{ value: '1', label: 'No pickup' },
{ value: '2', label: 'Arrange pickup' },
{ value: '3', label: 'Contact driver' },
]} />
<OurSelect <OurSelect
label="Drop off Type" label="Drop off Type"
type="text" type="text"
fieldName="pickup_type" fieldName="drop_off_type"
value={item.pickup_type || '0'} value={getItemFromList(item.drop_off_type, DropOffTypes, '0')}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={DropOffTypes} />
{ value: '0', label: 'Regularly scheduled dropoff' },
{ value: '1', label: 'No drop off' },
{ value: '2', label: 'Arrange drop off' },
{ value: '3', label: 'Contact driver' },
]} />
<OurSelect <OurSelect
label="Timepoint" label="Timepoint"
type="text" type="text"
fieldName="timepoint" fieldName="timepoint"
value={item.timepoint || '1'} value={getItemFromList(item.timepoint, TimePointChoices, '1')}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={TimePointChoices} />
{ value: '0', label: 'times are considered approximate.' },
{ value: '1', label: 'times are considered exact.' },
]} />
<div className="field is-grouped"> <div className="field is-grouped">
<div className="control"> <div className="control">

1
src/components/StopTimeOne.js

@ -33,6 +33,7 @@ class StopTimeOne extends Component {
Stop <a onClick={() => this.toggleEditMode()}>EDIT</a> Stop <a onClick={() => this.toggleEditMode()}>EDIT</a>
</p> </p>
<p className="title">{item.sequence}. {item.stop.stop_id}</p> <p className="title">{item.sequence}. {item.stop.stop_id}</p>
<div style={{maxWidth: '11rem'}}><small>{item.stop.name}</small></div>
</div> </div>
</div> </div>
<div className="level-item has-text-centered"> <div className="level-item has-text-centered">

112
src/components/TripForm.js

@ -4,14 +4,24 @@ import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom' import { Redirect, Link } from 'react-router-dom'
import StopTimeOne from './StopTimeOne' import StopTimeOne from './StopTimeOne'
import FrequencyOne from './FrequencyOne'
import StopTimeForm from './StopTimeForm' import StopTimeForm from './StopTimeForm'
import FrequencyForm from './FrequencyForm'
import HorizontalInput from './parts/HorizontalInput' import HorizontalInput from './parts/HorizontalInput'
import HorizontalSelect from './parts/HorizontalSelect' import HorizontalSelect from './parts/HorizontalSelect'
import { updateTrip, createTrip, deleteTrip } from '../actions/trip' import { updateTrip, createTrip, deleteTrip } from '../actions/trip'
import { getCalendar } from '../actions/calendar' import { getCalendar } from '../actions/calendar'
import { getRoute } from '../actions' import { getRoute } from '../actions'
import { getTrip } from '../actions/trip'
import { getStopTime } from '../actions/stoptime' import { getStopTime } from '../actions/stoptime'
import { getFrequency } from '../actions/frequency'
import store from '../store' import store from '../store'
import {
DirectionChoices, WheelChairAccessibles,
BikeAllowanceChoices
} from '../constants/choices'
import { getItemFromList } from '../utils'
const StyledTripForm = styled.div` const StyledTripForm = styled.div`
padding: 1rem; padding: 1rem;
@ -27,10 +37,11 @@ class TripForm extends Component {
state = { state = {
id: null, id: null,
trip_id: "", trip_id: '',
service: null, service: null,
frequency_set: [], frequency_set: [],
newStopTime: false, newStopTime: false,
newFrequency: false,
} }
constructor() { constructor() {
@ -45,6 +56,11 @@ class TripForm extends Component {
this.setState({ newStopTime: !this.state.newStopTime }) this.setState({ newStopTime: !this.state.newStopTime })
} }
toggleNewFrequency = () => {
this.setState({ newFrequency: !this.state.newFrequency })
}
handleChange(evt) { handleChange(evt) {
let updated = {} let updated = {}
updated[evt.target.name] = evt.target.value updated[evt.target.name] = evt.target.value
@ -53,9 +69,10 @@ class TripForm extends Component {
handleSubmit() { handleSubmit() {
const { id } = this.state const { id } = this.state
let body = {...this.state } let body = { ...this.state }
delete body.id delete body.id
delete body.newStopTime delete body.newStopTime
delete body.newFrequency
if (id !== null) { if (id !== null) {
store.dispatch(updateTrip(id, body)) store.dispatch(updateTrip(id, body))
} else { } else {
@ -84,16 +101,14 @@ class TripForm extends Component {
} }
} }
if (tripId !== undefined && state.id === null) { if (tripId !== undefined && state.id === null) {
const { route } = props const { trip } = props
const tRoute = route.results.filter(ele => ele.route_id === routeId) const matches = trip.results.filter(ele => ele.trip_id === tripId)
if (tRoute.length > 0) { if (matches.length > 0) {
const trips = tRoute[0].trip_set.filter(ele => ele.trip_id === tripId) store.dispatch(getStopTime(`trip=${matches[0].id}&limit=100`))
if (trips.length > 0) { store.dispatch(getFrequency(`trip=${matches[0].id}`))
store.dispatch(getStopTime(`trip=${trips[0].id}&limit=100`)) return matches[0]
return trips[0]
}
} else { } else {
store.dispatch(getRoute(`agency=${agencyId}`)) store.dispatch(getTrip(`route=${routeId}`))
} }
} }
return null return null
@ -104,7 +119,23 @@ class TripForm extends Component {
const { agencyId, routeId } = this.props.match.params const { agencyId, routeId } = this.props.match.params
const { fetching } = this.props.route const { fetching } = this.props.route
const { calendar } = this.props const { calendar } = this.props
const serviceChoices = calendar.results.map(ele => ({
...ele,
value: ele.id,
label: ele.service_id
}))
let serviceValue = null
if (one.service !== null) {
if (one.service && isNaN(one.service)) {
serviceValue = {
...one.service,
value: one.service.id,
label: one.service.service_id,
}
} else {
serviceValue = getItemFromList(one.service, serviceChoices)
}
}
return ( return (
<StyledTripForm> <StyledTripForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1> <h1 className="title">{one.name}&nbsp;&nbsp;</h1>
@ -134,20 +165,18 @@ class TripForm extends Component {
label="Service" label="Service"
type="text" type="text"
fieldName="service" fieldName="service"
value={one.service}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={calendar.results.map(ele => ({ value: ele.id, label: ele.service_id }))} />} value={serviceValue}
choices={serviceChoices}
/>}
<HorizontalSelect <HorizontalSelect
label="Direction ID" label="Direction ID"
type="text" type="text"
fieldName="direction_id" fieldName="direction_id"
value={one.direction_id || ''} value={getItemFromList(one.direction_id, DirectionChoices)}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={DirectionChoices} />
{ value: '0', label: 'outbound' },
{ value: '1', label: 'inbound' },
]} />
<HorizontalInput <HorizontalInput
label="Block ID" label="Block ID"
@ -160,25 +189,17 @@ class TripForm extends Component {
label="Wheelchair Accessible" label="Wheelchair Accessible"
type="text" type="text"
fieldName="wheelchair_accessible" fieldName="wheelchair_accessible"
value={one.wheelchair_accessible || '0'} value={getItemFromList(one.wheelchair_accessible, WheelChairAccessibles, '0')}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={WheelChairAccessibles} />
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]} />
<HorizontalSelect <HorizontalSelect
label="Bike Allowance" label="Bike Allowance"
type="text" type="text"
fieldName="bike_allowed" fieldName="bike_allowed"
value={one.bike_allowed || '0'} value={getItemFromList(one.bike_allowed, BikeAllowanceChoices, '0')}
handleChange={this.handleChange} handleChange={this.handleChange}
choices={[ choices={BikeAllowanceChoices} />
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]} />
</div> </div>
<div className="field is-grouped"> <div className="field is-grouped">
<div className="control"> <div className="control">
@ -203,24 +224,20 @@ class TripForm extends Component {
render () { render () {
const one = this.state const one = this.state
const { frequency, stoptime } = this.props
const { fetching } = this.props.trip const { fetching } = this.props.trip
// redirect to view page if no data // redirect to view page if no data
const { agencyId, routeId } = this.props.match.params const { agencyId, routeId } = this.props.match.params
// this is a create form let freq = (one.frequency_set.length > 0) ? one.frequency_set[0] : null
// if (tripId === undefined) { if (freq === null && frequency.count > 0) {
// if (one.justSubmit === true && !fetching) { freq = frequency.results[0]
// return <Redirect to={`/trip/`} /> }
// }
// return this.renderForm()
// }
// if (one.id === null && tripId.length > 0)
// return <Redirect to={`/trip/`} />
// redirect to trip list if submitted // redirect to trip list if submitted
if (one.justSubmit === true && !fetching) { if (one.justSubmit === true && !fetching) {
return <Redirect to={`/map/${agencyId}/route/${routeId}/trip`} /> return <Redirect to={`/map/${agencyId}/route/${routeId}/trip`} />
} }
let StopTimePane = null let StopTimePane = null
if (this.state.id !== null) { if (this.state.id !== null) {
StopTimePane = ( StopTimePane = (
@ -231,8 +248,9 @@ class TripForm extends Component {
trip={this.state.id} trip={this.state.id}
toggleEditMode={this.toggleNewStopTime} /> } toggleEditMode={this.toggleNewStopTime} /> }
</StyleBox> </StyleBox>
{this.props.stoptime.results.map( {stoptime.results
ele => <StopTimeOne key={`stb-${ele.id}`} item={ele} />)} .sort((a, b) => a.sequence - b.sequence)
.map(ele => <StopTimeOne key={`stb-${ele.id}`} item={ele} />)}
</div> </div>
) )
} }
@ -241,6 +259,13 @@ class TripForm extends Component {
<div className="columns"> <div className="columns">
<div className="column is-half"> <div className="column is-half">
{this.renderForm()} {this.renderForm()}
{!this.state.newFrequency && <button className="button is-outlined" onClick={this.toggleNewFrequency}>new Frequency</button>}
{this.state.newFrequency && <FrequencyForm {...this.props}
trip={this.state.id}
toggleEditMode={this.toggleNewFrequency} /> }
{frequency.results.map(
ele => <FrequencyOne key={`frqy-${ele.id}`} tripId={one.id} item={ele} />)}
</div> </div>
{StopTimePane} {StopTimePane}
</div> </div>
@ -254,6 +279,7 @@ const mapStateToProps = state => ({
trip: state.trip, trip: state.trip,
calendar: state.calendar, calendar: state.calendar,
stoptime: state.stoptime, stoptime: state.stoptime,
frequency: state.frequency,
}) })
const connectTripForm = connect( const connectTripForm = connect(

5
src/components/parts/HorizontalSelect.js

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import Select from 'react-select' import Select from 'react-select'
import 'react-select/dist/react-select.css'
const HorizontalInput = (props) => ( const HorizontalInput = (props) => (
@ -11,11 +10,11 @@ const HorizontalInput = (props) => (
<div className="field-body"> <div className="field-body">
<div className="field"> <div className="field">
<Select <Select
className="control" defaultOptions
classNamePrefix="control"
name={props.fieldName} name={props.fieldName}
value={props.value} value={props.value}
onChange={(data) => { onChange={(data) => {
console.log('change', data)
let evt = { let evt = {
target: { target: {
name: props.fieldName, name: props.fieldName,

4
src/components/parts/Select.js

@ -1,13 +1,13 @@
import React from 'react' import React from 'react'
import Select from 'react-select' import Select from 'react-select'
import 'react-select/dist/react-select.css'
const NormalSelect = (props) => ( const NormalSelect = (props) => (
<div className="field"> <div className="field">
<label className="label">{props.label}</label> <label className="label">{props.label}</label>
<Select <Select
className="control" defaultOptions
classNamePrefix="control"
name={props.fieldName} name={props.fieldName}
value={props.value} value={props.value}
onChange={(data) => { onChange={(data) => {

41
src/components/parts/SelectOptions.js

@ -0,0 +1,41 @@
import React from 'react'
import { components } from 'react-select'
export const StopOption = (props) => {
const { stop_id, name, stop_desc } = props.data.value
return (
<components.Option {...props}>
<code>{stop_id}</code> {name}
{stop_desc.length > 0 && <small><br />{stop_desc}</small>}
</components.Option>
)
}
export const FareAttrOption = (props) => {
const { fare_id, price, currency_type } = props.data.value
return (
<components.Option {...props}>
<code>{fare_id}</code> {price} {currency_type}
</components.Option>
)
}
export const RouteOption = (props) => {
const { route_id, short_name, long_name} = props.data.value
return (
<components.Option {...props}>
<code>{route_id}</code> {short_name}
<br/>{long_name}
</components.Option>
)
}
export const AgencyOption = (props) => {
const { agency_id, name } = props.data.value
return (
<components.Option {...props}>
<code>{agency_id}</code> {name}
</components.Option>
)
}

21
src/constants/ActionTypes.js

@ -8,12 +8,14 @@ export const GEO_MARKER_ADD = 'GEO_MARKER_ADD'
export const GEO_MARKER_RESET = 'GEO_MARKER_RESET' export const GEO_MARKER_RESET = 'GEO_MARKER_RESET'
export const GEO_MARKER_UPDATE = 'GEO_MARKER_UPDATE' export const GEO_MARKER_UPDATE = 'GEO_MARKER_UPDATE'
export const GEO_MAPCENTER_UPDATE = 'GEO_MAPCENTER_UPDATE'
export const GEO_DRAGMARKER_ENABLE = 'GEO_DRAGMARKER_ENABLE' export const GEO_DRAGMARKER_ENABLE = 'GEO_DRAGMARKER_ENABLE'
export const GEO_DRAGMARKER_DISABLE = 'GEO_DRAGMARKER_DISABLE' export const GEO_DRAGMARKER_DISABLE = 'GEO_DRAGMARKER_DISABLE'
export const GEO_DRAGMARKER_CHANGE = 'GEO_DRAGMARKER_CHANGE' export const GEO_DRAGMARKER_CHANGE = 'GEO_DRAGMARKER_CHANGE'
export const GEO_MAPCENTER_UPDATE = 'GEO_MAPCENTER_UPDATE'
export const GEO_LASTCENTER_UPDATE = 'GEO_LASTCENTER_UPDATE'
export const GEO_STOPMARKER_TOGGLE = 'GEO_STOPMARKER_TOGGLE'
export const GEO_STOP_AURA_TOGGLE = 'GEO_STOP_AURA_TOGGLE'
export const REQUEST_LOGIN = 'REQUEST_LOGIN' export const REQUEST_LOGIN = 'REQUEST_LOGIN'
export const SUCCESS_LOGIN = 'SUCCESS_LOGIN' export const SUCCESS_LOGIN = 'SUCCESS_LOGIN'
@ -69,6 +71,13 @@ export const CALENDAR_CREATE = 'CALENDAR_CREATE'
export const CALENDAR_DELETE = 'CALENDAR_DELETE' export const CALENDAR_DELETE = 'CALENDAR_DELETE'
export const CALENDAR_UPDATE = 'CALENDAR_UPDATE' export const CALENDAR_UPDATE = 'CALENDAR_UPDATE'
export const FREQUENCY_REQUEST = 'FREQUENCY_REQUEST'
export const FREQUENCY_FAILURE = 'FREQUENCY_FAILURE'
export const FREQUENCY_SUCCESS = 'FREQUENCY_SUCCESS'
// below items are SUCCESS for other tasks
export const FREQUENCY_CREATE = 'FREQUENCY_CREATE'
export const FREQUENCY_DELETE = 'FREQUENCY_DELETE'
export const FREQUENCY_UPDATE = 'FREQUENCY_UPDATE'
export const FAREATTR_REQUEST = 'FAREATTR_REQUEST' export const FAREATTR_REQUEST = 'FAREATTR_REQUEST'
export const FAREATTR_FAILURE = 'FAREATTR_FAILURE' export const FAREATTR_FAILURE = 'FAREATTR_FAILURE'
@ -77,3 +86,11 @@ export const FAREATTR_SUCCESS = 'FAREATTR_SUCCESS'
export const FAREATTR_CREATE = 'FAREATTR_CREATE' export const FAREATTR_CREATE = 'FAREATTR_CREATE'
export const FAREATTR_DELETE = 'FAREATTR_DELETE' export const FAREATTR_DELETE = 'FAREATTR_DELETE'
export const FAREATTR_UPDATE = 'FAREATTR_UPDATE' export const FAREATTR_UPDATE = 'FAREATTR_UPDATE'
export const FARERULE_REQUEST = 'FARERULE_REQUEST'
export const FARERULE_FAILURE = 'FARERULE_FAILURE'
export const FARERULE_SUCCESS = 'FARERULE_SUCCESS'
// below items are SUCCESS for other tasks
export const FARERULE_CREATE = 'FARERULE_CREATE'
export const FARERULE_DELETE = 'FARERULE_DELETE'
export const FARERULE_UPDATE = 'FARERULE_UPDATE'

2
src/constants/Api.js

@ -1,5 +1,7 @@
// export const URL = process.env.API_URL || '//localhost:8000'
export const URL = process.env.API_URL || 'https://api.goth.app' export const URL = process.env.API_URL || 'https://api.goth.app'
export const API_PREFIX = process.env.API_PREFIX || '/v1' export const API_PREFIX = process.env.API_PREFIX || '/v1'
export const API_URL = `${URL}${API_PREFIX}` export const API_URL = `${URL}${API_PREFIX}`
export const LOGIN = '/api-token-auth/' export const LOGIN = '/api-token-auth/'
export const RAVEN_DSN = 'https://7d87b4ca392d48589794769a223e9bfb@sentry.io/1248366'

76
src/constants/choices.js

@ -0,0 +1,76 @@
export const RouteTypeChoices = [
{ value: '0', label: 'Tram, Light rail' },
{ value: '1', label: 'Subway (within metro area)' },
{ value: '2', label: 'Rail' },
{ value: '3', label: 'Bus' },
{ value: '4', label: 'Ferry' },
{ value: '5', label: 'Cable car' },
{ value: '6', label: 'Gondola' },
{ value: '7', label: 'Funicular (steep inclines)' },
]
export const StopLocationTypes = [
{ value: '0', label: 'Stop' },
{ value: '1', label: 'Station' },
{ value: '2', label: 'Station Entrance/Exit' },
]
export const StopWheelChairInfo = [
{ value: '0', label: 'No information' },
{ value: '1', label: 'Possible (partially or fully)' },
{ value: '2', label: 'Not possible' },
]
export const DropOffTypes = [
{ value: '0', label: 'Regularly scheduled dropoff' },
{ value: '1', label: 'No drop off' },
{ value: '2', label: 'Arrange drop off' },
{ value: '3', label: 'Contact driver' },
]
export const TimePointChoices = [
{ value: '0', label: 'times are considered approximate.' },
{ value: '1', label: 'times are considered exact.' },
]
export const PickUpTypes = [
{ value: '0', label: 'Regularly scheduled pickup' },
{ value: '1', label: 'No pickup' },
{ value: '2', label: 'Arrange pickup' },
{ value: '3', label: 'Contact driver' },
]
export const DirectionChoices = [
{ value: '0', label: 'outbound' },
{ value: '1', label: 'inbound' },
]
export const WheelChairAccessibles = [
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]
export const BikeAllowanceChoices = [
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]
export const ExactTimeChoices = [
{ value: '0', label: 'Not exactly scheduled.' },
{ value: '1', label: 'Exactly scheduled' },
]
export const PaymentMethodChoices = [
{ value: '0', label: 'Paid on board' },
{ value: '1', label: 'Paid before boarding' },
]
export const TransferChoices = [
{ value: '0', label: 'No transfer' },
{ value: '1', label: 'Transfer once' },
{ value: '2', label: 'Transfer twice' },
{ value: '', label: 'Unlimited transfer' },
]

122
src/container/Geo.js

@ -6,15 +6,17 @@ import {
Map, TileLayer, CircleMarker, ZoomControl, Map, TileLayer, CircleMarker, ZoomControl,
FeatureGroup, GeoJSON, Marker } from 'react-leaflet' FeatureGroup, GeoJSON, Marker } from 'react-leaflet'
// import { EditControl } from 'react-leaflet-draw' // import { EditControl } from 'react-leaflet-draw'
import L from 'leaflet' import * as L from 'leaflet'
import { loggedIn } from '../reducers/auth' import { loggedIn } from '../reducers/auth'
import { import {
geoLocationFailed, geoLocationUpdate, getAgency, geoLocationFailed, geoLocationUpdate, getAgency,
draggableMarkerUpdate draggableMarkerUpdate, lastCenterUpdate, geoStopToggler,
geoStopAuraToggler,
} from '../actions' } from '../actions'
import FloatPane from '../components/FloatPane' import FloatPane from '../components/FloatPane'
import store from '../store' import store from '../store'
import { decodeGeoJson } from '../utils'
const FullPageBox = styled.div` const FullPageBox = styled.div`
@ -36,13 +38,39 @@ position: fixed;
text-align: center; text-align: center;
padding-top: 10px; padding-top: 10px;
right: 0; right: 0;
bottom: 100px; bottom: 150px;
z-index: 30; z-index: 30;
` `
const StopToggler = styled.div`
width: 80px
height: 40px;
background: #fefefebb;
border-radius: 10px;
color: #209cee;
position: fixed;
text-align: center;
padding-top: 10px;
right: 0;
bottom: 110px;
z-index: 30;
`
const StopAuraToggler = styled.div`
width: 80px
height: 40px;
background: #fefefebb;
border-radius: 10px;
color: #209cee;
position: fixed;
text-align: center;
padding-top: 10px;
right: 0;
bottom: 70px;
z-index: 30;
`
// TODO: filter for existing polygons // TODO: filter for existing polygons
// TODO: update mapCenter to recent active route/trip or current location
const stopIcon = L.divIcon({ const stopIcon = L.divIcon({
className: 'divIcon', className: 'divIcon',
@ -51,6 +79,15 @@ const stopIcon = L.divIcon({
iconAnchor: [12, 23] iconAnchor: [12, 23]
}) })
const mintStopIcon = L.icon({
iconUrl: '//static.10ninox.com/map/mint-marker-icon.png',
iconRetinaUrl: '//static.10ninox.com/map/mint-marker-icon-2x.png',
iconSize: [25, 41],
iconAnchor: [12, 40],
popupAnchor: [2, -22],
})
const dragableIcon = L.icon({ const dragableIcon = L.icon({
iconUrl: '//static.10ninox.com/map/pink-marker-icon.png', iconUrl: '//static.10ninox.com/map/pink-marker-icon.png',
iconRetinaUrl: '//static.10ninox.com/map/pink-marker-icon-2x.png', iconRetinaUrl: '//static.10ninox.com/map/pink-marker-icon-2x.png',
@ -101,9 +138,13 @@ class Geo extends Component {
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
const omCenter = this.props.geo.mapCenter const omCenter = this.props.geo.mapCenter
const newCenter = newProps.geo.mapCenter const newCenter = newProps.geo.mapCenter
let center = {
lastCenter: newProps.geo.lastCenter
}
if (omCenter[0] !== newCenter[0] || omCenter[1] !== newCenter[1]) { if (omCenter[0] !== newCenter[0] || omCenter[1] !== newCenter[1]) {
this.setState({mapCenter: newCenter}) center.mapCenter = newCenter
} }
this.setState(center)
} }
renderGeoJSON() { renderGeoJSON() {
@ -118,7 +159,7 @@ class Geo extends Component {
{polygons && polygons.filter(ele => ele.geojson).map(ele => ( {polygons && polygons.filter(ele => ele.geojson).map(ele => (
<GeoJSON <GeoJSON
key={`geojson-${ele.id}`} key={`geojson-${ele.id}`}
data={ele.geojson} data={decodeGeoJson(ele.geojson)}
style={{...style, ...ele.style}} /> style={{...style, ...ele.style}} />
))} ))}
</FeatureGroup> </FeatureGroup>
@ -150,18 +191,54 @@ class Geo extends Component {
pointToLayer={(feat, latlon) => { pointToLayer={(feat, latlon) => {
return ( return (
L.marker(latlon, { L.marker(latlon, {
icon: stopIcon icon: feat.properties.icon
}).bindPopup(`<a href="/#/map/stop/${feat.properties.stop_id}">${feat.properties.popupContent}</a>`) }).bindPopup(`<a href="/#/map/stop/${feat.properties.stop_id}">${feat.properties.popupContent}</a>`)
) )
}}/> }}/>
} }
renderStopMarkers() {
const { count, results, query } = this.props.stop
if (count < 1)
return null
const arrStops = results.map((ele) => ({
"type": "Feature",
"properties": {
"popupContent": `${ele.name}<br />${ele.stop_id}`,
"stop_id": `${ele.stop_id}`,
"icon": mintStopIcon,
},
"geometry": ele.geojson,
}))
const stopCollection = {
"type": "FeatureCollection",
"features": arrStops
}
return <GeoJSON
key={`stops-${query}-${count}`}
data={stopCollection}
pointToLayer={(feat, latlon) => {
return (
L.marker(latlon, {
icon: feat.properties.icon
}).bindPopup(`<a href="/#/map/stop/${feat.properties.stop_id}">${feat.properties.popupContent}</a>`)
)
}} />
}
renderStopAura() {
}
forceSetMapCenterToCurrent() { forceSetMapCenterToCurrent() {
const { geo } = this.props const { geo } = this.props
const { leafletElement } = this.refs.map const { leafletElement } = this.refs.map
const { zoom } = this.refs.map.viewport // another key is 'center' const { zoom } = this.refs.map.viewport // another key is 'center'
const curLoc = [geo.coords.latitude, geo.coords.longitude] const curLoc = [geo.coords.latitude, geo.coords.longitude]
leafletElement.setView(curLoc, zoom < 13 ? 13 : zoom) leafletElement.setView(curLoc, zoom > 13 ? 13 : zoom)
} }
render() { render() {
@ -178,14 +255,11 @@ class Geo extends Component {
opacity={1} opacity={1}
className='my-location-marker' /> className='my-location-marker' />
) : null ) : null
/*
draggable={true} const centerMarker = this.state.lastCenter ? (
onDragend={(e) => { console.log(e)}}
*/
const centerMarker = geo.coords ? (
<CircleMarker <CircleMarker
center={this.state.mapCenter} center={this.state.lastCenter}
radius={7} radius={6}
fillColor={'rgb(255, 25, 90)'} fillColor={'rgb(255, 25, 90)'}
fillOpacity={0.75} fillOpacity={0.75}
color={'black'} color={'black'}
@ -217,13 +291,24 @@ class Geo extends Component {
<i className="far fa-dot-circle"></i> <i className="far fa-dot-circle"></i>
</FollowMyLocation> </FollowMyLocation>
</a>} </a>}
<StopToggler onClick={_ => store.dispatch(geoStopToggler())}>
{geo.showStopMarker ? <p>show</p> : <p>hide</p>}
</StopToggler>
<StopAuraToggler onClick={_ => store.dispatch(geoStopAuraToggler())}>
{geo.showStopAura ? <p>A ON</p> : <p>A OFF</p>}
</StopAuraToggler>
<Map <Map
center={this.state.mapCenter} center={this.state.mapCenter}
zoom={13} zoom={geo.zoom}
length={4} length={4}
zoomControl={false} zoomControl={false}
animate animate
style={{flex: 1}} style={{flex: 1}}
onMoveend={(e) => {
const lc = e.target.getCenter()
const z = e.target.getZoom()
store.dispatch(lastCenterUpdate(lc.lat, lc.lng, z))
}}
ref='map'> ref='map'>
<TileLayer <TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
@ -236,6 +321,10 @@ class Geo extends Component {
{draggableMarker} {draggableMarker}
{this.renderGeoJSON()} {this.renderGeoJSON()}
{this.renderStopTime()} {this.renderStopTime()}
{geo.showStopMarker && this.renderStopMarkers()}
{geo.showStopAura && this.renderStopAura()}
<span>
</span>
</Map> </Map>
</FullPageBox> </FullPageBox>
) )
@ -248,6 +337,7 @@ const mapStateToProps = state => ({
agency: state.agency, agency: state.agency,
stoptime: state.stoptime, stoptime: state.stoptime,
geo: state.geo, geo: state.geo,
stop: state.stop,
}) })
export default connect( export default connect(
mapStateToProps, mapStateToProps,

15
src/container/Main.js

@ -4,6 +4,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'
import { loggedIn } from '../reducers/auth' import { loggedIn } from '../reducers/auth'
import { getAgency } from '../actions' import { getAgency } from '../actions'
import First from '../components/First'
import Nav from '../components/Nav' import Nav from '../components/Nav'
import Footer from '../components/Footer' import Footer from '../components/Footer'
import CalendarForm from '../components/CalendarForm' import CalendarForm from '../components/CalendarForm'
@ -12,6 +13,12 @@ import AgencyList from '../components/AgencyList'
import AgencyItem from '../components/AgencyItem' import AgencyItem from '../components/AgencyItem'
import AgencyForm from '../components/AgencyForm' import AgencyForm from '../components/AgencyForm'
import TripForm from '../components/TripForm' import TripForm from '../components/TripForm'
import FareList from '../components/FareList'
import FareRulesForm from '../components/FareRulesForm'
import FareAttributesForm from '../components/FareAttributesForm'
import FareAttributesDetail from '../components/FareAttributesDetail'
import { LOGIN_PATH } from '../constants/path' import { LOGIN_PATH } from '../constants/path'
import store from '../store' import store from '../store'
@ -47,9 +54,17 @@ class Main extends Component {
<Route path={`${match.url}agency/:agencyId/:agencyChild`} component={AgencyItem} /> <Route path={`${match.url}agency/:agencyId/:agencyChild`} component={AgencyItem} />
<Route exact path={`${match.url}agency`} component={AgencyList} /> <Route exact path={`${match.url}agency`} component={AgencyList} />
<Route exact path={`${match.url}fare`} component={FareList} />
<Route exact path={`${match.url}fare/attributes/new`} component={FareAttributesForm} />
<Route exact path={`${match.url}fare/attributes/:attribID/edit`} component={FareAttributesForm} />
<Route exact path={`${match.url}fare/attributes/:attribID`} component={FareAttributesDetail} />
<Route exact path={`${match.url}fare/rules/new`} component={FareRulesForm} />
<Route exact path={`${match.url}fare/rules/:ruleID`} component={FareRulesForm} />
<Route exact path={`${match.url}calendar/new`} component={CalendarForm} /> <Route exact path={`${match.url}calendar/new`} component={CalendarForm} />
<Route exact path={`${match.url}calendar/:serviceId`} component={CalendarForm} /> <Route exact path={`${match.url}calendar/:serviceId`} component={CalendarForm} />
<Route exact path={`${match.url}calendar`} component={CalendarList} /> <Route exact path={`${match.url}calendar`} component={CalendarList} />
<Route path={`${match.url}`} component={First} />
</Switch> </Switch>
</div> </div>
<Footer /> <Footer />

12
src/index.js

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import Raven from 'raven-js'
import './index.css' import './index.css'
import App from './App' import App from './App'
import registerServiceWorker from './registerServiceWorker' import registerServiceWorker from './registerServiceWorker'
@ -9,9 +10,20 @@ import { Provider } from 'react-redux'
import { persistStore } from 'redux-persist' import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import store from './store' import store from './store'
import { RAVEN_DSN } from './constants/Api'
import { registerObserver } from 'react-perf-devtool' import { registerObserver } from 'react-perf-devtool'
Raven.config(RAVEN_DSN, {
logger: 'my-logger',
whitelistUrls: [
/grunt\.goth\.com/
],
ignoreErrors: [ ],
includePaths: [
// /https?:\/\/(www\.)?getsentry\.com/
]
}).install()
const persistor = persistStore(store) const persistor = persistStore(store)

11
src/reducers/agency.js

@ -1,6 +1,7 @@
import { import {
AGENCY_CREATE, AGENCY_DELETE, AGENCY_UPDATE, AGENCY_CREATE, AGENCY_DELETE, AGENCY_UPDATE,
AGENCY_REQUEST, AGENCY_SUCCESS, AGENCY_FAILURE, AGENCY_REQUEST, AGENCY_SUCCESS, AGENCY_FAILURE,
GEO_POLYGON_RESET,
} from '../constants/ActionTypes' } from '../constants/ActionTypes'
@ -12,20 +13,26 @@ const agencyInitState = {
} }
const agency = (state = agencyInitState, action) => { const agency = (state = agencyInitState, action) => {
switch(action.type) { switch(action.type) {
// somehow search fetching stuck we need some way to reset that
case GEO_POLYGON_RESET:
return {
...state,
fetching: false,
}
case AGENCY_REQUEST: case AGENCY_REQUEST:
return { return {
...state, ...state,
fetching: true, fetching: true,
} }
case AGENCY_SUCCESS: case AGENCY_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...(prev ? state.results : []), ...(previous ? state.results : []),
...results, ...results,
] ]
} }

19
src/reducers/auth.js

@ -1,5 +1,6 @@
import { import {
REQUEST_LOGIN, SUCCESS_LOGIN, FAILED_LOGIN, SUCCESS_LOGOUT REQUEST_LOGIN, SUCCESS_LOGIN, FAILED_LOGIN, SUCCESS_LOGOUT,
STOP_FAILURE, CALENDAR_FAILURE, ROUTE_FAILURE, AGENCY_FAILURE,
} from '../constants/ActionTypes' } from '../constants/ActionTypes'
@ -35,9 +36,21 @@ const auth = (state = tokenInitialState, action) => {
token: null, token: null,
fetching: false, fetching: false,
msg: '', msg: '',
}; }
case STOP_FAILURE:
case CALENDAR_FAILURE:
case ROUTE_FAILURE:
case AGENCY_FAILURE:
if (action.payload.status === 403) {
return {
token: null,
fetching: false,
msg: '',
}
}
return state
default: default:
return state; return state
} }
} }

4
src/reducers/calendar.js

@ -19,14 +19,14 @@ const calendar = (state = calendarInitState, action) => {
fetching: true, fetching: true,
} }
case CALENDAR_SUCCESS: case CALENDAR_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...( (prev) ? state.results : [] ), ...( (previous) ? state.results : [] ),
...results, ...results,
] ]
} }

7
src/reducers/fareattr.js

@ -14,21 +14,20 @@ const fareAttrInitState = {
const fareAttr = (state = fareAttrInitState, action) => { const fareAttr = (state = fareAttrInitState, action) => {
switch(action.type) { switch(action.type) {
case FAREATTR_REQUEST: case FAREATTR_REQUEST:
const { query } = action.meta
return { return {
...state, ...state,
fetching: true, fetching: true,
query, query: action.meta !== undefined ? action.meta.query : state.query,
} }
case FAREATTR_SUCCESS: case FAREATTR_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...( (prev) ? state.results : [] ), ...( (previous) ? state.results : [] ),
...results, ...results,
] ]
} }

100
src/reducers/farerule.js

@ -0,0 +1,100 @@
import {
FARERULE_CREATE, FARERULE_DELETE, FARERULE_UPDATE,
FARERULE_REQUEST, FARERULE_SUCCESS, FARERULE_FAILURE,
} from '../constants/ActionTypes'
const fareRuleInitState = {
results: [],
previous: null,
next: null,
count: 0,
fetching: false,
query: '',
isNext: false,
}
const fareRule = (state = fareRuleInitState, action) => {
switch(action.type) {
case FARERULE_REQUEST:
let resetResult, resetCount, isNextReq = false
if (action.meta !== undefined) {
if (action.meta.query === 'next') {
isNextReq = true
} else {
resetResult = state.query !== action.meta.query ? [] : state.results
resetCount = state.query !== action.meta.query ? 0 : state.count
}
}
const updateQRC = (action.meta !== undefined) && !isNextReq
return {
...state,
fetching: true,
query: updateQRC ? action.meta.query : state.query,
results: updateQRC ? resetResult : state.results,
count: updateQRC ? resetCount : state.count,
}
case FARERULE_SUCCESS:
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
previous,
next,
results: [
...( (previous) ? state.results : [] ),
...results,
],
}
case FARERULE_UPDATE:
const { id } = action.payload
const oldResults = state.results
const targetInd = oldResults.findIndex(ele => ele.id === id)
return {
...state,
fetching: false,
results: [
...oldResults.slice(0, targetInd),
action.payload,
...oldResults.slice(targetInd + 1)
]
}
case FARERULE_CREATE:
let r
/* check if it's array by
- action.payload instanceof Array
- Array.isArray(action.payload)
*/
if (Array.isArray(action.payload)) {
r = [...state.results, ...action.payload]
} else {
r = [...state.results, action.payload]
}
return {
...state,
fetching: false,
count: state.count + 1,
results: r,
}
case FARERULE_DELETE:
const deleteInd = state.results.findIndex(ele => ele.id === action.meta.id)
return {
...state,
count: state.count - 1,
fetching: false,
results: [
...state.results.slice(0, deleteInd),
...state.results.slice(deleteInd + 1)
]
}
case FARERULE_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default fareRule

88
src/reducers/frequency.js

@ -0,0 +1,88 @@
import {
FREQUENCY_CREATE, FREQUENCY_DELETE, FREQUENCY_UPDATE,
FREQUENCY_REQUEST, FREQUENCY_SUCCESS, FREQUENCY_FAILURE,
GEO_POLYGON_RESET,
} from '../constants/ActionTypes'
const frequencyInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const frequency = (state = frequencyInitState, action) => {
switch (action.type) {
case GEO_POLYGON_RESET:
return {
...state,
fetching: false,
count: 0,
next: null,
query: '',
results: [],
}
case FREQUENCY_REQUEST:
return {
...state,
fetching: true,
query: action.meta !== undefined ? action.meta.query : state.query,
}
case FREQUENCY_SUCCESS:
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...((previous) ? state.results : []),
...results,
]
}
case FREQUENCY_UPDATE:
const { id } = action.payload
const oldResults = state.results
const targetInd = oldResults.findIndex(ele => ele.id === id)
return {
...state,
fetching: false,
results: [
...oldResults.slice(0, targetInd),
action.payload,
...oldResults.slice(targetInd + 1)
]
}
case FREQUENCY_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case FREQUENCY_DELETE:
const deleteInd = state.results.findIndex(ele => ele.id === action.meta.id)
return {
...state,
count: state.count - 1,
fetching: false,
results: [
...state.results.slice(0, deleteInd),
...state.results.slice(deleteInd + 1)
]
}
case FREQUENCY_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default frequency

22
src/reducers/geo.js

@ -2,8 +2,9 @@ import {
GEO_LOCATION_SUCCESS, GEO_LOCATION_FAILURE, GEO_LOCATION_SUCCESS, GEO_LOCATION_FAILURE,
GEO_MARKER_ADD, GEO_MARKER_RESET, GEO_MARKER_UPDATE, GEO_MARKER_ADD, GEO_MARKER_RESET, GEO_MARKER_UPDATE,
GEO_POLYGON_ADD, GEO_POLYGON_RESET, GEO_POLYGON_UPDATE, GEO_POLYGON_ADD, GEO_POLYGON_RESET, GEO_POLYGON_UPDATE,
GEO_MAPCENTER_UPDATE, GEO_MAPCENTER_UPDATE, GEO_LASTCENTER_UPDATE,
GEO_DRAGMARKER_CHANGE, GEO_DRAGMARKER_DISABLE, GEO_DRAGMARKER_ENABLE, GEO_DRAGMARKER_CHANGE, GEO_DRAGMARKER_DISABLE, GEO_DRAGMARKER_ENABLE,
GEO_STOPMARKER_TOGGLE, GEO_STOP_AURA_TOGGLE,
} from '../constants/ActionTypes' } from '../constants/ActionTypes'
const initialState = { const initialState = {
@ -12,13 +13,27 @@ const initialState = {
message: '', message: '',
polygons: [], polygons: [],
markers: [], markers: [],
lastCenter: [13.84626739, 100.538],
mapCenter: [13.84626739, 100.538], mapCenter: [13.84626739, 100.538],
zoom: 13,
showStopMarker: false,
showStopAura: false,
draggableMarker: false, draggableMarker: false,
draggableMarkerLatlon: [13.8462745, 100.5382592], draggableMarkerLatlon: [13.8462745, 100.5382592],
} }
const geo = (state = initialState, action) => { const geo = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case GEO_STOP_AURA_TOGGLE:
return {
...state,
showStopAura: !state.showStopAura,
}
case GEO_STOPMARKER_TOGGLE:
return {
...state,
showStopMarker: !state.showStopMarker,
}
case GEO_DRAGMARKER_ENABLE: case GEO_DRAGMARKER_ENABLE:
return { return {
...state, ...state,
@ -35,6 +50,11 @@ const geo = (state = initialState, action) => {
...state, ...state,
draggableMarkerLatlon: action.payload, draggableMarkerLatlon: action.payload,
} }
case GEO_LASTCENTER_UPDATE:
return {
...state,
...action.payload,
}
case GEO_MAPCENTER_UPDATE: case GEO_MAPCENTER_UPDATE:
return { return {
...state, ...state,

4
src/reducers/index.js

@ -4,8 +4,10 @@ import auth from './auth'
import agency from './agency' import agency from './agency'
import route from './route' import route from './route'
import fareattr from './fareattr' import fareattr from './fareattr'
import farerule from './farerule'
import calendar from './calendar' import calendar from './calendar'
import stoptime from './stoptime' import stoptime from './stoptime'
import frequency from './frequency'
import stop from './stop' import stop from './stop'
import trip from './trip' import trip from './trip'
@ -16,6 +18,8 @@ export default combineReducers({
route, route,
stop, stop,
fareattr, fareattr,
farerule,
frequency,
calendar, calendar,
stoptime, stoptime,
trip, trip,

4
src/reducers/route.js

@ -20,14 +20,14 @@ const route = (state = routeInitState, action) => {
query: action.meta !== undefined ? action.meta.query : state.query, query: action.meta !== undefined ? action.meta.query : state.query,
} }
case ROUTE_SUCCESS: case ROUTE_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...( (prev) ? state.results : [] ), ...( (previous) ? state.results : [] ),
...results, ...results,
] ]
} }

17
src/reducers/stop.js

@ -10,6 +10,7 @@ const stopInitState = {
next: null, next: null,
count: 0, count: 0,
fetching: false, fetching: false,
query: '',
} }
const stop = (state = stopInitState, action) => { const stop = (state = stopInitState, action) => {
switch(action.type) { switch(action.type) {
@ -20,25 +21,35 @@ const stop = (state = stopInitState, action) => {
fetching: false, fetching: false,
} }
case STOP_REQUEST: case STOP_REQUEST:
const q = action.payload && action.payload.query
return { return {
...state, ...state,
fetching: true, fetching: true,
query: q || '',
} }
case STOP_SUCCESS: case STOP_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...(prev ? state.results : []), ...(previous ? state.results : []),
...results, ...results,
] ]
} }
case STOP_UPDATE: case STOP_UPDATE:
const { id } = action.payload const { id } = action.payload
const oldResults = state.results let oldResults = state.results
if (action.meta && action.meta.opt === "merge") {
const rmStop = action.meta.mergeWith.stop
const rmInd = oldResults.findIndex(ele => ele.id === rmStop.id)
oldResults = [
...oldResults.slice(0, rmInd),
...oldResults.slice(rmInd + 1)
]
}
const targetInd = oldResults.findIndex(ele => ele.id === id) const targetInd = oldResults.findIndex(ele => ele.id === id)
return { return {
...state, ...state,

14
src/reducers/stoptime.js

@ -1,6 +1,7 @@
import { import {
STOPTIME_CREATE, STOPTIME_DELETE, STOPTIME_UPDATE, STOPTIME_CREATE, STOPTIME_DELETE, STOPTIME_UPDATE,
STOPTIME_REQUEST, STOPTIME_SUCCESS, STOPTIME_FAILURE, STOPTIME_REQUEST, STOPTIME_SUCCESS, STOPTIME_FAILURE,
GEO_POLYGON_RESET,
} from '../constants/ActionTypes' } from '../constants/ActionTypes'
@ -13,6 +14,15 @@ const stoptimeInitState = {
} }
const stoptime = (state = stoptimeInitState, action) => { const stoptime = (state = stoptimeInitState, action) => {
switch (action.type) { switch (action.type) {
case GEO_POLYGON_RESET:
return {
...state,
fetching: false,
count: 0,
next: null,
query: '',
results: [],
}
case STOPTIME_REQUEST: case STOPTIME_REQUEST:
return { return {
...state, ...state,
@ -20,14 +30,14 @@ const stoptime = (state = stoptimeInitState, action) => {
query: action.meta !== undefined ? action.meta.query : state.query, query: action.meta !== undefined ? action.meta.query : state.query,
} }
case STOPTIME_SUCCESS: case STOPTIME_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...((prev) ? state.results : []), ...((previous) ? state.results : []),
...results, ...results,
] ]
} }

4
src/reducers/trip.js

@ -18,14 +18,14 @@ const trip = (state = tripInitState, action) => {
fetching: true, fetching: true,
} }
case TRIP_SUCCESS: case TRIP_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, previous, results } = action.payload
return { return {
...state, ...state,
fetching: false, fetching: false,
count, count,
next, next,
results: [ results: [
...(prev ? state.results : []), ...(previous ? state.results : []),
...results, ...results,
] ]
} }

19
src/sagas.js

@ -0,0 +1,19 @@
import { call, put, takeLatest } from 'redux-saga/effects' // takeEvery
import { apiClient } from './utils/ApiClient'
import * as types from './constants/ActionTypes'
function* fetchStop(action) {
try {
const url = `/stop/?${action.payload.query || ''}`
const stops = yield call(apiClient, url)
yield put({type: types.STOP_SUCCESS, payload: stops.data})
} catch (e) {
yield put({type: types.STOP_FAILURE, message: e.message})
}
}
function* mySaga() {
yield takeLatest(types.STOP_REQUEST, fetchStop)
}
export default mySaga

16
src/store.js

@ -1,19 +1,28 @@
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import { createStore, applyMiddleware, compose } from 'redux' import { createStore, applyMiddleware, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { apiMiddleware } from 'redux-api-middleware' import { apiMiddleware } from 'redux-api-middleware'
import { createLogger } from 'redux-logger' import { createLogger } from 'redux-logger'
import { persistReducer } from 'redux-persist' import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native
import mySaga from './sagas'
import gruntApp from './reducers' import gruntApp from './reducers'
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage, storage,
blacklist: ['agency', 'route', 'calendar', 'geo'] whitelist: ['auth', 'geo']
// blacklist: ['agency', 'calendar']
} }
const middleware = [ thunk, apiMiddleware ] // create the saga middleware
const sagaMiddleware = createSagaMiddleware()
const middleware = [
thunk,
apiMiddleware,
sagaMiddleware, // mount sagaMiddleware on the store
]
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger()) middleware.push(createLogger())
} }
@ -26,4 +35,7 @@ const persistedReducer = persistReducer(persistConfig, gruntApp)
const store = createStore(persistedReducer, enhancer) const store = createStore(persistedReducer, enhancer)
// run the saga
sagaMiddleware.run(mySaga)
export default store export default store

9
src/utils/ApiClient.js

@ -2,13 +2,14 @@ import axios from 'axios'
import store from '../store' import store from '../store'
import { URL, API_PREFIX } from '../constants/Api' import { URL, API_PREFIX } from '../constants/Api'
export const apiClient = function(url) { export const apiClient = function(url, extra) {
const token = store.getState().auth.token const token = store.getState().auth.token
const params = { const params = {
baseURL: `${URL}${API_PREFIX}${url}`, url: `${URL}${API_PREFIX}${url}`,
headers: {'Authorization': 'Bearer ' + token} headers: { 'Authorization': 'Bearer ' + token },
...(extra || {}),
} }
return axios.get(params) return axios.request(params)
} }
export const RSAAHeaders = () => { export const RSAAHeaders = () => {

96
src/utils/index.js

@ -0,0 +1,96 @@
import { CancelToken } from 'axios'
import { apiClient } from './ApiClient'
import polyUtil from 'polyline-encoded'
export const decodeGeoJson = (encodedOne) => {
if (typeof encodedOne.coordinates === "string")
encodedOne.coordinates = polyUtil.decode(encodedOne.coordinates)
return encodedOne
}
export const getItemFromList = (targetValue, list, defaultValue) => {
if (!targetValue && defaultValue === undefined)
return ''
let f = list.filter(ele => ele.value === targetValue)
if (f.length > 0)
return f[0]
// default value
f = list.filter(ele => ele.value === defaultValue)
if (f.length > 0)
return f[0]
return ''
}
export const getStopsAsyncSelect = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/stop/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
value: {...i},
label: `${i.stop_id}-${i.name}`
})))
})
}
export const getFareAttrAsyncSelect = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/fare-attribute/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
value: {...i},
label: i.fare_id
})))
})
}
export const getRouteAsyncSelect = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/route/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
value: {...i},
label: i.route_id
})))
})
}
export const getAgencyAsyncSelect = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/agency/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
value: {...i},
label: i.name
})))
})
}
Loading…
Cancel
Save