Browse Source

StopForm works with draggable marker for location

master
sipp11 6 years ago
parent
commit
251786acd3
  1. 1
      package.json
  2. 25
      src/actions/index.js
  3. 64
      src/actions/trip.js
  4. 6
      src/components/RouteDetail.js
  5. 89
      src/components/StopForm.js
  6. 14
      src/components/StopList.js
  7. 186
      src/components/StopTimeForm.js
  8. 69
      src/components/StopTimeOne.js
  9. 251
      src/components/TripForm.js
  10. 24
      src/components/parts/Date.js
  11. 36
      src/components/parts/HorizontalSelect.js
  12. 15
      src/components/parts/Input.js
  13. 29
      src/components/parts/Select.js
  14. 13
      src/constants/ActionTypes.js
  15. 28
      src/container/Geo.js
  16. 2
      src/container/Main.js
  17. 19
      src/reducers/geo.js
  18. 2
      src/reducers/index.js
  19. 2
      src/reducers/route.js
  20. 3
      src/reducers/stoptime.js
  21. 76
      src/reducers/trip.js

1
package.json

@ -17,6 +17,7 @@
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-scripts": "1.1.4", "react-scripts": "1.1.4",
"react-select": "^1.2.1",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-api-middleware": "^2.3.0", "redux-api-middleware": "^2.3.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",

25
src/actions/index.js

@ -94,7 +94,30 @@ export const polygonReset = () => {
export const mapCenterUpdate = (lat, lon) => { export const mapCenterUpdate = (lat, lon) => {
return { return {
type: types.GEO_MAPCENTER_UPDATE, type: types.GEO_MAPCENTER_UPDATE,
payload: [lat, lon] payload: [lat, lon],
}
}
export const draggableMarkerUpdate = (lat, lon) => {
return {
type: types.GEO_DRAGMARKER_CHANGE,
payload: [lat, lon],
}
}
export const draggableMarkerEnable = (lat, lon) => {
let payload = (lat !== undefined && lon !== undefined) ? [lat, lon] : []
return {
type: types.GEO_DRAGMARKER_ENABLE,
payload,
}
}
export const draggableMarkerDisable = () => {
return {
type: types.GEO_DRAGMARKER_DISABLE
} }
} }

64
src/actions/trip.js

@ -0,0 +1,64 @@
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 getTrip = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/trip/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.trip.fetching,
types: [
types.TRIP_REQUEST,
types.TRIP_SUCCESS,
types.TRIP_FAILURE,
]
}
})
export const updateTrip = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/trip/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.TRIP_REQUEST,
types.TRIP_UPDATE,
types.TRIP_FAILURE,
]
}
})
export const createTrip = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/trip/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.TRIP_REQUEST,
types.TRIP_CREATE,
types.TRIP_FAILURE,
]
}
})
export const deleteTrip = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/trip/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.TRIP_REQUEST,
{
type: types.TRIP_DELETE,
meta: { id }
},
types.TRIP_FAILURE,
]
}
})

6
src/components/RouteDetail.js

