<file_sep>import React from 'react';
import './contact_list.css';
import PropTypes from 'prop-types';

function compare(a, b){
  if (a.firstName.toLowerCase() > b.firstName.toLowerCase()) return 1;
  if (b.firstName.toLowerCase() > a.firstName.toLowerCase()) return -1;
  if (a.lastName.toLowerCase() > b.lastName.toLowerCase()) return 1;
  if (b.lastName.toLowerCase() > a.lastName.toLowerCase()) return -1;
  return 0;
}

/**
 * Takes a list of contacts an display it
 * You can also select or remove, that will run a function in Book component
 */
const ContactList = (props) => (
  <ul className="contact-list">
    {props.contacts.length > 0 ? (
      props.contacts.sort(compare).map((contact, index) => (
        <li
          key={ index }
          onClick={() => props.selectContact(contact)}
          className={props.selectedId === contact.id ? 'selected' : ''}
        >
          {contact.firstName} {contact.lastName} <button onClick={() => props.removeContact(contact.id)}>Remove</button>
        </li>
      ))
    ) : (
      <li>No contacts</li>
    )}
  </ul>
);

ContactList.propTypes = {
  contacts: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      firstName: PropTypes.string.isRequired,
      lastName: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  selectedId: PropTypes.number,
  selectContact: PropTypes.func.isRequired,
  removeContact: PropTypes.func.isRequired
};

export default ContactList;
<file_sep>export const initialContacts = [
  { firstName: 'Obi-Wan', lastName: 'Kenobi', id: Math.random() },
  { firstName: 'Leia', lastName: 'Organa', id: Math.random() },
  { firstName: 'Han', lastName: 'Solo', id: Math.random() },
  { firstName: 'Luke', lastName: 'Skywalker', id: Math.random() },
  { firstName: 'Darth', lastName: 'Vader', id: Math.random() },
  { firstName: 'Master', lastName: 'Yoda', id: Math.random() },
];
<file_sep>import React  from 'react';
import './contact_form.css';
import PropTypes from 'prop-types';
import darth from './darth.ico';
import c3p0 from './C3PO-icon.png';

/**
 * This component fill the contact data and callback `addContact` or `editContact` depending if you passed `selectedContact`
 */
class ContactForm extends React.Component {
  initialState = {
    id: null, firstName: '', lastName: '', errorMessage: '',
  };

  constructor(props) {
    super(props);
    const selectedContact = props.selectedContact;
    this.state = (selectedContact && {
      id: selectedContact.id,
      firstName: selectedContact.firstName,
      lastName: selectedContact.lastName,
      errorMessage: '',
    }) || this.initialState;
  }

  /**
   * Filling contact info when a contact is selected
   */
  componentWillReceiveProps(nextProps) {
    const selectedContact = nextProps.selectedContact;
    if (selectedContact !== this.props.selectedContact) {
      this.setState({
        id: selectedContact.id, firstName: selectedContact.firstName, lastName: selectedContact.lastName,
      });
    }
  }

  handleInputChange = (e) => {
    const { name, value } = e.target;
    this.setState({
      [name]: value,
    });
  };

  handleSubmit = (e) => {
    e.preventDefault();

    if (!this.state.firstName || !this.state.lastName) {
      this.showErrorMessage();
      return;
    }

    if (this.state.id) {
      this.props.editContact(this.state.id, this.state.firstName, this.state.lastName);
    } else {
      this.props.addContact(this.state.firstName, this.state.lastName);
      this.setState(this.initialState); // Re-initialising the state after adding a new contact
    }
  };

  showErrorMessage = () => {
    this.setState({ errorMessage: 'First name & Last name must be defined' });
    setTimeout(() => {
      this.setState({ errorMessage: '' });
    }, 3000);
  };

  render() {
    return (
      <form className="contact-form" onSubmit={this.handleSubmit}>
        <h2>Contact Form</h2>
        <div className="field">
          <label><img width="20" height="20" src={darth} alt="address book" /> First Name: &nbsp;</label>
          <input type="text" name="firstName" placeholder="e.g: Eder"
                 value={this.state.firstName} onChange={this.handleInputChange} />
        </div>
        <div className="field">
          <label><img width="20" height="20" src={c3p0} alt="address book" /> Last Name: &nbsp;</label>
          <input type="text" name="lastName" placeholder="e.g: Bodelon"
                 value={this.state.lastName} onChange={this.handleInputChange} />
        </div>
        <button>{!this.state.id ? 'Add new' : 'Save'} contact</button>
        <div className="error">{this.state.errorMessage}</div>
      </form>
    )
  }
}

ContactForm.propTypes = {
  selectedContact: PropTypes.object,
  addContact: PropTypes.func.isRequired,
  editContact: PropTypes.func.isRequired
};

export default ContactForm
<file_sep>import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { combineReducers, createStore } from 'redux';
import { BrowserRouter, Route } from 'react-router-dom';

import * as reducers from '../reducers';
import Book from './book';

const store = createStore(combineReducers(reducers));

