Browse Source

RSAA - API

* Login/logout
* Agency CRUD
master
sipp11 6 years ago
parent
commit
e04bcda09f
  1. 7
      package.json
  2. 7
      public/index.html
  3. 12
      src/App.js
  4. 67
      src/actions/calendar.js
  5. 67
      src/actions/fareattr.js
  6. 137
      src/actions/index.js
  7. 207
      src/components/AgencyForm.js
  8. 87
      src/components/AgencyItem.js
  9. 47
      src/components/AgencyList.js
  10. 50
      src/components/FareAttrList.js
  11. 13
      src/components/Footer.js
  12. 62
      src/components/Login.js
  13. 96
      src/components/Nav.js
  14. 49
      src/components/RouteList.js
  15. 9
      src/components/Spinner.js
  16. 40
      src/constants/ActionTypes.js
  17. 4
      src/constants/Api.js
  18. 2
      src/constants/path.js
  19. 30
      src/container/Gone.js
  20. 69
      src/container/Main.js
  21. 81
      src/container/Public.js
  22. 5
      src/index.js
  23. 76
      src/reducers/agency.js
  24. 10
      src/reducers/auth.js
  25. 79
      src/reducers/calendar.js
  26. 79
      src/reducers/fareattr.js
  27. 6
      src/reducers/index.js
  28. 79
      src/reducers/route.js
  29. 5
      src/store.js
  30. 22
      src/utils/ApiClient.js
  31. 6
      src/utils/Auth.js

7
package.json

@ -12,14 +12,19 @@
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4",
"redux": "^4.0.0",
"redux-api-middleware": "^2.3.0",
"redux-logger": "^3.0.6",
"redux-persist": "^5.10.0",
"redux-thunk": "^2.3.0"
"redux-thunk": "^2.3.0",
"styled-components": "^3.3.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"react-perf-devtool": "^3.0.7"
}
}

7
public/index.html

@ -10,6 +10,8 @@
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://static.10ninox.com/css/bulma.min.css">
<script defer="defer" src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></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.
@ -19,7 +21,10 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Grunt FRONT</title>
<style>
html, body { background: #f5f5f5 }
</style>
</head>
<body>
<noscript>

12
src/App.js

@ -2,18 +2,20 @@ import React from 'react'
// import './App.css'
import { Switch, Route } from 'react-router-dom'
import Header from './container/Header'
import Footer from './components/Footer'
import Main from './container/Main'
import Gone from './container/Gone'
import Public from './container/Public'
import { LOGIN_PATH } from './constants/path'
const App = () => (
<div>
<Header />
<Switch>
<Route exact path='/' component={Main}/>
{/* both /roster and /roster/:number begin with /roster */}
<Route path='/roster/:id' component={Gone}/>
<Route path={LOGIN_PATH} component={Public}/>
<Route path='/' component={Main} />
</Switch>
<Footer />
</div>
)

67
src/actions/calendar.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 getCalendar = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/calendar/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.calendar.fetching || state.calendar.query === query,
types: [
{
type: types.CALENDAR_REQUEST,
meta: { query: query },
},
types.CALENDAR_SUCCESS,
types.CALENDAR_FAILURE,
]
}
})
export const updateCalendar = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/calendar/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.CALENDAR_REQUEST,
types.CALENDAR_UPDATE,
types.CALENDAR_FAILURE,
]
}
})
export const createCalendar = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/calendar/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.CALENDAR_REQUEST,
types.CALENDAR_CREATE,
types.CALENDAR_FAILURE,
]
}
})
export const deleteCalendar = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/calendar/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.CALENDAR_REQUEST,
{
type: types.CALENDAR_DELETE,
meta: { id }
},
types.CALENDAR_FAILURE,
]
}
})

67
src/actions/fareattr.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 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,
]
}
})

137
src/actions/index.js

