diff --git a/package.json b/package.json index 23d4b9e..2801be1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-scripts": "1.1.4", + "react-select": "^1.2.1", "redux": "^4.0.0", "redux-api-middleware": "^2.3.0", "redux-logger": "^3.0.6", diff --git a/src/actions/index.js b/src/actions/index.js index 97423d2..53dca7d 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -94,7 +94,30 @@ export const polygonReset = () => { export const mapCenterUpdate = (lat, lon) => { return { 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 } } diff --git a/src/actions/trip.js b/src/actions/trip.js new file mode 100644 index 0000000..2fb547c --- /dev/null +++ b/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, + ] + } +}) diff --git a/src/components/RouteDetail.js b/src/components/RouteDetail.js index ebacc45..3ee58fa 100644 --- a/src/components/RouteDetail.js +++ b/src/components/RouteDetail.js @@ -55,7 +55,7 @@ const TripList = (props) => ( onClick={() => { store.dispatch(getStopTime(`trip=${ele.id}&limit=100`))}}> {ele.trip_id} - (ID {ele.id})  + (ID {ele.id})  service {ele.service.service_id}
{ele.stoptime.count > 0 &&
@@ -108,6 +108,7 @@ class RouteDetail extends Component { componentWillReceiveProps(newProps) { if (this.props.route.count < newProps.route.count) { this.pushShapeToStore(this.props.match, newProps.route) + } } @@ -135,6 +136,7 @@ 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 ( ) } diff --git a/src/components/StopForm.js b/src/components/StopForm.js index c1887e3..a7b77b0 100644 --- a/src/components/StopForm.js +++ b/src/components/StopForm.js @@ -4,8 +4,10 @@ import { connect } from 'react-redux' import { Redirect, Link } from 'react-router-dom' import HorizontalInput from './parts/HorizontalInput' -import { mapCenterUpdate } from '../actions' -import { updateStop, createStop, deleteStop } from '../actions/stop' +import { + mapCenterUpdate, draggableMarkerEnable, draggableMarkerDisable +} from '../actions' +import { updateStop, createStop, deleteStop, getStop } from '../actions/stop' import store from '../store' const StyledStopForm = styled.div` @@ -27,6 +29,7 @@ class StopForm extends Component { location_type: '0', stop_timezone: 'Asia/Bangkok', wheelchair_boarding: '0', + latlon: [], // parent_station: null, } @@ -36,6 +39,7 @@ class StopForm extends Component { this.handleSubmit = this.handleSubmit.bind(this) this.handleDelete = this.handleDelete.bind(this) this.renderForm = this.renderForm.bind(this) + this.setToCurrentLocation = this.setToCurrentLocation.bind(this) } handleChange(evt) { @@ -45,10 +49,14 @@ class StopForm extends Component { } handleSubmit() { - const { id } = this.state - let body = { ...this.state } + const { id, latlon } = this.state + let body = { + ...this.state, + location: { latitude: latlon[0], longitude: latlon[1] }, + } delete body.id delete body.geojson + delete body.latlon if (id !== null) { store.dispatch(updateStop(id, body)) } else { @@ -63,33 +71,81 @@ class StopForm extends Component { this.setState({justSubmit: true}) } + componentWillUnmount() { + store.dispatch(draggableMarkerDisable()) + } + componentWillMount() { const { props } = this const { stopId } = props.match.params const { results } = props.stop const ones = results.filter(ele => ele.stop_id === stopId) if (ones.length > 0) { - this.setState(ones[0]) - props.updateBreadcrumb({ stopId }) - const { coordinates } = ones[0].geojson - store.dispatch(mapCenterUpdate(coordinates[1], coordinates[0])) + this.updateStopFromStore(ones[0]) + } else { + 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(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() { const one = this.state - const { stopId } = this.props.match.params + const { coords } = this.props const { results, fetching } = this.props.stop - const stops = results.filter(ele => ele.stop_id === stopId) + const { latlon } = one return (

{one.stop_id || 'New Stop'}  

+

Lat, Lon + {coords && -> current location} +
+ {latlon[0] && latlon[0].toFixed(4)}, {latlon[1] && latlon[1].toFixed(4)} +

+ + label="Description" + type="text" + fieldName="stop_desc" + value={one.stop_desc || ''} + handleChange={this.handleChange} /> + label="Zone ID" + type="text" + fieldName="zone_id" + value={one.zone_id || ''} + handleChange={this.handleChange} /> + label="Location Type" + type="text" + fieldName="location_type" + value={one.location_type || ''} + handleChange={this.handleChange} /> + label="Timezone" + type="text" + fieldName="stop_timezone" + value={one.stop_timezone || ''} + handleChange={this.handleChange} /> + label="Wheelchair" + type="text" + fieldName="wheelchair_boarding" + value={one.wheelchair_boarding || ''} + handleChange={this.handleChange} /> {/* */}
@@ -176,12 +238,13 @@ class StopForm extends Component { render () { const one = this.state const { fetching } = this.props.stop - // redirect to view page if no data - const { stopId } = this.props.match.params // redirect to agency list if submitted if (one.justSubmit === true && !fetching) { return } + if (this.state.back2list === true) { + return + } return this.renderForm() } @@ -190,6 +253,8 @@ class StopForm extends Component { const mapStateToProps = state => ({ stop: state.stop, + draggableMarkerLatlon: state.geo.draggableMarkerLatlon, + coords: state.geo.coords, }) const connectStopForm = connect( diff --git a/src/components/StopList.js b/src/components/StopList.js index 5e026e7..b708240 100644 --- a/src/components/StopList.js +++ b/src/components/StopList.js @@ -19,6 +19,10 @@ class StopList extends Component { store.dispatch(getStop()) } + handleStopSearch(evt) { + store.dispatch(getStop(`search=${evt.target.value}`)) + } + render() { const { results } = this.props.stop return ( @@ -34,7 +38,11 @@ class StopList extends Component {

- + { (e.key === 'Enter') && this.handleStopSearch(e) }} /> @@ -45,8 +53,8 @@ class StopList extends Component { {ele.stop_id} - {ele.name} ))} - {this.props.stop.length === 0 && - No stop yet} + {results.length === 0 && + No stop found} ) diff --git a/src/components/StopTimeForm.js b/src/components/StopTimeForm.js new file mode 100644 index 0000000..139423b --- /dev/null +++ b/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 ( + + + +

+ + { + let evt = { + target: { + name: 'stop', + value: data, + }} + return this.handleChange(evt) + }} + /> +
+ + + + + + + + + + + +
+
+ +
+ {item.id !== null &&
+ +
} + {this.props.toggleEditMode && + } +
+ + ) + } + +} + +export default StopTimeForm diff --git a/src/components/StopTimeOne.js b/src/components/StopTimeOne.js new file mode 100644 index 0000000..f2e0874 --- /dev/null +++ b/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) => ( + +
+
+

+ Stop this.toggleEditMode()}>EDIT +

+

{item.sequence}. {item.stop.stop_id}

+
+
+
+
+

Arrival

+

{item.arrival}

+
+
+
+
+

Departure

+

{item.departure}

+
+
+
+
+

Stop headsign

+

{item.stop_headsign || '-'}

+
+
+
+ ) + + render() { + const { item } = this.props + if (this.state.editMode) + return + return this.renderReadOnly(item) + } + +} + +export default StopTimeOne diff --git a/src/components/TripForm.js b/src/components/TripForm.js new file mode 100644 index 0000000..2f29a5b --- /dev/null +++ b/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 ( + +

{one.name}  

+
+ + + + + + + {!calendar.fetching && ({ value: ele.id, label: ele.service_id }))} />} + + + + + + + + +
+
+
+ +
+ {one.id !== null &&
+ +
} +
+ Cancel +
+
+
+ ) + } + + 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 + // } + // return this.renderForm() + // } + + // if (one.id === null && tripId.length > 0) + // return + + // redirect to trip list if submitted + if (one.justSubmit === true && !fetching) { + return + } + return ( +
+
+ {this.renderForm()} +
+
+ + {!this.state.newStopTime && } + {this.state.newStopTime && } + + {this.props.stoptime.results.map( + ele => )} +
+
+ ) + } + +} + +const mapStateToProps = state => ({ + route: state.route, + trip: state.trip, + calendar: state.calendar, + stoptime: state.stoptime, +}) + +const connectTripForm = connect( + mapStateToProps, +)(TripForm) +export default connectTripForm diff --git a/src/components/parts/Date.js b/src/components/parts/Date.js new file mode 100644 index 0000000..55c0270 --- /dev/null +++ b/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) => ( +
+ +

+ { + let evt = { + target: { + name: props.fieldName, + value: date[0].toISOString('YYYY-MM-DD').slice(0, 10) + }} + return props.handleChange(evt)}} /> +

+
+) + +export default Date diff --git a/src/components/parts/HorizontalSelect.js b/src/components/parts/HorizontalSelect.js new file mode 100644 index 0000000..9c65856 --- /dev/null +++ b/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) => ( +
+
+ +
+
+
+ +

+
+) + +export default Input diff --git a/src/components/parts/Select.js b/src/components/parts/Select.js new file mode 100644 index 0000000..596dede --- /dev/null +++ b/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) => ( +
+ +