Browse Source

Add GeoMap container

* This Geo container needs to be publicly available to comply with
Mapbox policy
master
sipp11 7 years ago
parent
commit
99220d0544
  1. 5
      package.json
  2. 12
      public/index.html
  3. 8
      src/App.js
  4. 63
      src/actions/index.js
  5. 67
      src/actions/stoptime.js
  6. 18
      src/components/AgencyItem.js
  7. 2
      src/components/AgencyList.js
  8. 26
      src/components/Breadcrumb.js
  9. 2
      src/components/CalendarList.js
  10. 157
      src/components/FloatPane.js
  11. 61
      src/components/Nav.js
  12. 167
      src/components/RouteDetail.js
  13. 47
      src/components/RouteList.js
  14. 2
      src/components/Spinner.js
  15. 20
      src/constants/ActionTypes.js
  16. 134
      src/container/Geo.js
  17. 4
      src/container/Main.js
  18. 31
      src/reducers/first.js
  19. 131
      src/reducers/geo.js
  20. 6
      src/reducers/index.js
  21. 79
      src/reducers/stoptime.js
  22. 2
      src/store.js

5
package.json

@ -3,12 +3,17 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"ajv": "^6.5.1",
"axios": "^0.18.0", "axios": "^0.18.0",
"leaflet": "^1.3.1",
"leaflet-draw": "^1.0.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"moment": "^2.22.2", "moment": "^2.22.2",
"react": "^16.4.1", "react": "^16.4.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-flatpickr": "^3.6.4", "react-flatpickr": "^3.6.4",
"react-leaflet": "^1.9.1",
"react-leaflet-draw": "^0.18.0",
"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",

12
public/index.html

@ -10,6 +10,8 @@
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<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.min.css" />
<link rel="stylesheet" href="https://static.10ninox.com/css/bulma-checkradio.min.css" /> <link rel="stylesheet" href="https://static.10ninox.com/css/bulma-checkradio.min.css" />
<script defer="defer" src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script> <script defer="defer" src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
@ -24,7 +26,15 @@
--> -->
<title>Grunt FRONT</title> <title>Grunt FRONT</title>
<style> <style>
html, body { background: #f5f5f5 } html, body { background: #f5f5f5; height: 100%; }
/* for leaflet draw */
.sr-only {
display: none;
}
/* map view needs some customization */
.leaflet-pane {
z-index: 20;
}
</style> </style>
</head> </head>
<body> <body>

8
src/App.js

@ -2,21 +2,19 @@ import React from 'react'
// import './App.css' // import './App.css'
import { Switch, Route } from 'react-router-dom' import { Switch, Route } from 'react-router-dom'
import Footer from './components/Footer'
import Main from './container/Main' import Main from './container/Main'
import Geo from './container/Geo'
import Public from './container/Public' import Public from './container/Public'
import { LOGIN_PATH } from './constants/path' import { LOGIN_PATH } from './constants/path'
const App = () => ( const App = () => (
<div>
<Switch> <Switch>
{/* both /roster and /roster/:number begin with /roster */} {/* both /roster and /roster/:number begin with /roster */}
<Route path={LOGIN_PATH} component={Public}/> <Route path={LOGIN_PATH} component={Public} />
<Route path='/map' component={Geo} />
<Route path='/' component={Main} /> <Route path='/' component={Main} />
</Switch> </Switch>
<Footer />
</div>
) )
export default App export default App

63
src/actions/index.js

@ -4,16 +4,6 @@ import { login } from '../utils/Auth'
import { RSAAHeaders } from '../utils/ApiClient' import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api' import { API_URL } from '../constants/Api'
export const incFirstCounter = _ => ({
type: types.FIRST_INCREMENT_COUNTER,
})
export const assignFirstId = objId => ({
type: types.FIRST_ASSIGN_ID,
newId: objId
})
// auth // auth
export const reqAuth = _ => { export const reqAuth = _ => {
return { return {
@ -48,6 +38,59 @@ export function fetchAuth(user, passwd) {
} }
} }
// geo
export const geoLocationUpdate = (position) => {
/*
Position {coords: Coordinates, timestamp: 1530214120419}
Coordinates {
latitude: 13.8462448, longitude: 100.53825479999999,
altitude: null, accuracy: 20,
altitudeAccuracy: null, speed: 20}
*/
const { coords } = position
const newCoords = {
accuracy: coords.accuracy,
altitude: coords.altitude,
altitudeAccuracy: coords.altitudeAccuracy,
heading: coords.heading,
latitude: coords.latitude,
longitude: coords.longitude,
speed: coords.speed,
}
return {
type: types.GEO_LOCATION_SUCCESS,
coords: newCoords,
timestamp: position.timestamp,
}
}
export const geoLocationFailed = (positionErr) => {
// PositionError {code: 3, message: "Timeout expired"}
return {
type: types.GEO_LOCATION_FAILURE,
code: positionErr.code,
message: positionErr.message,
}
}
export const polygonUpdate = (id, geojson, style) => {
return {
type: types.GEO_POLYGON_UPDATE,
payload: {
id,
geojson,
style: style || {},
}
}
}
export const polygonReset = () => {
return {
type: types.GEO_POLYGON_RESET
}
}
// AGENCY // AGENCY
export const getAgency = () => ({ export const getAgency = () => ({

67
src/actions/stoptime.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 getStopTime = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/stoptime/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.stoptime.fetching,
types: [
{
type: types.STOPTIME_REQUEST,
meta: { query: query },
},
types.STOPTIME_SUCCESS,
types.STOPTIME_FAILURE,
]
}
})
export const updateStopTime = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/stoptime/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.STOPTIME_REQUEST,
types.STOPTIME_UPDATE,
types.STOPTIME_FAILURE,
]
}
})
export const createStopTime = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/stoptime/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.STOPTIME_REQUEST,
types.STOPTIME_CREATE,
types.STOPTIME_FAILURE,
]
}
})
export const deleteStopTime = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/stoptime/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.STOPTIME_REQUEST,
{
type: types.STOPTIME_DELETE,
meta: { id }
},
types.STOPTIME_FAILURE,
]
}
})

