Browse Source

Service CRUD

* Add message when failing to auth
master
sipp11 6 years ago
parent
commit
1b201a9101
  1. 1
      package.json
  2. 7
      public/index.html
  3. 7
      src/actions/calendar.js
  4. 21
      src/components/AgencyForm.js
  5. 7
      src/components/AgencyItem.js
  6. 77
      src/components/AgencyList.js
  7. 217
      src/components/CalendarForm.js
  8. 112
      src/components/CalendarList.js
  9. 2
      src/components/FareAttrList.js
  10. 5
      src/components/Footer.js
  11. 1
      src/components/Login.js
  12. 30
      src/components/parts/HorizontalCheckbox.js
  13. 30
      src/components/parts/HorizontalDate.js
  14. 21
      src/components/parts/HorizontalInput.js
  15. 10
      src/container/Main.js
  16. 4
      src/container/Public.js
  17. 7
      src/reducers/auth.js
  18. 2
      src/reducers/calendar.js
  19. 22
      src/reducers/fareattr.js
  20. 2
      src/reducers/index.js
  21. 2
      src/store.js

1
package.json

@ -8,6 +8,7 @@
"moment": "^2.22.2",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-flatpickr": "^3.6.4",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4",

7
public/index.html

@ -8,9 +8,10 @@
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://static.10ninox.com/css/bulma.min.css">
<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" />
<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>
<!--
Notice the use of %PUBLIC_URL% in the tags above.

7
src/actions/calendar.js