@ -55,7 +55,7 @@ const TripList = (props) => (
onClick={() => { store.dispatch(getStopTime(`trip=${ele.id}&limit=100`))}}> onClick={() => { store.dispatch(getStopTime(`trip=${ele.id}&limit=100`))}}>
{ele.trip_id} {ele.trip_id}
<StyledTripDesc> <StyledTripDesc>
(<b>ID</b> {ele.id})&nbsp; (<b>ID</b> <Link to={`/trip/${props.agencyId}/${props.routeId}/${ele.trip_id}`}>{ele.id}</Link>)&nbsp;
<b>service</b> <Link to={`/calendar/${ele.service.service_id}`}>{ele.service.service_id}</Link> <b>service</b> <Link to={`/calendar/${ele.service.service_id}`}>{ele.service.service_id}</Link>
<br /> <br />
{ele.stoptime.count > 0 && <div> {ele.stoptime.count > 0 && <div>
@ -108,6 +108,7 @@ class RouteDetail extends Component {
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (this.props.route.count < newProps.route.count) { if (this.props.route.count < newProps.route.count) {
this.pushShapeToStore(this.props.match, newProps.route) this.pushShapeToStore(this.props.match, newProps.route)
} }
} }
@ -135,6 +136,7 @@ class RouteDetail extends Component {
} }
const item = tRoute[0] const item = tRoute[0]
const baseUrl = `/map/${agencyId}/route/${routeId}` const baseUrl = `/map/${agencyId}/route/${routeId}`
// TODO: change TripList to use from store.trip (when update will change automatically)
return ( return (
<nav className="panel"> <nav className="panel">
<p className="panel-heading"> <p className="panel-heading">
@ -151,7 +153,7 @@ class RouteDetail extends Component {
<Route exact path={`${baseUrl}/fare`} render={(props) => ( <Route exact path={`${baseUrl}/fare`} render={(props) => (
<FareRuleList farerules={item.farerule_set} {...props} />)} /> <FareRuleList farerules={item.farerule_set} {...props} />)} />
<Route exact path={`${baseUrl}/trip`} render={(props) => ( <Route exact path={`${baseUrl}/trip`} render={(props) => (
<TripList trips={item.trip_set} {...props} />)} /> <TripList trips={item.trip_set} routeId={routeId} agencyId={agencyId} {...props} />)} />
</nav> </nav>
) )
} }

89
src/components/StopForm.js

