├── .gitignore ├── client ├── reducers │ ├── index.js │ └── recipeReducer.js ├── components │ ├── ViewRecipe │ │ └── ViewRecipeContainer.jsx │ ├── LandingPage │ │ └── LandingPageContainer.jsx │ └── AddRecipe │ │ └── AddRecipeContainer.jsx ├── index.html ├── index.js ├── constants │ └── actionTypes.js ├── store.js ├── styles │ └── styles.css ├── App.jsx └── actions │ └── actions.js ├── server ├── model │ └── recipeModel.js ├── router │ └── api.js ├── server.js └── controller │ └── recipeController.js ├── webpack.config.js ├── README.md ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | node_modules/ 4 | package-lock.json 5 | 6 | # output from webpack 7 | build/*.js 8 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import recipesReducer from './recipeReducer' 3 | // combine reducers 4 | const reducers = combineReducers({ 5 | recipes: recipesReducer, 6 | }); 7 | 8 | // make the combined reducers available for import 9 | export default reducers; -------------------------------------------------------------------------------- /server/model/recipeModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const URI = 'postgres://eypzjjtz:7MIQ7fduyif1rB-YkL5cK_3y8sXVVvSl@lallah.db.elephantsql.com:5432/eypzjjtz'; 4 | 5 | const pool = new Pool({connectionString: URI}); 6 | 7 | module.exports = { 8 | query: (text, parameter, callback) => { 9 | console.log('Querying the Wunderpuss database'); 10 | return pool.query(text, parameter, callback); 11 | } 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /server/router/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const recipeController = require('../controller/recipeController'); 4 | 5 | router.get('/', recipeController.getData, (req, res) => { 6 | res.status(200).json(res.locals.recipes); 7 | }); 8 | 9 | router.post('/', 10 | recipeController.addToRecipes, 11 | recipeController.addToIngredients, 12 | // recipeController.addToJoin, //need to create this controller 13 | (req, res) => { 14 | res.status(200).json({}); 15 | }); 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /client/components/ViewRecipe/ViewRecipeContainer.jsx: -------------------------------------------------------------------------------- 1 | //page that shows an individual recipe and all of its info 2 | import React, { Component } from 'react'; 3 | import * as actions from '../../actions/actions' 4 | import { connect } from 'react-redux'; 5 | 6 | class ViewRecipeContainer extends Component { 7 | 8 | constructor(props){ 9 | super(props) 10 | } 11 | 12 | render() { 13 | 14 | return ( 15 |
16 |

This is View Recipe Container