@ -10,12 +10,9 @@ export const getCalendar = (query) => ({
endpoint: `${API_URL}/calendar/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.calendar.fetching || state.calendar.query === query,
bailout: (state) => state.calendar.fetching,
types: [
{
type: types.CALENDAR_REQUEST,
meta: { query: query },
},
types.CALENDAR_REQUEST,
types.CALENDAR_SUCCESS,
types.CALENDAR_FAILURE,
]

21
src/components/AgencyForm.js

@ -3,31 +3,14 @@ import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import HorizontalInput from './parts/HorizontalInput'
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 {

7
src/components/AgencyItem.js

@ -31,9 +31,7 @@ class AgencyItem extends Component {
return (
<StyledAgencyItem>
<h1 className="title">
{one.name}&nbsp;&nbsp;
<Link to={`/agency/${one.agency_id}/edit`} className="button">Edit</Link></h1>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="columns">
<div className="column is-3 is-hidden-mobile">
<div className="content">
@ -56,11 +54,12 @@ class AgencyItem extends Component {
</div>
</div>
<div className="column is-9">
<div className="tabs">
<div className="tabs is-centered">
<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>
<li><Link to={`/agency/${one.agency_id}/edit`}>Edit</Link></li>
</ul>
</div>
<Route path={`/agency/:agencyId/route/:routeId`} component={RouteList} />

77
src/components/AgencyList.js

@ -6,10 +6,18 @@ import { Link } from 'react-router-dom'
import { getAgency } from '../actions'
import store from '../store'
// const StyledAgencyList = styled.section`
// padding: 1em;
// background: papayawhip;
// `;
const StyledBox = styled.div`
padding: 1rem;
background: #fafafa;
`
const FakeRow = styled.nav`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
class AgencyList extends Component {
@ -22,13 +30,60 @@ class AgencyList extends Component {
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>
<StyledBox>
<h1 className="title">Agency</h1>
<div className="columns">
<div className="column is-12">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`/agency/new`}>
<i className="fas fa-plus" /> New agency
</Link>
</p>
</nav>
{results && Object.keys(results).map(i => (
<FakeRow className="level panel" key={i}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Agency ID</p>
<p className="title">
<Link to={`/agency/${results[i].agency_id}`}>
{results[i].agency_id}</Link></p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Contact</p>
<p className="title">{results[i].url
&& <a href={results[i].url}>web</a>}
{(results[i].url && results[i].email) && <span> / </span>}
{results[i].email
&& <a href={`mailto:${results[i].email}`}>email</a>}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Language</p>
<p className="title">{results[i].lang || '-'}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Timezone</p>
<p className="title">{results[i].timezone || '-'}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Phone</p>
<p className="title">{results[i].phone || '-'}</p>
</div>
</div>
</FakeRow>
))}
</div>
</div>
</StyledBox>
)
}
}

217
src/components/CalendarForm.js

@ -0,0 +1,217 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import { updateCalendar, createCalendar, deleteCalendar } from '../actions/calendar'
import store from '../store'
import HorizontalInput from './parts/HorizontalInput'
import HorizontalDate from './parts/HorizontalDate'
import HorizontalCheckbox from './parts/HorizontalCheckbox'
const StyledCalendarForm = styled.div`
padding: 1rem;
background: #fafafa;
`;
class CalendarForm extends Component {
state = {
id: null,
service_id: "",
start_date: "",
end_date: "",
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
sunday: 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)
}
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(updateCalendar(id, body))
} else {
store.dispatch(createCalendar(body))
}
this.setState({justSubmit: true})
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteCalendar(id))
this.setState({justSubmit: true})
}
componentWillMount() {
const { props } = this
const { serviceId } = props.match.params
const { results } = props.calendar
const ones = results.filter(ele => ele.service_id === serviceId)
if (ones.length > 0) {
this.setState(ones[0])
}
}
renderForm() {
const one = this.state
const { fetching } = this.props.calendar
return (
<StyledCalendarForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<HorizontalInput
label="Calendar ID"
type="text"
fieldName="service_id"
value={one.service_id || ''}
handleChange={this.handleChange} />
<HorizontalDate
label="Start Date"
type="text"
fieldName="start_date"
value={one.start_date || ''}
handleChange={this.handleChange} />
<HorizontalDate
key="end_date"
label="End Date"
type="text"
fieldName="end_date"
value={one.end_date || ''}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Monday"
label="Monday"
type="text"
fieldName="monday"
value={one.monday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Tuesday"
label="Tuesday"
type="text"
fieldName="tuesday"
value={one.tuesday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Wednesday"
label="Wednesday"
type="text"
fieldName="wednesday"
value={one.wednesday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Thursday"
label="Thursday"
type="text"
fieldName="thursday"
value={one.thursday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Friday"
label="Friday"
type="text"
fieldName="friday"
value={one.friday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Saturday"
label="Saturday"
type="text"
fieldName="saturday"
value={one.saturday || false}
handleChange={this.handleChange} />
<HorizontalCheckbox
key="Sunday"
label="Sunday"
type="text"
fieldName="sunday"
value={one.sunday || false}
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={`/calendar`} className="button is-text">Cancel</Link>
</div>
</div>
</StyledCalendarForm>
)
}
render () {
const one = this.state
const { fetching } = this.props.calendar
// redirect to view page if no data
const { serviceId } = this.props.match.params
// this is a create form
if (serviceId === undefined) {
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/calendar`} />
}
return this.renderForm()
}
if (one.id === null && serviceId.length > 0)
return <Redirect to={`/calendar`} />
// redirect to calendar list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/calendar`} />
}
return this.renderForm()
}
}
const mapStateToProps = state => ({
calendar: state.calendar
})
const connectCalendarFom = connect(
mapStateToProps,
)(CalendarForm)
export default connectCalendarFom

112
src/components/CalendarList.js