@ -4,8 +4,10 @@ import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom' import { Redirect, Link } from 'react-router-dom'
import HorizontalInput from './parts/HorizontalInput' import HorizontalInput from './parts/HorizontalInput'
import { mapCenterUpdate } from '../actions' import {
import { updateStop, createStop, deleteStop } from '../actions/stop' mapCenterUpdate, draggableMarkerEnable, draggableMarkerDisable
} from '../actions'
import { updateStop, createStop, deleteStop, getStop } from '../actions/stop'
import store from '../store' import store from '../store'
const StyledStopForm = styled.div` const StyledStopForm = styled.div`
@ -27,6 +29,7 @@ class StopForm extends Component {
location_type: '0', location_type: '0',
stop_timezone: 'Asia/Bangkok', stop_timezone: 'Asia/Bangkok',
wheelchair_boarding: '0', wheelchair_boarding: '0',
latlon: [],
// parent_station: null, // parent_station: null,
} }
@ -36,6 +39,7 @@ class StopForm extends Component {
this.handleSubmit = this.handleSubmit.bind(this) this.handleSubmit = this.handleSubmit.bind(this)
this.handleDelete = this.handleDelete.bind(this) this.handleDelete = this.handleDelete.bind(this)
this.renderForm = this.renderForm.bind(this) this.renderForm = this.renderForm.bind(this)
this.setToCurrentLocation = this.setToCurrentLocation.bind(this)
} }
handleChange(evt) { handleChange(evt) {
@ -45,10 +49,14 @@ class StopForm extends Component {
} }
handleSubmit() { handleSubmit() {
const { id } = this.state const { id, latlon } = this.state
let body = { ...this.state } let body = {
...this.state,
location: { latitude: latlon[0], longitude: latlon[1] },
}
delete body.id delete body.id
delete body.geojson delete body.geojson
delete body.latlon
if (id !== null) { if (id !== null) {
store.dispatch(updateStop(id, body)) store.dispatch(updateStop(id, body))
} else { } else {
@ -63,24 +71,72 @@ class StopForm extends Component {
this.setState({justSubmit: true}) this.setState({justSubmit: true})
} }
componentWillUnmount() {
store.dispatch(draggableMarkerDisable())
}
componentWillMount() { componentWillMount() {
const { props } = this const { props } = this
const { stopId } = props.match.params const { stopId } = props.match.params
const { results } = props.stop const { results } = props.stop
const ones = results.filter(ele => ele.stop_id === stopId) const ones = results.filter(ele => ele.stop_id === stopId)
if (ones.length > 0) { if (ones.length > 0) {
this.setState(ones[0]) this.updateStopFromStore(ones[0])
props.updateBreadcrumb({ stopId }) } else {
const { coordinates } = ones[0].geojson store.dispatch(getStop(`search=${stopId}`))
}
}
updateStopFromStore(one) {
this.props.updateBreadcrumb({ stopId: one.stop_id })
const { coordinates } = one.geojson
this.setState({
...one,
latlon: [coordinates[1], coordinates[0]],
})
store.dispatch(mapCenterUpdate(coordinates[1], coordinates[0])) store.dispatch(mapCenterUpdate(coordinates[1], coordinates[0]))
store.dispatch(draggableMarkerEnable(coordinates[1], coordinates[0]))
} }
componentWillReceiveProps(newProps) {
// if stop changes, we need to refetch
const { stopId } = this.props.match.params
const newStopId = newProps.match.params.stopId
if (stopId !== newStopId) {
store.dispatch(getStop(`search=${newStopId}`))
// form setup this way won't change value in textinput, so
// we get back to StopList (with getStop invoked with target stop)
this.setState({back2list: true})
}
if (stopId !== undefined && this.state.stop_id === null) {
const { results } = newProps.stop
const ones = results.filter(ele => ele.stop_id === stopId)
if (ones.length === 0) {
this.setState({ back2list: true })
} else {
this.updateStopFromStore(ones[0])
}
return
}
const oLatlon = this.props.draggableMarkerLatlon
const newLatlon = newProps.draggableMarkerLatlon
if (oLatlon[0] !== newLatlon[0] || oLatlon[1] !== newLatlon[1]) {
this.setState({latlon: newLatlon})
}
}
setToCurrentLocation() {
const { coords } = this.props
store.dispatch(draggableMarkerEnable(coords.latitude, coords.longitude))
} }
renderForm() { renderForm() {
const one = this.state const one = this.state
const { stopId } = this.props.match.params const { coords } = this.props
const { results, fetching } = this.props.stop const { results, fetching } = this.props.stop
const stops = results.filter(ele => ele.stop_id === stopId) const { latlon } = one
return ( return (
<StyledStopForm> <StyledStopForm>
<h1 className="title">{one.stop_id || 'New Stop'}&nbsp;&nbsp;</h1> <h1 className="title">{one.stop_id || 'New Stop'}&nbsp;&nbsp;</h1>
@ -99,6 +155,12 @@ class StopForm extends Component {
value={one.name || ''} value={one.name || ''}
handleChange={this.handleChange} /> handleChange={this.handleChange} />
<p><b>Lat, Lon</b>
{coords && <a onClick={this.setToCurrentLocation.bind(this)}> -> current location</a>}
<br />
{latlon[0] && latlon[0].toFixed(4)}, {latlon[1] && latlon[1].toFixed(4)}
</p>
<HorizontalInput <HorizontalInput
label="Stop code" label="Stop code"
type="text" type="text"
@ -145,7 +207,7 @@ class StopForm extends Component {
label="Parent Station" label="Parent Station"
type="text" type="text"
fieldName="parent_station" fieldName="parent_station"
defaultValue="0" value="0"
value={one.parent_station || ''} value={one.parent_station || ''}
handleChange={this.handleChange} /> */} handleChange={this.handleChange} /> */}
</div> </div>
@ -176,12 +238,13 @@ class StopForm extends Component {
render () { render () {
const one = this.state const one = this.state
const { fetching } = this.props.stop const { fetching } = this.props.stop
// redirect to view page if no data
const { stopId } = this.props.match.params
// redirect to agency list if submitted // redirect to agency list if submitted
if (one.justSubmit === true && !fetching) { if (one.justSubmit === true && !fetching) {
return <Redirect to={`/map/stop`} /> return <Redirect to={`/map/stop`} />
} }
if (this.state.back2list === true) {
return <Redirect to={`/map/stop`} />
}
return this.renderForm() return this.renderForm()
} }
@ -190,6 +253,8 @@ class StopForm extends Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
stop: state.stop, stop: state.stop,
draggableMarkerLatlon: state.geo.draggableMarkerLatlon,
coords: state.geo.coords,
}) })
const connectStopForm = connect( const connectStopForm = connect(

14
src/components/StopList.js

@ -19,6 +19,10 @@ class StopList extends Component {
store.dispatch(getStop()) store.dispatch(getStop())
} }
handleStopSearch(evt) {
store.dispatch(getStop(`search=${evt.target.value}`))
}
render() { render() {
const { results } = this.props.stop const { results } = this.props.stop
return ( return (
@ -34,7 +38,11 @@ class StopList extends Component {
</p> </p>
<div className="panel-block"> <div className="panel-block">
<p className="control has-icons-left"> <p className="control has-icons-left">
<input className="input is-small" type="text" placeholder="search" /> <input
className="input is-small"
type="text"
placeholder="search"
onKeyPress={(e) => { (e.key === 'Enter') && this.handleStopSearch(e) }} />
<span className="icon is-small is-left"> <span className="icon is-small is-left">
<i className="fas fa-search" aria-hidden="true"></i> <i className="fas fa-search" aria-hidden="true"></i>
</span> </span>
@ -45,8 +53,8 @@ class StopList extends Component {
<Link to={`/map/stop/${ele.stop_id}`}>{ele.stop_id} - {ele.name}</Link> <Link to={`/map/stop/${ele.stop_id}`}>{ele.stop_id} - {ele.name}</Link>
</span> </span>
))} ))}
{this.props.stop.length === 0 && <span key="empty-farerule" className="panel-block"> {results.length === 0 && <span key="empty-farerule" className="panel-block">
No stop yet</span>} No stop found</span>}
</nav> </nav>
</StyledBox> </StyledBox>
) )

186
src/components/StopTimeForm.js

@ -0,0 +1,186 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import Select from 'react-select'
import Input from './parts/Input'
import OurSelect from './parts/Select'
import {
updateStopTime, createStopTime, deleteStopTime
} from '../actions/stoptime'
import store from '../store'
import { API_URL } from '../constants/Api'
const StyleBox = styled.div`
padding: 5px;
background: white;
margin-bottom: 1rem;
`
class StopTimeForm extends Component {
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(updateStopTime(id, body))
} else {
store.dispatch(createStopTime(body))
}
this.props.toggleEditMode()
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteStopTime(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
}
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 (
<StyleBox>
<Input
label="Sequence"
type="text"
fieldName="sequence"
value={item.sequence || ''}
handleChange={this.handleChange} />
<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)
}}
/>
</div>
<Input
label="Arrival"
type="text"
fieldName="arrival"
value={item.arrival || ''}
handleChange={this.handleChange} />
<Input
label="Departure"
type="text"
fieldName="departure"
value={item.departure || ''}
handleChange={this.handleChange} />
<Input
label="Stop Headsign"
type="text"
fieldName="stop_headsign"
value={item.stop_headsign || ''}
handleChange={this.handleChange} />
<OurSelect
label="Pickup Type"
type="text"
fieldName="pickup_type"
value={item.pickup_type || '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' },
]} />
<OurSelect
label="Drop off Type"
type="text"
fieldName="pickup_type"
value={item.pickup_type || '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' },
]} />
<OurSelect
label="Timepoint"
type="text"
fieldName="timepoint"
value={item.timepoint || '1'}
handleChange={this.handleChange}
choices={[
{ value: '0', label: 'times are considered approximate.' },
{ value: '1', label: 'times are considered exact.' },
]} />
<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 StopTimeForm

