├── .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 | [](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 |
16 |
17 | ## Video Demo
18 |
19 | [](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 |
--------------------------------------------------------------------------------