@ -0,0 +1,112 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getCalendar } from '../actions/calendar'
import store from '../store'
const StyledBox = styled.div`
padding: 1rem;
background: #fafafa;
`
const GapBox = styled.span`
margin-right: 5px;
color: ${props => props.checked ? 'green' : 'gray'};;
`
const FakeRow = styled.nav`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
const CheckboxIcon = (props) => (
<GapBox checked={props.checked}>
{props.checked === true && <span><i className="fas fa-check-square"></i></span>}
{props.checked !== true && <span><i className="fas fa-square"></i></span>}
</GapBox>
)
class CalendarList extends Component {
componentWillMount() {
const { count } = this.props.calendar
if (count === 0)
store.dispatch(getCalendar())
}
render() {
const { results } = this.props.calendar
const { match } = this.props
return (
<StyledBox>
<h1 className="title">Service</h1>
<div className="columns">
<div className="column is-12">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/new`}>
<i className="fas fa-plus" /> New service
</Link>
</p>
</nav>
{results && Object.keys(results).map(i => (
<FakeRow className="level panel" key={i}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Service ID</p>
<p className="title"><Link to={`${match.url}/${results[i].service_id}`}>{results[i].service_id}</Link></p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Start</p>
<p className="title">{results[i].start_date}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">End</p>
<p className="title">{results[i].end_date}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">M T W Th F Sa Su</p>
<p className="title">
<CheckboxIcon disabled={true} checked={results[i].monday} />
<CheckboxIcon disabled={true} checked={results[i].tuesday} />
<CheckboxIcon disabled={true} checked={results[i].wednesday} />
<CheckboxIcon disabled={true} checked={results[i].thursday} />
<CheckboxIcon disabled={true} checked={results[i].friday} />
<CheckboxIcon disabled={true} checked={results[i].saturday} />
<CheckboxIcon disabled={true} checked={results[i].sunday} />
</p>
</div>
</div>
</FakeRow>
))}
</div>
</div>
</StyledBox>
)
}
}
const mapStateToProps = state => ({
calendar: state.calendar
})
const connectCalendarList = connect(
mapStateToProps,
{},
)(CalendarList)
export default styled(connectCalendarList)`
color: palevioletred;
font-weight: bold;
`

2
src/components/FareAttrList.js