69
src/components/StopTimeOne.js

@ -0,0 +1,69 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import StopTimeForm from './StopTimeForm'
const StyledRow = styled.div`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
class StopTimeOne 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">
Stop <a onClick={() => this.toggleEditMode()}>EDIT</a>
</p>
<p className="title">{item.sequence}. {item.stop.stop_id}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Arrival</p>
<p className="title">{item.arrival}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Departure</p>
<p className="title">{item.departure}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Stop headsign</p>
<p className="title">{item.stop_headsign || '-'}</p>
</div>
</div>
</StyledRow>
)
render() {
const { item } = this.props
if (this.state.editMode)
return <StopTimeForm item={item}
toggleEditMode={this.toggleEditMode} />
return this.renderReadOnly(item)
}
}
export default StopTimeOne

251
src/components/TripForm.js

@ -0,0 +1,251 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import StopTimeOne from './StopTimeOne'
import StopTimeForm from './StopTimeForm'
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 { getStopTime } from '../actions/stoptime'
import store from '../store'
const StyledTripForm = styled.div`
padding: 1rem;
background: #fafafa;
`
const StyleBox = styled.div`
padding: 5px;
background: white;
margin-bottom: 1rem;
`
class TripForm extends Component {
state = {
id: null,
trip_id: "",
email: "",
fare_url: "",
lang: "",
name: "",
phone: "",
timezone: "",
url: "",
newStopTime: false,
}
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)
}
toggleNewStopTime = () => {
this.setState({ newStopTime: !this.state.newStopTime })
}
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
delete body.newStopTime
if (id !== null) {
store.dispatch(updateTrip(id, body))
} else {
store.dispatch(createTrip(body))
}
this.setState({justSubmit: true})
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteTrip(id))
this.setState({justSubmit: true})
}
static getDerivedStateFromProps(props, state) {
const { agencyId, routeId, tripId } = props.match.params
if (!props.calendar.fetching && props.calendar.count === 0)
store.dispatch(getCalendar())
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]
}
} else {
store.dispatch(getRoute(`agency=${agencyId}`))
}
}
return null
}
renderForm() {
const one = this.state
const { agencyId, routeId } = this.props.match.params
const { fetching } = this.props.route
const { calendar } = this.props
return (
<StyledTripForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<HorizontalInput
label="Trip ID"
type="text"
fieldName="trip_id"
value={one.trip_id || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Headsign"
type="text"
fieldName="trip_headsign"
value={one.trip_headsign || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Name"
type="text"
fieldName="short_name"
value={one.short_name || ''}
handleChange={this.handleChange} />
{!calendar.fetching && <HorizontalSelect
label="Service"
type="text"
fieldName="service"
value={one.service ? one.service.id : ''}
handleChange={this.handleChange}
choices={calendar.results.map(ele => ({ value: ele.id, label: ele.service_id }))} />}
<HorizontalSelect
label="Direction ID"
type="text"
fieldName="direction_id"
value={one.direction_id || ''}
handleChange={this.handleChange}
choices={[
{ value: '0', label: 'outbound' },
{ value: '1', label: 'inbound' },
]} />
<HorizontalInput
label="Block ID"
type="text"
fieldName="block_id"
value={one.block_id || ''}
handleChange={this.handleChange} />
<HorizontalSelect
label="Wheelchair Accessible"
type="text"
fieldName="wheelchair_accessible"
value={one.wheelchair_accessible || '0'}
handleChange={this.handleChange}
choices={[
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]} />
<HorizontalSelect
label="Bike Allowance"
type="text"
fieldName="bike_allowed"
value={one.bike_allowed || '0'}
handleChange={this.handleChange}
choices={[
{ value: '0', label: 'No information' },
{ value: '1', label: 'Yes' },
{ value: '2', label: 'Not possible' },
]} />
</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={`/map/${agencyId}/route/${routeId}/trip`} className="button is-text">Cancel</Link>
</div>
</div>
</StyledTripForm>
)
}
render () {
const one = this.state
const { fetching } = this.props.trip
// redirect to view page if no data
const { agencyId, routeId, tripId } = 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/`} />
// redirect to trip list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/map/${agencyId}/route/${routeId}/trip`} />
}
return (
<div className="columns">
<div className="column is-half">
{this.renderForm()}
</div>
<div className="column is-half">
<StyleBox>
{!this.state.newStopTime && <button className="button is-outlined" onClick={this.toggleNewStopTime}>new StopTime</button>}
{this.state.newStopTime && <StopTimeForm {...this.props}
trip={this.state.id}
toggleEditMode={this.toggleNewStopTime} /> }
</StyleBox>
{this.props.stoptime.results.map(
ele => <StopTimeOne key={`stb-${ele.id}`} item={ele} />)}
</div>
</div>
)
}
}
const mapStateToProps = state => ({
route: state.route,
trip: state.trip,
calendar: state.calendar,
stoptime: state.stoptime,
})
const connectTripForm = connect(
mapStateToProps,
)(TripForm)
export default connectTripForm