@ -1,5 +1,9 @@
import { RSAA } from 'redux-api-middleware'
import * as types from '../constants/ActionTypes'
import { login } from '../utils/Auth'
import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api'
export const incFirstCounter = _ => ({
type: types.FIRST_INCREMENT_COUNTER,
@ -31,6 +35,12 @@ export const failAuth = (body) => {
}
}
export const successSignout = () => {
return {
type: types.SUCCESS_LOGOUT
}
}
export function fetchAuth(user, passwd) {
return (dispatch) => {
dispatch(reqAuth())
@ -38,10 +48,125 @@ export function fetchAuth(user, passwd) {
}
}
// token
export const setToken = (data) => {
return {
type: types.SET_TOKEN,
token: data,
// AGENCY
export const getAgency = () => ({
[RSAA]: {
endpoint: `${API_URL}/agency/`,
method: 'GET',
bailout: (state) => state.agency.fetching,
headers: RSAAHeaders,
types: [
types.AGENCY_REQUEST,
types.AGENCY_SUCCESS,
types.AGENCY_FAILURE,
],
}
}
})
export const updateAgency = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/agency/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.AGENCY_REQUEST,
types.AGENCY_UPDATE,
types.AGENCY_FAILURE,
]
}
})
export const createAgency = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/agency/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.AGENCY_REQUEST,
types.AGENCY_CREATE,
types.AGENCY_FAILURE,
]
}
})
export const deleteAgency = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/agency/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.AGENCY_REQUEST,
{
type: types.AGENCY_DELETE,
meta: { id }
},
types.AGENCY_FAILURE,
]
}
})
// ROUTE
export const getRoute = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/route/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.route.fetching || state.route.query === query,
types: [
{
type: types.ROUTE_REQUEST,
meta: { query: query },
},
types.ROUTE_SUCCESS,
types.ROUTE_FAILURE,
]
}
})
export const updateRoute = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/route/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.ROUTE_REQUEST,
types.ROUTE_UPDATE,
types.ROUTE_FAILURE,
]
}
})
export const createRoute = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/route/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.ROUTE_REQUEST,
types.ROUTE_CREATE,
types.ROUTE_FAILURE,
]
}
})
export const deleteRoute = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/route/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.ROUTE_REQUEST,
{
type: types.ROUTE_DELETE,
meta: { id }
},
types.ROUTE_FAILURE,
]
}
})

207
src/components/AgencyForm.js

