diff --git a/lab-nathan/.babelrc b/lab-nathan/.babelrc
new file mode 100644
index 0000000..cf6ae40
--- /dev/null
+++ b/lab-nathan/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["es2015", "react"],
+ "plugins": ["transform-object-rest-spread"]
+}
\ No newline at end of file
diff --git a/lab-nathan/.dev.env b/lab-nathan/.dev.env
new file mode 100644
index 0000000..dd660b8
--- /dev/null
+++ b/lab-nathan/.dev.env
@@ -0,0 +1 @@
+NODE_ENV='dev'
\ No newline at end of file
diff --git a/lab-nathan/.eslintrc.json b/lab-nathan/.eslintrc.json
new file mode 100644
index 0000000..467fe81
--- /dev/null
+++ b/lab-nathan/.eslintrc.json
@@ -0,0 +1,10 @@
+{
+ "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jest/recommended"],
+ "parser": "babel-eslint",
+ "env": {
+ "browser": true,
+ "node": true,
+ "jest": true
+ },
+ "plugins": ["jest"]
+}
\ No newline at end of file
diff --git a/lab-nathan/.gitignore b/lab-nathan/.gitignore
new file mode 100644
index 0000000..893f389
--- /dev/null
+++ b/lab-nathan/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+build
+.vscode
+coverage
\ No newline at end of file
diff --git a/lab-nathan/package.json b/lab-nathan/package.json
new file mode 100644
index 0000000..a6831d4
--- /dev/null
+++ b/lab-nathan/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "lab-nathan",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack",
+ "watch": "webpack-dev-server --inline --hot",
+ "test": "jest --coverage",
+ "test-watch": "jest --watchAll"
+ },
+ "jest": {
+ "globals": {
+ "__DEBUG__": false,
+ "process.env": {
+ "NODE_ENV": "testing"
+ }
+ }
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
+ "babel-preset-es2015": "^6.24.1",
+ "babel-preset-react": "^6.24.1",
+ "clean-webpack-plugin": "^0.1.16",
+ "css-loader": "^0.28.5",
+ "dotenv": "^4.0.0",
+ "extract-text-webpack-plugin": "^3.0.0",
+ "file-loader": "^0.11.2",
+ "html-webpack-plugin": "^2.30.1",
+ "jest": "^20.0.4",
+ "node-sass": "^4.5.3",
+ "prop-types": "^15.5.10",
+ "react": "^15.6.1",
+ "react-dom": "^15.6.1",
+ "react-redux": "^5.0.6",
+ "react-router-dom": "^4.2.2",
+ "react-test-renderer": "^15.6.1",
+ "redux": "^3.7.2",
+ "sass-loader": "^6.0.6",
+ "superagent": "^3.6.0",
+ "uglifyjs-webpack-plugin": "^0.4.6",
+ "url-loader": "^0.5.9",
+ "uuid": "^3.1.0",
+ "webpack": "^3.5.5",
+ "webpack-dev-server": "^2.7.1"
+ },
+ "devDependencies": {
+ "babel-eslint": "^7.2.3",
+ "eslint": "^4.5.0",
+ "eslint-plugin-jest": "^20.0.3",
+ "eslint-plugin-react": "^7.3.0"
+ }
+}
diff --git a/lab-nathan/src/__test__/category-actions.test.js b/lab-nathan/src/__test__/category-actions.test.js
new file mode 100644
index 0000000..45d5518
--- /dev/null
+++ b/lab-nathan/src/__test__/category-actions.test.js
@@ -0,0 +1,29 @@
+import { categoryCreate, categoryUpdate, categoryDelete } from '../actions/category-actions.js';
+
+describe('Category Actions', () => {
+ test('categoryCreate returns a CATEGORY_CREATE action', () => {
+ let action = categoryCreate({ name: 'test title' });
+ expect(action.type).toEqual('CATEGORY_CREATE');
+ expect(action.payload.id).toBeTruthy();
+ expect(action.payload.timestamp).toBeTruthy();
+ expect(action.payload.name).toBe('test title');
+ });
+
+ test('categoryDelete returns a CATEGORY_DELETE action', () => {
+ let category = { id: '01234', timestamp: new Date(), title: 'test title' };
+ let action = categoryDelete(category);
+ expect(action).toEqual({
+ type: 'CATEGORY_DELETE',
+ payload: category
+ });
+ });
+
+ test('categoryUpdate returns a CATEGORY_UPDATE action', () => {
+ let category = { id: '01234', timestamp: new Date(), title: 'test title' };
+ let action = categoryUpdate(category);
+ expect(action).toEqual({
+ type: 'CATEGORY_UPDATE',
+ payload: category
+ });
+ });
+});
\ No newline at end of file
diff --git a/lab-nathan/src/__test__/category-reducer.test.js b/lab-nathan/src/__test__/category-reducer.test.js
new file mode 100644
index 0000000..a48d535
--- /dev/null
+++ b/lab-nathan/src/__test__/category-reducer.test.js
@@ -0,0 +1,71 @@
+import categoryReducer from '../reducers/category-reducer.js';
+
+describe('Category Reducer', () => {
+ test('initialState should be an empty array', () => {
+ let result = categoryReducer(undefined, { type: null });
+ expect(result).toEqual([]);
+ });
+
+ test('if no action type is presented, the state should be returned', () => {
+ let state = [
+ { id: 'someid', title: 'some title', },
+ { id: 'anotherid', title: 'another title' }
+ ];
+ let result = categoryReducer(state, { type: null });
+ expect(result).toEqual(state);
+ });
+
+ test('CATEGORY_CREATE should append a category to the categories array', () => {
+ let action = {
+ type: 'CATEGORY_CREATE',
+ payload: 'sample payload'
+ };
+
+ let result = categoryReducer([], action);
+ expect(result.length).toBe(1);
+ expect(result[0]).toBe(action.payload);
+ });
+
+ test('CATEGORY_UPDATE should update a category', () => {
+ let createAction = {
+ type: 'CATEGORY_CREATE',
+ payload: { name: 'sample payload', id: "1" }
+ };
+
+ let createResult = categoryReducer([], createAction);
+
+ let createAction2 = {
+ type: 'CATEGORY_CREATE',
+ payload: { name: 'sample payload 2', id: "2" }
+ };
+
+ let createResult2 = categoryReducer(createResult, createAction2);
+
+ let updateAction = {
+ type: 'CATEGORY_UPDATE',
+ payload: { name: 'updated payload', id: "1" }
+ };
+
+ let updateResult = categoryReducer(createResult2, updateAction);
+
+ expect(updateResult[0]).toBe(updateAction.payload);
+ });
+
+ test('CATEGORY_DELETE should delete a category', () => {
+ let createAction = {
+ type: 'CATEGORY_CREATE',
+ payload: { name: 'sample payload', id: "1" }
+ };
+
+ let createResult = categoryReducer([], createAction);
+
+ let deleteAction = {
+ type: 'CATEGORY_DELETE',
+ payload: { name: 'sample payload', id: "1" }
+ };
+
+ let deleteResult = categoryReducer(createResult, deleteAction);
+
+ expect(deleteResult.length).toBe(0);
+ });
+});
\ No newline at end of file
diff --git a/lab-nathan/src/__test__/expense-actions.test.js b/lab-nathan/src/__test__/expense-actions.test.js
new file mode 100644
index 0000000..be724ac
--- /dev/null
+++ b/lab-nathan/src/__test__/expense-actions.test.js
@@ -0,0 +1,30 @@
+import { expenseCreate, expenseUpdate, expenseDelete } from '../actions/expense-actions.js';
+
+describe('Expense Actions', () => {
+ test('expenseCreate returns a EXPENSE_CREATE action', () => {
+ let {payload, type} = expenseCreate({ name: 'test name', budget: "1342" });
+ expect(type).toEqual('EXPENSE_CREATE');
+ expect(payload.id).toBeTruthy();
+ expect(payload.timestamp).toBeTruthy();
+ expect(payload.name).toBe('test name');
+ expect(payload.budget).toBe("1342");
+ });
+
+ test('expenseDelete returns a EXPENSE_DELETE action', () => {
+ let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" };
+ let action = expenseDelete(expense);
+ expect(action).toEqual({
+ type: 'EXPENSE_DELETE',
+ payload: expense
+ });
+ });
+
+ test('expenseUpdate returns a EXPENSE_UPDATE action', () => {
+ let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" };
+ let action = expenseUpdate(expense);
+ expect(action).toEqual({
+ type: 'EXPENSE_UPDATE',
+ payload: expense
+ });
+ });
+});
\ No newline at end of file
diff --git a/lab-nathan/src/__test__/expense-reducer.test.js b/lab-nathan/src/__test__/expense-reducer.test.js
new file mode 100644
index 0000000..5e3b236
--- /dev/null
+++ b/lab-nathan/src/__test__/expense-reducer.test.js
@@ -0,0 +1,72 @@
+import expenseReducer from '../reducers/expense-reducer.js';
+
+describe('Expense Reducer', () => {
+ test('initialState should be an empty object', () => {
+ let result = expenseReducer(undefined, { type: null });
+ expect(result).toEqual({});
+ });
+
+ test('if no action type is presented, the state should be returned', () => {
+ let state = {
+ 0: [{ id: 'someid', title: 'some title', }],
+ 1: [{ id: 'anotherid', title: 'another title' }]
+ };
+ let result = expenseReducer(state, { type: null });
+ expect(result).toEqual(state);
+ });
+
+ test('CATEGORY_CREATE should create an empty array at the supplied category id', () => {
+ let action = {
+ type: 'CATEGORY_CREATE',
+ payload: { name: 'sample payload', id: "1" }
+ };
+
+ let result = expenseReducer({}, action);
+ expect(result[1]).toEqual([]);
+ });
+
+ test('CATEGORY_DELETE should delete the array with the supplied category id', () => {
+ let action = {
+ type: 'CATEGORY_DELETE',
+ payload: { name: 'sample payload', id: "1" }
+ };
+
+ let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action);
+ expect(result).toEqual({});
+ });
+
+ test('EXPENSE_CREATE should append a expense to the categories array', () => {
+ let action = {
+ type: 'EXPENSE_CREATE',
+ payload: { id: 'someid', categoryId: '1', title: 'another title' }
+ };
+
+ let result = expenseReducer({ 1: [] }, action);
+ expect(result[1].length).toBe(1);
+ expect(result[1][0]).toBe(action.payload);
+ });
+
+ test('EXPENSE_UPDATE should update a expense', () => {
+ let action = {
+ type: 'EXPENSE_UPDATE',
+ payload: { id: 'someid', categoryId: '1', title: 'updated title' }
+ };
+
+ let result = expenseReducer({
+ 1: [ { id: 'someid', categoryId: '1', title: 'another title' }, { id: 'someid2', categoryId: '1', title: 'another title2' } ],
+ 2: [ { id: 'someid', categoryId: '2', title: 'another title' }, { id: 'someid2', categoryId: '2', title: 'another title2' } ]
+ }, action);
+
+ expect(result[1][0]).toBe(action.payload);
+ });
+
+ test('EXPENSE_DELETE should delete a expense', () => {
+ let action = {
+ type: 'EXPENSE_DELETE',
+ payload: { id: 'someid', categoryId: '1', title: 'updated title' }
+ };
+
+ let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action);
+ expect(result[1].length).toBe(0);
+ });
+});
\ No newline at end of file
diff --git a/lab-nathan/src/actions/category-actions.js b/lab-nathan/src/actions/category-actions.js
new file mode 100644
index 0000000..ec0aff0
--- /dev/null
+++ b/lab-nathan/src/actions/category-actions.js
@@ -0,0 +1,20 @@
+import uuid from 'uuid/v1';
+
+export const categoryCreate = (category) => {
+ category.id = uuid();
+ category.timestamp = new Date();
+ return {
+ type: 'CATEGORY_CREATE',
+ payload: category
+ }
+};
+
+export const categoryUpdate = (category) => ({
+ type: 'CATEGORY_UPDATE',
+ payload: category
+});
+
+export const categoryDelete = (category) => ({
+ type: 'CATEGORY_DELETE',
+ payload: category
+});
diff --git a/lab-nathan/src/actions/expense-actions.js b/lab-nathan/src/actions/expense-actions.js
new file mode 100644
index 0000000..6582567
--- /dev/null
+++ b/lab-nathan/src/actions/expense-actions.js
@@ -0,0 +1,20 @@
+import uuid from 'uuid/v1';
+
+export const expenseCreate = (expense) => {
+ expense.id = uuid();
+ expense.timestamp = new Date();
+ return {
+ type: 'EXPENSE_CREATE',
+ payload: expense
+ }
+};
+
+export const expenseUpdate = (expense) => ({
+ type: 'EXPENSE_UPDATE',
+ payload: expense
+});
+
+export const expenseDelete = (expense) => ({
+ type: 'EXPENSE_DELETE',
+ payload: expense
+});
diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js
new file mode 100644
index 0000000..73a9a95
--- /dev/null
+++ b/lab-nathan/src/components/app/app.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { BrowserRouter, Route } from 'react-router-dom';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import reducers from '../../reducers/reducers.js';
+import Dashboard from '../dashboard/dashboard.js';
+
+const store = createStore(reducers);
+
+class App extends React.Component {
+ componentDidMount() {
+ store.subscribe(() => {
+ console.log('__STATE__', store.getState());
+ });
+
+ store.dispatch({ type: null });
+ }
+
+ render() {
+ return (
+