17 |
18 | ) 19 | } 20 | } 21 | 22 | export default ViewRecipeContainer; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Scratch Project 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App.jsx'; 3 | import { render } from 'react-dom'; 4 | import './styles/styles.css'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { Provider } from 'react-redux'; 7 | import store from './store'; 8 | 9 | render( 10 | // App is wrapped in provider in its own file, where we have our react-router routing for redux 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | 19 | // ReactDOM.render(, document.getElementById('root')); 20 | console.log('Yeeeeeeeees!') 21 | -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | //export consts that reference action type strings 2 | 3 | export const HOME_RECIPES = "HOME_RECIPES"; 4 | export const RETRIEVE_RECIPE = "RETRIEVE_RECIPE"; 5 | export const CREATE_RECIPE = "CREATE_RECIPE"; 6 | export const INPUT_RECIPE_DATA = "INPUT_RECIPE_DATA"; 7 | export const ITEMS_HAS_ERRORED = "ITEMS_HAS_ERRORED"; 8 | export const ITEMS_IS_LOADING = "ITEMS_IS_LOADING"; 9 | export const ITEMS_FETCH_DATA_SUCCESS = "ITEMS_FETCH_DATA_SUCCESS"; 10 | export const UPDATE_NAME = "UPDATE_NAME"; 11 | export const UPDATE_INSTRUCTIONS = "UPDATE_INSTRUCTIONS"; 12 | export const UPDATE_INGREDIENTS = "UPDATE_INGREDIENTS"; 13 | export const UPDATE_IMAGELINK = "UPDATE_IMAGELINK"; 14 | export const POST_SUCCESS = "POST_SUCCESS"; 15 | 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 5 | mode: process.env.NODE_ENV, 6 | entry: './client/index.js', 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'bundle.js' 10 | }, 11 | 12 | devServer: { 13 | publicPath: '/build/', 14 | contentBase: './client', 15 | proxy: { 16 | '/': 'http://localhost:3000' 17 | } 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.jsx?/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-env', '@babel/preset-react'] 29 | } 30 | } 31 | }, 32 | { 33 | test: /\.css/, 34 | use: ['style-loader', 'css-loader'], 35 | } 36 | ] 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import reducers from './reducers/index'; 3 | // thunk is a Redux middleware that allows for asynchronus functionality in our actions (eg. fetch calls) 4 | // documentation and resources here: https://github.com/reduxjs/redux-thunk 5 | // helpful step by step guide to using thunk: https://medium.com/@stowball/a-dummys-guide-to-redux-and-thunk-in-react-d8904a7005d3 6 | import thunk from 'redux-thunk' 7 | 8 | 9 | // Redux only accepts one "store enhancer", so we must use this variable in order to pass both composeWithDevTools and middleware to our store 10 | // https://github.com/jhen0409/react-native-debugger/issues/280 11 | 12 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 13 | 14 | const store = createStore( 15 | reducers, 16 | composeEnhancer(applyMiddleware(thunk)), 17 | ); 18 | 19 | export default store; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Recipedia: Recipe Aggregator 3 | by Alex Kim, Julie Pinchak, Jinseon Shin, Casey Walker 4 | 5 | ## This is a recipe wikidedia like SPA that utilize the following tech stack: 6 | ### React, React Router, Redux, Redux Thunk for the front end 7 | ### Express, Postgres (ElephantSQL) for the backend 8 | ### Webpack for build/binding 9 | 10 | The database consist of three tables: recipes, ingredients and joint table. 11 | 12 | ## Stretch Features 13 | - Sending addrecipe input data to landing page upon clicking "Submit New Recipe" button 14 | - Create View Recipe pages: so each recipe has its own page 15 | - Link each recipe on landing page to its own page 16 | - Login Authentication 17 | - User can add, edit, delete their own recipes 18 | - Search feature by keywords and ingredients 19 | - Filter feature by ingredients (so user can search for recipes with ingredients they already have) 20 | - Feature an ingredient on landing page and some recipes that use that ingredient 21 | - Beautifying the pages 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wunderpuss-Photogenicus 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": " NODE_ENV=production node server/server.js", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": " NODE_ENV=development concurrently \"nodemon server/server.js\" \"webpack-dev-server --open\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Wunderpuss-Photogenicus/scratch-project.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/Wunderpuss-Photogenicus/scratch-project/issues" 20 | }, 21 | "homepage": "https://github.com/Wunderpuss-Photogenicus/scratch-project#readme", 22 | "dependencies": { 23 | "body-parser": "^1.18.3", 24 | "express": "^4.16.3", 25 | "node-fetch": "^2.3.0", 26 | "pg": "^8.3.3", 27 | "react": "^16.5.2", 28 | "react-dom": "^16.5.2", 29 | "react-hot-loader": "^4.6.3", 30 | "react-redux": "^7.2.1", 31 | "react-router": "^4.3.1", 32 | "react-router-dom": "^4.3.1", 33 | "redux": "^4.0.5", 34 | "redux-devtools-extension": "^2.13.8", 35 | "redux-thunk": "^2.3.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.11.6", 39 | "@babel/preset-env": "^7.11.5", 40 | "@babel/preset-react": "^7.10.4", 41 | "babel-loader": "^8.1.0", 42 | "concurrently": "^5.0.0", 43 | "cross-env": "^6.0.3", 44 | "css-loader": "^4.3.0", 45 | "nodemon": "^2.0.4", 46 | "style-loader": "^1.2.1", 47 | "webpack": "^4.44.1", 48 | "webpack-cli": "^3.3.12", 49 | "webpack-dev-server": "^3.11.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/styles/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 0px; 3 | margin-left: 0px; 4 | padding-top: 40px; 5 | background-color: rgb(148, 233, 233); 6 | font-family: 'Syne', sans-serif; 7 | } 8 | 9 | .page { 10 | margin-left: 5px; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | img { 16 | height: 200px; 17 | } 18 | 19 | .img_and_ing { 20 | display: flex; 21 | flex-direction: row; 22 | } 23 | 24 | .ing_list { 25 | margin-left: 20px; 26 | } 27 | 28 | .ing_list p { 29 | font-size: 16px; 30 | } 31 | 32 | li { 33 | font-size: 16px; 34 | list-style: none; 35 | } 36 | 37 | .navbar { 38 | position: fixed; 39 | margin-top: -40px; 40 | margin-left: 0px; 41 | margin-right: 0px; 42 | width: 100%; 43 | height: 40px; 44 | display: flex; 45 | flex-direction: row; 46 | background-color: azure; 47 | justify-content: space-between; 48 | align-items: center; 49 | background-color: darkblue; 50 | } 51 | 52 | .link { 53 | margin-left: 8px; 54 | margin-right: 8px; 55 | text-decoration: none; 56 | color: rgb(185, 207, 214); 57 | } 58 | 59 | .searchButton { 60 | display: absolute; 61 | } 62 | 63 | .material-icons { 64 | font-size: 20px; 65 | vertical-align: middle; 66 | color: rgb(185, 207, 214); 67 | margin-left: 3px; 68 | } 69 | 70 | .landingPage { 71 | display: relative; 72 | margin-top: 30px; 73 | } 74 | 75 | .landingPage div { 76 | font-size: 24px; 77 | color: darkblue; 78 | } 79 | 80 | .landingPage img { 81 | border-radius: 5px; 82 | } 83 | 84 | .search input { 85 | border: 1px solid darkblue; 86 | background-color: rgb(228, 234, 236); 87 | color: navy; 88 | border-radius: 3px; 89 | width: 400px; 90 | } 91 | 92 | .inputFields { 93 | margin-top: -16px; 94 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const apiRouter = require('./router/api.js'); 5 | 6 | const PORT = 3000; 7 | 8 | // Parsing request body 9 | app.use(express.json()); 10 | app.use(express.urlencoded({extended: true})); 11 | 12 | // Statically serve everything from the build folder (so our bundle) 13 | app.use('/build', express.static('build')); 14 | 15 | // Use api router handler 16 | app.use('/api', apiRouter); 17 | 18 | // Serve index.html on route '/' 19 | app.get('/', (req, res) => res.status(200).sendFile(path.resolve(__dirname, '../client/index.html'))); 20 | 21 | // Serve index.html (which routes to App, which handles the routes to different components) 22 | app.get('/addrecipe', (req, res) => res.status(200).sendFile(path.resolve(__dirname, '../client/index.html'))); 23 | 24 | // Serve index.html (which routes to App, which handles the routes to different components) 25 | app.get('/viewrecipe', (req, res) => res.status(200).sendFile(path.resolve(__dirname, '../client/index.html'))); 26 | 27 | // Error for if someone tries to go to a page that doesn't exist 28 | app.use((req, res) => { 29 | res.sendStatus(404); 30 | }); 31 | 32 | // Global error handler for if there is a middleware/api error 33 | app.use((err, req, res, next) => { 34 | const defaultErr = { 35 | log: "Express error handler caught unknown middleware error", 36 | status: 500, 37 | message: {err: "An error occurred."} 38 | }; 39 | const errObj = Object.assign({}, defaultErr, err); 40 | console.log(errObj.log); 41 | return res.status(errObj.status).json(errObj.message); 42 | }); 43 | 44 | // Begin listening on port number PORT 45 | app.listen(PORT, () => console.log('The server is running on Wunderpuss-3000')); -------------------------------------------------------------------------------- /client/components/LandingPage/LandingPageContainer.jsx: -------------------------------------------------------------------------------- 1 | //main page rendered by '/' 2 | //displays list of recipes 3 | 4 | import React, { Component } from 'react'; 5 | import * as actions from '../../actions/actions' 6 | import { connect } from 'react-redux'; 7 | 8 | 9 | const mapStateToProps = (state) => ({ 10 | recipesList: state.recipes.recipesList, //pass down recipesList array 11 | itemsHaveErrored: state.recipes.itemsHaveErrored, 12 | itemsAreLoading: state.recipes.itemsAreLoading 13 | }) 14 | 15 | const mapDispatchToProps = (dispatch) => ({ 16 | //this is our handler function that dispatches an action that returns itemsIsLoading and also runs our fetch request 17 | //if the fetch request is successful, it will also dispatch itemsFetchDataSuccess, which will return our data to global state 18 | itemsFetchData: (url) => dispatch(actions.itemsFetchData(url)) 19 | }) 20 | 21 | class LandingPageContainer extends Component { 22 | 23 | constructor(props){ 24 | super(props) 25 | } 26 | 27 | componentDidMount() { 28 | this.props.itemsFetchData('/api'); 29 | } 30 | 31 | render() { 32 | 33 | const arr = this.props.recipesList.map(el => { 34 | return ( 35 |
36 |
{el.title}
37 |
38 | 39 |
40 |
Ingredients:
41 |