24
src/components/parts/Date.js

@ -0,0 +1,24 @@
import React from 'react'
import Flatpickr from 'react-flatpickr'
import 'flatpickr/dist/themes/airbnb.css'
const Date = (props) => (
<div className="field">
<label className="label">{props.label}</label>
<p className="control">
<Flatpickr
className="input"
defaultValue={props.value}
onChange={(date) => {
let evt = {
target: {
name: props.fieldName,
value: date[0].toISOString('YYYY-MM-DD').slice(0, 10)
}}
return props.handleChange(evt)}} />
</p>
</div>
)
export default Date

36
src/components/parts/HorizontalSelect.js

@ -0,0 +1,36 @@
import React from 'react'
import Select from 'react-select'
import 'react-select/dist/react-select.css'
const HorizontalInput = (props) => (
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">{props.label}</label>
</div>
<div className="field-body">
<div className="field">
<Select
className="control"
name={props.fieldName}
value={props.value}
onChange={(data) => {
console.log('change', data)
let evt = {
target: {
name: props.fieldName,
value: data ? data.value : '',
}}
return props.handleChange(evt)
}}
options={props.choices || [
{ value: 'one', label: 'change choices with' },
{ value: 'two', label: 'props.choices' },
]}
/>
</div>
</div>
</div>
)
export default HorizontalInput

