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.expanded": true
}
"todo-tree.expanded": true,
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#e2c34a"
}
}

15
package.json

@ -1,14 +1,21 @@
{
"name": "grunt-front",
"version": "0.1.0",
"version": "0.3.0",
"private": true,
"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",
"axios": "^0.18.0",
"leaflet": "^1.3.1",
"i": "^0.3.6",
"leaflet": "^1.3.2",
"leaflet-draw": "^1.0.2",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"npm": "^6.4.1",
"polyline-encoded": "0.0.8",
"raven-js": "^3.26.4",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-flatpickr": "^3.6.4",
@ -17,11 +24,13 @@
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"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-api-middleware": "^2.3.0",
"redux-logger": "^3.0.6",
"redux-persist": "^5.10.0",
"redux-saga": "^0.16.0",
"redux-thunk": "^2.3.0",
"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/
-->
<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="//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-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.
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",
"name": "Create React App Sample",
"short_name": "Grunt GoTH",
"name": "Grunt for GoTH GTFS",
"icons": [
{
"src": "favicon.ico",
"src": "https://static.10ninox.com/goth/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"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",

22
src/App.js

@ -1,6 +1,12 @@
import React from 'react'
// import './App.css'
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 Geo from './container/Geo'
@ -8,6 +14,22 @@ import Public from './container/Public'
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 = () => (
<Switch>
{/* 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) => {
// PositionError {code: 3, message: "Timeout expired"}
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) => {
return {
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) => ({
[RSAA]: {
endpoint: `${API_URL}/stop/`,

7
src/components/CalendarList.js

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

2
src/components/FareAttrList.js

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

6
src/components/Footer.js

@ -1,13 +1,15 @@
import React from 'react'
import { version } from '../../package.json'
const Footer = () =>
<footer className="footer">
<div className="content has-text-centered">
<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>
</div>
</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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
let invalidEmail = false
let goodPassword = null
@ -16,11 +17,11 @@ const Login = (props) => (
onChange={props.updateField}
placeholder="yourname@example.com" />
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
<FontAwesomeIcon icon="envelope"/>
</span>
{ invalidEmail &&
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
<FontAwesomeIcon icon="exclamation-triangle" />
</span>}
</div>
{ invalidEmail &&
@ -36,11 +37,11 @@ const Login = (props) => (
onKeyPress={(e) => {(e.key === 'Enter') && props.fetchAuth()}}
placeholder="Password" />
<span className="icon is-small is-left">
<i className="fas fa-key"></i>
<FontAwesomeIcon icon="key"/>
</span>
{ goodPassword === true &&
<span className="icon is-small is-right">
<i className="fas fa-check"></i>
<FontAwesomeIcon icon="check"/>
</span> }
</div>
{ goodPassword === true &&

29
src/components/Nav.js

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

38
src/components/RouteDetail.js

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

60
src/components/RouteForm.js

@ -5,8 +5,13 @@ import { Redirect, Link } from 'react-router-dom'
import Input from './parts/Input'
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 { getItemFromList } from '../utils'
import { RouteTypeChoices } from '../constants/choices'
const StyledRouteForm = styled.div`
padding: 1rem;
@ -66,23 +71,39 @@ class RouteForm extends Component {
this.setState({justSubmit: true})
}
componentWillMount() {
const { props } = this
componentDidMount() {
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 { results } = props.route
const ones = results.filter(ele => ele.route_id === routeId)
if (ones.length > 0) {
this.setState(ones[0])
props.updateBreadcrumb({ agencyId, routeId })
} else {
const agencies = props.agency.results.filter(ele => ele.agency_id === agencyId)
if (routeId === undefined
&& state.agency === null
&& props.agency.count > 0) {
// this is for new route
const { results } = props.agency
const agencies = results.filter(ele => ele.agency_id === agencyId)
if (agencies.length > 0) {
let d = {}
d["agency_id"] = agencies[0].id
this.setState(d)
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() {
@ -126,18 +147,9 @@ class RouteForm extends Component {
<Select
label="Route Type"
fieldName="route_type"
value={one.route_type || ''}
value={getItemFromList(one.route_type, RouteTypeChoices)}
handleChange={this.handleChange}
choices={[
{ 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)' },
]} />
choices={RouteTypeChoices} />
<Input
label="Route URL"

14
src/components/RouteList.js

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

4
src/components/Spinner.js

@ -1,8 +1,10 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const Spinner = (props) => (
<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>
)

85
src/components/StopForm.js

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

32
src/components/StopList.js

@ -2,25 +2,39 @@ 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 { getStop } from '../actions/stop'
import store from '../store'
import { STOP_REQUEST } from '../constants/ActionTypes'
const StyledBox = styled.div`
background: #fafafa;
`
class StopList extends Component {
componentWillMount() {
const { count } = this.props.stop
if (count === 0)
store.dispatch(getStop())
store.dispatch({
type: STOP_REQUEST,
payload: { query: `` }
})
}
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() {
@ -33,8 +47,9 @@ class StopList extends Component {
</p>
<p className="panel-tabs">
<Link className="link is-info" to={`/map/stop/new`}>
<i className="fas fa-plus" /> New stop
<FontAwesomeIcon icon="plus" /> New stop
</Link>
<a className="link is-info" onClick={this.fetchStopNearby.bind(this)}>Near</a>
</p>
<div className="panel-block">
<p className="control has-icons-left">
@ -45,7 +60,7 @@ class StopList extends Component {
onChange={(e) => { this.handleStopSearch(e) } }
onKeyPress={(e) => { (e.key === 'Enter') && this.handleStopSearch(e) }} />
<span className="icon is-small is-left">
<i className="fas fa-search" aria-hidden="true"></i>
<FontAwesomeIcon icon="search" ariaHidden="true"/>
</span>
</p>
</div>
@ -54,7 +69,7 @@ class StopList extends Component {
<Link to={`/map/stop/${ele.stop_id}`}>{ele.stop_id} - {ele.name}</Link>
</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>}
</nav>
</StyledBox>
@ -63,7 +78,8 @@ class StopList extends Component {
}
const mapStateToProps = state => ({
stop: state.stop
stop: state.stop,
lastCenter: state.geo.lastCenter,
})
const connectStopList = connect(
mapStateToProps,

84
src/components/StopTimeForm.js

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

1
src/components/StopTimeOne.js

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

5
src/components/parts/HorizontalSelect.js

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

4
src/components/parts/Select.js

@ -1,13 +1,13 @@
import React from 'react'
import Select from 'react-select'
import 'react-select/dist/react-select.css'
const NormalSelect = (props) => (
<div className="field">
<label className="label">{props.label}</label>
<Select
className="control"
defaultOptions
classNamePrefix="control"
name={props.fieldName}
value={props.value}
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_UPDATE = 'GEO_MARKER_UPDATE'
export const GEO_MAPCENTER_UPDATE = 'GEO_MAPCENTER_UPDATE'
export const GEO_DRAGMARKER_ENABLE = 'GEO_DRAGMARKER_ENABLE'
export const GEO_DRAGMARKER_DISABLE = 'GEO_DRAGMARKER_DISABLE'
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 SUCCESS_LOGIN = 'SUCCESS_LOGIN'
@ -69,6 +71,13 @@ export const CALENDAR_CREATE = 'CALENDAR_CREATE'
export const CALENDAR_DELETE = 'CALENDAR_DELETE'
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_FAILURE = 'FAREATTR_FAILURE'
@ -77,3 +86,11 @@ export const FAREATTR_SUCCESS = 'FAREATTR_SUCCESS'
export const FAREATTR_CREATE = 'FAREATTR_CREATE'
export const FAREATTR_DELETE = 'FAREATTR_DELETE'
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 API_PREFIX = process.env.API_PREFIX || '/v1'
export const API_URL = `${URL}${API_PREFIX}`
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,
FeatureGroup, GeoJSON, Marker } from 'react-leaflet'
// import { EditControl } from 'react-leaflet-draw'
import L from 'leaflet'
import * as L from 'leaflet'
import { loggedIn } from '../reducers/auth'
import {
geoLocationFailed, geoLocationUpdate, getAgency,
draggableMarkerUpdate
draggableMarkerUpdate, lastCenterUpdate, geoStopToggler,
geoStopAuraToggler,
} from '../actions'
import FloatPane from '../components/FloatPane'
import store from '../store'
import { decodeGeoJson } from '../utils'
const FullPageBox = styled.div`
@ -36,13 +38,39 @@ position: fixed;
text-align: center;
padding-top: 10px;
right: 0;
bottom: 100px;
bottom: 150px;
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: update mapCenter to recent active route/trip or current location
const stopIcon = L.divIcon({
className: 'divIcon',
@ -51,6 +79,15 @@ const stopIcon = L.divIcon({
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({
iconUrl: '//static.10ninox.com/map/pink-marker-icon.png',
iconRetinaUrl: '//static.10ninox.com/map/pink-marker-icon-2x.png',
@ -101,9 +138,13 @@ class Geo extends Component {
componentWillReceiveProps(newProps) {
const omCenter = this.props.geo.mapCenter
const newCenter = newProps.geo.mapCenter
let center = {
lastCenter: newProps.geo.lastCenter
}
if (omCenter[0] !== newCenter[0] || omCenter[1] !== newCenter[1]) {
this.setState({mapCenter: newCenter})
center.mapCenter = newCenter
}
this.setState(center)
}
renderGeoJSON() {
@ -118,7 +159,7 @@ class Geo extends Component {
{polygons && polygons.filter(ele => ele.geojson).map(ele => (
<GeoJSON
key={`geojson-${ele.id}`}
data={ele.geojson}
data={decodeGeoJson(ele.geojson)}
style={{...style, ...ele.style}} />
))}
</FeatureGroup>
@ -150,18 +191,54 @@ class Geo extends Component {
pointToLayer={(feat, latlon) => {
return (
L.marker(latlon, {
icon: stopIcon
icon: feat.properties.icon
}).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() {
const { geo } = this.props
const { leafletElement } = this.refs.map
const { zoom } = this.refs.map.viewport // another key is 'center'
const curLoc = [geo.coords.latitude, geo.coords.longitude]
leafletElement.setView(curLoc, zoom < 13 ? 13 : zoom)
leafletElement.setView(curLoc, zoom > 13 ? 13 : zoom)
}
render() {
@ -178,14 +255,11 @@ class Geo extends Component {
opacity={1}
className='my-location-marker' />
) : null
/*
draggable={true}
onDragend={(e) => { console.log(e)}}
*/
const centerMarker = geo.coords ? (
const centerMarker = this.state.lastCenter ? (
<CircleMarker
center={this.state.mapCenter}
radius={7}
center={this.state.lastCenter}
radius={6}
fillColor={'rgb(255, 25, 90)'}
fillOpacity={0.75}
color={'black'}
@ -217,13 +291,24 @@ class Geo extends Component {
<i className="far fa-dot-circle"></i>
</FollowMyLocation>
</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
center={this.state.mapCenter}
zoom={13}
zoom={geo.zoom}
length={4}
zoomControl={false}
animate
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'>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
@ -236,6 +321,10 @@ class Geo extends Component {
{draggableMarker}
{this.renderGeoJSON()}
{this.renderStopTime()}
{geo.showStopMarker && this.renderStopMarkers()}
{geo.showStopAura && this.renderStopAura()}
<span>
</span>
</Map>
</FullPageBox>
)
@ -248,6 +337,7 @@ const mapStateToProps = state => ({
agency: state.agency,
stoptime: state.stoptime,
geo: state.geo,
stop: state.stop,
})
export default connect(
mapStateToProps,

15
src/container/Main.js

@ -4,6 +4,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'
import { loggedIn } from '../reducers/auth'
import { getAgency } from '../actions'
import First from '../components/First'
import Nav from '../components/Nav'
import Footer from '../components/Footer'
import CalendarForm from '../components/CalendarForm'
@ -12,6 +13,12 @@ import AgencyList from '../components/AgencyList'
import AgencyItem from '../components/AgencyItem'
import AgencyForm from '../components/AgencyForm'
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 store from '../store'
@ -47,9 +54,17 @@ class Main extends Component {
<Route path={`${match.url}agency/:agencyId/:agencyChild`} component={AgencyItem} />
<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/:serviceId`} component={CalendarForm} />
<Route exact path={`${match.url}calendar`} component={CalendarList} />
<Route path={`${match.url}`} component={First} />
</Switch>
</div>
<Footer />

12
src/index.js

@ -1,5 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Raven from 'raven-js'
import './index.css'
import App from './App'
import registerServiceWorker from './registerServiceWorker'
@ -9,9 +10,20 @@ import { Provider } from 'react-redux'
import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react'
import store from './store'
import { RAVEN_DSN } from './constants/Api'
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)

11
src/reducers/agency.js

@ -1,6 +1,7 @@
import {
AGENCY_CREATE, AGENCY_DELETE, AGENCY_UPDATE,
AGENCY_REQUEST, AGENCY_SUCCESS, AGENCY_FAILURE,
GEO_POLYGON_RESET,
} from '../constants/ActionTypes'
@ -12,20 +13,26 @@ const agencyInitState = {
}
const agency = (state = agencyInitState, action) => {
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:
return {
...state,
fetching: true,
}
case AGENCY_SUCCESS:
const { count, next, prev, results } = action.payload
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...(prev ? state.results : []),
...(previous ? state.results : []),
...results,
]
}

19
src/reducers/auth.js

@ -1,5 +1,6 @@
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'
@ -35,9 +36,21 @@ const auth = (state = tokenInitialState, action) => {
token: null,
fetching: false,
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:
return state;
return state
}
}

4
src/reducers/calendar.js

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

7
src/reducers/fareattr.js

@ -14,21 +14,20 @@ const fareAttrInitState = {
const fareAttr = (state = fareAttrInitState, action) => {
switch(action.type) {
case FAREATTR_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
query: action.meta !== undefined ? action.meta.query : state.query,
}
case FAREATTR_SUCCESS:
const { count, next, prev, results } = action.payload
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...( (prev) ? state.results : [] ),
...( (previous) ? state.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_MARKER_ADD, GEO_MARKER_RESET, GEO_MARKER_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_STOPMARKER_TOGGLE, GEO_STOP_AURA_TOGGLE,
} from '../constants/ActionTypes'
const initialState = {
@ -12,13 +13,27 @@ const initialState = {
message: '',
polygons: [],
markers: [],
lastCenter: [13.84626739, 100.538],
mapCenter: [13.84626739, 100.538],
zoom: 13,
showStopMarker: false,
showStopAura: false,
draggableMarker: false,
draggableMarkerLatlon: [13.8462745, 100.5382592],
}
const geo = (state = initialState, action) => {
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:
return {
...state,
@ -35,6 +50,11 @@ const geo = (state = initialState, action) => {
...state,
draggableMarkerLatlon: action.payload,
}
case GEO_LASTCENTER_UPDATE:
return {
...state,
...action.payload,
}
case GEO_MAPCENTER_UPDATE:
return {
...state,

4
src/reducers/index.js

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

4
src/reducers/route.js

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

17
src/reducers/stop.js

@ -10,6 +10,7 @@ const stopInitState = {
next: null,
count: 0,
fetching: false,
query: '',
}
const stop = (state = stopInitState, action) => {
switch(action.type) {
@ -20,25 +21,35 @@ const stop = (state = stopInitState, action) => {
fetching: false,
}
case STOP_REQUEST:
const q = action.payload && action.payload.query
return {
...state,
fetching: true,
query: q || '',
}
case STOP_SUCCESS:
const { count, next, prev, results } = action.payload
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...(prev ? state.results : []),
...(previous ? state.results : []),
...results,
]
}
case STOP_UPDATE:
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)
return {
...state,

14
src/reducers/stoptime.js

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

4
src/reducers/trip.js

@ -18,14 +18,14 @@ const trip = (state = tripInitState, action) => {
fetching: true,
}
case TRIP_SUCCESS:
const { count, next, prev, results } = action.payload
const { count, next, previous, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...(prev ? state.results : []),
...(previous ? state.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 { createStore, applyMiddleware, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { apiMiddleware } from 'redux-api-middleware'
import { createLogger } from 'redux-logger'
import { persistReducer } from 'redux-persist'
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'
const persistConfig = {
key: 'root',
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') {
middleware.push(createLogger())
}
@ -26,4 +35,7 @@ const persistedReducer = persistReducer(persistConfig, gruntApp)
const store = createStore(persistedReducer, enhancer)
// run the saga
sagaMiddleware.run(mySaga)
export default store

9
src/utils/ApiClient.js

@ -2,13 +2,14 @@ import axios from 'axios'
import store from '../store'
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 params = {
baseURL: `${URL}${API_PREFIX}${url}`,
headers: {'Authorization': 'Bearer ' + token}
url: `${URL}${API_PREFIX}${url}`,
headers: { 'Authorization': 'Bearer ' + token },
...(extra || {}),
}
return axios.get(params)
return axios.request(params)
}
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