{el.ingredients}

42 |
43 |
44 |
45 | ) 46 | }) 47 | 48 | return ( 49 |
50 | {arr} 51 |
52 | ) 53 | } 54 | } 55 | 56 | 57 | export default connect(mapStateToProps, mapDispatchToProps)(LandingPageContainer) -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import Wrapper from './containers/MainContainer.jsx'; 3 | import { render } from 'react-dom'; 4 | import { BrowserRouter, Route, Switch} from 'react-router-dom'; 5 | import { Link } from 'react-router-dom'; 6 | import LandingPageContainer from './components/LandingPage/LandingPageContainer.jsx' 7 | import ViewRecipeContainer from './components/ViewRecipe/ViewRecipeContainer.jsx' 8 | import AddRecipeContainer from './components/AddRecipe/AddRecipeContainer.jsx' 9 | 10 | class App extends Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | /* wrapping App here as opposed to in index.js to follow redux documenation for react router setup: 15 | https://redux.js.org/advanced/usage-with-react-router 16 | https://react-redux.js.org/api/provider 17 | */ 18 | render() { 19 | //fixed navigation bar 20 | return( 21 |
22 |
23 | Add Recipe 24 |
25 | 26 | search 27 |
28 | Login 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | ) 43 | } 44 | } 45 | 46 | 47 | // 48 | 49 | export default App; -------------------------------------------------------------------------------- /client/reducers/recipeReducer.js: -------------------------------------------------------------------------------- 1 | //import action types 2 | import * as types from '../constants/actionTypes' 3 | 4 | //set initial state 5 | const initialState = { 6 | recipesList: [], //home_recipe: recipes from get request to database 7 | newRecipe: { 8 | name: '', 9 | imageLink: '', 10 | ingredients: '', 11 | instructions: '', 12 | creator: 1, 13 | }, 14 | retrievedRecipe: {}, 15 | itemsHaveErrored: false, 16 | itemsAreLoading: false 17 | } 18 | 19 | //declare reducer and its methods 20 | const recipeReducer = (state = initialState, action) => { 21 | let itemsHaveErrored; 22 | let itemsAreLoading; 23 | let recipesList; 24 | 25 | switch (action.type){ 26 | case types.ITEMS_HAS_ERRORED: 27 | itemsHaveErrored = action.payload; 28 | return { 29 | ...state, 30 | itemsHaveErrored 31 | } 32 | 33 | case types.ITEMS_IS_LOADING: 34 | itemsAreLoading = action.payload; 35 | return { 36 | ...state, 37 | itemsAreLoading 38 | } 39 | 40 | case types.ITEMS_FETCH_DATA_SUCCESS: 41 | recipesList = action.payload; 42 | return { 43 | ...state, 44 | recipesList 45 | } 46 | 47 | case types.UPDATE_NAME: 48 | return { 49 | ...state, 50 | newRecipe : { 51 | ...state.newRecipe, 52 | name: action.payload 53 | } 54 | } 55 | 56 | case types.UPDATE_INSTRUCTIONS: 57 | return { 58 | ...state, 59 | newRecipe : { 60 | ...state.newRecipe, 61 | instructions: action.payload 62 | } 63 | } 64 | 65 | case types.UPDATE_INGREDIENTS: 66 | return { 67 | ...state, 68 | newRecipe : { 69 | ...state.newRecipe, 70 | ingredients: action.payload 71 | } 72 | } 73 | 74 | case types.UPDATE_IMAGELINK: 75 | return { 76 | ...state, 77 | newRecipe : { 78 | ...state.newRecipe, 79 | imageLink: action.payload 80 | } 81 | } 82 | 83 | default: 84 | return state; 85 | } 86 | } 87 | 88 | export default recipeReducer -------------------------------------------------------------------------------- /client/components/AddRecipe/AddRecipeContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import * as actions from '../../actions/actions' 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | newRecipe: state.recipes.newRecipe, 7 | itemsHaveErrored: state.recipes.itemsHaveErrored, 8 | itemsAreLoading: state.recipes.itemsAreLoading 9 | }) 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | updateName: (data) => dispatch(actions.updateName(data)), 13 | updateInstructions: (data) => dispatch(actions.updateInstructions(data)), 14 | updateIngredients: (data) => dispatch(actions.updateIngredients(data)), 15 | updateImageLink: (data) => dispatch(actions.updateImageLink(data)), 16 | addRecipe: (data) => dispatch(actions.addRecipe(data)) 17 | }) 18 | 19 | 20 | 21 | class AddRecipeContainer extends Component { 22 | 23 | constructor(props){ 24 | super(props) 25 | } 26 | 27 | render() { 28 | 29 | return ( 30 |
31 |
32 |