15
src/components/parts/Input.js

@ -0,0 +1,15 @@
import React from 'react'
const Input = (props) => (
<div className="field">
<label className="label">{props.label}</label>
<p className="control">
<input className="input" type={props.type || 'text'}
name={props.fieldName}
onChange={props.handleChange}
defaultValue={props.value} />
</p>
</div>
)
export default Input

29
src/components/parts/Select.js

@ -0,0 +1,29 @@
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"
name={props.fieldName}
value={props.value}
onChange={(data) => {
let evt = {
target: {
name: props.fieldName,
value: data ? data.value : '',
}}
return props.handleChange(evt)
}}
options={props.choices || [
{ value: 'one', label: 'change choices with' },
{ value: 'two', label: 'props.choices' },
]}
/>
</div>
)
export default NormalSelect

13
src/constants/ActionTypes.js

@ -10,6 +10,11 @@ export const GEO_MARKER_UPDATE = 'GEO_MARKER_UPDATE'
export const GEO_MAPCENTER_UPDATE = 'GEO_MAPCENTER_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 REQUEST_LOGIN = 'REQUEST_LOGIN' export const REQUEST_LOGIN = 'REQUEST_LOGIN'
export const SUCCESS_LOGIN = 'SUCCESS_LOGIN' export const SUCCESS_LOGIN = 'SUCCESS_LOGIN'
export const FAILED_LOGIN = 'FAILED_LOGIN' export const FAILED_LOGIN = 'FAILED_LOGIN'
@ -48,6 +53,14 @@ export const STOP_CREATE = 'STOP_CREATE'
export const STOP_DELETE = 'STOP_DELETE' export const STOP_DELETE = 'STOP_DELETE'
export const STOP_UPDATE = 'STOP_UPDATE' export const STOP_UPDATE = 'STOP_UPDATE'
export const TRIP_REQUEST = 'TRIP_REQUEST'
export const TRIP_FAILURE = 'TRIP_FAILURE'
export const TRIP_SUCCESS = 'TRIP_SUCCESS'
// below items are SUCCESS for other tasks
export const TRIP_CREATE = 'TRIP_CREATE'
export const TRIP_DELETE = 'TRIP_DELETE'
export const TRIP_UPDATE = 'TRIP_UPDATE'
export const CALENDAR_REQUEST = 'CALENDAR_REQUEST' export const CALENDAR_REQUEST = 'CALENDAR_REQUEST'
export const CALENDAR_FAILURE = 'CALENDAR_FAILURE' export const CALENDAR_FAILURE = 'CALENDAR_FAILURE'
export const CALENDAR_SUCCESS = 'CALENDAR_SUCCESS' export const CALENDAR_SUCCESS = 'CALENDAR_SUCCESS'

28
src/container/Geo.js