18
src/components/AgencyItem.js

@ -3,9 +3,6 @@ import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link, Redirect, Route } from 'react-router-dom' import { Link, Redirect, Route } from 'react-router-dom'
// import { getRoute } from '../actions'
// import store from '../store'
import Spinner from './Spinner' import Spinner from './Spinner'
import RouteList from './RouteList' import RouteList from './RouteList'
import FareAttrList from './FareAttrList' import FareAttrList from './FareAttrList'
@ -18,11 +15,14 @@ background: #fafafa;
class AgencyItem extends Component { class AgencyItem extends Component {
render() { render() {
const { props } = this const { agency, match, updateBreadcrumb, children } = this.props
const { agencyId, agencyChild } = props.match.params const { agencyId, agencyChild } = match.params
const { results, fetching } = props.agency const { results, fetching } = agency
const one = results.filter(ele => ele.agency_id === agencyId)[0] const one = results.filter(ele => ele.agency_id === agencyId)[0]
if (updateBreadcrumb !== undefined)
updateBreadcrumb(match.params)
if (!fetching && one === undefined) if (!fetching && one === undefined)
return <Redirect to='/agency' /> return <Redirect to='/agency' />
@ -69,7 +69,7 @@ class AgencyItem extends Component {
{agencyChild === undefined && <p>Pick options on the tab</p>} {agencyChild === undefined && <p>Pick options on the tab</p>}
</div> </div>
</div> </div>
{props.children} {children}
</StyledAgencyItem> </StyledAgencyItem>
) )
} }
@ -80,7 +80,7 @@ const mapStateToProps = state => ({
route: state.route, route: state.route,
}) })
const connectAgencyList = connect( const connectAgencyItem = connect(
mapStateToProps, mapStateToProps,
)(AgencyItem) )(AgencyItem)
export default connectAgencyList export default connectAgencyItem

2
src/components/AgencyList.js

