├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── actions ├── __test__ │ ├── addVaccination.js │ ├── addVaccinationFailure.js │ ├── addVaccinationSuccess.js │ ├── fetchVaccinations.js │ ├── fetchVaccinationsFailure.js │ ├── fetchVaccinationsSuccess.js │ ├── pickVaccine.js │ ├── switchToChooseVaccineRoute.js │ ├── switchToDetailRoute.js │ └── switchToListRoute.js ├── addVaccination.js ├── addVaccinationFailure.js ├── addVaccinationSuccess.js ├── fetchVaccinations.js ├── fetchVaccinationsFailure.js ├── fetchVaccinationsSuccess.js ├── pickVaccine.js ├── switchToChooseVaccineRoute.js ├── switchToDetailRoute.js └── switchToListRoute.js ├── components ├── Button │ ├── index.js │ └── styles.js ├── Card │ ├── index.js │ └── styles.js ├── ChooseDate │ ├── DateInput │ │ ├── index.android.js │ │ ├── index.ios.js │ │ └── styles.js │ ├── index.js │ └── styles.js ├── ChooseVaccine │ ├── index.js │ └── styles.js ├── Detail │ ├── index.js │ └── styles.js ├── List │ ├── index.js │ └── styles.js └── Logo │ ├── icon.js │ └── index.js ├── constants ├── actions.js ├── storage.js └── vaccines.js ├── containers ├── ChooseDate.js ├── ChooseVaccine.js ├── Detail.js └── List.js ├── exp.json ├── main.js ├── package.json ├── reducers ├── __test__ │ ├── addForm.js │ ├── currentVaccination.js │ └── vaccinations.js ├── activeRoute.js ├── addForm.js ├── currentVaccination.js ├── index.js └── vaccinations.js ├── sagas ├── __test__ │ ├── fetchVaccinations.js │ ├── index.js │ ├── saveVaccinations.js │ └── startup.js ├── fetchVaccinations.js ├── index.js ├── saveVaccinations.js └── startup.js ├── selectors ├── __test__ │ ├── addForm.js │ ├── currentVaccination.js │ ├── vaccinations.js │ └── vaccines.js ├── addForm.js ├── currentVaccination.js ├── vaccinations.js └── vaccines.js ├── store └── configureStore.js └── testHelper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-native-stage-0/decorator-support" 4 | ], 5 | "env": { 6 | "development": { 7 | "plugins": [ 8 | "transform-react-jsx-source" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "comma-dangle": [2, "always-multiline"], 11 | "react/prop-types": 0, 12 | "react/jsx-no-bind": 0, 13 | "react/jsx-filename-extension": 0, 14 | "new-cap": 0, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | .exponent/* 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | sudo: false 5 | script: 6 | - npm run lint 7 | - npm test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nik Graf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carte Jaune 2 | 3 | A Redux/ExponentJS (React Native) app to keep track of your vaccinations. 4 | 5 | [![Build Status](https://travis-ci.org/nikgraf/CarteJaune.svg?branch=master)](https://travis-ci.org/nikgraf/CarteJaune) 6 | 7 | ## Install 8 | 9 | `npm Install` 10 | 11 | ## Run 12 | 13 | see ExponentJS tutorial on https://exponentjs.com/ 14 | 15 | screen shot 2016-01-26 at 08 35 38 16 | 17 | ## Video Demo 18 | 19 | [![Carte Jaune](http://img.youtube.com/vi/XaiVZ4RkZ6M/maxresdefault.jpg)](http://www.youtube.com/watch?v=XaiVZ4RkZ6M) 20 | 21 | ## Tests 22 | 23 | All of the business logics is well tested: 24 | - [Action creator tests](https://github.com/nikgraf/CarteJaune/tree/master/actions/__test__) 25 | - [Reducer tests](https://github.com/nikgraf/CarteJaune/tree/master/reducers/__test__) 26 | - [Selector tests](https://github.com/nikgraf/CarteJaune/tree/master/selectors/__test__) 27 | - [Saga tests](https://github.com/nikgraf/CarteJaune/tree/master/sagas/__test__) 28 | 29 | ## Support 30 | 31 | I built this application for the ExponentJS contest to win a React Conference Ticket. If you want to support me please download the ExponentJs Application and vote in the contest for my application. 32 | 33 | - https://itunes.com/apps/exponent 34 | - https://play.google.com/store/apps/details?id=host.exp.exponent 35 | -------------------------------------------------------------------------------- /actions/__test__/addVaccination.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import addVaccination from '../addVaccination'; 3 | import { ADD_VACCINATION } from '../../constants/actions'; 4 | 5 | describe('addVaccination', () => { 6 | it('returns ADD_VACCINATION action', () => { 7 | const expected = { 8 | type: ADD_VACCINATION, 9 | vaccineId: 2, 10 | vaccinationDate: 3, 11 | }; 12 | 13 | expect(addVaccination(2, 3)).to.deep.equal(expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /actions/__test__/addVaccinationFailure.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import addVaccinationFailure from '../addVaccinationFailure'; 3 | import { ADD_VACCINATION_FAILURE } from '../../constants/actions'; 4 | 5 | describe('addVaccinationFailure', () => { 6 | it('returns ADD_VACCINATION_FAILURE action', () => { 7 | const expected = { 8 | type: ADD_VACCINATION_FAILURE, 9 | }; 10 | 11 | expect(addVaccinationFailure()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/__test__/addVaccinationSuccess.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import addVaccinationSuccess from '../addVaccinationSuccess'; 3 | import { ADD_VACCINATION_SUCCESS } from '../../constants/actions'; 4 | 5 | describe('addVaccinationSuccess', () => { 6 | it('returns ADD_VACCINATION_SUCCESS action', () => { 7 | const expected = { 8 | type: ADD_VACCINATION_SUCCESS, 9 | }; 10 | 11 | expect(addVaccinationSuccess()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/__test__/fetchVaccinations.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import fetchVaccinations from '../fetchVaccinations'; 3 | import { FETCH_VACCINATIONS } from '../../constants/actions'; 4 | 5 | describe('fetchVaccinations', () => { 6 | it('returns FETCH_VACCINATIONS action', () => { 7 | const expected = { 8 | type: FETCH_VACCINATIONS, 9 | }; 10 | 11 | expect(fetchVaccinations()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/__test__/fetchVaccinationsFailure.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import fetchVaccinationsFailure from '../fetchVaccinationsFailure'; 3 | import { FETCH_VACCINATIONS_FAILURE } from '../../constants/actions'; 4 | 5 | describe('fetchVaccinationsFailure', () => { 6 | it('returns FETCH_VACCINATIONS_FAILURE action', () => { 7 | const expected = { 8 | type: FETCH_VACCINATIONS_FAILURE, 9 | }; 10 | 11 | expect(fetchVaccinationsFailure()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/__test__/fetchVaccinationsSuccess.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import fetchVaccinationsSuccess from '../fetchVaccinationsSuccess'; 3 | import { FETCH_VACCINATIONS_SUCCESS } from '../../constants/actions'; 4 | 5 | describe('addVaccination', () => { 6 | it('returns FETCH_VACCINATIONS_SUCCESS action', () => { 7 | const expected = { 8 | type: FETCH_VACCINATIONS_SUCCESS, 9 | vaccinations: [1, 2], 10 | }; 11 | 12 | expect(fetchVaccinationsSuccess([1, 2])).to.deep.equal(expected); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /actions/__test__/pickVaccine.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import pickVaccine from '../pickVaccine'; 3 | import { PICK_VACCINE } from '../../constants/actions'; 4 | 5 | describe('pickVaccine', () => { 6 | it('returns PICK_VACCINE action', () => { 7 | const expected = { 8 | type: PICK_VACCINE, 9 | vaccineId: 2, 10 | }; 11 | 12 | expect(pickVaccine(2)).to.deep.equal(expected); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /actions/__test__/switchToChooseVaccineRoute.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import switchToChooseVaccineRoute from '../switchToChooseVaccineRoute'; 3 | import { SWITCH_TO_CHOOSE_VACCINE_ROUTE } from '../../constants/actions'; 4 | 5 | describe('switchToChooseVaccineRoute', () => { 6 | it('returns SWITCH_TO_CHOOSE_VACCINE_ROUTE action', () => { 7 | const expected = { 8 | type: SWITCH_TO_CHOOSE_VACCINE_ROUTE, 9 | }; 10 | 11 | expect(switchToChooseVaccineRoute()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/__test__/switchToDetailRoute.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import switchToDetailRoute from '../switchToDetailRoute'; 3 | import { SWITCH_TO_DETAIL_ROUTE } from '../../constants/actions'; 4 | 5 | describe('switchToDetailRoute', () => { 6 | it('returns SWITCH_TO_DETAIL_ROUTE action', () => { 7 | const expected = { 8 | type: SWITCH_TO_DETAIL_ROUTE, 9 | id: 'abc', 10 | }; 11 | 12 | expect(switchToDetailRoute('abc')).to.deep.equal(expected); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /actions/__test__/switchToListRoute.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import switchToListRoute from '../switchToListRoute'; 3 | import { SWITCH_TO_LIST_ROUTE } from '../../constants/actions'; 4 | 5 | describe('switchToListRoute', () => { 6 | it('returns SWITCH_TO_LIST_ROUTE action', () => { 7 | const expected = { 8 | type: SWITCH_TO_LIST_ROUTE, 9 | }; 10 | 11 | expect(switchToListRoute()).to.deep.equal(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /actions/addVaccination.js: -------------------------------------------------------------------------------- 1 | import { ADD_VACCINATION } from '../constants/actions'; 2 | 3 | export default (vaccineId, vaccinationDate) => ({ 4 | type: ADD_VACCINATION, 5 | vaccineId, 6 | vaccinationDate, 7 | }); 8 | -------------------------------------------------------------------------------- /actions/addVaccinationFailure.js: -------------------------------------------------------------------------------- 1 | import { ADD_VACCINATION_FAILURE } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: ADD_VACCINATION_FAILURE, 5 | }); 6 | -------------------------------------------------------------------------------- /actions/addVaccinationSuccess.js: -------------------------------------------------------------------------------- 1 | import { ADD_VACCINATION_SUCCESS } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: ADD_VACCINATION_SUCCESS, 5 | }); 6 | -------------------------------------------------------------------------------- /actions/fetchVaccinations.js: -------------------------------------------------------------------------------- 1 | import { FETCH_VACCINATIONS } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: FETCH_VACCINATIONS, 5 | }); 6 | -------------------------------------------------------------------------------- /actions/fetchVaccinationsFailure.js: -------------------------------------------------------------------------------- 1 | import { FETCH_VACCINATIONS_FAILURE } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: FETCH_VACCINATIONS_FAILURE, 5 | }); 6 | -------------------------------------------------------------------------------- /actions/fetchVaccinationsSuccess.js: -------------------------------------------------------------------------------- 1 | import { FETCH_VACCINATIONS_SUCCESS } from '../constants/actions'; 2 | 3 | export default (vaccinations) => ({ 4 | type: FETCH_VACCINATIONS_SUCCESS, 5 | vaccinations, 6 | }); 7 | -------------------------------------------------------------------------------- /actions/pickVaccine.js: -------------------------------------------------------------------------------- 1 | import { PICK_VACCINE } from '../constants/actions'; 2 | 3 | export default (vaccineId) => ({ 4 | type: PICK_VACCINE, 5 | vaccineId, 6 | }); 7 | -------------------------------------------------------------------------------- /actions/switchToChooseVaccineRoute.js: -------------------------------------------------------------------------------- 1 | import { SWITCH_TO_CHOOSE_VACCINE_ROUTE } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: SWITCH_TO_CHOOSE_VACCINE_ROUTE, 5 | }); 6 | -------------------------------------------------------------------------------- /actions/switchToDetailRoute.js: -------------------------------------------------------------------------------- 1 | import { SWITCH_TO_DETAIL_ROUTE } from '../constants/actions'; 2 | 3 | export default (id) => ({ 4 | type: SWITCH_TO_DETAIL_ROUTE, 5 | id, 6 | }); 7 | -------------------------------------------------------------------------------- /actions/switchToListRoute.js: -------------------------------------------------------------------------------- 1 | import { SWITCH_TO_LIST_ROUTE } from '../constants/actions'; 2 | 3 | export default () => ({ 4 | type: SWITCH_TO_LIST_ROUTE, 5 | }); 6 | -------------------------------------------------------------------------------- /components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Text, 4 | TouchableHighlight, 5 | } from 'react-native'; 6 | import styles from './styles'; 7 | 8 | export default class Button extends Component { 9 | state = { 10 | active: false, 11 | }; 12 | 13 | onHighlight() { 14 | this.setState({ active: true }); 15 | } 16 | 17 | onUnhighlight() { 18 | this.setState({ active: false }); 19 | } 20 | 21 | onPress() { 22 | if (!this.props.disabled) { 23 | this.props.onPress(); 24 | } 25 | } 26 | 27 | render() { 28 | const colorStyle = { 29 | color: this.props.disabled ? '#ccc' : '#000', 30 | }; 31 | const underlayColor = this.props.disabled ? '#E0F4FF' : '#B8CCD8'; 32 | return ( 33 | 40 | {this.props.children} 41 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /components/Button/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | button: { 5 | borderRadius: 5, 6 | alignSelf: 'stretch', 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | overflow: 'hidden', 10 | backgroundColor: '#E0F4FF', 11 | borderBottomWidth: 1, 12 | borderBottomColor: '#B8CCD8', 13 | }, 14 | buttonText: { 15 | fontSize: 18, 16 | margin: 5, 17 | textAlign: 'center', 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /components/Card/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | } from 'react-native'; 5 | import styles from './styles'; 6 | 7 | // eslint-disable-next-line react/prefer-stateless-function 8 | export default class Card extends Component { 9 | render() { 10 | return ( 11 | 14 | {this.props.children} 15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/Card/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | backgroundColor: '#FFF', 6 | marginLeft: 10, 7 | marginRight: 10, 8 | paddingLeft: 10, 9 | paddingRight: 10, 10 | borderBottomColor: '#f3f3f3', 11 | borderBottomWidth: 1, 12 | marginBottom: 10, 13 | borderRadius: 3, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /components/ChooseDate/DateInput/index.android.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | import { 5 | Text, 6 | TextInput, 7 | View, 8 | } from 'react-native'; 9 | import moment from 'moment'; 10 | import dateformat from 'dateformat'; 11 | import styles from './styles'; 12 | 13 | export default class DatePickerAndroid extends Component { 14 | 15 | constructor(props) { 16 | super(); 17 | this.state = { 18 | isValid: true, 19 | date: dateformat(props.date, 'yyyy/mm/dd'), 20 | }; 21 | } 22 | 23 | updateDate(text) { 24 | const isValid = moment(text, 'YYYY/MM/DD', true).isValid(); 25 | if (!isValid) { 26 | this.setState({ 27 | isValid: false, 28 | date: text, 29 | }); 30 | this.props.onInvalidDateChange(); 31 | return; 32 | } 33 | 34 | this.setState({ 35 | date: text, 36 | isValid: true, 37 | }); 38 | this.props.onDateChange(new Date(text)); 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 | 50 | 51 | {!this.state.isValid && 'Please enter a valid Date: YYYY/MM/DD'} 52 | 53 | 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/ChooseDate/DateInput/index.ios.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | DatePickerIOS, 4 | } from 'react-native'; 5 | 6 | // eslint-disable-next-line react/prefer-stateless-function 7 | export default class CustomDatePickerIOS extends Component { 8 | render() { 9 | return ( 10 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/ChooseDate/DateInput/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | textInput: { 5 | height: 50, 6 | borderColor: '#ddd', 7 | borderWidth: 1, 8 | marginLeft: 10, 9 | marginRight: 10, 10 | marginTop: 40, 11 | backgroundColor: '#fff', 12 | borderRadius: 4, 13 | fontSize: 20, 14 | textAlign: 'center', 15 | }, 16 | errorMessage: { 17 | color: '#DD3333', 18 | marginTop: 10, 19 | height: 20, 20 | textAlign: 'center', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /components/ChooseDate/index.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | import { 5 | Text, 6 | View, 7 | } from 'react-native'; 8 | import styles from './styles'; 9 | import Button from '../Button'; 10 | // eslint-disable-next-line import/no-unresolved 11 | import DateInput from './DateInput'; 12 | 13 | export default class ChooseDate extends Component { 14 | state = { 15 | vaccinationDate: new Date(), 16 | isValidDate: true, 17 | }; 18 | 19 | onUpdateDate(date) { 20 | this.setState({ 21 | vaccinationDate: date, 22 | isValidDate: true, 23 | }); 24 | } 25 | 26 | onInvalidDateChange() { 27 | this.setState({ 28 | isValidDate: false, 29 | }); 30 | } 31 | 32 | onPress() { 33 | this.props.addVaccination(this.props.addForm.get('vaccineId'), this.state.vaccinationDate); 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 40 | Pick the Vaccination Date 41 | 42 | 43 | 48 | 55 | 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/ChooseDate/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | backgroundColor: '#FFFBE6', 6 | position: 'absolute', 7 | top: 0, 8 | bottom: 0, 9 | left: 0, 10 | right: 0, 11 | }, 12 | header: { 13 | fontSize: 24, 14 | paddingBottom: 20, 15 | paddingTop: 40, 16 | textAlign: 'center', 17 | }, 18 | addButton: { 19 | marginLeft: 10, 20 | marginRight: 10, 21 | marginTop: 30, 22 | paddingTop: 30, 23 | paddingBottom: 30, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /components/ChooseVaccine/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ListView, 4 | Text, 5 | TouchableOpacity, 6 | View, 7 | } from 'react-native'; 8 | import styles from './styles'; 9 | 10 | export default class ChooseVaccine extends Component { 11 | 12 | state = { 13 | dataSource: new ListView.DataSource({ 14 | rowHasChanged: (row1, row2) => row1 !== row2, 15 | }), 16 | }; 17 | 18 | componentWillMount() { 19 | this.setState({ 20 | dataSource: this.state.dataSource.cloneWithRows(this.props.vaccines.toJS()), 21 | }); 22 | } 23 | 24 | renderItem(vaccine) { 25 | return ( 26 | this.props.pickVaccine(vaccine.id)}> 27 | 28 | { vaccine.name } 29 | { vaccine.diseases.join(', ')} 30 | 31 | 32 | ); 33 | } 34 | 35 | render() { 36 | return ( 37 | 38 | Pick a Vaccine 39 | 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/ChooseVaccine/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | backgroundColor: '#FFFBE6', 6 | position: 'absolute', 7 | top: 0, 8 | bottom: 0, 9 | left: 0, 10 | right: 0, 11 | }, 12 | header: { 13 | fontSize: 24, 14 | paddingBottom: 20, 15 | paddingTop: 40, 16 | textAlign: 'center', 17 | }, 18 | row: { 19 | flex: 1, 20 | paddingTop: 20, 21 | paddingBottom: 20, 22 | paddingLeft: 20, 23 | paddingRight: 20, 24 | borderBottomWidth: 1, 25 | borderBottomColor: '#f6f6f6', 26 | }, 27 | listView: { 28 | backgroundColor: '#FFF', 29 | marginLeft: 10, 30 | marginRight: 10, 31 | marginBottom: 10, 32 | borderBottomColor: '#f3f3f3', 33 | borderBottomWidth: 1, 34 | borderRadius: 3, 35 | }, 36 | footer: { 37 | alignItems: 'center', 38 | paddingTop: 10, 39 | }, 40 | disease: { 41 | color: '#999', 42 | fontSize: 10, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /components/Detail/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | } from 'react-native'; 6 | import dateformat from 'dateformat'; 7 | import Button from '../Button'; 8 | import Card from '../Card'; 9 | import styles from './styles'; 10 | 11 | export default class Detail extends Component { 12 | 13 | render() { 14 | const { currentVaccination } = this.props; 15 | if (!currentVaccination) { return null; } 16 | 17 | return ( 18 | 19 | 25 | 26 | {currentVaccination.getIn(['vaccine', 'name'])} 27 | 28 | { currentVaccination.getIn(['vaccine', 'diseases']).join(', ')} 29 | 30 | 31 | {dateformat(currentVaccination.get('date'), 'ddd, dS mmmm, yyyy')} 32 | 33 | 34 | 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /components/Detail/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | backgroundColor: '#FFFBE6', 6 | position: 'absolute', 7 | top: 0, 8 | bottom: 0, 9 | left: 0, 10 | right: 0, 11 | paddingTop: 80, 12 | }, 13 | name: { 14 | fontSize: 20, 15 | textAlign: 'center', 16 | marginBottom: 50, 17 | marginTop: 50, 18 | }, 19 | vaccinationDate: { 20 | textAlign: 'center', 21 | marginBottom: 20, 22 | }, 23 | disease: { 24 | color: '#999', 25 | fontSize: 12, 26 | marginBottom: 30, 27 | textAlign: 'center', 28 | }, 29 | backButton: { 30 | paddingLeft: 30, 31 | paddingRight: 30, 32 | marginLeft: 10, 33 | marginTop: 20, 34 | marginBottom: 10, 35 | paddingTop: 10, 36 | paddingBottom: 10, 37 | alignSelf: 'flex-start', 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /components/List/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ListView, 4 | Text, 5 | TouchableOpacity, 6 | View, 7 | } from 'react-native'; 8 | import dateformat from 'dateformat'; 9 | import styles from './styles'; 10 | import Logo from '../Logo'; 11 | import Button from '../Button'; 12 | import Card from '../Card'; 13 | 14 | export default class List extends Component { 15 | 16 | state = { 17 | dataSource: new ListView.DataSource({ 18 | rowHasChanged: (row1, row2) => row1 !== row2, 19 | }), 20 | }; 21 | 22 | componentWillMount() { 23 | this.setState({ 24 | dataSource: this.state.dataSource.cloneWithRows(this.props.vaccinations.toJS()), 25 | }); 26 | } 27 | 28 | componentWillReceiveProps(nextProps) { 29 | this.setState({ 30 | dataSource: this.state.dataSource.cloneWithRows(nextProps.vaccinations.toJS()), 31 | }); 32 | } 33 | 34 | getDisease() { 35 | const diseases = this.props.vaccinations 36 | .toList() 37 | .map((vaccination) => vaccination.getIn(['vaccine', 'diseases'])) 38 | .flatten(true); 39 | return diseases 40 | .toSet() 41 | .toList() 42 | .sort() 43 | .join(', '); 44 | } 45 | 46 | // eslint-disable-next-line class-methods-use-this 47 | renderHeader() { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | Carte Jaune 55 | 56 | 57 | Vaccines 58 | 59 | ); 60 | } 61 | 62 | renderDiseases() { 63 | return ( 64 | 65 | Covered Diseases 66 | 67 | {this.getDisease()} 68 | 69 | 70 | ); 71 | } 72 | 73 | renderFooter() { 74 | return ( 75 | 76 | {this.props.vaccinations.size ? this.renderDiseases() : null} 77 | 84 | Made with ♡ by Nik Graf 85 | 86 | Open Source Code: https://www.github.com/nikgraf/CarteJaune 87 | 88 | 89 | Logo made by Freepik is licensed under CC BY 3.0 90 | 91 | 92 | This App is designed for educational purposes only and is not intended 93 | to serve as medical advice. 94 | 95 | 96 | ); 97 | } 98 | 99 | renderItem(vaccination) { 100 | return ( 101 | this.props.switchToDetailRoute(vaccination.listId)}> 102 | 103 | {vaccination.vaccine.name} 104 | { vaccination.vaccine.diseases.join(', ')} 105 | 106 | {dateformat(vaccination.date, 'ddd, dS mmmm, yyyy')} 107 | 108 | 109 | 110 | ); 111 | } 112 | 113 | render() { 114 | return ( 115 | 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /components/List/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | add: { 5 | flex: 1, 6 | justifyContent: 'center', 7 | alignItems: 'center', 8 | paddingTop: 20, 9 | paddingBottom: 20, 10 | borderTopColor: '#ddd', 11 | borderTopWidth: 1, 12 | }, 13 | addText: { 14 | fontSize: 20, 15 | }, 16 | container: { 17 | flex: 1, 18 | justifyContent: 'center', 19 | alignItems: 'center', 20 | paddingBottom: 15, 21 | paddingTop: 15, 22 | }, 23 | header: { 24 | flex: 1, 25 | alignItems: 'center', 26 | flexDirection: 'row', 27 | justifyContent: 'center', 28 | marginTop: 20, 29 | }, 30 | headerText: { 31 | fontSize: 32, 32 | paddingTop: 20, 33 | paddingLeft: 10, 34 | }, 35 | logo: { 36 | width: 40, 37 | height: 40, 38 | }, 39 | listView: { 40 | paddingTop: 20, 41 | backgroundColor: '#FFFBE6', 42 | }, 43 | name: { 44 | fontSize: 20, 45 | textAlign: 'center', 46 | }, 47 | vaccinationDate: { 48 | textAlign: 'center', 49 | }, 50 | diseases: { 51 | paddingTop: 10, 52 | paddingBottom: 20, 53 | paddingLeft: 20, 54 | paddingRight: 20, 55 | }, 56 | diseasesText: { 57 | lineHeight: 24, 58 | }, 59 | label: { 60 | paddingTop: 20, 61 | textAlign: 'center', 62 | marginBottom: 10, 63 | color: '#421C00', 64 | }, 65 | addButton: { 66 | marginLeft: 20, 67 | marginRight: 20, 68 | marginTop: 50, 69 | paddingTop: 30, 70 | paddingBottom: 30, 71 | }, 72 | plus: { 73 | fontSize: 30, 74 | lineHeight: 30, 75 | marginRight: 10, 76 | }, 77 | disease: { 78 | color: '#999', 79 | fontSize: 10, 80 | marginBottom: 10, 81 | }, 82 | promo: { 83 | textAlign: 'center', 84 | fontSize: 13, 85 | marginTop: 70, 86 | marginBottom: 8, 87 | color: '#666', 88 | }, 89 | smallPromo: { 90 | textAlign: 'center', 91 | fontSize: 11, 92 | marginBottom: 8, 93 | color: '#666', 94 | }, 95 | disclaimer: { 96 | textAlign: 'center', 97 | fontSize: 9, 98 | marginBottom: 20, 99 | color: '#666', 100 | marginLeft: 100, 101 | marginRight: 100, 102 | }, 103 | }); 104 | -------------------------------------------------------------------------------- /components/Logo/icon.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | 3 | const logo = 'data:image/png;base64,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'; 4 | 5 | export default logo; 6 | -------------------------------------------------------------------------------- /components/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Image, 4 | } from 'react-native'; 5 | import icon from './icon'; 6 | 7 | // eslint-disable-next-line react/prefer-stateless-function 8 | export default class Logo extends Component { 9 | render() { 10 | return ( 11 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /constants/actions.js: -------------------------------------------------------------------------------- 1 | export const ADD_VACCINATION = 'ADD_VACCINATION'; 2 | export const ADD_VACCINATION_SUCCESS = 'ADD_VACCINATION_SUCCESS'; 3 | export const ADD_VACCINATION_FAILURE = 'ADD_VACCINATION_FAILURE'; 4 | export const SWITCH_TO_CHOOSE_VACCINE_ROUTE = 'SWITCH_TO_CHOOSE_VACCINE_ROUTE'; 5 | export const SWITCH_TO_DETAIL_ROUTE = 'SWITCH_TO_DETAIL_ROUTE'; 6 | export const SWITCH_TO_LIST_ROUTE = 'SWITCH_TO_LIST_ROUTE'; 7 | export const PICK_VACCINE = 'PICK_VACCINE'; 8 | export const FETCH_VACCINATIONS = 'FETCH_VACCINATIONS'; 9 | export const FETCH_VACCINATIONS_SUCCESS = 'FETCH_VACCINATIONS_SUCCESS'; 10 | export const FETCH_VACCINATIONS_FAILURE = 'FETCH_VACCINATIONS_FAILURE'; 11 | -------------------------------------------------------------------------------- /constants/storage.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const KEY = '@Vaccinations:me'; 3 | -------------------------------------------------------------------------------- /constants/vaccines.js: -------------------------------------------------------------------------------- 1 | import { fromJS, OrderedMap } from 'immutable'; 2 | 3 | const vaccines = fromJS([ 4 | { 5 | name: 'AVA (BioThrax)', 6 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 7 | diseases: ['Anthrax'], 8 | }, 9 | { 10 | name: 'VAR (Varivax)', 11 | id: 'f3e08a56-003c-4b46-9dea-216298401ca0', 12 | diseases: ['Varicella (Chickenpox)'], 13 | }, 14 | { 15 | name: 'MMRV (ProQuad)', 16 | id: '3373721d-3d14-490c-9fa9-69a223888322', 17 | diseases: [ 18 | 'Varicella (Chickenpox)', 19 | 'Measles', 20 | 'Mumps', 21 | 'Rubella (German Measles)', 22 | ], 23 | }, 24 | { 25 | name: 'HepA (Havrix, Vaqta)', 26 | id: 'a9144edf-13a2-4ce5-b6af-14eb38fd848c', 27 | diseases: ['Hepatitis A'], 28 | }, 29 | { 30 | name: 'HepA-HepB (Twinrix)', 31 | id: '6888fd1a-af4f-4f33-946d-40d4c473c9cc', 32 | diseases: ['Hepatitis A', 'Hepatitis B'], 33 | }, 34 | { 35 | name: 'HepB (Engerix-B, Recombivax HB)', 36 | id: 'ca079856-a561-4bc9-9bef-e62429ed3a38', 37 | diseases: ['Hepatitis B'], 38 | }, 39 | { 40 | name: 'Hib-HepB (Comvax)', 41 | id: '7305d769-0d1e-4bef-bd09-6998dc839825', 42 | diseases: ['Hepatitis B', 'Haemophilus influenzae type b (Hib)'], 43 | }, 44 | { 45 | name: 'Hib (ActHIB, PedvaxHIB, Hiberix)', 46 | id: 'd241f0c7-9920-4bc6-8f34-288a13e03f4d', 47 | diseases: ['Haemophilus influenzae type b (Hib)'], 48 | }, 49 | { 50 | name: 'HPV4 (Gardasil)', 51 | id: 'c2fef03c-db7f-483b-af70-50560712b189', 52 | diseases: ['Human Papillomavirus (HPV)'], 53 | }, 54 | { 55 | name: 'HPV2 (Cervarix)', 56 | id: '286f55e4-e727-4fc4-86b0-5a08ea712a77', 57 | diseases: ['Human Papillomavirus (HPV)'], 58 | }, 59 | { 60 | name: 'TIV (Afluria, Agriflu, FluLaval, Fluarix, Fluvirin, Fluzone, Fluzone High-Dose, Fluzone Intradermal)', // eslint-disable-line max-len 61 | id: '60e85a31-6a54-48e1-b0b7-deb28120675b', 62 | diseases: ['Seasonal Influenza (Flu)'], 63 | }, 64 | { 65 | name: 'LAIV (FluMist)', 66 | id: '9e67e321-9a7f-426f-ba9b-28885f93f9b9', 67 | diseases: ['Seasonal Influenza (Flu)'], 68 | }, 69 | { 70 | name: 'JE (Ixiaro)', 71 | id: '5ce00584-3350-442d-ac6c-7f19567eff8a', 72 | diseases: ['Japanese Encephalitis'], 73 | }, 74 | { 75 | name: 'MMR (M-M-R II)', 76 | id: 'd10b7bf0-d51e-4117-a6a4-08bdb5cb682a', 77 | diseases: ['Measles', 'Mumps', 'Rubella (German Measles)'], 78 | }, 79 | { 80 | name: 'MCV4 (Menactra)', 81 | id: '6295fe11-f0ce-4967-952c-f271416cc300', 82 | diseases: ['Meningococcal'], 83 | }, 84 | { 85 | name: 'MPSV4 (Menomune)', 86 | id: '65f6d6d0-6dd8-49c9-95da-ed9fa403ae96', 87 | diseases: ['Meningococcal'], 88 | }, 89 | { 90 | name: 'MODC (Menveo)', 91 | id: 'be10b480-7934-46be-a488-66540aac2881', 92 | diseases: ['Meningococcal'], 93 | }, 94 | { 95 | name: 'Tdap (Adacel, Boostrix)', 96 | id: '0c6c33fb-f4dc-44c6-8684-625099f6fa21', 97 | diseases: ['Pertussis (Whooping Cough)', 'Tetanus (Lockjaw)', 'Diphtheria'], 98 | }, 99 | { 100 | name: 'PCV13 (Prevnar13)', 101 | id: 'd8c5a723-21e2-49a6-a921-705da16563e1', 102 | diseases: ['Pneumococcal'], 103 | }, 104 | { 105 | name: 'PPSV23 (Pneumovax 23)', 106 | id: '4005de2f-8e6d-40ae-bb5f-068ac56885b8', 107 | diseases: ['Pneumococcal'], 108 | }, 109 | { 110 | name: 'Polio (Ipol)', 111 | id: '9c1582f2-8a7b-4bae-8ba5-656efe33fb29', 112 | diseases: ['Polio'], 113 | }, 114 | { 115 | name: 'Rabies (Imovax Rabies, RabAvert)', 116 | id: '2bfeeb1f-b7a7-4ce6-aae1-72e840a93e2e', 117 | diseases: ['Rabies'], 118 | }, 119 | { 120 | name: 'RV1 (Rotarix)', 121 | id: '8ddfa840-7558-469a-a53b-19a40d016518', 122 | diseases: ['Rotavirus'], 123 | }, 124 | { 125 | name: 'RV5 (RotaTeq)', 126 | id: '9281ddcb-5ef3-47e6-a249-6b2b8bee1e7f', 127 | diseases: ['Rotavirus'], 128 | }, 129 | { 130 | name: 'ZOS (Zostavax)', 131 | id: '2921b034-8a4c-46f5-9753-70a112dfec3f', 132 | diseases: ['Shingles (Herpes Zoster)'], 133 | }, 134 | { 135 | name: 'Vaccinia (ACAM2000)', 136 | id: 'e26378f4-5d07-4b5f-9c93-53816c0faf9f', 137 | diseases: ['Smallpox'], 138 | }, 139 | { 140 | name: 'DTaP (Daptacel, Infanrix)', 141 | id: 'b23e765e-a05b-4a24-8095-03d79e47a8aa', 142 | diseases: [ 143 | 'Tetanus (Lockjaw)', 144 | 'Pertussis (Whooping Cough)', 145 | 'Diphtheria', 146 | ], 147 | }, 148 | { 149 | name: 'Td (Decavac, generic)', 150 | id: '1af45230-cb2a-4242-81ac-2430cd64f8ce', 151 | diseases: ['Tetanus (Lockjaw)', 'Diphtheria'], 152 | }, 153 | { 154 | name: 'DT (-generic-)', 155 | id: '6eb77e28-aaa1-4e29-b124-5793a4bd6f1f', 156 | diseases: ['Tetanus (Lockjaw)', 'Diphtheria'], 157 | }, 158 | { 159 | name: 'TT (-generic-)', 160 | id: 'd6cf7277-831c-43c6-a1fa-7109d3325168', 161 | diseases: ['Tetanus (Lockjaw)'], 162 | }, 163 | { 164 | name: 'DTaP-IPV (Kinrix)', 165 | id: 'a8ecfef5-5f09-442c-84c3-4dfbcd99b3b8', 166 | diseases: [ 167 | 'Tetanus (Lockjaw)', 168 | 'Polio', 169 | 'Pertussis (Whooping Cough)', 170 | 'Diphtheria', 171 | ], 172 | }, 173 | { 174 | name: 'DTaP-HepB-IPV (Pediarix)', 175 | id: '10bc0626-7b0a-4a42-b1bf-2742f0435c37', 176 | diseases: [ 177 | 'Tetanus (Lockjaw)', 178 | 'Polio', 179 | 'Hepatitis B', 180 | 'Pertussis (Whooping Cough)', 181 | 'Diphtheria', 182 | ], 183 | }, 184 | { 185 | name: 'DTaP-IPV/Hib (Pentacel)', 186 | id: 'dcbb9691-1544-44fc-a9ca-351946010876', 187 | diseases: [ 188 | 'Tetanus (Lockjaw)', 189 | 'Polio', 190 | 'Haemophilus influenzae type b (Hib)', 191 | 'Pertussis (Whooping Cough)', 192 | 'Diphtheria', 193 | ], 194 | }, 195 | { 196 | name: 'DTaP/Hib', 197 | id: 'e817c55d-e3db-4963-9fec-04d5823f6915', 198 | diseases: [ 199 | 'Tetanus (Lockjaw)', 200 | 'Diphtheria', 201 | 'Haemophilus influenzae type b (Hib)', 202 | 'Pertussis (Whooping Cough)', 203 | ], 204 | }, 205 | { 206 | name: 'BCG (TICE BCG, Mycobax)', 207 | id: '8f2049a1-a1e3-44e1-947e-debbf3cafecc', 208 | diseases: ['Tuberculosis (TB)'], 209 | }, 210 | { 211 | name: 'Typhoid Oral (Vivotif)', 212 | id: '060f44be-e1e7-4575-ba0f-62611f03384b', 213 | diseases: ['Typhoid Fever'], 214 | }, 215 | { 216 | name: 'Typhoid Polysaccharide (Typhim Vi)', 217 | id: '87009829-1a48-4330-91e1-6bcd7ab04ee1', 218 | diseases: ['Typhoid Fever'], 219 | }, 220 | { 221 | name: 'YF (YF-Vax)', 222 | id: '24d5bfc4-d69a-4311-bb10-8980dddafa20', 223 | diseases: ['Yellow Fever'], 224 | }, 225 | ]); 226 | 227 | const keyedVaccines = vaccines.reduce((result, item) => ( 228 | result.set(item.get('id'), item) 229 | ), OrderedMap()); 230 | 231 | export default keyedVaccines.sortBy(vaccine => vaccine.get('name').toLowerCase()); 232 | -------------------------------------------------------------------------------- /containers/ChooseDate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import ChooseDate from '../components/ChooseDate'; 5 | import addVaccination from '../actions/addVaccination'; 6 | import addForm from '../selectors/addForm'; 7 | 8 | const actions = { 9 | addVaccination, 10 | }; 11 | 12 | const selectors = { 13 | addForm, 14 | }; 15 | 16 | // eslint-disable-next-line react/prefer-stateless-function 17 | class ChooseDateContainer extends Component { 18 | render() { 19 | return ; 20 | } 21 | } 22 | 23 | export default connect(createStructuredSelector(selectors), actions)(ChooseDateContainer); 24 | -------------------------------------------------------------------------------- /containers/ChooseVaccine.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import ChooseVaccine from '../components/ChooseVaccine'; 5 | import pickVaccine from '../actions/pickVaccine'; 6 | import vaccines from '../selectors/vaccines'; 7 | 8 | const actions = { 9 | pickVaccine, 10 | }; 11 | 12 | const selectors = { 13 | vaccines, 14 | }; 15 | 16 | // eslint-disable-next-line react/prefer-stateless-function 17 | class ChooseVaccineContainer extends Component { 18 | render() { 19 | return ; 20 | } 21 | } 22 | 23 | export default connect(createStructuredSelector(selectors), actions)(ChooseVaccineContainer); 24 | -------------------------------------------------------------------------------- /containers/Detail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import Detail from '../components/Detail'; 5 | import vaccines from '../selectors/vaccines'; 6 | import currentVaccination from '../selectors/currentVaccination'; 7 | import switchToListRoute from '../actions/switchToListRoute'; 8 | 9 | const actions = { 10 | switchToListRoute, 11 | }; 12 | 13 | const selectors = { 14 | currentVaccination, 15 | vaccines, 16 | }; 17 | 18 | // eslint-disable-next-line react/prefer-stateless-function 19 | class DetailContainer extends Component { 20 | render() { 21 | return ; 22 | } 23 | } 24 | 25 | export default connect(createStructuredSelector(selectors), actions)(DetailContainer); 26 | -------------------------------------------------------------------------------- /containers/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import List from '../components/List'; 5 | import switchToChooseVaccineRoute from '../actions/switchToChooseVaccineRoute'; 6 | import switchToDetailRoute from '../actions/switchToDetailRoute'; 7 | import vaccinations from '../selectors/vaccinations'; 8 | import vaccines from '../selectors/vaccines'; 9 | 10 | const actions = { 11 | switchToChooseVaccineRoute, 12 | switchToDetailRoute, 13 | }; 14 | 15 | const selectors = { 16 | vaccinations, 17 | vaccines, 18 | }; 19 | 20 | // eslint-disable-next-line react/prefer-stateless-function 21 | class ListContainer extends Component { 22 | render() { 23 | return ; 24 | } 25 | } 26 | 27 | export default connect(createStructuredSelector(selectors), actions)(ListContainer); 28 | -------------------------------------------------------------------------------- /exp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Carte Jaune", 3 | "description": "A Redux/ExponentJS (React Native) app to keep track of your vaccinations.", 4 | "slug": "carte-jaune", 5 | "sdkVersion": "9.0.0", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "primaryColor": "#cccccc", 9 | "iconUrl": "https://s3-us-west-2.amazonaws.com/votexp/carte-jaune-logo-big.png", 10 | "notification": { 11 | "iconUrl": "https://s3.amazonaws.com/exp-us-standard/placeholder-push-icon-blue-circle.png", 12 | "color": "#000000" 13 | }, 14 | "loading": { 15 | "iconUrl": "https://s3-us-west-2.amazonaws.com/votexp/carte-jaune-logo-big.png", 16 | "hideExponentText": false 17 | }, 18 | "packagerOpts": { 19 | "assetExts": [ 20 | "ttf" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import Exponent from 'exponent'; // eslint-disable-line no-unused-vars 2 | import React, { Component } from 'react'; 3 | import { 4 | AppRegistry, 5 | Navigator, 6 | } from 'react-native'; 7 | import { Provider } from 'react-redux'; 8 | import { Scene, Router } from 'react-native-router-flux'; 9 | import configureStore from './store/configureStore'; 10 | import ChooseVaccine from './containers/ChooseVaccine'; 11 | import ChooseDate from './containers/ChooseDate'; 12 | import List from './containers/List'; 13 | import Detail from './containers/Detail'; 14 | 15 | const store = configureStore(); 16 | 17 | // eslint-disable-next-line react/prefer-stateless-function 18 | class ProviderWrapper extends Component { 19 | // eslint-disable-next-line class-methods-use-this 20 | render() { 21 | return ( 22 | 23 | 24 | 29 | 35 | 41 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | AppRegistry.registerComponent('main', () => ProviderWrapper); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CarteJaune", 3 | "version": "0.0.0", 4 | "main": "main.js", 5 | "author": null, 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/nikgraf/CarteJaune.git" 9 | }, 10 | "dependencies": { 11 | "@exponent/vector-icons": "^1.0.1", 12 | "babel-core": "^6.5.2", 13 | "babel-eslint": "^6.1.2", 14 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 15 | "babel-polyfill": "^6.13.0", 16 | "babel-preset-react-native": "^1.4.0", 17 | "babel-preset-react-native-stage-0": "^1.0.1", 18 | "chai": "^3.5.0", 19 | "chai-immutable": "^1.5.3", 20 | "dateformat": "^1.0.12", 21 | "dirty-chai": "^1.2.2", 22 | "eslint": "^3.5.0", 23 | "eslint-config-airbnb": "^11.0.0", 24 | "eslint-plugin-import": "^1.14.0", 25 | "eslint-plugin-jsx-a11y": "^2.2.1", 26 | "eslint-plugin-mocha": "^4.5.1", 27 | "eslint-plugin-react": "^6.2.0", 28 | "exponent": "^9.0.2", 29 | "immutable": "^3.8.1", 30 | "mocha": "^3.0.2", 31 | "moment": "^2.11.2", 32 | "react": "15.2.1", 33 | "react-native": "github:exponentjs/react-native#sdk-9.0.0", 34 | "react-native-mock": "^0.2.6", 35 | "react-native-router-flux": "^3.35.0", 36 | "react-redux": "^4.4.0", 37 | "redux": "^3.3.1", 38 | "redux-immutablejs": "0.0.8", 39 | "redux-logger": "^2.6.0", 40 | "redux-saga": "^0.9.1", 41 | "reselect": "^2.0.3", 42 | "sinon": "^1.17.3", 43 | "uuid": "^2.0.1" 44 | }, 45 | "scripts": { 46 | "lint": "eslint ./", 47 | "test": "node_modules/.bin/mocha --compilers js:babel-core/register --require testHelper.js **/__test__/*.js", 48 | "test:watch": "npm test -- --watch" 49 | }, 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /reducers/__test__/addForm.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { expect } from 'chai'; 3 | import reducer from '../addForm'; 4 | import { 5 | PICK_VACCINE, 6 | SWITCH_TO_CHOOSE_VACCINE_ROUTE, 7 | } from '../../constants/actions'; 8 | 9 | describe('addForm', () => { 10 | it('returns an initial state', () => { 11 | const nextState = reducer(undefined, { 12 | type: 'SOMETHING', 13 | }); 14 | expect(nextState).to.deep.equal(Map()); 15 | }); 16 | 17 | it('removes the id when switching to the form to have clean state', () => { 18 | const nextState = reducer(Map({ vaccineId: 'abc' }), { 19 | type: SWITCH_TO_CHOOSE_VACCINE_ROUTE, 20 | }); 21 | expect(nextState).to.deep.equal(Map()); 22 | }); 23 | 24 | it('add vaccine id to state in case it was picked', () => { 25 | const nextState = reducer(Map(), { 26 | type: PICK_VACCINE, 27 | vaccineId: 'cdf', 28 | }); 29 | expect(nextState.get('vaccineId')).to.equal('cdf'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /reducers/__test__/currentVaccination.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import reducer from '../currentVaccination'; 3 | import { 4 | SWITCH_TO_DETAIL_ROUTE, 5 | SWITCH_TO_LIST_ROUTE, 6 | } from '../../constants/actions'; 7 | 8 | describe('currentVaccination', () => { 9 | it('returns an initial state', () => { 10 | const nextState = reducer(undefined, { 11 | type: 'SOMETHING', 12 | }); 13 | expect(nextState).to.be.null(); 14 | }); 15 | 16 | it('sets the current vaccination id on SWITCH_TO_DETAIL_ROUTE', () => { 17 | const nextState = reducer(undefined, { 18 | type: SWITCH_TO_DETAIL_ROUTE, 19 | id: 3225, 20 | }); 21 | expect(nextState).to.equal(3225); 22 | }); 23 | 24 | it('resets the current vaccination id to null on SWITCH_TO_LIST_ROUTE', () => { 25 | const nextState = reducer(4444, { 26 | type: SWITCH_TO_LIST_ROUTE, 27 | }); 28 | expect(nextState).to.be.null(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /reducers/__test__/vaccinations.js: -------------------------------------------------------------------------------- 1 | import { OrderedMap } from 'immutable'; 2 | import { expect } from 'chai'; 3 | import reducer from '../vaccinations'; 4 | import { 5 | ADD_VACCINATION, 6 | FETCH_VACCINATIONS_SUCCESS, 7 | } from '../../constants/actions'; 8 | 9 | describe('vaccinations', () => { 10 | it('returns an initial state', () => { 11 | const nextState = reducer(undefined, { 12 | type: 'SOMETHING', 13 | }); 14 | expect(nextState).to.deep.equal(OrderedMap()); 15 | }); 16 | 17 | it('sets the current vaccination id on ADD_VACCINATION', () => { 18 | const nextState = reducer(undefined, { 19 | type: ADD_VACCINATION, 20 | vaccineId: 222, 21 | vaccinationDate: 444, 22 | }); 23 | expect(nextState.first().get('id')).to.equal(222); 24 | expect(nextState.first().get('date')).to.equal(444); 25 | }); 26 | 27 | it('sets the state to the vavvinations from the FETCH_VACCINATIONS_SUCCESS action', () => { 28 | const nextState = reducer(4444, { 29 | type: FETCH_VACCINATIONS_SUCCESS, 30 | vaccinations: OrderedMap({ id: 1, date: 3 }), 31 | }); 32 | const expected = OrderedMap({ id: 1, date: 3 }); 33 | expect(nextState).to.deep.equal(expected); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /reducers/activeRoute.js: -------------------------------------------------------------------------------- 1 | import { Actions } from 'react-native-router-flux'; 2 | import { 3 | ADD_VACCINATION_SUCCESS, 4 | PICK_VACCINE, 5 | SWITCH_TO_CHOOSE_VACCINE_ROUTE, 6 | SWITCH_TO_DETAIL_ROUTE, 7 | SWITCH_TO_LIST_ROUTE, 8 | } from '../constants/actions'; 9 | 10 | export default (state = 'list', action) => { 11 | switch (action.type) { 12 | case SWITCH_TO_CHOOSE_VACCINE_ROUTE: 13 | setTimeout(() => Actions.chooseVaccine(), 0); 14 | return 'add'; 15 | case ADD_VACCINATION_SUCCESS: 16 | setTimeout(() => Actions.list(), 0); 17 | return 'list'; 18 | case PICK_VACCINE: 19 | setTimeout(() => Actions.chooseDate(), 0); 20 | return 'chooseDate'; 21 | case SWITCH_TO_DETAIL_ROUTE: 22 | setTimeout(() => Actions.detail(), 0); 23 | return 'detail'; 24 | case SWITCH_TO_LIST_ROUTE: 25 | setTimeout(() => Actions.list(), 0); 26 | return 'list'; 27 | default: 28 | return state; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /reducers/addForm.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { 3 | PICK_VACCINE, 4 | SWITCH_TO_CHOOSE_VACCINE_ROUTE, 5 | } from '../constants/actions'; 6 | 7 | export default (state = Map(), action) => { 8 | switch (action.type) { 9 | case SWITCH_TO_CHOOSE_VACCINE_ROUTE: 10 | return state.delete('vaccineId'); 11 | case PICK_VACCINE: 12 | return state.set('vaccineId', action.vaccineId); 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /reducers/currentVaccination.js: -------------------------------------------------------------------------------- 1 | import { 2 | SWITCH_TO_DETAIL_ROUTE, 3 | SWITCH_TO_LIST_ROUTE, 4 | } from '../constants/actions'; 5 | 6 | export default (state = null, action) => { 7 | switch (action.type) { 8 | case SWITCH_TO_DETAIL_ROUTE: 9 | return action.id; 10 | case SWITCH_TO_LIST_ROUTE: 11 | return null; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutablejs'; 2 | import activeRoute from './activeRoute'; 3 | import addForm from './addForm'; 4 | import vaccinations from './vaccinations'; 5 | import currentVaccination from './currentVaccination'; 6 | 7 | export default combineReducers({ 8 | activeRoute, 9 | addForm, 10 | currentVaccination, 11 | vaccinations, 12 | }); 13 | -------------------------------------------------------------------------------- /reducers/vaccinations.js: -------------------------------------------------------------------------------- 1 | import { OrderedMap, Map } from 'immutable'; 2 | import uuid from 'uuid'; 3 | import { 4 | ADD_VACCINATION, 5 | FETCH_VACCINATIONS_SUCCESS, 6 | } from '../constants/actions'; 7 | 8 | export default (state = OrderedMap(), action) => { 9 | switch (action.type) { 10 | case ADD_VACCINATION: 11 | return state.set(uuid(), Map({ 12 | id: action.vaccineId, 13 | date: action.vaccinationDate, 14 | })); 15 | case FETCH_VACCINATIONS_SUCCESS: 16 | return action.vaccinations; 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /sagas/__test__/fetchVaccinations.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { call, put, take } from 'redux-saga/effects'; 3 | import { 4 | executeFetchVaccinations, 5 | fetchVaccinations, 6 | watchFetchVaccinations, 7 | } from '../fetchVaccinations'; 8 | import { FETCH_VACCINATIONS } from '../../constants/actions'; 9 | import fetchVaccinationsSuccess from '../../actions/fetchVaccinationsSuccess'; 10 | import fetchVaccinationsFailure from '../../actions/fetchVaccinationsFailure'; 11 | 12 | describe('fetchVaccination', () => { 13 | describe('watchVaccination', () => { 14 | it('waits for the FETCH_VACCINATIONS action', () => { 15 | const generator = watchFetchVaccinations(); 16 | const expectTake = generator.next().value; 17 | expect(expectTake).to.deep.equal(take(FETCH_VACCINATIONS)); 18 | }); 19 | }); 20 | 21 | describe('fetchVaccination', () => { 22 | it('fetches the vaccinations from the store & triggers fetchVaccinationsSuccess', () => { 23 | const generator = fetchVaccinations(); 24 | const expectCall = generator.next().value; 25 | expect(expectCall).to.deep.equal(call(executeFetchVaccinations)); 26 | const expectPut = generator.next().value; 27 | expect(expectPut).to.deep.equal(put(fetchVaccinationsSuccess())); 28 | }); 29 | 30 | it('triggers fetchVaccinationsFailure in case an error occures', () => { 31 | const generator = fetchVaccinations(); 32 | generator.next(); 33 | const value = generator.throw({ error: 'retrieving failed' }).value; 34 | expect(value).to.deep.equal(put(fetchVaccinationsFailure())); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /sagas/__test__/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { fork } from 'redux-saga/effects'; 3 | import root from '../index'; 4 | import { watchFetchVaccinations } from '../fetchVaccinations'; 5 | import { watchAddVaccination } from '../saveVaccinations'; 6 | import startup from '../startup'; 7 | 8 | describe('root', () => { 9 | it('should fork all the generators', () => { 10 | const generator = root(); 11 | const expected = [ 12 | fork(watchAddVaccination), 13 | fork(watchFetchVaccinations), 14 | fork(startup), 15 | ]; 16 | expect(generator.next().value).to.deep.equal(expected); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /sagas/__test__/saveVaccinations.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { call, take, put, select } from 'redux-saga/effects'; 3 | import { 4 | executeSaveVaccinations, 5 | saveVaccinations, 6 | watchAddVaccination, 7 | } from '../saveVaccinations'; 8 | import { ADD_VACCINATION } from '../../constants/actions'; 9 | import addVaccinationSuccess from '../../actions/addVaccinationSuccess'; 10 | import addVaccinationFailure from '../../actions/addVaccinationFailure'; 11 | import vaccinationsSelector from '../../selectors/vaccinations'; 12 | 13 | describe('saveVaccination', () => { 14 | describe('watchAddVaccination', () => { 15 | it('waits for the ADD_VACCINATION action', () => { 16 | const generator = watchAddVaccination(); 17 | const takeValue = generator.next().value; 18 | expect(takeValue).to.deep.equal(take(ADD_VACCINATION)); 19 | }); 20 | }); 21 | 22 | describe('saveVaccination', () => { 23 | it('saves the vaccinations to the store and triggers addVaccinationSuccess', () => { 24 | const generator = saveVaccinations(); 25 | const selectValue = generator.next().value; 26 | expect(selectValue).to.deep.equal(select(vaccinationsSelector)); 27 | const callValue = generator.next({ 1: 2 }).value; 28 | expect(callValue).to.deep.equal(call(executeSaveVaccinations, { 1: 2 })); 29 | const putValue = generator.next().value; 30 | expect(putValue).to.deep.equal(put(addVaccinationSuccess())); 31 | }); 32 | 33 | it('triggers addVaccinationFailure in case an error occures', () => { 34 | const generator = saveVaccinations(); 35 | generator.next(); 36 | const value = generator.throw({ error: 'saving failed' }).value; 37 | expect(value).to.deep.equal(put(addVaccinationFailure())); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /sagas/__test__/startup.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { put } from 'redux-saga/effects'; 3 | import fetchVaccinations from '../../actions/fetchVaccinations'; 4 | import startup from '../startup'; 5 | 6 | describe('startup', () => { 7 | it('should fetch the vaccinations', () => { 8 | const generator = startup(); 9 | const expectPut = generator.next().value; 10 | expect(expectPut).to.deep.equal(put(fetchVaccinations())); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /sagas/fetchVaccinations.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from 'react-native'; 2 | import { takeLatest } from 'redux-saga'; 3 | import { call, put } from 'redux-saga/effects'; 4 | import { fromJS, OrderedMap } from 'immutable'; 5 | import { FETCH_VACCINATIONS } from '../constants/actions'; 6 | import fetchVaccinationsSuccess from '../actions/fetchVaccinationsSuccess'; 7 | import fetchVaccinationsFailure from '../actions/fetchVaccinationsFailure'; 8 | import { KEY } from '../constants/storage'; 9 | 10 | /** 11 | * Returns a promise of the actual fetching from the persitent storage. 12 | */ 13 | export const executeFetchVaccinations = () => ( 14 | AsyncStorage.getItem(KEY) 15 | .then((result) => { 16 | if (result === null) return OrderedMap(); 17 | return fromJS(JSON.parse(result)); 18 | }) 19 | ); 20 | 21 | /** 22 | * Retrieves the vaccinations from the presistent storage. 23 | */ 24 | export function* fetchVaccinations() { 25 | try { 26 | const vaccinations = yield call(executeFetchVaccinations); 27 | yield put(fetchVaccinationsSuccess(vaccinations)); 28 | } catch (error) { 29 | yield put(fetchVaccinationsFailure()); 30 | } 31 | } 32 | 33 | /* 34 | * Waits for an FETCH_VACCINATIONS to trigger fetchVaccinations. 35 | * 36 | * Whenever fetchVaccinations is in progress it doesn't trigger a new call of it. 37 | */ 38 | export function* watchFetchVaccinations() { 39 | yield* takeLatest(FETCH_VACCINATIONS, fetchVaccinations); 40 | } 41 | -------------------------------------------------------------------------------- /sagas/index.js: -------------------------------------------------------------------------------- 1 | import { fork } from 'redux-saga/effects'; 2 | import { watchAddVaccination } from './saveVaccinations'; 3 | import { watchFetchVaccinations } from './fetchVaccinations'; 4 | import startup from './startup'; 5 | 6 | /* 7 | * The entry point for all the sagas used in this application. 8 | */ 9 | export default function* root() { 10 | yield [ 11 | fork(watchAddVaccination), 12 | fork(watchFetchVaccinations), 13 | fork(startup), 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /sagas/saveVaccinations.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from 'react-native'; 2 | import { takeEvery } from 'redux-saga'; 3 | import { call, put, select } from 'redux-saga/effects'; 4 | import { ADD_VACCINATION } from '../constants/actions'; 5 | import addVaccinationSuccess from '../actions/addVaccinationSuccess'; 6 | import addVaccinationFailure from '../actions/addVaccinationFailure'; 7 | import { KEY } from '../constants/storage'; 8 | import vaccinationsSelector from '../selectors/vaccinations'; 9 | 10 | /* 11 | * Returns the Promise of the actual saving from the persitent storage. 12 | */ 13 | export const executeSaveVaccinations = (data) => ( 14 | AsyncStorage.setItem(KEY, JSON.stringify(data.toJS())) 15 | ); 16 | 17 | /* 18 | * Retrieves the vaccinations from the Redux store to save them in the presitent storage. 19 | */ 20 | export function* saveVaccinations() { 21 | try { 22 | const vaccinations = yield select(vaccinationsSelector); 23 | yield call(executeSaveVaccinations, vaccinations); 24 | yield put(addVaccinationSuccess()); 25 | } catch (error) { 26 | yield put(addVaccinationFailure()); 27 | } 28 | } 29 | 30 | /* 31 | * Waits for an ADD_VACCINATION to trigger saveVaccinations. 32 | * 33 | * Will trigger a new call of saveVaccinations on every ADD_VACCINATION. 34 | */ 35 | export function* watchAddVaccination() { 36 | yield* takeEvery(ADD_VACCINATION, saveVaccinations); 37 | } 38 | -------------------------------------------------------------------------------- /sagas/startup.js: -------------------------------------------------------------------------------- 1 | import { put } from 'redux-saga/effects'; 2 | import fetchVaccinations from '../actions/fetchVaccinations'; 3 | 4 | export default function* startup() { 5 | yield put(fetchVaccinations()); 6 | } 7 | -------------------------------------------------------------------------------- /selectors/__test__/addForm.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { expect } from 'chai'; 3 | import selector from '../addForm'; 4 | 5 | describe('addForm', () => { 6 | it('retrieves addForm from the state', () => { 7 | expect(selector(Map({ addForm: 'form' }))).to.equal('form'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /selectors/__test__/currentVaccination.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { expect } from 'chai'; 3 | import selector from '../currentVaccination'; 4 | 5 | describe('currentVaccination', () => { 6 | it('retrieves currentVaccination from the state', () => { 7 | const state = fromJS({ 8 | vaccinations: { 9 | 1: { 10 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 11 | }, 12 | 2: { 13 | id: 'f3e08a56-003c-4b46-9dea-216298401ca0', 14 | }, 15 | }, 16 | currentVaccination: '1', 17 | }); 18 | const expected = fromJS({ 19 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 20 | vaccine: { 21 | name: 'AVA (BioThrax)', 22 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 23 | diseases: ['Anthrax'], 24 | }, 25 | listId: '1', 26 | }); 27 | expect(selector(state)).to.equal(expected); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /selectors/__test__/vaccinations.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { expect } from 'chai'; 3 | import selector from '../vaccinations'; 4 | 5 | describe('vaccinations', () => { 6 | it('retrieves with data populated vaccinations from the state', () => { 7 | const state = fromJS({ 8 | vaccinations: { 9 | 1: { 10 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 11 | }, 12 | }, 13 | }); 14 | const expected = fromJS({ 15 | 1: { 16 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 17 | vaccine: { 18 | name: 'AVA (BioThrax)', 19 | id: '8b013618-439e-4829-b88f-98a44b420ee8', 20 | diseases: ['Anthrax'], 21 | }, 22 | listId: '1', 23 | }, 24 | }); 25 | expect(selector(state)).to.equal(expected); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /selectors/__test__/vaccines.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import selector from '../vaccines'; 3 | import vaccines from '../../constants/vaccines'; 4 | 5 | describe('addForm', () => { 6 | it('retrieves the vaccines from constants', () => { 7 | expect(selector()).to.deep.equal(vaccines); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /selectors/addForm.js: -------------------------------------------------------------------------------- 1 | export default (state) => state.get('addForm'); 2 | -------------------------------------------------------------------------------- /selectors/currentVaccination.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import vaccinations from './vaccinations'; 3 | 4 | const currentId = (state) => state.get('currentVaccination'); 5 | 6 | export default createSelector( 7 | [vaccinations, currentId], 8 | (list, id) => list.get(id) 9 | ); 10 | -------------------------------------------------------------------------------- /selectors/vaccinations.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { Map } from 'immutable'; 3 | import shallowVaccines from './vaccines'; 4 | 5 | const shallowVaccinations = (state) => state.get('vaccinations'); 6 | 7 | export default createSelector( 8 | [shallowVaccinations, shallowVaccines], 9 | (vaccinations, vaccines) => { 10 | if (!vaccinations) return Map(); 11 | return vaccinations.map((vaccination, id) => ( 12 | vaccination 13 | .set('vaccine', vaccines.get(vaccination.get('id'))) 14 | .set('listId', id) 15 | )); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /selectors/vaccines.js: -------------------------------------------------------------------------------- 1 | import vaccines from '../constants/vaccines'; 2 | 3 | export default () => vaccines; 4 | -------------------------------------------------------------------------------- /store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import sagaMiddleware from 'redux-saga'; 3 | import createLogger from 'redux-logger'; 4 | import reducer from '../reducers'; 5 | import sagas from '../sagas'; 6 | 7 | const loggerMiddleware = createLogger({ 8 | stateTransformer: state => state.toJS(), 9 | }); 10 | 11 | const createStoreWithMiddleware = applyMiddleware( 12 | sagaMiddleware(sagas), 13 | loggerMiddleware 14 | )(createStore); 15 | 16 | export default (initialState) => ( 17 | createStoreWithMiddleware(reducer, initialState) 18 | ); 19 | -------------------------------------------------------------------------------- /testHelper.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import dirtyChai from 'dirty-chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | 5 | require('babel-polyfill'); 6 | require('react-native-mock/mock'); 7 | 8 | chai.use(dirtyChai); 9 | chai.use(chaiImmutable); 10 | --------------------------------------------------------------------------------