Recipe Name.

33 | this.props.updateName(e.target.value)}/> 38 | 39 |

Instructions. Number each step (eg. “1. Cut tofu into cubes 2. Heat oil in pan”)

40 | this.props.updateInstructions(e.target.value)}/> 45 | 46 |

Ingredients. Separate each ingredient with a comma (eg. “14oz tofu, 6 cloves garlic”)

47 | this.props.updateIngredients(e.target.value)}/> 52 | 53 |

Image Link.

54 | this.props.updateImageLink(e.target.value)}/> 59 | 60 |
61 |
62 | 63 |
64 | ) 65 | } 66 | } 67 | 68 | export default connect(mapStateToProps, mapDispatchToProps)(AddRecipeContainer) -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | // import actionType constants 2 | import * as types from '../constants/actionTypes' 3 | 4 | 5 | //export functions that return action objects 6 | //objects have two props 7 | //type: a reference to the consts, tells which reducer method to use 8 | //payload: whatever data the reducer will need 9 | 10 | export const retrieveAllRecipes = () => ({ 11 | type: types.HOME_RECIPES, 12 | }) 13 | 14 | export const retrieveRecipe = (recipeId) => ({ 15 | type: types.RETRIEVE_RECIPE, 16 | payload: recipeId 17 | }) 18 | 19 | export const createRecipe = () => ({ 20 | type: types.CREATE_RECIPE, 21 | payload: null 22 | }) 23 | 24 | //this action will get dispatched if our fetch request errors doesn't return data 25 | export const itemsHasErrored = (bool) => ({ 26 | type: types.ITEMS_HAS_ERRORED, 27 | payload: bool 28 | }) 29 | 30 | //this action will be dispatched twice per fetch request: 31 | // first: at the same time our fetch request is invoked 32 | // second: once our request returns either an error or valid data 33 | export const itemsIsLoading = (bool) => ({ 34 | type: types.ITEMS_IS_LOADING, 35 | payload: bool 36 | }) 37 | 38 | //this action will be dispatched from our async fetch request, and will return our fetched data to state 39 | export const itemsFetchDataSuccess = (data) => ({ 40 | type: types.ITEMS_FETCH_DATA_SUCCESS, 41 | payload: data 42 | }) 43 | 44 | export const itemsFetchData = (url) => { 45 | return (dispatch) => { 46 | dispatch(itemsIsLoading(true)); 47 | 48 | fetch(url) 49 | .then((response) => { 50 | if (!response.ok) { 51 | throw Error(response.statusText); 52 | } 53 | dispatch(itemsIsLoading(false)); 54 | return response; 55 | }) 56 | .then((response) => response.json()) 57 | .then((items) => { 58 | console.log(items) 59 | dispatch(itemsFetchDataSuccess(items)) 60 | }) 61 | .catch(() => dispatch(itemsHasErrored(true))); 62 | } 63 | } 64 | export const postSuccess = () => ({ 65 | type: types.POST_SUCCESS, 66 | }) 67 | 68 | export const addRecipe = (data) => { 69 | return (dispatch) => { 70 | dispatch(itemsIsLoading(true)); 71 | 72 | fetch('/api', { 73 | method: 'POST', 74 | headers: { 75 | "Content-Type": "Application/JSON" 76 | }, 77 | body: JSON.stringify(data) 78 | }) 79 | .then((response) => { 80 | if (!response.ok) { 81 | throw Error(response.statusText); 82 | } 83 | dispatch(itemsIsLoading(false)); 84 | return response; 85 | }) 86 | .then((response) => response.json()) 87 | .then((data) => { 88 | console.log(data) 89 | // dispatch(postSuccess()) 90 | }) 91 | .catch(() => dispatch(itemsHasErrored(true))); 92 | } 93 | } 94 | 95 | export const updateName = (data) => ({ 96 | type: types.UPDATE_NAME, 97 | payload: data 98 | }) 99 | 100 | export const updateInstructions = (data) => ({ 101 | type: types.UPDATE_INSTRUCTIONS, 102 | payload: data 103 | }) 104 | 105 | export const updateIngredients = (data) => ({ 106 | type: types.UPDATE_INGREDIENTS, 107 | payload: data 108 | }) 109 | 110 | export const updateImageLink = (data) => ({ 111 | type: types.UPDATE_IMAGELINK, 112 | payload: data 113 | }) 114 | -------------------------------------------------------------------------------- /server/controller/recipeController.js: -------------------------------------------------------------------------------- 1 | const e = require('express'); 2 | const db = require('../model/recipeModel'); 3 | 4 | const recipeController = {}; 5 | 6 | 7 | //retrieving data from database 8 | recipeController.getData = (req, res, next) => { 9 | const test = 'SELECT r.title, r.img_link, i.name as ingredient FROM recipes r LEFT JOIN ing_join_recipe j ON r.id = j.recipe_id LEFT JOIN ingredients i ON j.ingredient_id = i.id;' 10 | db.query(test) 11 | //brute force solution we're so sorry 12 | .then(data => { 13 | // create result array of objects 14 | console.log(data.rows); 15 | // iterate through data array 16 | const resultArray = []; 17 | const nameArray =[]; 18 | for (let i = 0; i < data.rows.length; i++) { 19 | if (!nameArray.includes(data.rows[i].title)) { 20 | nameArray.push(data.rows[i].title); 21 | resultArray.push({ 22 | title: data.rows[i].title, 23 | img_link: data.rows[i].img_link, 24 | ingredients: [data.rows[i].ingredient] 25 | }); 26 | } else { 27 | resultArray.forEach(el => { 28 | if (el.title === data.rows[i].title) { 29 | el.ingredients.push(", " + data.rows[i].ingredient); 30 | } 31 | }) 32 | } 33 | } 34 | res.locals.recipes = resultArray; 35 | console.log(res.locals.recipes); 36 | return next(); 37 | }) 38 | .catch(err => { 39 | return next(err); 40 | }); 41 | } 42 | 43 | 44 | recipeController.addToRecipes = (req, res, next) => { 45 | const { name, imageLink, ingredients, instructions, creator } = req.body; 46 | const query = { 47 | text: 'INSERT INTO recipes (title, instructions, img_link, created_by) VALUES ($1, $2, $3, $4) RETURNING id;', 48 | values: [name, instructions, imageLink, creator] 49 | } 50 | db.query(query) 51 | .then(data => { 52 | res.locals.id = data.rows[0]['id']; 53 | res.locals.ingredients = ingredients; 54 | return next(); 55 | }) 56 | .catch(err => { 57 | return next(err); 58 | }); 59 | } 60 | 61 | recipeController.addToIngredients = (req, res, next) => { 62 | const { ingredients } = req.body; 63 | const arr = ingredients.split(','); 64 | 65 | for (let i = 0; i < arr.length; i++) { 66 | // This query checks if the ingredient is in the table. If not, it add the ingredient to the table. Note here to use SELECT syntax instead of VALUES 67 | const queryStr = `INSERT INTO ingredients (name) SELECT ('${arr[i]}') WHERE not exists (SELECT * FROM ingredients WHERE name='${arr[i]}') RETURNING id;`; 68 | 69 | db.query(queryStr) 70 | .then(data => { 71 | console.log(data.rows[0]); 72 | return next(); 73 | }) 74 | .catch(err => { 75 | return next(err); 76 | }); 77 | } 78 | } 79 | 80 | // recipeController.addToJoin = (req, res, next) => { 81 | // const { ingredients } = req.body; 82 | // const arr = ingredients.split(','); 83 | 84 | // for (let i = 0; i < arr.length; i++) { 85 | // const queryStr = `INSERT INTO ingredients (name) SELECT ('${arr[i]}') WHERE not exists (SELECT * FROM ingredients WHERE name='${arr[i]}') RETURNING id;`; 86 | 87 | // db.query(queryStr) 88 | // .then(data => { 89 | // console.log(data.rows[0]); 90 | // return next(); 91 | // }) 92 | // .catch(err => { 93 | // return next(err); 94 | // }); 95 | // } 96 | // } 97 | 98 | /* 99 | Query templates: 100 | 101 | ***Joining recipe with ingredients*** 102 | SELECT r.title, i.name as ingredient, j.quantity 103 | FROM recipes r 104 | JOIN ing_join_recipe j 105 | ON r.id = j.recipe_id 106 | LEFT JOIN ingredients i 107 | ON j.ingredient_id = i.id; 108 | 109 | ***Inserting new recipe*** 110 | INSERT INTO recipes (title, instructions, img_link, created_by) 111 | VALUES ( 112 | 'Tofu Stir Fry', 113 | 'STEP 1 Combine rice with tofu and stir fry', 114 | 'https://www.eatingbirdfood.com/wp-content/uploads/2019/11/Tofu-Stir-Fry-3.jpg', 115 | 1 (always set this to one until auth is set up and users table is created) 116 | ); 117 | 118 | ***Inserting new ingredients*** 119 | INSERT INTO ingredients (name) 120 | VALUES ('tofu'); 121 | 122 | ***Inserting to join table*** 123 | INSERT INTO ing_join_recipe (recipe_id, ingredient_id, quantity) 124 | VALUES ( 125 | 3, 126 | 1, 127 | '1 cup' 128 | ); 129 | 130 | 131 | */ 132 | 133 | module.exports = recipeController; --------------------------------------------------------------------------------