@ -42,7 +42,7 @@ class AgencyList extends Component {
</p> </p>
</nav> </nav>
{results && Object.keys(results).map(i => ( {results && Object.keys(results).map(i => (
<FakeRow className="level panel" key={i}> <FakeRow className="level panel" key={results[i].agency_id}>
<div className="level-item has-text-centered"> <div className="level-item has-text-centered">
<div> <div>
<p className="heading">Agency ID</p> <p className="heading">Agency ID</p>

26
src/components/Breadcrumb.js

@ -0,0 +1,26 @@
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
const StyledBreadcrumb = styled.nav`
padding: 0 5px;
font-size: 0.7rem;
margin-bottom: 5px !important;
`
const Breadcrumb = (props) => (
<StyledBreadcrumb className="breadcrumb has-succeeds-separator" aria-label="breadcrumbs">
<ul>
<li><Link to='/map'>ALL</Link></li>
{props.agencyId && <li className={`${!props.routeId && 'is-active'}`}>
<Link to={`/map/${props.agencyId}`} aria-current="page">
{props.agencyId}</Link></li>}
{props.routeId && <li className={`${props.routeId && 'is-active'}`}>
<Link to={`/map/${props.agencyId}/route/${props.routeId}`} aria-current="page">
{props.routeId}</Link></li>}
</ul>
</StyledBreadcrumb>
)
export default Breadcrumb

2
src/components/CalendarList.js

@ -56,7 +56,7 @@ class CalendarList extends Component {
</p> </p>
</nav> </nav>
{results && Object.keys(results).map(i => ( {results && Object.keys(results).map(i => (
<FakeRow className="level panel" key={i}> <FakeRow className="level panel" key={results[i].service_id}>
<div className="level-item has-text-centered"> <div className="level-item has-text-centered">
<div> <div>
<p className="heading">Service ID</p> <p className="heading">Service ID</p>

157
src/components/FloatPane.js

@ -0,0 +1,157 @@
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { logout } from '../utils/Auth'
import RouteList from './RouteList'
import RouteDetail from './RouteDetail'
import Breadcrumb from './Breadcrumb'
import { polygonReset } from '../actions'
import store from '../store'
const StyledLink = styled(Link)`
&[data-active] {
color: red;
}
`
const StyledLRPaddingNav = styled.nav`
padding-right: 10px;
padding-left: 0;
margin-bottom: 5px !important;
`
const StyledFloatPane = styled.div`
min-width: 300px;
height: 100%;
width: 25vw;
z-index: 30;
background: #fefefefe;
border-radius: 0 5px 5px 0;
position: fixed;
left: ${props => props.hidePane ? '-500px' : 0};
top: 0px;
`
const StyledPaneToggler = styled.div`
width: 20px
height: 30px;
background: #fefefefe;
color: #209cee;
position: fixed;
text-align: center;
left: ${props => props.hidePane ? 0 : '25vw'};
top: 30px;
border-radius: 0 5px 5px 0;
@media (max-width: 1200px) {
left: ${props => props.hidePane ? 0 : '300px'};
}
`
const StyledScrollY = styled.div`
height: 100vh;
overflow: auto;
padding-bottom: 5rem;
`
const SimpleAgencyList = (props) => (
<nav className="panel">
<p className="panel-heading">
Agency
</p>
{props.agencies.map(ele => (
<Link key={ele.agency_id} className="panel-block" to={`/map/${ele.agency_id}`}>
{ele.name}
</Link>))}
</nav>
)
class FloatPane extends Component {
state = { hidePane: false, breadcrumb: {} }
renderTopLevel(loggedIn) {
return (
<StyledLRPaddingNav className="level is-mobile">
<div className="level-left">
<StyledLink to="/" className="navbar-item">
<img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="112" height="28" />
</StyledLink>
</div>
<div className="level-right">
<a className="button is-small is-primary"
onClick={() => store.dispatch(polygonReset())}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Clear PG</span>
</a>
{loggedIn &&
<a className="button is-small is-outlined"
onClick={() => logout()}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Logout</span>
</a>
}
{!loggedIn &&
<Link className="button is-small is-outlined is-info" to='/login'>
<span className="icon">
<i className="fas fa-sign-in-alt"></i>
</span>
<span>Sign in</span>
</Link>
}
</div>
</StyledLRPaddingNav>
)
}
togglePane() {
this.setState({ hidePane: !this.state.hidePane })
}
handleChildMatchParams(params) {
this.setState({ breadcrumb: params })
}
render () {
const { loggedIn, agency } = this.props
const { hidePane } = this.state
const { results } = agency
return (
<StyledFloatPane hidePane={hidePane}>
<StyledPaneToggler hidePane={hidePane} onClick={this.togglePane.bind(this)}>
<i className='fas fa-align-justify' />
</StyledPaneToggler>
{this.renderTopLevel(loggedIn)}
<StyledScrollY>
<Breadcrumb {...this.state.breadcrumb} />
<Route exact path={`/map/:agencyId/route/:routeId/:routeParams?`} render={(props) => (
<RouteDetail {...props}
updateBreadcrumb={this.handleChildMatchParams.bind(this)} />)} />
<Route exact path={`/map`} render={(props) => (
<SimpleAgencyList agencies={results} {...props} />)} />
<Route exact path={`/map/stop`} component={RouteList} />
<Route exact path={`/map/:agencyId`} render={(props) => (
<RouteList {...props}
updateBreadcrumb={this.handleChildMatchParams.bind(this)} />)} />
</StyledScrollY>
</StyledFloatPane>
)
}
}
const mapStateToProps = state => ({
agency: state.agency,
})
export default connect(
mapStateToProps
)(FloatPane)

61
src/components/Nav.js

@ -4,33 +4,21 @@ import styled from 'styled-components'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { logout } from '../utils/Auth' import { logout } from '../utils/Auth'
import { polygonReset } from '../actions'
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
&[data-active] { &[data-active] {
color: red; color: red;
} }
`; `
class Nav extends Component { class Nav extends Component {
state = { expandNavbar: false } state = { expandNavbar: false }
render() { renderLoggedIn() {
const { props } = this const { props } = this
return ( return (
<nav className="navbar">
<div className="navbar-brand">
<StyledLink to="/" className="navbar-item">
<img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="112" height="28" />
</StyledLink>
<div className="navbar-burger burger" onClick={() => { this.setState({expandNavbar: !this.state.expandNavbar}) }}>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div className={`navbar-menu ${(this.state.expandNavbar ? 'is-active' : '')}`}>
<div className="navbar-start"> <div className="navbar-start">
<StyledLink to="/" className="navbar-item"> <StyledLink to="/" className="navbar-item">
Home Home
@ -63,12 +51,46 @@ class Nav extends Component {
</StyledLink> </StyledLink>
</div> </div>
</div> </div>
<StyledLink to="/map" className="navbar-item">
Map
</StyledLink>
</div> </div>
)
}
render() {
const { loggedIn } = this.props
return (
<nav className="navbar">
<div className="navbar-brand">
<StyledLink to="/" className="navbar-item">
<img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="112" height="28" />
</StyledLink>
<div className="navbar-burger burger" onClick={() => { this.setState({expandNavbar: !this.state.expandNavbar}) }}>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div className={`navbar-menu ${(this.state.expandNavbar ? 'is-active' : '')}`}>
{loggedIn && this.renderLoggedIn()}
<div className="navbar-end"> <div className="navbar-end">
<div className="navbar-item"> <div className="navbar-item">
<div className="field is-grouped"> <div className="field is-grouped">
<p className="control"> <p className="control">
<a className="button is-primary"
onClick={() => polygonReset()}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Clear polygons</span>
</a>
</p>
<p className="control">
{loggedIn &&
<a className="button is-primary" <a className="button is-primary"
onClick={() => logout()}> onClick={() => logout()}>
<span className="icon"> <span className="icon">
@ -76,6 +98,15 @@ class Nav extends Component {
</span> </span>
<span>Logout</span> <span>Logout</span>
</a> </a>
}
{!loggedIn &&
<Link className="button is-primary" to='/login'>
<span className="icon">
<i className="fas fa-sign-in-alt"></i>
</span>
<span>Sign in</span>
</Link>
}
</p> </p>
</div> </div>
</div> </div>

167
src/components/RouteDetail.js

@ -0,0 +1,167 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { Link, Route } from 'react-router-dom'
import { connect } from 'react-redux'
import Spinner from './Spinner'
import { getRoute, polygonUpdate } from '../actions'
import { getStopTime } from '../actions/stoptime'
import store from '../store'
/*
TODO: add shape
TODO: add Route detail
*/
const StyledTripDesc = styled.div`
font-size: 0.87rem;
line-height: 0.85rem;
margin-left: 10px;
`
const RouteDesc = (props) => (
<div>
<span className="panel-block">
ID: {props.route.id}
</span>
<span className="panel-block">
long_name: {props.route.long_name}
</span>
<span className="panel-block">
short_name: {props.route.short_name}
</span>
<span className="panel-block">
type: {props.route.route_type}
</span>
<span className="panel-block">
color: <br/>Text #{props.route.text_color || '-'} <br />BG #{props.route.route_text_color}
</span>
<span className="panel-block">
Sort order: {props.route.route_sort_order}
</span>
<span className="panel-block">
URL: {props.route.route_url || '-'}
</span>
<span className="panel-block">
desc: {props.route.desc || '-'}
</span>
<span className="panel-block">
shapes: {props.route.geosjson !== null ? 'yes' : 'n/a'}
</span>
</div>
)
const TripList = (props) => (
<div>
{props.trips.map(ele => (
<span key={ele.id} className="panel-block"
onClick={() => { store.dispatch(getStopTime(`trip=${ele.id}`))}}>
{ele.trip_id}
<StyledTripDesc>
(<b>ID</b> {ele.id})&nbsp;
<b>service</b> <Link to={`/calendar/${ele.service.service_id}`}>{ele.service.service_id}</Link>
<br />
{ele.stoptime.count > 0 && <div>
<b>Stop</b> #{ele.stoptime.count}
<br />
<b>Time period</b> {ele.stoptime.period[0]} - {ele.stoptime.period[1]}
</div>}
</StyledTripDesc>
</span>
))}
{props.trips.length === 0 && <span key="empty-trips" className="panel-block">
No trip set
</span>}
</div>
)
const FareRuleList = (props) => (
<div>
{props.farerules.map(ele => (
<span key={ele.id} className="panel-block">
{ele.fare.fare_id} - {ele.fare.price}
</span>
))}
{props.farerules.length === 0 && <span key="empty-farerule" className="panel-block">
No fare rule set
</span>}
</div>
)
class RouteDetail extends Component {
state = {
tab: 'detail',
}
componentDidMount() {
const { updateBreadcrumb, match } = this.props
updateBreadcrumb(match.params)
}
componentWillMount() {
const { route, match } = this.props
if (route.count === 0) {
store.dispatch(getRoute(`agency=${match.params.agencyId}`))
} else {
this.pushShapeToStore(match, route)
}
}
componentWillReceiveProps(newProps) {
if (this.props.route.count < newProps.route.count) {
this.pushShapeToStore(this.props.match, newProps.route)
}
}
pushShapeToStore(match, routeStore) {
const { routeId } = match.params
const tRoute = routeStore.results.filter(ele => ele.route_id === routeId)
if (tRoute.length === 0)
return
const one = tRoute[0]
const oneStyle = {}
if (one.route_color)
oneStyle['color'] = `#${one.route_color}`
store.dispatch(polygonUpdate(one.route_id, one.geojson, oneStyle))
}
render() {
const { route, match } = this.props
const { routeId, agencyId, routeParams } = match.params
const tRoute = route.results.filter(ele => ele.route_id === routeId)
if (tRoute.length === 0) {
return <Spinner show={true} />
}
const item = tRoute[0]
const baseUrl = `/map/${agencyId}/route/${routeId}`
return (
<nav className="panel">
<p className="panel-heading">
{item.long_name} <small>#{item.id}</small>
</p>
<p className="panel-tabs">
<Link to={`${baseUrl}`} className={`${!routeParams && 'is-active'}`}>detail</Link>
<Link to={`${baseUrl}/trip`} className={`${routeParams === 'trip' && 'is-active'}`}>trip</Link>
<Link to={`${baseUrl}/fare`} className={`${routeParams === 'fare' && 'is-active'}`}>fare</Link>
</p>
<Route exact path={`${baseUrl}`} render={(props) => (
<RouteDesc route={item} {...props} />)} />
<Route exact path={`${baseUrl}/fare`} render={(props) => (
<FareRuleList farerules={item.farerule_set} {...props} />)} />
<Route exact path={`${baseUrl}/trip`} render={(props) => (
<TripList trips={item.trip_set} {...props} />)} />
</nav>
)
}
}
const mapStateToProps = state => ({
route: state.route,
})
export default connect(
mapStateToProps
)(RouteDetail)

47
src/components/RouteList.js

@ -1,16 +1,27 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Spinner from './Spinner' import Spinner from './Spinner'
import { getRoute } from '../actions' import { getRoute } from '../actions'
import store from '../store' import store from '../store'
const StyledLongName = styled.div`
margin-left: 1rem;
font-size: 0.9rem;
font-style: italic;
color: #444;
`
class RouteList extends Component { class RouteList extends Component {
componentDidMount() { componentDidMount() {
const { agencyId } = this.props.match.params const { updateBreadcrumb, match } = this.props
const { agencyId } = match.params
store.dispatch(getRoute(`agency=${agencyId}`)) store.dispatch(getRoute(`agency=${agencyId}`))
if (updateBreadcrumb)
updateBreadcrumb(match.params)
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
@ -20,20 +31,36 @@ class RouteList extends Component {
} }
render() { render() {
const { route } = this.props const { route, match } = this.props
const { agencyId } = match.params
return ( return (
<div> <nav className="panel">
<h3 className="title">Routes</h3> <p className="panel-heading">
<Spinner show={route.fetching} /> Route
<ul> </p>
{route.count === 0 && <li>No route</li>} {/*<p className="panel-tabs">
<a className="is-active">all</a>
<a>public</a>
<a>private</a>
<a>sources</a>
<a>forks</a>
</p>*/}
<Spinner className="panel-block" show={route.fetching} />
{route.count === 0 && <div className="panel-block">No route</div>}
{route.count > 0 {route.count > 0
&& route.results.map(ele => ( && route.results.map(ele => (
<li key={ele.route_id}>{ ele.route_id }</li>) <Link key={ele.route_id}
className="panel-block"
to={`/map/${agencyId}/route/${ele.route_id}`}>
<span className="panel-icon">
<i className="fas fa-code-branch" aria-hidden="true"></i>
</span>
{ele.short_name}
{ele.long_name.length > 0 && <StyledLongName>{ ele.long_name }</StyledLongName>}
</Link>)
)} )}
</ul> </nav>
</div>
) )
} }
} }

2
src/components/Spinner.js

@ -2,7 +2,7 @@ import React from 'react'
const Spinner = (props) => ( const Spinner = (props) => (
<span> <span>
{props.show && <span><i className="fas fa-spinner" /></span>} {props.show && <span className={props.className}><i className="fas fa-spinner" /></span>}
</span> </span>
) )

20
src/constants/ActionTypes.js

@ -1,14 +1,18 @@
export const FIRST_INCREMENT_COUNTER = 'FIRST_INCREMENT_COUNTER' export const GEO_LOCATION_SUCCESS = 'GEO_LOCATION_SUCCESS'
export const FIRST_ASSIGN_ID = 'FIRST_ASSIGN_ID' export const GEO_LOCATION_FAILURE = 'GEO_LOCATION_FAILURE'
// export const SET_TOKEN = 'SET_TOKEN' export const GEO_POLYGON_ADD = 'GEO_POLYGON_ADD'
export const GEO_POLYGON_RESET = 'GEO_POLYGON_RESET'
export const GEO_POLYGON_UPDATE = 'GEO_POLYGON_UPDATE'
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 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'
export const SUCCESS_LOGOUT = 'SUCCESS_LOGOUT' export const SUCCESS_LOGOUT = 'SUCCESS_LOGOUT'
export const AGENCY_REQUEST = 'AGENCY_REQUEST' export const AGENCY_REQUEST = 'AGENCY_REQUEST'
export const AGENCY_FAILURE = 'AGENCY_FAILURE' export const AGENCY_FAILURE = 'AGENCY_FAILURE'
export const AGENCY_SUCCESS = 'AGENCY_SUCCESS' export const AGENCY_SUCCESS = 'AGENCY_SUCCESS'
@ -18,6 +22,14 @@ export const AGENCY_DELETE = 'AGENCY_DELETE'
export const AGENCY_UPDATE = 'AGENCY_UPDATE' export const AGENCY_UPDATE = 'AGENCY_UPDATE'
export const STOPTIME_REQUEST = 'STOPTIME_REQUEST'
export const STOPTIME_FAILURE = 'STOPTIME_FAILURE'
export const STOPTIME_SUCCESS = 'STOPTIME_SUCCESS'
// below items are SUCCESS for other tasks
export const STOPTIME_CREATE = 'STOPTIME_CREATE'
export const STOPTIME_DELETE = 'STOPTIME_DELETE'
export const STOPTIME_UPDATE = 'STOPTIME_UPDATE'
export const ROUTE_REQUEST = 'ROUTE_REQUEST' export const ROUTE_REQUEST = 'ROUTE_REQUEST'
export const ROUTE_FAILURE = 'ROUTE_FAILURE' export const ROUTE_FAILURE = 'ROUTE_FAILURE'
export const ROUTE_SUCCESS = 'ROUTE_SUCCESS' export const ROUTE_SUCCESS = 'ROUTE_SUCCESS'

134
src/container/Geo.js

@ -0,0 +1,134 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
// import { Redirect, Route, Switch } from 'react-router-dom'
import {
Map, TileLayer, CircleMarker, ZoomControl,
FeatureGroup, GeoJSON } from 'react-leaflet'
// import { EditControl } from 'react-leaflet-draw'
// import L from 'leaflet'
import { loggedIn } from '../reducers/auth'
import { geoLocationFailed, geoLocationUpdate, getAgency } from '../actions'
import FloatPane from '../components/FloatPane'
import store from '../store'
const FullPageBox = styled.div`
height: 100%;
min-height: 100vh;
z-index: 1;
flex: 1;
display: flex;
flex-direction: column;
`
class Geo extends Component {
constructor(props) {
super(props)
this.renderGeoJSON = this.renderGeoJSON.bind(this)
}
componentWillMount() {
const { count } = this.props.agency
if (count === 0)
store.dispatch(getAgency())
}
componentWillUnmount() {
navigator.geolocation.clearWatch(this.watchID)
}
componentDidMount() {
/* if (!navigator.geolocation) {
getCurrentPosition: (success, failure) => {
const failureMsg = "Your browser doesn't support geolocation."
console.log(success, failure)
// failure(dispatch(locationError(failureMsg)))
}
} */
navigator.geolocation.getCurrentPosition(
(position) => store.dispatch(geoLocationUpdate(position)),
(error) => store.dispatch(geoLocationFailed(error)),
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 1000 },
)
this.watchID = navigator.geolocation.watchPosition(
(position) => store.dispatch(geoLocationUpdate(position)),
(error) => store.dispatch(geoLocationFailed(error)),
)
}
renderGeoJSON() {
const { polygons } = this.props.geo
const style = {
color: '#a63eff',
weight: 5,
opacity: 0.65
}
return (
<FeatureGroup>
{polygons && polygons.map(ele => (
<GeoJSON
key={`geojson-${ele.id}`}
data={ele.geojson}
style={{...style, ...ele.style}} />
))}
</FeatureGroup>
)
}
render() {
const { loggedIn, geo } = this.props
const myLocationMarker = geo.coords ? (
<CircleMarker
center={[geo.coords.latitude, geo.coords.longitude]}
radius={8}
fillColor={'rgb(33, 150, 243)'}
fillOpacity={0.9}
color={'white'}
stroke
weight={2}
opacity={1}
className='my-location-marker' />
) : null
// const mapCenter = geo.coords
// ? [geo.coords.latitude, geo.coords.longitude]
// : [13.84626739, 100.538]
const mapCenter = [13.84626739, 100.538]
return (
<FullPageBox>
{/* <Nav loggedIn={loggedIn} /> */}
<FloatPane loggedIn={loggedIn} {...this.props} />
<Map
center={mapCenter}
zoom={13}
length={4}
zoomControl={false}
animate
style={{flex: 1}}
ref='map'>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
// url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
url='https://api.tiles.mapbox.com/v4/sipp11.p4efho4p/{z}/{x}/{y}.png?access_token=pk.eyJ1Ijoic2lwcDExIiwiYSI6ImNpa2hzbGpzcDAyYWl0eWo3azhkaHR3aWIifQ.cc4CGgGKkpP_8XVa2BUwtQ'
/>
<ZoomControl position="topright" />
{myLocationMarker}
{this.renderGeoJSON()}
</Map>
</FullPageBox>
)
}
}
const mapStateToProps = state => ({
loggedIn: loggedIn(state.auth),
agency: state.agency,
geo: state.geo,
})
export default connect(
mapStateToProps,
{}
)(Geo)

4
src/container/Main.js

@ -5,6 +5,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'
import { loggedIn } from '../reducers/auth' import { loggedIn } from '../reducers/auth'
import { getAgency } from '../actions' import { getAgency } from '../actions'
import Nav from '../components/Nav' import Nav from '../components/Nav'
import Footer from '../components/Footer'
import CalendarForm from '../components/CalendarForm' import CalendarForm from '../components/CalendarForm'
import CalendarList from '../components/CalendarList' import CalendarList from '../components/CalendarList'
import AgencyList from '../components/AgencyList' import AgencyList from '../components/AgencyList'
@ -34,7 +35,7 @@ class Main extends Component {
} }
return ( return (
<div> <div>
<Nav /> <Nav loggedIn={loggedIn} />
<div className="container is-widescreen"> <div className="container is-widescreen">
<Switch> <Switch>
<Route exact path={`${match.url}agency/new`} component={AgencyForm} /> <Route exact path={`${match.url}agency/new`} component={AgencyForm} />
@ -48,6 +49,7 @@ class Main extends Component {
<Route exact path={`${match.url}calendar`} component={CalendarList} /> <Route exact path={`${match.url}calendar`} component={CalendarList} />
</Switch> </Switch>
</div> </div>
<Footer />
</div> </div>
) )
} }

31
src/reducers/first.js

@ -1,31 +0,0 @@
import {
FIRST_INCREMENT_COUNTER, FIRST_ASSIGN_ID
} from '../constants/ActionTypes'
const initialState = {
counter: 1,
obj: {}
}
const first = (state = initialState, action) => {
switch (action.type) {
case FIRST_INCREMENT_COUNTER:
return {
...state,
counter: state.counter +1
}
case FIRST_ASSIGN_ID:
return {
...state,
obj: action.newId
}
default:
return state
}
}
export default first
export const getCounter = state => state.counter
export const getId = state => state.id

131
src/reducers/geo.js

@ -0,0 +1,131 @@
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,
STOPTIME_SUCCESS,
} from '../constants/ActionTypes'
const initialState = {
timestamp: 0,
coords: null,
message: '',
polygons: [],
markers: [],
}
const geo = (state = initialState, action) => {
switch (action.type) {
case GEO_LOCATION_SUCCESS:
/*
action = {
coords: {
latitude: 13.8462448,
longitude: 100.53825479999999,
altitude: null,
altitudeAccuracy: null,
accuracy: 20,
speed: 20,
},
timestamp: 1530214120419
}
*/
return {
...state,
message: '',
timestamp: action.timestamp,
coords: {...action.coords},
}
case GEO_LOCATION_FAILURE:
// action = {code: 3, message: "Timeout expired"}
return {
...state,
timestamp: 0,
coords: null,
message: action.message,
}
case GEO_POLYGON_ADD:
return {
...state,
polygons: [
...state.polygons,
action.payload,
],
}
case STOPTIME_SUCCESS:
/* add all stop into another polygon // we will assign unique id as
`stoptime` so it won't get in other ways
*/
const stInd = state.polygons.findIndex(ele => ele.id === 'stoptime')
let polyWstoptime = [...state.polygons]
let stopFeatures = {
"type": "FeatureCollection",
"features": []
}
action.payload.results.map(ele => {
stopFeatures.features.push({
"type": "Feature",
"properties": {
"name": `${ele.stop.name} - ${ele.arrival}`,
},
"geometry": ele.stop.geojson,
})
return true
})
const n = { id: 'stoptime', geojson: stopFeatures }
if (stInd > -1) {
polyWstoptime[stInd] = n
} else {
polyWstoptime.push(n)
}
return {
...state,
polygons: polyWstoptime,
}
case GEO_POLYGON_UPDATE:
const polyInd = state.polygons.findIndex(ele => ele.id === action.payload.id)
const oldPolygons = state.polygons
let newPolys = [...oldPolygons]
if (polyInd > -1) {
newPolys[polyInd] = action.payload
} else {
newPolys.push(action.payload)
}
return {
...state,
polygons: newPolys,
}
case GEO_POLYGON_RESET:
return {
...state,
polygons: [],
}
case GEO_MARKER_ADD:
return {
...state,
markers: [
...state.markers,
action.polygon,
],
}
case GEO_MARKER_UPDATE:
const markerInd = state.markers.findIndex(ele => ele.id === action.marker.id)
const oldMarkers = state.markers
return {
...state,
markers: [
...oldMarkers.slice(0, markerInd),
action.polygon,
...oldMarkers.slice(markerInd + 1),
],
}
case GEO_MARKER_RESET:
return {
...state,
markers: [],
}
default:
return state
}
}
export default geo

6
src/reducers/index.js

@ -1,16 +1,18 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import first from './first' import geo from './geo'
import auth from './auth' import auth from './auth'
import agency from './agency' import agency from './agency'
import route from './route' import route from './route'
import fareattr from './fareattr' import fareattr from './fareattr'
import calendar from './calendar' import calendar from './calendar'
import stoptime from './stoptime'
export default combineReducers({ export default combineReducers({
auth, auth,
first, geo,
agency, agency,
route, route,
fareattr, fareattr,
calendar, calendar,
stoptime,
}) })

79
src/reducers/stoptime.js

@ -0,0 +1,79 @@
import {
STOPTIME_CREATE, STOPTIME_DELETE, STOPTIME_UPDATE,
STOPTIME_REQUEST, STOPTIME_SUCCESS, STOPTIME_FAILURE,
} from '../constants/ActionTypes'
const stoptimeInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const stoptime = (state = stoptimeInitState, action) => {
switch (action.type) {
case STOPTIME_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
}
case STOPTIME_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...((prev) ? state.results : []),
...results,
]
}
case STOPTIME_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 STOPTIME_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case STOPTIME_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 STOPTIME_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default stoptime

2
src/store.js

@ -10,7 +10,7 @@ import gruntApp from './reducers'
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage, storage,
blacklist: ['agency', 'route', 'calendar'] blacklist: ['agency', 'route', 'calendar', 'geo']
} }
const middleware = [ thunk, apiMiddleware ] const middleware = [ thunk, apiMiddleware ]

Loading…
Cancel
Save