@ -0,0 +1,207 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import { updateAgency, createAgency, deleteAgency } from '../actions';
import store from '../store'
const StyledAgencyForm = styled.div`
padding: 1rem;
background: #fafafa;
`;
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">
<p className="control">
<input className="input" type={props.type || 'text'}
name={props.fieldName}
onChange={props.handleChange}
defaultValue={props.value} />
</p>
</div>
</div>
</div>
)
class AgencyForm extends Component {
state = {
id: null,
agency_id: "",
email: "",
fare_url: "",
lang: "",
name: "",
phone: "",
timezone: "",
url: "",
}
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(updateAgency(id, body))
} else {
store.dispatch(createAgency(body))
}
this.setState({justSubmit: true})
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteAgency(id))
this.setState({justSubmit: true})
}
componentWillMount() {
const { props } = this
const { agencyId } = props.match.params
const { results } = props.agency
const ones = results.filter(ele => ele.agency_id === agencyId)
if (ones.length > 0) {
this.setState(ones[0])
}
}
renderForm() {
const one = this.state
const { fetching } = this.props.agency
return (
<StyledAgencyForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<HorizontalInput
label="Agency ID"
type="text"
fieldName="agency_id"
value={one.agency_id || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Name"
type="text"
fieldName="name"
value={one.name || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="URL"
type="text"
fieldName="url"
value={one.url || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Timezone"
type="text"
fieldName="timezone"
value={one.timezone || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Phone"
type="text"
fieldName="phone"
value={one.phone || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Language"
type="text"
fieldName="lang"
value={one.lang || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Fare URL"
type="text"
fieldName="fare_url"
value={one.fare_url || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Email"
type="email"
fieldName="email"
value={one.email || ''}
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={`/agency/${one.agency_id}`} className="button is-text">Cancel</Link>
</div>
</div>
</StyledAgencyForm>
)
}
render () {
const one = this.state
const { fetching } = this.props.agency
// redirect to view page if no data
const { agencyId } = this.props.match.params
// this is a create form
if (agencyId === undefined) {
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/agency/`} />
}
return this.renderForm()
}
if (one.id === null && agencyId.length > 0)
return <Redirect to={`/agency/`} />
// redirect to agency list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/agency/${one.agency_id || ''}`} />
}
return this.renderForm()
}
}
const mapStateToProps = state => ({
agency: state.agency
})
const connectAgencyList = connect(
mapStateToProps,
)(AgencyForm)
export default connectAgencyList

87
src/components/AgencyItem.js

@ -0,0 +1,87 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Link, Redirect, Route } from 'react-router-dom'
// import { getRoute } from '../actions'
// import store from '../store'
import Spinner from './Spinner'
import RouteList from './RouteList'
import FareAttrList from './FareAttrList'
const StyledAgencyItem = styled.div`
padding: 1rem;
background: #fafafa;
`
class AgencyItem extends Component {
render() {
const { props } = this
const { agencyId, agencyChild } = props.match.params
const { results, fetching } = props.agency
const one = results.filter(ele => ele.agency_id === agencyId)[0]
if (!fetching && one === undefined)
return <Redirect to='/agency' />
if (fetching)
return <Spinner show={true} />
return (
<StyledAgencyItem>
<h1 className="title">
{one.name}&nbsp;&nbsp;
<Link to={`/agency/${one.agency_id}/edit`} className="button">Edit</Link></h1>
<div className="columns">
<div className="column is-3 is-hidden-mobile">
<div className="content">
<dl>
<dt>agency_id</dt>
<dd>{one.agency_id}</dd>
<dt>URL</dt>
<dd>{one.url}</dd>
<dt>Timezone</dt>
<dd>{one.timezone || '-'}</dd>
<dt>Phone</dt>
<dd>{one.phone || '-'}</dd>
<dt>Language</dt>
<dd>{one.lang || '-'}</dd>
<dt>Fare URL</dt>
<dd>{one.fare_url || '-'}</dd>
<dt>Email</dt>
<dd>{one.email || '-'}</dd>
</dl>
</div>
</div>
<div className="column is-9">
<div className="tabs">
<ul>
<li className={`${agencyChild === undefined && "is-active"}`}><Link to={`/agency/${one.agency_id}`}>Overview</Link></li>
<li className={`${agencyChild === 'route' && "is-active"}`}><Link to={`/agency/${one.agency_id}/route`}>Route</Link></li>
<li className={`${agencyChild === 'fare-attr' && "is-active"}`}><Link to={`/agency/${one.agency_id}/fare-attr`}>Fare Attributes</Link></li>
</ul>
</div>
<Route path={`/agency/:agencyId/route/:routeId`} component={RouteList} />
<Route path={`/agency/:agencyId/route`} component={RouteList} />
<Route path={`/agency/:agencyId/fare-attr/:fareAttrId`} component={FareAttrList} />
<Route path={`/agency/:agencyId/fare-attr`} component={FareAttrList} />
{agencyChild === undefined && <p>Pick options on the tab</p>}
</div>
</div>
{props.children}
</StyledAgencyItem>
)
}
}
const mapStateToProps = state => ({
agency: state.agency,
route: state.route,
})
const connectAgencyList = connect(
mapStateToProps,
)(AgencyItem)
export default connectAgencyList

47
src/components/AgencyList.js

@ -0,0 +1,47 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getAgency } from '../actions'
import store from '../store'
// const StyledAgencyList = styled.section`
// padding: 1em;
// background: papayawhip;
// `;
class AgencyList extends Component {
componentWillMount() {
const { count } = this.props.agency
if (count === 0)
store.dispatch(getAgency())
}
render() {
const { results } = this.props.agency
return (
<ul>
{results && Object.keys(results).map(i => (
<li key={i}>
<Link to={`/agency/${results[i].agency_id}`}>{results[i].agency_id}</Link>
</li>
))}
</ul>
)
}
}
const mapStateToProps = state => ({
agency: state.agency
})
const connectAgencyList = connect(
mapStateToProps,
{},
)(AgencyList)
export default styled(connectAgencyList)`
color: palevioletred;
font-weight: bold;
`

50
src/components/FareAttrList.js

@ -0,0 +1,50 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Spinner from './Spinner'
import { getFareAttr } from '../actions/fareattr'
import store from '../store'
class FareAttrList extends Component {
componentDidMount() {
const { agencyId } = this.props.match.params
store.dispatch(getFareAttr(`agency=${agencyId}`))
}
componentWillReceiveProps(newProps) {
const { agencyId } = newProps.match.params
if (agencyId !== this.props.match.params.agencyId)
store.dispatch(getFareAttr(`agency=${agencyId}`))
}
render() {
const { fareattr } = this.props
return (
<div>
<h3 className="title">Fare Attributes</h3>
<Spinner show={fareattr.fetching} />
<ul>
{fareattr.count === 0 && <li>No fare attribute</li>}
{fareattr.count > 0
&& fareattr.results.map(ele => (
<li key={ele.fare_id}>{ ele.fare_id }</li>)
)}
</ul>
</div>
)
}
}
const mapStateToProps = state => ({
fareattr: state.fareattr,
})
const connectFareAttrList = connect(
mapStateToProps,
)(FareAttrList)
export default connectFareAttrList

13
src/components/Footer.js

@ -0,0 +1,13 @@
import React from 'react'
const Footer = () =>
<footer className="footer">
<div className="content has-text-centered">
<p>
<strong>Go</strong><sup>TH</sup>
</p>
</div>
</footer>
export default Footer

62
src/components/Login.js

@ -0,0 +1,62 @@
import React from 'react'
let invalidEmail = false
let goodPassword = null
const Login = (props) => (
<div>
<h1 className="title">Login</h1>
<div className="field">
<label className="label">Email</label>
<div className="control has-icons-left has-icons-right">
<input className={`input ${invalidEmail && 'is-danger'}`}
type="email"
name="email"
onChange={props.updateField}
placeholder="yourname@example.com" />
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
</span>
{ invalidEmail &&
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
</span>}
</div>
{ invalidEmail &&
<p className="help is-danger">This email is invalid</p> }
</div>
<div className="field">
<label className="label">Password</label>
<div className="control has-icons-left has-icons-right">
<input type="password"
name="password"
className={`input ${goodPassword === true && 'is-success'}`}
onChange={props.updateField}
onKeyPress={(e) => {(e.key === 'Enter') && props.fetchAuth()}}
placeholder="Password" />
<span className="icon is-small is-left">
<i className="fas fa-key"></i>
</span>
{ goodPassword === true &&
<span className="icon is-small is-right">
<i className="fas fa-check"></i>
</span> }
</div>
{ goodPassword === true &&
<p className="help is-success">This password is great</p>}
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-link"
onClick={props.fetchAuth}>
Submit</button>
</div>
{/* <div className="control">
<button className="button is-text">Cancel</button>
</div> */}
</div>
</div>
)
export default Login

96
src/components/Nav.js

@ -0,0 +1,96 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { logout } from '../utils/Auth'
const StyledLink = styled(Link)`
&[data-active] {
color: red;
}
`;
class Nav extends Component {
state = { expandNavbar: false }
render() {
const { props } = this
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">
<StyledLink to="/" className="navbar-item">
Home
</StyledLink>
<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" />
&nbsp; new service calendar
</StyledLink>
</div>
</div>
<div className="navbar-item has-dropdown is-hoverable">
<StyledLink to="/agency" className="navbar-link">
Agency
</StyledLink>
<div className="navbar-dropdown is-boxed">
{props.agency.count > 0
&& props.agency.results.map(ele => (
<StyledLink key={`nav$a${ele.agency_id}`} to={`/agency/${ele.agency_id}`} className="navbar-item">
{ele.name || ele.agency_id}
</StyledLink>
))}
<StyledLink to={`/agency/new`} className="navbar-item">
<i className="fas fa-plus" />
&nbsp;Add new agency
</StyledLink>
</div>
</div>
</div>
<div className="navbar-end">
<div className="navbar-item">
<div className="field is-grouped">
<p className="control">
<a className="button is-primary"
onClick={() => logout()}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Logout</span>
</a>
</p>
</div>
</div>
</div>
</div>
</nav>
)
}
}
const mapStateToProps = state => ({
agency: state.agency,
expandNavbar: false,
})
export default connect(
mapStateToProps,
)(Nav)

49
src/components/RouteList.js

@ -0,0 +1,49 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Spinner from './Spinner'
import { getRoute } from '../actions'
import store from '../store'
class RouteList extends Component {
componentDidMount() {
const { agencyId } = this.props.match.params
store.dispatch(getRoute(`agency=${agencyId}`))
}
componentWillReceiveProps(newProps) {
const { agencyId } = newProps.match.params
if (agencyId !== this.props.match.params.agencyId)
store.dispatch(getRoute(`agency=${agencyId}`))
}
render() {
const { route } = this.props
return (
<div>
<h3 className="title">Routes</h3>
<Spinner show={route.fetching} />
<ul>
{route.count === 0 && <li>No route</li>}
{route.count > 0
&& route.results.map(ele => (
<li key={ele.route_id}>{ ele.route_id }</li>)
)}
</ul>
</div>
)
}
}
const mapStateToProps = state => ({
route: state.route,
})
const connectRouteList = connect(
mapStateToProps,
)(RouteList)
export default connectRouteList

9
src/components/Spinner.js

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

40
src/constants/ActionTypes.js

@ -1,9 +1,45 @@
export const FIRST_INCREMENT_COUNTER = 'FIRST_INCREMENT_COUNTER'
export const FIRST_ASSIGN_ID = 'FIRST_ASSIGN_ID'
export const SET_TOKEN = 'SET_TOKEN'
// export const SET_TOKEN = 'SET_TOKEN'
export const REQUEST_LOGIN = 'REQUEST_LOGIN'
export const SUCCESS_LOGIN = 'SUCCESS_LOGIN'
export const FAILED_LOGIN = 'FAILED_LOGIN'
export const SUCCESS_LOGOUT = 'SUCCESS_LOGOUT'
export const AGENCY_REQUEST = 'AGENCY_REQUEST'
export const AGENCY_FAILURE = 'AGENCY_FAILURE'
export const AGENCY_SUCCESS = 'AGENCY_SUCCESS'
// below items are SUCCESS for other tasks
export const AGENCY_CREATE = 'AGENCY_CREATE'
export const AGENCY_DELETE = 'AGENCY_DELETE'
export const AGENCY_UPDATE = 'AGENCY_UPDATE'
export const ROUTE_REQUEST = 'ROUTE_REQUEST'
export const ROUTE_FAILURE = 'ROUTE_FAILURE'
export const ROUTE_SUCCESS = 'ROUTE_SUCCESS'
// below items are SUCCESS for other tasks
export const ROUTE_CREATE = 'ROUTE_CREATE'
export const ROUTE_DELETE = 'ROUTE_DELETE'
export const ROUTE_UPDATE = 'ROUTE_UPDATE'
export const CALENDAR_REQUEST = 'CALENDAR_REQUEST'
export const CALENDAR_FAILURE = 'CALENDAR_FAILURE'
export const CALENDAR_SUCCESS = 'CALENDAR_SUCCESS'
// below items are SUCCESS for other tasks
export const CALENDAR_CREATE = 'CALENDAR_CREATE'
export const CALENDAR_DELETE = 'CALENDAR_DELETE'
export const CALENDAR_UPDATE = 'CALENDAR_UPDATE'
export const FAREATTR_REQUEST = 'FAREATTR_REQUEST'
export const FAREATTR_FAILURE = 'FAREATTR_FAILURE'
export const FAREATTR_SUCCESS = 'FAREATTR_SUCCESS'
// below items are SUCCESS for other tasks
export const FAREATTR_CREATE = 'FAREATTR_CREATE'
export const FAREATTR_DELETE = 'FAREATTR_DELETE'
export const FAREATTR_UPDATE = 'FAREATTR_UPDATE'

4
src/constants/Api.js

@ -1,3 +1,5 @@
export const URL = process.env.API_URL || 'https://showtimes.everyday.in.th'
export const URL = process.env.API_URL || '//localhost:8000'
export const API_PREFIX = process.env.API_PREFIX || '/v1'
export const API_URL = `${URL}${API_PREFIX}`
export const LOGIN = '/api-token-auth/'

2
src/constants/path.js

@ -0,0 +1,2 @@
export const LOGIN_PATH = '/login'

30
src/container/Gone.js

@ -1,30 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { getToken, loggedIn } from '../reducers/auth'
import { fetchAuth } from '../actions'
const Gone = (props) => (
<div>
Gone { props.match.params.id }
<hr />
<Link to={`/`}>Main</Link>
<hr />
{props.loggedIn ? 'Logged in': 'Nah anonymous'}
<hr />
<button onClick={_ => props.fetchAuth("sipp11", "gi23ft")}>Login</button>
</div>
)
const mapStateToProps = state => ({
token: getToken(state.auth),
auth: state.auth,
})
export default connect(
mapStateToProps,
{ loggedIn, fetchAuth }
)(Gone)

69
src/container/Main.js

@ -1,25 +1,64 @@
import React from 'react'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { Redirect, Route, Switch } from 'react-router-dom'
import { getCounter } from '../reducers/first'
import { incFirstCounter } from '../actions'
import { loggedIn } from '../reducers/auth'
import { getAgency } from '../actions'
const Main = (props) => (
<div>
Main
import Nav from '../components/Nav'
import AgencyList from '../components/AgencyList'
import AgencyItem from '../components/AgencyItem'
import AgencyForm from '../components/AgencyForm'
import { LOGIN_PATH } from '../constants/path'
import store from '../store'
<Link to={`/roster/12`}>12</Link>
<p>Counter: { props.counter }</p>
<p><button onClick={() => props.incFirstCounter() }>Increment</button></p>
</div>
)
class Main extends Component {
componentWillMount() {
const { count } = this.props.agency
if (count === 0)
store.dispatch(getAgency())
}
// componentWillReceiveProps(nextProps) {
// const oldPathname = this.props.location.pathname
// const { pathname } = nextProps.location
// console.log(oldPathname, pathname)
// }
render() {
const { match, loggedIn } = this.props
if (loggedIn !== true) {
return <Redirect to={LOGIN_PATH} />
}
return (
<div>
<Nav />
<div className="container is-widescreen">
<Switch>
<Route exact path={`${match.url}agency/new`} component={AgencyForm} />
<Route exact path={`${match.url}agency/:agencyId`} component={AgencyItem} />
<Route path={`${match.url}agency/:agencyId/edit`} component={AgencyForm} />
<Route path={`${match.url}agency/:agencyId/:agencyChild`} component={AgencyItem} />
<Route exact path={`${match.url}agency`} component={AgencyList} />
<Route exact path={`${match.url}calendar/new`} component={AgencyForm} />
<Route exact path={`${match.url}calendar/:serviceId`} component={AgencyForm} />
<Route path={`${match.url}calendar/:serviceId/edit`} component={AgencyForm} />
<Route exact path={`${match.url}calendar`} component={AgencyList} />
</Switch>
</div>
</div>
)
}
}
const mapStateToProps = state => ({
counter: getCounter(state.first)
loggedIn: loggedIn(state.auth),
agency: state.agency,
})
export default connect(
mapStateToProps,
{ incFirstCounter }
)(Main)
{ }
)(Main)

81
src/container/Public.js

@ -0,0 +1,81 @@
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
import { getToken, loggedIn } from '../reducers/auth'
import { fetchAuth } from '../actions'
import Login from '../components/Login'
class Public extends Component {
state = {
email: null, password: null
}
constructor() {
super()
this.updateField = this.updateField.bind(this)
this.handleFetchAuth = this.handleFetchAuth.bind(this)
}
updateField(evt) {
const { name, value } = evt.target
let newState = {}
newState[name] = value
this.setState(newState)
}
handleFetchAuth() {
const { dispatch } = this.props
const { email, password } = this.state
dispatch(fetchAuth(email, password))
}
render() {
const { loggedIn } = this.props
return (
<div style={{minHeight: '100vh'}}
className="columns is-mobile is-centered is-desktop is-vcentered">
<div className="column is-centered is-12-mobile is-two-thirds-tablet
is-half-desktop is-one-third-widescreen is-one-quarter-fullhd">
<div className="card">
<div className="card-content">
{/* {<h1 className="title">Public</h1> { props.match.params.id }
<Link to={`/`}>Main</Link>
<hr />} */}
{loggedIn === true
? <Redirect to={'/'} />
: <Login
fetchAuth={this.handleFetchAuth}
updateField={this.updateField} /> }
{/* <hr /> */}
</div>
{/* <footer className="card-footer">
<p className="card-footer-item">
<span>
<button className="button is-info"
onClick={_ => props.fetchAuth("sipp11", "gi23ft")}>
Login</button>
</span>
</p>
</footer> */}
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => ({
token: getToken(state.auth),
loggedIn: loggedIn(state.auth),
})
const mapDispatchToProps = (dispatch) => {
return {
dispatch,
fetchAuth,
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Public)

5
src/index.js

@ -10,6 +10,8 @@ import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react'
import store from './store'
import { registerObserver } from 'react-perf-devtool'
const persistor = persistStore(store)
@ -24,4 +26,7 @@ ReactDOM.render((
), document.getElementById('root'))
// ReactDOM.render(<App />, document.getElementById('root'))
if (process.env.NODE_ENV !== 'production') {
registerObserver()
}
registerServiceWorker()

76
src/reducers/agency.js

@ -0,0 +1,76 @@
import {
AGENCY_CREATE, AGENCY_DELETE, AGENCY_UPDATE,
AGENCY_REQUEST, AGENCY_SUCCESS, AGENCY_FAILURE,
} from '../constants/ActionTypes'
const agencyInitState = {
results: [],
next: null,
count: 0,
fetching: false,
}
const agency = (state = agencyInitState, action) => {
switch(action.type) {
case AGENCY_REQUEST:
return {
...state,
fetching: true,
}
case AGENCY_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...(prev ? state.results : []),
...results,
]
}
case AGENCY_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 AGENCY_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case AGENCY_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 AGENCY_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default agency

10
src/reducers/auth.js

@ -1,5 +1,5 @@
import {
SET_TOKEN, REQUEST_LOGIN, SUCCESS_LOGIN, FAILED_LOGIN,
REQUEST_LOGIN, SUCCESS_LOGIN, FAILED_LOGIN, SUCCESS_LOGOUT
} from '../constants/ActionTypes'
@ -7,7 +7,7 @@ const tokenInitialState = {
token: null,
fetching: false,
}
const token = (state = tokenInitialState, action) => {
const auth = (state = tokenInitialState, action) => {
switch(action.type) {
case REQUEST_LOGIN:
return {
@ -24,9 +24,9 @@ const token = (state = tokenInitialState, action) => {
token: null,
fetching: false,
}
case SET_TOKEN:
case SUCCESS_LOGOUT:
return {
token: action.token,
token: null,
fetching: false,
};
default:
@ -34,7 +34,7 @@ const token = (state = tokenInitialState, action) => {
}
}
export default token
export default auth
export const getToken = state => state.token
export const loggedIn = state => state && state.token != null

79
src/reducers/calendar.js

@ -0,0 +1,79 @@
import {
CALENDAR_CREATE, CALENDAR_DELETE, CALENDAR_UPDATE,
CALENDAR_REQUEST, CALENDAR_SUCCESS, CALENDAR_FAILURE,
} from '../constants/ActionTypes'
const calendarInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const calendar = (state = calendarInitState, action) => {
switch(action.type) {
case CALENDAR_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
}
case CALENDAR_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...( (prev) ? state.results : [] ),
...results,
]
}
case CALENDAR_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 CALENDAR_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case CALENDAR_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 CALENDAR_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default calendar

79
src/reducers/fareattr.js

@ -0,0 +1,79 @@
import {
CALENDAR_CREATE, CALENDAR_DELETE, CALENDAR_UPDATE,
CALENDAR_REQUEST, CALENDAR_SUCCESS, CALENDAR_FAILURE,
} from '../constants/ActionTypes'
const calendarInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const calendar = (state = calendarInitState, action) => {
switch(action.type) {
case CALENDAR_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
}
case CALENDAR_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...( (prev) ? state.results : [] ),
...results,
]
}
case CALENDAR_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 CALENDAR_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case CALENDAR_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 CALENDAR_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default calendar

6
src/reducers/index.js

@ -1,8 +1,14 @@
import { combineReducers } from 'redux'
import first from './first'
import auth from './auth'
import agency from './agency'
import route from './route'
import fareattr from './fareattr'
export default combineReducers({
auth,
first,
agency,
route,
fareattr,
})

79
src/reducers/route.js

@ -0,0 +1,79 @@
import {
ROUTE_CREATE, ROUTE_DELETE, ROUTE_UPDATE,
ROUTE_REQUEST, ROUTE_SUCCESS, ROUTE_FAILURE,
} from '../constants/ActionTypes'
const routeInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const route = (state = routeInitState, action) => {
switch(action.type) {
case ROUTE_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
}
case ROUTE_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...( (prev) ? state.results : [] ),
...results,
]
}
case ROUTE_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 ROUTE_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case ROUTE_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 ROUTE_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default route

5
src/store.js

@ -1,5 +1,6 @@
import thunk from 'redux-thunk'
import { createStore, applyMiddleware, compose } from 'redux'
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
@ -9,10 +10,10 @@ import gruntApp from './reducers'
const persistConfig = {
key: 'root',
storage,
blacklist: ['blah']
blacklist: ['agency', 'route']
}
const middleware = [ thunk ]
const middleware = [ thunk, apiMiddleware ]
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger())
}

22
src/utils/ApiClient.js

@ -1,12 +1,20 @@
import axios from 'axios'
import store from '../store'
import { URL } from '../config/Api'
import { URL, API_PREFIX } from '../constants/Api'
export const apiClient = function() {
const token = store.getState().token
export const apiClient = function(url) {
const token = store.getState().auth.token
const params = {
baseURL: URL,
headers: {'Authorization': 'Token ' + token}
baseURL: `${URL}${API_PREFIX}${url}`,
headers: {'Authorization': 'Bearer ' + token}
}
return axios.create(params)
}
return axios.get(params)
}
export const RSAAHeaders = () => {
const token = store.getState().auth.token
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
}
}

6
src/utils/Auth.js

@ -1,7 +1,7 @@
import axios from 'axios'
import _ from 'lodash'
import store from '../store'
import { failAuth, successAuth } from '../actions'
import { failAuth, successAuth, successSignout } from '../actions'
import { URL, LOGIN } from '../constants/Api'
export function InvalidCredentialsException(message) {
@ -27,3 +27,7 @@ export function login(username, password) {
throw error
})
}
export function logout() {
return store.dispatch(successSignout())
}

Loading…
Cancel
Save