├── .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 |
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 |
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;
--------------------------------------------------------------------------------