@ -9,7 +9,10 @@ import {
import L from 'leaflet' import L from 'leaflet'
import { loggedIn } from '../reducers/auth' import { loggedIn } from '../reducers/auth'
import { geoLocationFailed, geoLocationUpdate, getAgency } from '../actions' import {
geoLocationFailed, geoLocationUpdate, getAgency,
draggableMarkerUpdate
} from '../actions'
import FloatPane from '../components/FloatPane' import FloatPane from '../components/FloatPane'
import store from '../store' import store from '../store'
@ -31,7 +34,15 @@ const stopIcon = L.divIcon({
className: 'divIcon', className: 'divIcon',
html: '<div><img src="//static.traffy.xyz/icon/bus-stop-48.png" width="24" height="24"></div>', html: '<div><img src="//static.traffy.xyz/icon/bus-stop-48.png" width="24" height="24"></div>',
popupAnchor: [2, -22], popupAnchor: [2, -22],
iconAnchor: [12, 22] iconAnchor: [12, 23]
})
const dragableIcon = L.icon({
iconUrl: '//static.10ninox.com/map/pink-marker-icon.png',
iconRetinaUrl: '//static.10ninox.com/map/pink-marker-icon-2x.png',
iconSize: [25, 41],
iconAnchor: [12, 40],
popupAnchor: [2, -22],
}) })
@ -113,6 +124,7 @@ class Geo extends Component {
"type": "Feature", "type": "Feature",
"properties": { "properties": {
"popupContent": `${ele.stop.name} - ${ele.arrival}`, "popupContent": `${ele.stop.name} - ${ele.arrival}`,
"stop_id": `${ele.stop.stop_id}`,
"icon": stopIcon, "icon": stopIcon,
}, },
"geometry": ele.stop.geojson, "geometry": ele.stop.geojson,
@ -125,7 +137,7 @@ class Geo extends Component {
return ( return (
L.marker(latlon, { L.marker(latlon, {
icon: stopIcon icon: stopIcon
}).bindPopup(`${feat.properties.popupContent}`) }).bindPopup(`<a href="/#/map/stop/${feat.properties.stop_id}">${feat.properties.popupContent}</a>`)
) )
}}/> }}/>
} }
@ -160,6 +172,15 @@ class Geo extends Component {
opacity={0.75} opacity={0.75}
className='my-location-marker' /> className='my-location-marker' />
) : null ) : null
const draggableMarker = geo.draggableMarker ? (
<Marker
icon={dragableIcon}
draggable={true}
onDragend={(e) => { store.dispatch(draggableMarkerUpdate(e.target._latlng.lat, e.target._latlng.lng)) }}
position={geo.draggableMarkerLatlon}
/>
) : null
// const mapCenter = geo.coords // const mapCenter = geo.coords
// ? [geo.coords.latitude, geo.coords.longitude] // ? [geo.coords.latitude, geo.coords.longitude]
// : [13.84626739, 100.538] // : [13.84626739, 100.538]
@ -185,6 +206,7 @@ class Geo extends Component {
<ZoomControl position="topright" /> <ZoomControl position="topright" />
{myLocationMarker} {myLocationMarker}
{centerMarker} {centerMarker}
{draggableMarker}
{this.renderGeoJSON()} {this.renderGeoJSON()}
{this.renderStopTime()} {this.renderStopTime()}
</Map> </Map>

2
src/container/Main.js

@ -11,6 +11,7 @@ import CalendarList from '../components/CalendarList'
import AgencyList from '../components/AgencyList' import AgencyList from '../components/AgencyList'
import AgencyItem from '../components/AgencyItem' import AgencyItem from '../components/AgencyItem'
import AgencyForm from '../components/AgencyForm' import AgencyForm from '../components/AgencyForm'
import TripForm from '../components/TripForm'
import { LOGIN_PATH } from '../constants/path' import { LOGIN_PATH } from '../constants/path'
import store from '../store' import store from '../store'
@ -38,6 +39,7 @@ class Main extends Component {
<Nav loggedIn={loggedIn} /> <Nav loggedIn={loggedIn} />
<div className="container is-widescreen"> <div className="container is-widescreen">
<Switch> <Switch>
<Route exact path={`${match.url}trip/:agencyId/:routeId/:tripId`} component={TripForm} />
<Route exact path={`${match.url}agency/new`} component={AgencyForm} /> <Route exact path={`${match.url}agency/new`} component={AgencyForm} />
<Route exact path={`${match.url}agency/:agencyId`} component={AgencyItem} /> <Route exact path={`${match.url}agency/:agencyId`} component={AgencyItem} />
<Route path={`${match.url}agency/:agencyId/edit`} component={AgencyForm} /> <Route path={`${match.url}agency/:agencyId/edit`} component={AgencyForm} />

19
src/reducers/geo.js

@ -3,6 +3,7 @@ import {
GEO_MARKER_ADD, GEO_MARKER_RESET, GEO_MARKER_UPDATE, GEO_MARKER_ADD, GEO_MARKER_RESET, GEO_MARKER_UPDATE,
GEO_POLYGON_ADD, GEO_POLYGON_RESET, GEO_POLYGON_UPDATE, GEO_POLYGON_ADD, GEO_POLYGON_RESET, GEO_POLYGON_UPDATE,
GEO_MAPCENTER_UPDATE, GEO_MAPCENTER_UPDATE,
GEO_DRAGMARKER_CHANGE, GEO_DRAGMARKER_DISABLE, GEO_DRAGMARKER_ENABLE,
} from '../constants/ActionTypes' } from '../constants/ActionTypes'
const initialState = { const initialState = {
@ -12,10 +13,28 @@ const initialState = {
polygons: [], polygons: [],
markers: [], markers: [],
mapCenter: [13.84626739, 100.538], mapCenter: [13.84626739, 100.538],
draggableMarker: false,
draggableMarkerLatlon: [13.8462745, 100.5382592],
} }
const geo = (state = initialState, action) => { const geo = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case GEO_DRAGMARKER_ENABLE:
return {
...state,
draggableMarker: true,
draggableMarkerLatlon: action.payload,
}
case GEO_DRAGMARKER_DISABLE:
return {
...state,
draggableMarker: false,
}
case GEO_DRAGMARKER_CHANGE:
return {
...state,
draggableMarkerLatlon: action.payload,
}
case GEO_MAPCENTER_UPDATE: case GEO_MAPCENTER_UPDATE:
return { return {
...state, ...state,

2
src/reducers/index.js

@ -7,6 +7,7 @@ import fareattr from './fareattr'
import calendar from './calendar' import calendar from './calendar'
import stoptime from './stoptime' import stoptime from './stoptime'
import stop from './stop' import stop from './stop'
import trip from './trip'
export default combineReducers({ export default combineReducers({
auth, auth,
@ -17,4 +18,5 @@ export default combineReducers({
fareattr, fareattr,
calendar, calendar,
stoptime, stoptime,
trip,
}) })

2
src/reducers/route.js

@ -17,7 +17,7 @@ const route = (state = routeInitState, action) => {
return { return {
...state, ...state,
fetching: true, fetching: true,
query: action.meta !== undefined ? action.meta.query : '', query: action.meta !== undefined ? action.meta.query : state.query,
} }
case ROUTE_SUCCESS: case ROUTE_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, prev, results } = action.payload

3
src/reducers/stoptime.js

@ -14,11 +14,10 @@ const stoptimeInitState = {
const stoptime = (state = stoptimeInitState, action) => { const stoptime = (state = stoptimeInitState, action) => {
switch (action.type) { switch (action.type) {
case STOPTIME_REQUEST: case STOPTIME_REQUEST:
const { query } = action.meta
return { return {
...state, ...state,
fetching: true, fetching: true,
query, query: action.meta !== undefined ? action.meta.query : state.query,
} }
case STOPTIME_SUCCESS: case STOPTIME_SUCCESS:
const { count, next, prev, results } = action.payload const { count, next, prev, results } = action.payload

76
src/reducers/trip.js

@ -0,0 +1,76 @@
import {
TRIP_CREATE, TRIP_DELETE, TRIP_UPDATE,
TRIP_REQUEST, TRIP_SUCCESS, TRIP_FAILURE,
} from '../constants/ActionTypes'
const tripInitState = {
results: [],
next: null,
count: 0,
fetching: false,
}
const trip = (state = tripInitState, action) => {
switch(action.type) {
case TRIP_REQUEST:
return {
...state,
fetching: true,
}
case TRIP_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...(prev ? state.results : []),
...results,
]
}
case TRIP_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 TRIP_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case TRIP_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 TRIP_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default trip
Loading…
Cancel
Save