@ -28,7 +28,7 @@ class FareAttrList extends Component {
<h3 className="title">Fare Attributes</h3>
<Spinner show={fareattr.fetching} />
<ul>
{fareattr.count === 0 && <li>No fare attribute</li>}
{fareattr.count === 0 && <li key="nah">No fare attribute</li>}
{fareattr.count > 0
&& fareattr.results.map(ele => (
<li key={ele.fare_id}>{ ele.fare_id }</li>)

5
src/components/Footer.js

@ -3,9 +3,8 @@ import React from 'react'
const Footer = () =>
<footer className="footer">
<div className="content has-text-centered">
<p>
<strong>Go</strong><sup>TH</sup>
</p>
<img src="https://static.10ninox.com/goth-rect-640x160.svg" alt="GoTH" width="168" height="42" />
{/* <strong>Go</strong><sup>TH</sup> */}
</div>
</footer>

1
src/components/Login.js

@ -6,6 +6,7 @@ let goodPassword = null
const Login = (props) => (
<div>
<h1 className="title">Login</h1>
{props.failMessage && <div className="notification is-warning">{props.failMessage}</div>}
<div className="field">
<label className="label">Email</label>
<div className="control has-icons-left has-icons-right">

30
src/components/parts/HorizontalCheckbox.js

@ -0,0 +1,30 @@
import React from 'react'
const HorizontalInput = (props) => (
<div className="field is-horizontal">
<div className="field-label is-normal">
{/* <label for={`id_${props.fieldName}`} className="label">{props.label}</label> */}
</div>
<div className="field-body">
<div className="field">
<input
className="is-checkradio has-background-color is-info"
id={`id_${props.fieldName}`}
type="checkbox"
name={props.fieldName}
defaultChecked={props.value}
onChange={(evt) => {
let res = {
target: {
name: evt.target.name,
value: evt.target.checked
}
}
return props.handleChange(res)}} />
<label htmlFor={`id_${props.fieldName}`}>{props.label}</label>
</div>
</div>
</div>
)
export default HorizontalInput

30
src/components/parts/HorizontalDate.js

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

21
src/components/parts/HorizontalInput.js

@ -0,0 +1,21 @@
import React from 'react'
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>
)
export default HorizontalInput

10
src/container/Main.js

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

4
src/container/Public.js

@ -31,7 +31,7 @@ class Public extends Component {
}
render() {
const { loggedIn } = this.props
const { loggedIn, failMessage } = this.props
return (
<div style={{minHeight: '100vh'}}
className="columns is-mobile is-centered is-desktop is-vcentered">
@ -45,6 +45,7 @@ class Public extends Component {
{loggedIn === true
? <Redirect to={'/'} />
: <Login
failMessage={failMessage}
fetchAuth={this.handleFetchAuth}
updateField={this.updateField} /> }
{/* <hr /> */}
@ -66,6 +67,7 @@ class Public extends Component {
}
const mapStateToProps = state => ({
failMessage: state.auth.msg,
token: getToken(state.auth),
loggedIn: loggedIn(state.auth),
})

7
src/reducers/auth.js

@ -6,6 +6,7 @@ import {
const tokenInitialState = {
token: null,
fetching: false,
msg: '',
}
const auth = (state = tokenInitialState, action) => {
switch(action.type) {
@ -13,21 +14,27 @@ const auth = (state = tokenInitialState, action) => {
return {
token: null,
fetching: true,
msg: ''
}
case SUCCESS_LOGIN:
return {
token: action.body.token,
fetching: false,
msg: '',
}
case FAILED_LOGIN:
const { data } = action.body.response
const msg = Object.keys(data).map(ele => data[ele]).join(',')
return {
token: null,
fetching: false,
msg
}
case SUCCESS_LOGOUT:
return {
token: null,
fetching: false,
msg: '',
};
default:
return state;

2
src/reducers/calendar.js

@ -14,11 +14,9 @@ const calendarInitState = {
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

22
src/reducers/fareattr.js

@ -1,26 +1,26 @@
import {
CALENDAR_CREATE, CALENDAR_DELETE, CALENDAR_UPDATE,
CALENDAR_REQUEST, CALENDAR_SUCCESS, CALENDAR_FAILURE,
FAREATTR_CREATE, FAREATTR_DELETE, FAREATTR_UPDATE,
FAREATTR_REQUEST, FAREATTR_SUCCESS, FAREATTR_FAILURE,
} from '../constants/ActionTypes'
const calendarInitState = {
const fareAttrInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const calendar = (state = calendarInitState, action) => {
const fareAttr = (state = fareAttrInitState, action) => {
switch(action.type) {
case CALENDAR_REQUEST:
case FAREATTR_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
}
case CALENDAR_SUCCESS:
case FAREATTR_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
@ -32,7 +32,7 @@ const calendar = (state = calendarInitState, action) => {
...results,
]
}
case CALENDAR_UPDATE:
case FAREATTR_UPDATE:
const { id } = action.payload
const oldResults = state.results
const targetInd = oldResults.findIndex(ele => ele.id === id)
@ -45,7 +45,7 @@ const calendar = (state = calendarInitState, action) => {
...oldResults.slice(targetInd + 1)
]
}
case CALENDAR_CREATE:
case FAREATTR_CREATE:
return {
...state,
fetching: false,
@ -55,7 +55,7 @@ const calendar = (state = calendarInitState, action) => {
action.payload,
]
}
case CALENDAR_DELETE:
case FAREATTR_DELETE:
const deleteInd = state.results.findIndex(ele => ele.id === action.meta.id)
return {
...state,
@ -66,7 +66,7 @@ const calendar = (state = calendarInitState, action) => {
...state.results.slice(deleteInd + 1)
]
}
case CALENDAR_FAILURE:
case FAREATTR_FAILURE:
return {
...state,
fetching: false,
@ -76,4 +76,4 @@ const calendar = (state = calendarInitState, action) => {
}
}
export default calendar
export default fareAttr

2
src/reducers/index.js

@ -4,6 +4,7 @@ import auth from './auth'
import agency from './agency'
import route from './route'
import fareattr from './fareattr'
import calendar from './calendar'
export default combineReducers({
auth,
@ -11,4 +12,5 @@ export default combineReducers({
agency,
route,
fareattr,
calendar,
})

2
src/store.js

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

Loading…
Cancel
Save