diff --git a/package.json b/package.json
index c6d9612..fd61de2 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,14 @@
"debug": "^2.2.0",
"evil-icons": "^1.8.0",
"express": "^4.13.4",
+ "prop-types": "^15.5.10",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"react-evil-icons": "^0.4.0",
"react-redux": "^5.0.5",
"redux": "^3.5.2",
"sancus": "^1.0.3",
+ "universal-cookie": "^2.0.8",
"webpack": "^3.3.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-dev-server": "^2.5.1",
diff --git a/src/actions/contact.js b/src/actions/contact.js
new file mode 100644
index 0000000..31b2130
--- /dev/null
+++ b/src/actions/contact.js
@@ -0,0 +1,31 @@
+import * as types from './types';
+
+export const addContact = (name, phone, email) => ({
+ type: types.ADD_CONTACT,
+ name,
+ phone,
+ email
+});
+
+export const deleteContact = id => ({
+ type: types.DELETE_CONTACT,
+ id
+});
+
+export const saveContact = (id, name, phone, email) => ({
+ type: types.SAVE_CONTACT,
+ id,
+ name,
+ phone,
+ email
+});
+
+export const addFavourite = id => ({
+ type: types.ADD_FAVOURITE,
+ id
+});
+
+export const deleteFavourite = id => ({
+ type: types.DELETE_FAVOURITE,
+ id
+});
diff --git a/src/actions/types.js b/src/actions/types.js
new file mode 100644
index 0000000..5bc5647
--- /dev/null
+++ b/src/actions/types.js
@@ -0,0 +1,5 @@
+export const ADD_CONTACT = 'ADD_CONTACT';
+export const DELETE_CONTACT = 'DELETE_CONTACT';
+export const SAVE_CONTACT = 'SAVE_CONTACT';
+export const ADD_FAVOURITE = 'ADD_FAVOURITE';
+export const DELETE_FAVOURITE = 'DELETE_FAVOURITE';
diff --git a/src/app/app.jsx b/src/app/app.jsx
index b1474ec..3add506 100644
--- a/src/app/app.jsx
+++ b/src/app/app.jsx
@@ -1,16 +1,40 @@
import React from 'react';
import './app.scss';
-import { Column, Row, Title, Button } from '../components';
+import { Column, Row, Title, ContactEdit, Modal, Button } from '../components';
+import { Contacts, Favourites } from '../containers';
-const onAddClick = (event) => {
-};
+class App extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ editModalVisible: false
+ };
+ }
-const App = () => (
-
- Contact Book
-
-
-);
+ onClick = () => {
+ this.setState({ editModalVisible: true });
+ }
+
+ render = () => (
+
+ Contact Book
+
+
+
+
+ { this.setState({ editModalVisible: false }); }}
+ visible={this.state.editModalVisible}
+ >
+ { this.setState({ editModalVisible: false }); }}
+ contact={{ id: -1, name: '', phone: '', email: '' }}
+ />
+
+
+ )
+}
export default App;
diff --git a/src/app/app.scss b/src/app/app.scss
index c161449..13715f1 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -4,3 +4,42 @@
align-items: center;
width: 100%;
}
+
+.contact-margin {
+ margin: 10px;
+}
+
+.modal {
+ width: 50%;
+ height: 50%;
+ background-color: white;
+}
+
+.contact-edit-modal {
+ width: 50%;
+ min-width: 900px;
+ height: 50px;
+ background-color: white;
+}
+
+.blank {
+ height: 50px;
+}
+
+.name-width {
+ width: 300px;
+}
+
+.phone-width {
+ width: 200px;
+}
+
+.email-width {
+ width: 300px;
+}
+
+.button {
+ width: 100px;
+ height: 30px;
+ margin: auto 10px;
+}
diff --git a/src/components/controls/button.jsx b/src/components/controls/button.jsx
index 2b1a0d9..d2776c9 100644
--- a/src/components/controls/button.jsx
+++ b/src/components/controls/button.jsx
@@ -10,19 +10,21 @@ const resolveButtonLabel = (children, label) => {
return 'Label';
};
-const Button = ({ children, label, onClick }) => (
-
+const Button = ({ className, children, label, onClick }) => (
+
);
Button.propTypes = {
children: PropTypes.string,
label: PropTypes.string,
- onClick: PropTypes.func.isRequired
+ onClick: PropTypes.func.isRequired,
+ className: PropTypes.string.isRequired
};
Button.defaultProps = {
label: null,
- children: null
+ children: null,
+ className: 'button'
};
export default Button;
diff --git a/src/components/controls/contact-edit.jsx b/src/components/controls/contact-edit.jsx
new file mode 100644
index 0000000..1cbe786
--- /dev/null
+++ b/src/components/controls/contact-edit.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import '../../app/app.scss';
+import { Column, Row } from '../grid';
+import { Button } from '../controls';
+import { ContactSaveButton, ContactDeleteButton } from '../../containers';
+
+class ContactEdit extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ name: this.props.contact.name,
+ phone: this.props.contact.phone,
+ email: this.props.contact.email
+ };
+ }
+
+ render = () => (
+
+
+
+ { this.setState({ name: e.target.value }); }}
+ />
+
+
+ { this.setState({ phone: e.target.value }); }}
+ />
+
+
+ { this.setState({ email: e.target.value }); }}
+ />
+
+
+
+ )
+}
+
+ContactEdit.propTypes = {
+ contact: PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ phone: PropTypes.string,
+ email: PropTypes.string }).isRequired,
+ close: PropTypes.func.isRequired
+};
+
+export default ContactEdit;
diff --git a/src/components/controls/contact-list.jsx b/src/components/controls/contact-list.jsx
new file mode 100644
index 0000000..7b321cf
--- /dev/null
+++ b/src/components/controls/contact-list.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import '../../app/app.scss';
+import Contact from './contact';
+import { Column, Row } from '../grid';
+
+const ContactList = ({ contacts, defaultText, defaultEmptyText }) => (
+
+ {
+ contacts.length > 0
+ ?
+ {defaultText}
+ {contacts.map(contact => (
+
+ ))}
+
+ : defaultEmptyText
+ }
+
+);
+
+ContactList.propTypes = {
+ contacts: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ phone: PropTypes.string,
+ email: PropTypes.string,
+ favourite: PropTypes.bool
+ })).isRequired,
+ defaultText: PropTypes.string.isRequired,
+ defaultEmptyText: PropTypes.string.isRequired
+};
+
+export default ContactList;
diff --git a/src/components/controls/contact.jsx b/src/components/controls/contact.jsx
new file mode 100644
index 0000000..02be424
--- /dev/null
+++ b/src/components/controls/contact.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Button, ContactEdit, Modal } from '../controls';
+import { ContactFavouriteButton, ContactUnfavouriteButton } from '../../containers';
+import '../../app/app.scss';
+import { Column, Row } from '../grid';
+import { Label } from '../typography';
+
+class Contact extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ editModalVisible: false
+ };
+ }
+
+ render = () => (
+
+
+
+
+
+ )
+}
+
+Contact.propTypes = {
+ contact: PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ phone: PropTypes.string,
+ email: PropTypes.string,
+ favourite: PropTypes.bool
+ }).isRequired
+};
+
+export default Contact;
diff --git a/src/components/controls/index.js b/src/components/controls/index.js
index 36d4bc7..fed1b3d 100644
--- a/src/components/controls/index.js
+++ b/src/components/controls/index.js
@@ -1,4 +1,8 @@
import IconButton from './icon-button';
import Button from './button';
+import ContactEdit from './contact-edit';
+import ContactList from './contact-list';
+import Contact from './contact';
+import Modal from './modal';
-export { IconButton, Button };
+export { Button, ContactEdit, ContactList, Contact, IconButton, Modal };
diff --git a/src/components/controls/modal.jsx b/src/components/controls/modal.jsx
new file mode 100644
index 0000000..024f7f8
--- /dev/null
+++ b/src/components/controls/modal.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+class Modal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ styles: this.getStyles(),
+ overlayVisible: false,
+ modalVisible: false
+ };
+ }
+
+ componentWillReceiveProps(newProps) {
+ let visible = {};
+ if (newProps.visible) {
+ visible = {
+ overlayVisible: true,
+ modalVisible: true
+ };
+ } else {
+ visible = {
+ overlayVisible: false,
+ modalVisible: false
+ };
+ }
+ this.setState(visible);
+ }
+
+ getStyles = () => ({
+ overlay: {
+ background: 'rgba(0,0,0,0.6)',
+ bottom: 0,
+ display: 'block',
+ left: 0,
+ overflowY: 'auto',
+ position: 'fixed',
+ right: 0,
+ top: 0,
+ zIndex: 9999999
+ },
+ wrapper: {
+ bottom: 0,
+ boxSizing: 'border-box',
+ display: 'table',
+ height: '100%',
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ textAlign: 'center',
+ top: 0,
+ width: '100%'
+ },
+ subWrapper: {
+ display: 'table-cell',
+ verticalAlign: 'middle'
+ },
+ modal: {
+ margin: '0 auto'
+ }
+ })
+
+ stopPropagation = (e) => {
+ e.stopPropagation();
+ }
+
+ renderModal = () => {
+ if (this.state.modalVisible) {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+ return null;
+ }
+
+ renderContentOverlay = () => (
+
+ {this.renderModal()}
+
+ )
+
+ renderOverlay = () => {
+ if (this.state.overlayVisible) {
+ return (
+
+
+ {this.renderContentOverlay()}
+
+
+ );
+ }
+ return null;
+ }
+
+ render() {
+ return (
+
+ {this.renderOverlay()}
+
+ );
+ }
+}
+
+Modal.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ onClickOverlay: PropTypes.func.isRequired
+ // visible: PropTypes.bool
+};
+
+Modal.defaultProps = {
+ className: 'modal'
+};
+
+export default Modal;
diff --git a/src/components/index.js b/src/components/index.js
index ddadfeb..519cc6d 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,5 +1,5 @@
import './resets.scss';
-export { IconButton, Button } from './controls';
+export { Button, ContactEdit, ContactList, Contact, IconButton, Modal } from './controls';
export { Box, Column, Row } from './grid';
export { Title, Subtitle, Label, Description } from './typography';
diff --git a/src/containers/contact-delete-button.jsx b/src/containers/contact-delete-button.jsx
new file mode 100644
index 0000000..cf15b38
--- /dev/null
+++ b/src/containers/contact-delete-button.jsx
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import { deleteContact } from '../actions/contact';
+import Button from '../components/controls/button';
+
+const mapDispatchToProps = (dispatch, props) => ({
+ onClick: () => {
+ dispatch(deleteContact(props.id));
+ props.onClick();
+ },
+ label: 'Delete'
+});
+
+const ContactDeleteButton = connect(
+ null,
+ mapDispatchToProps
+)(Button);
+
+export default ContactDeleteButton;
diff --git a/src/containers/contact-favourite-button.jsx b/src/containers/contact-favourite-button.jsx
new file mode 100644
index 0000000..f305c63
--- /dev/null
+++ b/src/containers/contact-favourite-button.jsx
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+
+import { addFavourite } from '../actions/contact';
+import Button from '../components/controls/button';
+
+const mapDispatchToProps = (dispatch, props) => ({
+ onClick: () => {
+ dispatch(addFavourite(props.id));
+ },
+ label: 'Favourite'
+});
+
+const ContactFavouriteButton = connect(
+ null,
+ mapDispatchToProps
+)(Button);
+
+export default ContactFavouriteButton;
diff --git a/src/containers/contact-save-button.jsx b/src/containers/contact-save-button.jsx
new file mode 100644
index 0000000..d2d7cc4
--- /dev/null
+++ b/src/containers/contact-save-button.jsx
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux';
+
+import { saveContact, addContact } from '../actions/contact';
+import Button from '../components/controls/button';
+
+const mapDispatchToProps = (dispatch, props) => ({
+ onClick: () => {
+ if (props.contact.id < 0) {
+ dispatch(addContact(props.contact.name, props.contact.phone, props.contact.email));
+ } else {
+ dispatch(saveContact(props.contact.id, props.contact.name, props.contact.phone, props.contact.email));
+ }
+ props.onClick();
+ },
+ label: 'Save Contact'
+});
+
+const ContactSaveButton = connect(
+ null,
+ mapDispatchToProps
+)(Button);
+
+export default ContactSaveButton;
diff --git a/src/containers/contact-unfavourite-button.jsx b/src/containers/contact-unfavourite-button.jsx
new file mode 100644
index 0000000..bbe7551
--- /dev/null
+++ b/src/containers/contact-unfavourite-button.jsx
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+
+import { deleteFavourite } from '../actions/contact';
+import Button from '../components/controls/button';
+
+const mapDispatchToProps = (dispatch, props) => ({
+ onClick: () => {
+ dispatch(deleteFavourite(props.id));
+ },
+ label: 'Unfavourite'
+});
+
+const ContactUnfavouriteButton = connect(
+ null,
+ mapDispatchToProps
+)(Button);
+
+export default ContactUnfavouriteButton;
diff --git a/src/containers/contacts.jsx b/src/containers/contacts.jsx
new file mode 100644
index 0000000..81289aa
--- /dev/null
+++ b/src/containers/contacts.jsx
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import ContactList from '../components/controls/contact-list';
+
+const mapStateToProps = state => ({
+ defaultText: 'CONTACTS',
+ defaultEmptyText: "You don't have any contact. Please click 'Add Contact' button to add contact.",
+ contacts: state.Contact.contacts
+});
+
+const Contacts = connect(
+ mapStateToProps,
+ null
+)(ContactList);
+
+export default Contacts;
diff --git a/src/containers/favourites.jsx b/src/containers/favourites.jsx
new file mode 100644
index 0000000..f229b69
--- /dev/null
+++ b/src/containers/favourites.jsx
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import ContactList from '../components/controls/contact-list';
+
+const mapStateToProps = state => ({
+ defaultText: 'FAVOURITE CONTACTS',
+ defaultEmptyText: "You don't have any favourite contacts.",
+ contacts: state.Contact.contacts.filter(element => (element.favourite))
+});
+
+const Favourites = connect(
+ mapStateToProps,
+ null
+)(ContactList);
+
+export default Favourites;
diff --git a/src/containers/index.js b/src/containers/index.js
new file mode 100644
index 0000000..b709937
--- /dev/null
+++ b/src/containers/index.js
@@ -0,0 +1,15 @@
+import ContactDeleteButton from './contact-delete-button';
+import ContactFavouriteButton from './contact-favourite-button';
+import ContactSaveButton from './contact-save-button';
+import ContactUnfavouriteButton from './contact-unfavourite-button';
+import Contacts from './contacts';
+import Favourites from './favourites';
+
+export {
+ ContactDeleteButton,
+ ContactFavouriteButton,
+ ContactSaveButton,
+ ContactUnfavouriteButton,
+ Contacts,
+ Favourites
+};
diff --git a/src/index.jsx b/src/index.jsx
index 3930082..46f9b3b 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,10 +1,18 @@
import React from 'react';
import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+import { createStore } from 'redux';
+
import App from './app/app';
+import Reducers from './reducer';
try {
+ const store = createStore(Reducers);
const appContainer = document.getElementById('app');
- render(, appContainer);
+ render(
+
+
+ , appContainer);
} catch (e) {
/* eslint-disable no-console */
console.warn('main.js is unable to find application container.');
diff --git a/src/reducer/contact.jsx b/src/reducer/contact.jsx
new file mode 100644
index 0000000..29e1637
--- /dev/null
+++ b/src/reducer/contact.jsx
@@ -0,0 +1,92 @@
+import Cookies from 'universal-cookie';
+
+import {
+ ADD_CONTACT,
+ DELETE_CONTACT,
+ SAVE_CONTACT,
+ ADD_FAVOURITE,
+ DELETE_FAVOURITE
+} from '../actions/types';
+
+const cookies = new Cookies();
+const initialState = {
+ contacts: cookies.get('contacts') ? cookies.get('contacts') : []
+};
+
+const contact = (state = initialState, action) => {
+ let contacts = [];
+ switch (action.type) {
+ case ADD_CONTACT:
+ contacts = [
+ ...state.contacts,
+ {
+ id: state.contacts.length === 0 ? 0 : state.contacts[state.contacts.length - 1].id + 1,
+ name: action.name,
+ phone: action.phone,
+ email: action.email,
+ favourite: false
+ }
+ ];
+ cookies.set('contacts', contacts);
+ return {
+ ...state, contacts
+ };
+ case DELETE_CONTACT:
+ contacts = Object.values(Object.assign({}, state.contacts));
+ if (contacts.findIndex(element => (element.id === action.id)) > -1) {
+ contacts.splice(contacts.findIndex(element => (element.id === action.id)), 1);
+ cookies.set('contacts', contacts);
+ return {
+ ...state,
+ contacts
+ };
+ }
+ return state;
+ case SAVE_CONTACT:
+ contacts = state.contacts.map((element) => {
+ if (element.id === action.id) {
+ return {
+ id: action.id,
+ name: action.name,
+ phone: action.phone,
+ email: action.email,
+ favourite: element.favourite
+ };
+ }
+ return element;
+ });
+ cookies.set('contacts', contacts);
+ return {
+ ...state,
+ contacts
+ };
+ case ADD_FAVOURITE:
+ contacts = state.contacts.map((element) => {
+ if (element.id === action.id) {
+ return { ...element, favourite: true };
+ }
+ return element;
+ });
+ cookies.set('contacts', contacts);
+ return {
+ ...state,
+ contacts
+ };
+ case DELETE_FAVOURITE:
+ contacts = state.contacts.map((element) => {
+ if (element.id === action.id) {
+ return { ...element, favourite: false };
+ }
+ return element;
+ });
+ cookies.set('contacts', contacts);
+ return {
+ ...state,
+ contacts
+ };
+ default:
+ return state;
+ }
+};
+
+export default contact;
diff --git a/src/reducer/index.js b/src/reducer/index.js
new file mode 100644
index 0000000..271a6e5
--- /dev/null
+++ b/src/reducer/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux';
+import Contact from './contact';
+
+const Reducers = combineReducers({
+ Contact
+});
+
+export default Reducers;