it('renders without crashing', () => {
  const div = document.createElement('Book');
  ReactDOM.render(
    <Provider store={store}>
      <BrowserRouter>
        <Route path="/" component={Book}/>
      </BrowserRouter>
    </Provider>
  , div);
  ReactDOM.unmountComponentAtNode(div);
});
<file_sep># React Book Address Example

Basic book address created in `React`, `Redux`. Just for fun.
<file_sep>import { matchesText, makeContactFromAction } from './utils';
import { addContact, editContact } from '../actions/book_actions';

it('#matchesText should work', () => {
  expect(matchesText('text', 't')).toBe(true);
  expect(matchesText('text', 'bla')).toBe(false);
  expect(matchesText('text', '')).toBe(true);
});

it('#makeContactFromAction should work', () => {
  const addContactAction = addContact('test', 'test');
  expect(makeContactFromAction(addContactAction)).toEqual(jasmine.objectContaining({
    firstName: 'test', lastName: 'test',
  }));
  expect(makeContactFromAction(editContact(1, 'test', 'test'))).toEqual({
    firstName: 'test', lastName: 'test', id: 1,
  });
});

<file_sep>import * as types from '../constants/action_types';
import * as actions from './book_actions';

describe('Book actions', () => {
  const mockContact = {
    id: 1,
    firstName: 'eder',
    lastName: 'bodelon',
  };

  it(`selectContact should create ${types.SELECT_CONTACT} action`, () => {
    expect(actions.selectContact(mockContact.id)).toEqual({
      type: types.SELECT_CONTACT,
      id: mockContact.id,
    })
  });

  it(`addContact should create ${types.ADD_CONTACT} action`, () => {
    expect(actions.addContact(mockContact.firstName, mockContact.lastName)).toEqual({
      type: types.ADD_CONTACT,
      firstName: mockContact.firstName,
      lastName: mockContact.lastName,
    })
  });

  it(`removeContact should create ${types.REMOVE_CONTACT} action`, () => {
    expect(actions.removeContact(mockContact.id)).toEqual({
      type: types.REMOVE_CONTACT,
      id: mockContact.id,
    });
  });

  it(`editContact should create ${types.EDIT_CONTACT} action`, () => {
    expect(actions.editContact(mockContact.id, mockContact.firstName, mockContact.lastName)).toEqual({
      type: types.EDIT_CONTACT,
      id: mockContact.id,
      firstName: mockContact.firstName,
      lastName: mockContact.lastName,
    });
  });
});
<file_sep>/**
 * Helper that matches a text inside a string
 * @param text
 * @param match
 * @returns {boolean}
 */
export function matchesText(text, match) {
  return text.toLowerCase().indexOf(match.toLowerCase()) > -1;
}

/**
 * Helper function that creates a Contact object from action
 * @see ADD_CONTACT | EDIT_CONTACT
 * @param action(ADD_CONTACT || EDIT_CONTACT)
 * @returns {{firstName: string, lastName: string, id: number}}
 */
export function makeContactFromAction(action) {
  return {
    firstName: action.firstName,
    lastName: action.lastName,
    id: action.id || Math.random(),
  }
}
<file_sep>export const ADD_CONTACT = 'ADD_CONTACT';
export const REMOVE_CONTACT = 'REMOVE_CONTACT';
export const EDIT_CONTACT = 'EDIT_CONTACT';
export const SELECT_CONTACT = 'SELECT_CONTACT';
<file_sep>export { default as book } from './book_reducer';
<file_sep>import React from 'react';
import { connect } from 'react-redux';
import './book.css';

import { addContact, removeContact, editContact, selectContact } from '../actions/book_actions';
import ContactList from '../components/contact_list';
import ContactForm from '../components/contact_form';
import Search from '../components/search';
import { matchesText } from '../utils/utils';
import {Link} from "react-router-dom";

class Book extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      search: '',
    };
  }

  search(e) {
    e.preventDefault();
    const text = e.target.value;
    this.setState({ search: text });
  }

  render() {
    const contacts = this.props.book.contacts.filter((c) => {
      return matchesText(c.firstName, this.state.search) || matchesText(c.lastName, this.state.search);
    });
    const selectedContact = this.props.book.selectedContact;
    return (
      <div className="flex-container book">
        <section>
          <Link to="/">Go to Readme</Link>
          <h2>Address Book</h2>
          <Search value={this.state.search} search={(e) => this.search(e)}/>
          <ContactList
            contacts={contacts}
            selectedId={selectedContact && selectedContact.id}
            selectContact={(contact) => this.props.dispatch(selectContact(contact.id))}
            removeContact={(id) => this.props.dispatch(removeContact(id))}
          />
        </section>
        <section className='contact-form-container'>
          <ContactForm
            addContact={(firstName, lastName) => this.props.dispatch(addContact(firstName, lastName))}
            editContact={(id, firstName, lastName) => this.props.dispatch(editContact(id, firstName, lastName))}
            selectedContact={selectedContact}
          />
        </section>
      </div>
    );
  }
}

export default connect(state => ({
  book: state.book
}))(Book);
