diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..15a15b2
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..24eb271
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index c410316..e3fcde8 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "classnames": "^2.2.6",
+ "prop-types": "^15.6.2",
"react": "^16.6.0",
"react-dom": "^16.6.0",
- "react-scripts": "2.1.1"
+ "react-redux": "^5.1.1",
+ "react-router-dom": "^4.3.1",
+ "react-scripts": "2.1.1",
+ "redux": "^4.0.1"
},
"scripts": {
"start": "react-scripts start",
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 7e261ca..0000000
--- a/src/App.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { Component } from 'react';
-import logo from './logo.svg';
-import './App.css';
-
-class App extends Component {
- render() {
- return (
-
- );
- }
-}
-
-export default App;
diff --git a/src/actions/recipes.js b/src/actions/recipes.js
new file mode 100644
index 0000000..13e8c96
--- /dev/null
+++ b/src/actions/recipes.js
@@ -0,0 +1,24 @@
+import * as consts from '../constants/actions-types';
+
+export const addRecipe = (recipe) => ({
+ type: consts.ADD_RECIPE,
+ payload: {
+ title: recipe.title,
+ description: recipe.description,
+ id: recipe.id
+ }
+});
+
+export const toggleRecipe = (recipe) => ({
+ type: consts.TOGGLE_RECIPE,
+ payload: recipe.id
+});
+
+export const fetchRecipes = () => ({
+ type: consts.API,
+ payload: {
+ url: 'recipes.json',
+ success: consts.SET_RECIPES
+ }
+
+})
diff --git a/src/actions/user.js b/src/actions/user.js
new file mode 100644
index 0000000..9868770
--- /dev/null
+++ b/src/actions/user.js
@@ -0,0 +1,6 @@
+import {SET_USER} from '../constants/actions-types'
+
+export const changeUser = (name) => ({
+ type: SET_USER,
+ payload: {name}
+});
\ No newline at end of file
diff --git a/src/components/Recipes/AddRecipe.js b/src/components/Recipes/AddRecipe.js
new file mode 100644
index 0000000..c716f0e
--- /dev/null
+++ b/src/components/Recipes/AddRecipe.js
@@ -0,0 +1,41 @@
+import React, {Component} from "react";
+import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
+import {addRecipe} from '../../actions/recipes';
+import {withRouter} from 'react-router-dom';
+import {getID} from "../../services/utils";
+
+class AddRecipe extends Component {
+
+ onSubmit(e) {
+ e.preventDefault();
+
+ const id = getID();
+ this.props.addRecipe({id, title: this.title.value, description: this.description.value});
+ this.title.value = '';
+ this.description.value = '';
+ }
+
+ render() {
+ return (
+
+
+ )
+ }
+}
+
+AddRecipe.propTypes = {
+ addRecipe: PropTypes.func.isRequired
+};
+
+
+export default withRouter(connect(null, {addRecipe})(AddRecipe));
diff --git a/src/components/Recipes/Recipe.js b/src/components/Recipes/Recipe.js
new file mode 100644
index 0000000..b46fdf1
--- /dev/null
+++ b/src/components/Recipes/Recipe.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { NavLink } from 'react-router-dom';
+
+export const Recipe = ({recipe}) => (
+
+ {recipe.title}
+
+);
+
+Recipe.propTypes = {
+ recipe: PropTypes.object.isRequired
+};
diff --git a/src/components/Recipes/RecipeDetails.js b/src/components/Recipes/RecipeDetails.js
new file mode 100644
index 0000000..58e5f04
--- /dev/null
+++ b/src/components/Recipes/RecipeDetails.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import ToggleBox from "../toggle-box/ToggleBox";
+import {toggleRecipe} from "../../actions/recipes";
+
+const RecipeDetails = ({ recipe, toggleRecipe }) => (
+
+
{ recipe.title }
+
{ recipe.description }
+
toggleRecipe(recipe)} />
+
+
+);
+
+const RecipeDetailsWrapper = ({ recipe, toggleRecipe }) =>
+ recipe
+ ?
+ : Not found
;
+
+
+RecipeDetails.propTypes = {
+ recipe: PropTypes.object.isRequired,
+ toggleRecipe: PropTypes.func.isRequired
+};
+
+const mapStateToProps = (state, ownProps) => {
+ const id = parseInt(ownProps.match.params.id, 10);
+
+ return {
+ recipe: state.recipes.find(recipe => recipe.id === id)
+ };
+};
+
+
+export default connect(mapStateToProps,{toggleRecipe})(RecipeDetailsWrapper);
diff --git a/src/components/Recipes/Recipes.css b/src/components/Recipes/Recipes.css
new file mode 100644
index 0000000..7fb9c96
--- /dev/null
+++ b/src/components/Recipes/Recipes.css
@@ -0,0 +1,58 @@
+.recipes {
+ border: 2px solid orange;
+ padding: 20px;
+ height: 100%;
+ margin: 20px auto;
+}
+
+.recipe {
+ padding: 20px;
+ border: 5px solid orange;
+ border-radius: 4px;
+ margin-bottom: 5px;
+ display: block;
+}
+
+.recipe.current {
+ font-weight: bold;
+ color: black;
+ display: block;
+ border-color: black;
+}
+
+.actions {
+ display: flex;
+ justify-content: space-between;
+}
+
+.fetch-recipes {
+ text-decoration: underline;
+ cursor: pointer;
+}
+.favorite {
+ font-weight: bold;
+ background: yellow;
+}
+
+.recipes-view {
+ display: flex;
+ text-align: left;
+}
+
+.recipes-view .middle-grid {
+ width: 30%;
+ margin-right: 20px;
+ margin-top: 5px;
+}
+
+
+.recipes-view .left-grid {
+ width: 40%;
+ border: 2px solid orange;
+ text-align: center;
+ box-shadow: 6px 4px 6px -2px;
+ padding: 15px;
+ height: 100%;
+ line-height: 30px;
+ margin-top: 25px;
+}
\ No newline at end of file
diff --git a/src/components/Recipes/Recipes.js b/src/components/Recipes/Recipes.js
new file mode 100644
index 0000000..7c7ac0e
--- /dev/null
+++ b/src/components/Recipes/Recipes.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import {Recipe} from "./Recipe";
+import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
+import {toggleRecipe as toggle, fetchRecipes} from '../../actions/recipes';
+import {Link, withRouter} from 'react-router-dom';
+
+const Recipes = (props) => (
+
+ {props.recipes.map(recipe =>
)}
+
+ Add Recipe
+ Change user name
+ reset Recipes
+
+
+);
+
+Recipes.propTypes = {
+ recipes: PropTypes.array.isRequired,
+ toggle: PropTypes.func.isRequired,
+ fetchRecipes: PropTypes.func.isRequired
+};
+
+const mapStateToProps = (state) => {
+ return {
+ recipes: state.recipes
+ }
+};
+
+export default withRouter(connect(mapStateToProps, {toggle, fetchRecipes})(Recipes));
\ No newline at end of file
diff --git a/src/components/Recipes/RecipesView.js b/src/components/Recipes/RecipesView.js
new file mode 100644
index 0000000..81881cf
--- /dev/null
+++ b/src/components/Recipes/RecipesView.js
@@ -0,0 +1,60 @@
+import React, {Component} from 'react';
+import Recipes from './Recipes';
+import AddRecipe from './AddRecipe';
+import {Route, Switch, withRouter} from 'react-router-dom';
+import RecipeDetails from "./RecipeDetails";
+import ChangeUser from "../change-user/ChangeUser";
+import {fetchRecipes} from '../../actions/recipes';
+import PropTypes from 'prop-types';
+import './Recipes.css';
+import {connect} from "react-redux";
+import User from "../user/User";
+
+
+/**
+ * add actions to the components and constants
+ */
+
+class RecipesView extends Component {
+
+ componentDidMount() {
+ this.refresh();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+ Welcome
}/>
+
+
+
+
+
+
+
+
+ )
+ }
+
+ refresh() {
+ this.props.fetchRecipes();
+ }
+}
+
+RecipesView.propTypes = {
+ fetchRecipes: PropTypes.func.isRequired
+}
+
+const mapStateToProps = (state) => {
+ return {
+ }
+};
+export default withRouter(connect(mapStateToProps, {fetchRecipes})(RecipesView));
diff --git a/src/App.css b/src/components/app/App.css
similarity index 100%
rename from src/App.css
rename to src/components/app/App.css
diff --git a/src/components/app/App.js b/src/components/app/App.js
new file mode 100644
index 0000000..0dce50c
--- /dev/null
+++ b/src/components/app/App.js
@@ -0,0 +1,29 @@
+import React, {Component} from 'react';
+import './App.css';
+import RecipesView from "../Recipes/RecipesView";
+import Header from "../header/Header";
+import {Footer} from "../footer/Footer";
+
+
+/**Connection with redux:
+ * clean up and fetch recipes
+ *
+ * @param props
+ * @returns {*}
+ * @constructor
+ */
+
+class App extends Component {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+
+}
+
+export default App;
diff --git a/src/App.test.js b/src/components/app/App.test.js
similarity index 100%
rename from src/App.test.js
rename to src/components/app/App.test.js
diff --git a/src/components/change-user/ChangeUser.js b/src/components/change-user/ChangeUser.js
new file mode 100644
index 0000000..07a4b2e
--- /dev/null
+++ b/src/components/change-user/ChangeUser.js
@@ -0,0 +1,39 @@
+import React,{Component} from 'react';
+import PropTypes from'prop-types';
+import {connect} from "react-redux";
+import {changeUser} from "../../actions/user";
+import {withRouter} from "react-router-dom";
+
+
+
+class ChangeUser extends Component {
+
+ submit(e){
+ e.preventDefault();
+ this.props.changeUser(this.userName.value);
+ this.userName.value = '';
+ this.props.history.push(`/`);
+
+ }
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+ChangeUser.propTypes = {
+ changeUser: PropTypes.func.isRequired
+};
+
+
+export default withRouter(connect(null,{changeUser})(ChangeUser))
+
diff --git a/src/components/footer/Footer.css b/src/components/footer/Footer.css
new file mode 100644
index 0000000..7b7f0b2
--- /dev/null
+++ b/src/components/footer/Footer.css
@@ -0,0 +1,8 @@
+.basic-footer {
+ background: orange;
+ padding: 10px;
+ color: white;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/components/footer/Footer.js b/src/components/footer/Footer.js
new file mode 100644
index 0000000..3534e57
--- /dev/null
+++ b/src/components/footer/Footer.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import './Footer.css';
+
+export const Footer = () => (
+
+)
\ No newline at end of file
diff --git a/src/components/header/Header.css b/src/components/header/Header.css
new file mode 100644
index 0000000..6b84e2f
--- /dev/null
+++ b/src/components/header/Header.css
@@ -0,0 +1,5 @@
+.basic-header {
+ background: orange;
+ padding: 10px;
+ color: white;
+}
\ No newline at end of file
diff --git a/src/components/header/Header.js b/src/components/header/Header.js
new file mode 100644
index 0000000..9ff1247
--- /dev/null
+++ b/src/components/header/Header.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import './Header.css';
+import User from "../user/User";
+
+const Header = () => (
+
+);
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/toggle-box/ToggleBox.css b/src/components/toggle-box/ToggleBox.css
new file mode 100644
index 0000000..73002fc
--- /dev/null
+++ b/src/components/toggle-box/ToggleBox.css
@@ -0,0 +1,17 @@
+.toggle-box {
+ border: 1px solid #d16d51;
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 4px;
+ text-transform: uppercase;
+ font-size: 8px;
+ cursor: pointer;
+}
+
+.toggle-box:hover {
+ background-color: #fdf2ef;
+}
+
+.toggle-box.active {
+ background-color: orange;
+}
\ No newline at end of file
diff --git a/src/components/toggle-box/ToggleBox.js b/src/components/toggle-box/ToggleBox.js
new file mode 100644
index 0000000..6a03432
--- /dev/null
+++ b/src/components/toggle-box/ToggleBox.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import './ToggleBox.css';
+import classNames from 'classnames';
+
+const ToggleBox = (props) => (
+
+ {props.title}
+
+
+)
+
+ToggleBox.propsTypes = {
+ active: PropTypes.bool.isRequired,
+ toggle: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired
+};
+
+export default ToggleBox;
\ No newline at end of file
diff --git a/src/components/user/User.js b/src/components/user/User.js
new file mode 100644
index 0000000..f2785e2
--- /dev/null
+++ b/src/components/user/User.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {connect} from "react-redux";
+
+const User = ({user}) => (
+ {user.name}
+)
+
+User.propTypes = {
+ user: PropTypes.object.isRequired
+};
+
+const mapToStateProps = state => ({
+ user: state.user
+});
+
+export default connect(mapToStateProps)(User);
\ No newline at end of file
diff --git a/src/constants/actions-types.js b/src/constants/actions-types.js
new file mode 100644
index 0000000..95fd95a
--- /dev/null
+++ b/src/constants/actions-types.js
@@ -0,0 +1,5 @@
+export const ADD_RECIPE = 'ADD_RECIPE';
+export const TOGGLE_RECIPE = 'TOGGLE_RECIPE';
+export const SET_USER = 'SET_USER';
+export const SET_RECIPES = 'SET_RECIPES';
+export const API = 'API';
\ No newline at end of file
diff --git a/src/constants/envirement.js b/src/constants/envirement.js
new file mode 100644
index 0000000..f17576d
--- /dev/null
+++ b/src/constants/envirement.js
@@ -0,0 +1 @@
+export const BASE_URL = 'https://s3.amazonaws.com/500tech-shared/';
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index cee5f34..dfbe5eb 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,14 +1,18 @@
body {
- margin: 0;
- padding: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
+
+.form-label {
+ display: block;
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 0c5e75d..1266cc1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
-import App from './App';
+import App from './components/app/App';
import * as serviceWorker from './serviceWorker';
+import store from "./store";
+import {Provider} from 'react-redux';
+import {BrowserRouter as Router} from 'react-router-dom';
-ReactDOM.render(, document.getElementById('root'));
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
diff --git a/src/middleware/api.js b/src/middleware/api.js
new file mode 100644
index 0000000..21988bb
--- /dev/null
+++ b/src/middleware/api.js
@@ -0,0 +1,17 @@
+import {BASE_URL} from "../constants/envirement";
+import {API} from "../constants/actions-types";
+
+const apiMiddleware = ({dispatch}) => next => action => {
+ if (action.type !== API) {
+ return next(action);
+ }
+
+ const {url, success} = action.payload;
+
+ fetch(BASE_URL + url)
+ .then(response => response.json())
+ .then(payload => dispatch({type: success, payload}))
+ .catch(error => console.log('Error:', error));
+}
+
+export default apiMiddleware;
\ No newline at end of file
diff --git a/src/middleware/log.js b/src/middleware/log.js
new file mode 100644
index 0000000..c5d22e0
--- /dev/null
+++ b/src/middleware/log.js
@@ -0,0 +1,6 @@
+const log = () => next => action => {
+ console.log(`ACTION idan : ${action.type}`, action);
+ next(action);
+}
+
+export default log;
\ No newline at end of file
diff --git a/src/reducers/recipes.js b/src/reducers/recipes.js
new file mode 100644
index 0000000..64a671f
--- /dev/null
+++ b/src/reducers/recipes.js
@@ -0,0 +1,27 @@
+import * as consts from "../constants/actions-types";
+
+const initialState = [];
+
+const reducer = (recipes = initialState, action) => {
+ console.log(`Got Action ${action.type}`);
+
+ switch (action.type) {
+ case consts.ADD_RECIPE:
+ const newRecipe = Object.assign({},action.payload,{favorite:false});
+ return recipes.concat(newRecipe);
+
+
+ case consts.TOGGLE_RECIPE:
+ return recipes.map(recipe => recipe.id !== action.payload
+ ? recipe
+ : Object.assign({}, recipe, {favorite: !recipe.favorite}));
+
+ case consts.SET_RECIPES:
+ return action.payload;
+
+ default:
+ return recipes;
+ }
+};
+
+export default reducer;
\ No newline at end of file
diff --git a/src/reducers/user.js b/src/reducers/user.js
new file mode 100644
index 0000000..cada15e
--- /dev/null
+++ b/src/reducers/user.js
@@ -0,0 +1,17 @@
+import * as consts from '../constants/actions-types';
+
+const initalState = {
+ name: 'Idan Naim'
+}
+
+
+const reducer = (user = initalState, action) => {
+ switch (action.type) {
+ case consts.SET_USER:
+ return Object.assign({}, user, action.payload);
+ default:
+ return user;
+ }
+};
+
+export default reducer;
\ No newline at end of file
diff --git a/src/root.js b/src/root.js
new file mode 100644
index 0000000..f978c4d
--- /dev/null
+++ b/src/root.js
@@ -0,0 +1,8 @@
+import {combineReducers} from "redux";
+import recipes from './reducers/recipes';
+import user from './reducers/user';
+
+export default combineReducers({
+ recipes,
+ user
+});
diff --git a/src/services/utils.js b/src/services/utils.js
new file mode 100644
index 0000000..ebb46d5
--- /dev/null
+++ b/src/services/utils.js
@@ -0,0 +1,2 @@
+let id = 0;
+export const getID = () => id += 1;
diff --git a/src/store.js b/src/store.js
new file mode 100644
index 0000000..7f0b5e5
--- /dev/null
+++ b/src/store.js
@@ -0,0 +1,14 @@
+import reducer from './root'
+import {applyMiddleware, createStore} from 'redux';
+import logMiddleware from "./middleware/log";
+import apiMiddleware from "./middleware/api";
+
+
+const store = createStore(reducer, applyMiddleware(
+ logMiddleware,
+ apiMiddleware
+));
+
+window.store = store;
+
+export default store;
\ No newline at end of file