├── .gitignore ├── client ├── constants │ └── actionTypes.js ├── store.js ├── reducers │ ├── index.js │ └── informationReducer.js ├── actions │ └── actions.js ├── index.js ├── template.html ├── container │ └── InfoContainer.jsx ├── App.jsx ├── components │ ├── Login.jsx │ ├── DetailedWeather.jsx │ ├── FrontPage.jsx │ ├── WeatherView.jsx │ ├── NewsView.jsx │ └── ActivitiesView.jsx └── styles.css ├── server ├── routes │ ├── news.js │ ├── weather.js │ ├── location.js │ ├── businesses.js │ └── favorites.js ├── models │ ├── models.js │ └── db_postgres_create.sql ├── controllers │ ├── weatherController.js │ ├── newsController.js │ ├── locationController.js │ ├── businessesController.js │ └── favoritesController.js └── server.js ├── API-Data-Examples ├── Yelp_Businesses_search.json ├── Yelp_Businesses_Reviews.json ├── Yelp_Businesses_id.json ├── Weather%20API%20Example.txt └── NewsApi.json ├── package.json ├── Yelp API Example.txt ├── webpack.config.js └── Weather API Example.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log 4 | .env -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_CITY = 'ADD_CITY'; 2 | export const ADD_USER = 'ADD_USER'; 3 | export const ADD_WEATHER = 'ADD_WEATHER'; 4 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reducers from './reducers/index'; 3 | 4 | const store = createStore( 5 | reducers, 6 | ); 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import informationReducer from './informationReducer.js'; 3 | 4 | export default combineReducers({ 5 | informationReducer: informationReducer, 6 | }); -------------------------------------------------------------------------------- /server/routes/news.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const newsController = require('../controllers/newsController.js'); 3 | 4 | const router = express.Router(); 5 | 6 | // location route to get location data 7 | router.get('/:countrycode', newsController.getNews, (req, res) => { 8 | res.status(200).json(res.locals); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /server/routes/weather.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const weatherController = require('../controllers/weatherController'); 3 | 4 | const router = express.Router(); 5 | 6 | // location route to get location data 7 | router.get('/', weatherController.getWeather, (req, res) => { 8 | res.status(200).json(res.locals); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /server/models/models.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const pool = new Pool({ 4 | connectionString: process.env.PG_URI, 5 | }); 6 | 7 | // See db_postgres_create.sql for table schema 8 | 9 | module.exports = { 10 | query: (text, params, callback) => { 11 | console.log('executed query', text); 12 | return pool.query(text, params, callback); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from "../constants/actionTypes"; 2 | 3 | export const addCity = (data) => ({ 4 | type: types.ADD_CITY, 5 | payload: data, 6 | }); 7 | 8 | export const addUser = (data) => ({ 9 | type: types.ADD_USER, 10 | payload: data, 11 | }); 12 | 13 | export const addWeather = (data) => ({ 14 | type: types.ADD_WEATHER, 15 | payload: data, 16 | }); 17 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App.jsx'; 6 | import store from './store.js'; 7 | 8 | render( 9 | 10 | 11 | 12 | , 13 | , 14 | document.getElementById('app'), 15 | ); 16 | -------------------------------------------------------------------------------- /client/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Local Information 6 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /client/container/InfoContainer.jsx: -------------------------------------------------------------------------------- 1 | // import React, { useState, useEffect } from 'react'; 2 | // import { connect } from 'react-redux'; 3 | // import * as actions from '../actions/actions'; 4 | // import NewsView from './NewsView.jsx'; 5 | // import ActivitiesView from './ActivitiesView.jsx'; 6 | 7 | 8 | // const Search = (props) => { 9 | // const [city, setCity] = useState('NYC'); 10 | 11 | // return ( 12 | // <> 13 | // ) 14 | 15 | // } 16 | 17 | // export default InfoContainer; -------------------------------------------------------------------------------- /server/routes/location.js: -------------------------------------------------------------------------------- 1 | // require express package 2 | const express = require('express'); 3 | // creater router 4 | const router = express.Router(); 5 | // require our controller 6 | const locationsController = require('../controllers/locationController'); 7 | 8 | // location route to get location data 9 | router.get('/:location', 10 | locationsController.getLocationData, 11 | locationsController.getCountryCode, 12 | (req, res) => { 13 | const { latitude, longitude, countryCode } = res.locals; 14 | res.status(200).json({ latitude, longitude, countryCode }); 15 | }); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | } from "react-router-dom"; 7 | import Login from './components/Login.jsx'; 8 | import './styles.css'; 9 | import DetailedWeather from './components/DetailedWeather.jsx'; 10 | import FrontPage from './components/FrontPage.jsx'; 11 | 12 | const App = props => { 13 | return ( 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /client/reducers/informationReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../constants/actionTypes"; 2 | const initialState = { 3 | city: 'NYC', 4 | lat: '40.712775', 5 | long: '-74.005973', 6 | countryCode: 'US', 7 | currentUser: '', 8 | weatherDays: [], 9 | }; 10 | 11 | const informationReducer = (state = initialState, action) => { 12 | switch (action.type) { 13 | case types.ADD_CITY: 14 | console.log('action payload ', action.payload); 15 | return { 16 | city: action.payload.city, 17 | lat: action.payload.latitude, 18 | long: action.payload.longitude, 19 | countryCode: action.payload.countryCode, 20 | }; 21 | case types.ADD_USER: 22 | return { 23 | ...state, 24 | currentUser: action.payload, 25 | }; 26 | case types.ADD_WEATHER: 27 | console.log('action payload ', action.payload); 28 | return { 29 | ...state, 30 | weatherDays: action.payload, 31 | }; 32 | default: 33 | return state; 34 | } 35 | } 36 | 37 | export default informationReducer; 38 | -------------------------------------------------------------------------------- /server/controllers/weatherController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const weatherController = {}; 4 | const API_KEY = process.env.WEATHER_API_KEY; 5 | const EXCLUSIONS = 'minutely,hourly,alerts'; 6 | 7 | // get information from weather api 8 | weatherController.getWeather = async (req, res, next) => { 9 | const { latitude, longitude } = req.query; 10 | 11 | // log error if latitude or longitude are undefined 12 | if (latitude === undefined || longitude === undefined) { 13 | return next({ 14 | log: 15 | 'weatherController.getWeather: ERROR: latitude and/or longitude are undefined', 16 | message: { 17 | err: 18 | 'weatherController.getWeather: ERROR: Check server logs for details', 19 | }, 20 | }); 21 | } 22 | 23 | const url = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&exclude=${EXCLUSIONS}&appid=${API_KEY}`; 24 | 25 | fetch(url) 26 | .then((data) => data.json()) 27 | .then((weatherData) => { 28 | res.locals.weather = weatherData; 29 | return next(); 30 | }) 31 | .catch((error) => next(error)); 32 | }; 33 | 34 | module.exports = weatherController; 35 | -------------------------------------------------------------------------------- /API-Data-Examples/Yelp_Businesses_search.json: -------------------------------------------------------------------------------- 1 | // GET https://api.yelp.com/v3/businesses/search 2 | 3 | { 4 | "total": 8228, 5 | "businesses": [ 6 | { 7 | "rating": 4, 8 | "price": "$", 9 | "phone": "+14152520800", 10 | "id": "E8RJkjfdcwgtyoPMjQ_Olg", 11 | "alias": "four-barrel-coffee-san-francisco", 12 | "is_closed": false, 13 | "categories": [ 14 | { 15 | "alias": "coffee", 16 | "title": "Coffee & Tea" 17 | } 18 | ], 19 | "review_count": 1738, 20 | "name": "Four Barrel Coffee", 21 | "url": "https://www.yelp.com/biz/four-barrel-coffee-san-francisco", 22 | "coordinates": { 23 | "latitude": 37.7670169511878, 24 | "longitude": -122.42184275 25 | }, 26 | "image_url": "http://s3-media2.fl.yelpcdn.com/bphoto/MmgtASP3l_t4tPCL1iAsCg/o.jpg", 27 | "location": { 28 | "city": "San Francisco", 29 | "country": "US", 30 | "address2": "", 31 | "address3": "", 32 | "state": "CA", 33 | "address1": "375 Valencia St", 34 | "zip_code": "94103" 35 | }, 36 | "distance": 1604.23, 37 | "transactions": ["pickup", "delivery"] 38 | } 39 | ], 40 | "region": { 41 | "center": { 42 | "latitude": 37.767413217936834, 43 | "longitude": -122.42820739746094 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/controllers/newsController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const newsController = {}; 4 | 5 | const API_KEY = process.env.NEWS_API_KEY; 6 | const CATEGORIES = [ 7 | 'business', 8 | 'entertainment', 9 | 'general', 10 | 'health', 11 | 'science', 12 | 'sports', 13 | 'technology', 14 | ]; 15 | 16 | // get information from google maps api 17 | newsController.getNews = async (req, res, next) => { 18 | // `/news/${props.countryCode}?category=${category}`; 19 | const { countrycode } = req.params; 20 | const { category } = req.query; 21 | const promises = []; 22 | res.locals.news = {}; 23 | // store fetch request promise for each CATEGORIES element 24 | CATEGORIES.forEach((categoryName) => { 25 | const url = `https://newsapi.org/v2/top-headlines?country=${countrycode}&category=${categoryName}&pageSize=5&page=1&apiKey=${API_KEY}`; 26 | promises.push(fetch(url).then((data) => data.json())); 27 | }); 28 | 29 | // wait for all promises in promises array have resolved 30 | await Promise.all(promises) 31 | .then((results) => { 32 | results.forEach((newResultObj, index) => { 33 | res.locals.news[CATEGORIES[index]] = newResultObj.articles; 34 | }); 35 | }) 36 | .catch((error) => 37 | next({ 38 | message: `Error in newsController.getNews; ERROR: ${JSON.stringify( 39 | error 40 | )}`, 41 | }) 42 | ); 43 | return next(); 44 | }; 45 | 46 | module.exports = newsController; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "start": "concurrently \"webpack-dev-server --open --mode development\" \"nodemon server/server.js\"", 10 | "dev": "nodemon server/server.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.11.6", 17 | "@babel/preset-env": "^7.11.5", 18 | "@babel/preset-react": "^7.10.4", 19 | "babel-loader": "^8.1.0", 20 | "concurrently": "^5.3.0", 21 | "css-loader": "^4.3.0", 22 | "dotenv": "^8.2.0", 23 | "html-loader": "^1.3.1", 24 | "html-webpack-harddisk-plugin": "^1.0.2", 25 | "html-webpack-plugin": "^4.5.0", 26 | "sass": "^1.26.11", 27 | "sass-loader": "^10.0.2", 28 | "style-loader": "^1.2.1", 29 | "webpack": "^4.44.2", 30 | "webpack-cli": "^3.3.12", 31 | "webpack-dev-server": "^3.11.0" 32 | }, 33 | "dependencies": { 34 | "bcrypt": "^5.0.0", 35 | "body-parser": "^1.19.0", 36 | "express": "^4.17.1", 37 | "jsonwebtoken": "^8.5.1", 38 | "node-fetch": "^2.6.1", 39 | "nodemon": "^2.0.4", 40 | "pg": "^8.4.0", 41 | "react": "^16.13.1", 42 | "react-bootstrap": "^1.3.0", 43 | "react-dom": "^16.13.1", 44 | "react-redux": "^7.2.1", 45 | "react-router": "^5.2.0", 46 | "react-router-dom": "^5.2.0", 47 | "redux": "^4.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/routes/businesses.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const businessesController = require('../controllers/businessesController.js'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/:category', businessesController.getBusinesses, (req, res) => { 7 | /* include lat and lon in query string 8 | * 9 | * example url where 'restaurants' is the category: 10 | * http://localhost:5000/businesses/restaurants?lat=40.712775&lon=-74.005973 11 | * 12 | * server response from the url above should be an array of 5 objects. 13 | * each object contains information on a restaurant 14 | * 15 | * see yelp api documentation for all valid categories 16 | */ 17 | 18 | const { businesses } = res.locals; 19 | res.status(200).json(businesses); 20 | }); 21 | 22 | router.get('/:category/:priceLevel', businessesController.getBusinessesByPrice, (req, res) => { 23 | /* include lat and lon in query string 24 | * 25 | * example url where 'restaurants' is the category and '1' is the price level: 26 | * http://localhost:5000/businesses/restaurants/1?lat=40.712775&lon=-74.005973 27 | * 28 | * server response from the url above should be an array of 5 objects. 29 | * each object contains information on a restaurant whose price range is that of 1 30 | * 31 | * valid price levels: 32 | * 1 = $ 33 | * 2 = $$ 34 | * 3 = $$$ 35 | * 4 = $$$$ 36 | * 37 | * see yelp api documentation for more details 38 | */ 39 | const { businesses } = res.locals; 40 | res.json(businesses); 41 | }); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const path = require('path'); 4 | 5 | const app = express(); 6 | const PORT = 5000; 7 | 8 | // routers 9 | const businessesRouter = require('./routes/businesses.js'); 10 | const locationRouter = require('./routes/location.js'); 11 | const newsRouter = require('./routes/news.js'); 12 | const weatherRouter = require('./routes/weather.js'); 13 | const favoritesRouter = require('./routes/favorites.js'); 14 | 15 | // application-level middleware 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: true })); 18 | 19 | // route handlers 20 | app.use('/businesses', businessesRouter); 21 | app.use('/location', locationRouter); 22 | app.use('/news', newsRouter); 23 | app.use('/weather', weatherRouter); 24 | app.use('/favorites', favoritesRouter); 25 | 26 | if (process.env.NODE_ENV === 'production') { 27 | app.use('/build', express.static(path.resolve(__dirname, '..build'))); 28 | app.get('/', (req, res) => { 29 | res.sendFile(path.resolve(__dirname, '../template.html')); 30 | }); 31 | } 32 | 33 | // catch-all route handler 34 | app.use('*', (req, res) => res.sendStatus(404)); 35 | 36 | // global error handler 37 | app.use((err, req, res, next) => { 38 | const defaultErr = { 39 | log: 'Express error handler caught unknown middleware error', 40 | status: 400, 41 | message: { err: `An error occured. ERROR: ${JSON.stringify(err)}` }, 42 | }; 43 | 44 | const errObj = Object.assign({}, defaultErr, err); 45 | 46 | console.log(errObj.log); 47 | 48 | return res.status(errObj.status).json(errObj.message); 49 | }); 50 | 51 | app.listen(PORT, () => { 52 | console.log(`Server listening on port: ${PORT}`); 53 | }); 54 | -------------------------------------------------------------------------------- /Yelp API Example.txt: -------------------------------------------------------------------------------- 1 | Yelp businesses response example 2 | { 3 | “id”: “vRrVSB-LegwUwIxpkeRVtQ”, 4 | “alias”: “le-bernardin-new-york”, 5 | “name”: “Le Bernardin”, 6 | “image_url”: “https://s3-media4.fl.yelpcdn.com/bphoto/c54rsVNmAhn_Ccz4tIXI_Q/o.jpg”, 7 | “is_closed”: false, 8 | “url”: “https://www.yelp.com/biz/le-bernardin-new-york?adjust_creative=MraceK43kbvzPpYaB7uTBQ&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=MraceK43kbvzPpYaB7uTBQ”, 9 | “review_count”: 2537, 10 | “categories”: [ 11 | { 12 | “alias”: “french”, 13 | “title”: “French” 14 | }, 15 | { 16 | “alias”: “lounges”, 17 | “title”: “Lounges” 18 | } 19 | ], 20 | “rating”: 4.5, 21 | “coordinates”: { 22 | “latitude”: 40.761557, 23 | “longitude”: -73.981763 24 | }, 25 | “transactions”: [], 26 | “price”: “$$$$“, 27 | “location”: { 28 | “address1": “155 W 51st St”, 29 | “address2": “”, 30 | “address3": “The Equitable Bldg”, 31 | “city”: “New York”, 32 | “zip_code”: “10019”, 33 | “country”: “US”, 34 | “state”: “NY”, 35 | “display_address”: [ 36 | “155 W 51st St”, 37 | “The Equitable Bldg”, 38 | “New York, NY 10019” 39 | ] 40 | }, 41 | “phone”: “+12125541515”, 42 | “display_phone”: “(212) 554-1515”, 43 | “distance”: 6328.07179784047 44 | }, 45 | -------------------------------------------------------------------------------- /server/controllers/locationController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const locationController = {}; 4 | 5 | const API_KEY = process.env.GEOCODE_API_KEY; 6 | 7 | // get information from google maps api 8 | locationController.getLocationData = (req, res, next) => { 9 | const { location } = req.params; 10 | // TODO: handle possible parsing of params in case of spaces in location 'los angeles' 11 | const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${location}&key=${API_KEY}`; 12 | fetch(url) 13 | .then((data) => data.json()) 14 | .then((locationData) => { 15 | const { address_components, geometry } = locationData.results[0]; 16 | const { lat, lng } = geometry.location; 17 | res.locals.latitude = lat; 18 | res.locals.longitude = lng; 19 | res.locals.addressComponenets = address_components; 20 | 21 | return next(); 22 | }) 23 | .catch((error) => 24 | next({ 25 | message: `Error in locationController.getLocationData; ERROR: ${JSON.stringify( 26 | error 27 | )}`, 28 | }) 29 | ); 30 | }; 31 | 32 | // get countrycode from google geocode api address_components section in results 33 | locationController.getCountryCode = (req, res, next) => { 34 | try { 35 | // iterate through res.locals.addressComponenets 36 | for (let i = 0; i < res.locals.addressComponenets.length; i += 1) { 37 | const obj = res.locals.addressComponenets[i]; 38 | /* if the object types key contains value of "country", 39 | store lowercase version of "short_name" key's value */ 40 | if (obj.types.includes('country')) { 41 | res.locals.countryCode = obj.short_name.toLowerCase(); 42 | return next(); 43 | } 44 | } 45 | res.locals.countryCode = null; 46 | return next(); 47 | } catch (error) { 48 | return next({ 49 | message: `Error in locationController.getCountryCode; ERROR: ${JSON.stringify( 50 | error 51 | )}`, 52 | }); 53 | } 54 | }; 55 | 56 | module.exports = locationController; 57 | -------------------------------------------------------------------------------- /server/routes/favorites.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const favoritesController = require('../controllers/favoritesController.js'); 4 | 5 | /* 6 | router.get( 7 | '/', 8 | favoritesController.getFavBusinesses, 9 | favoritesController.getFavNews, 10 | (req, res) => { 11 | const { favBusinesses, favNews } = res.locals; 12 | // for the frontend: 13 | //{ favBusinesses: {_id: businessObj, _id: businessObj, ...}, 14 | // favNews: {_id: newsObj, _id: newsObj, ...} 15 | const favorites = { businesses: favBusinesses, news: favNews }; 16 | res.status(200).send(favBusinesses); 17 | } 18 | ); 19 | */ 20 | 21 | // BUSINESS/ACTIVITY 22 | 23 | router.post('/business', favoritesController.addFavBusiness, (req, res) => { 24 | res.status(200).send(res.locals.message); 25 | }); 26 | 27 | // saved favorite business info outdated - should fetch current info from yelp to return to user 28 | // need user_id and business_id from query 29 | // should this be with favoritesControllers or businessesControllers? 30 | router.get('/business', favoritesController.updateFavBusiness, (req, res) => { 31 | res.status(200).json(res.locals.businessInfo); 32 | }); 33 | 34 | // delete favorite business - remove entry in user_fav_business - might have to consider deletion cascade 35 | // need user_id and business_id from query 36 | router.delete( 37 | '/business', 38 | favoritesController.deleteFavBusiness, 39 | (req, res) => { 40 | res.status(200).send(res.locals.message); 41 | } 42 | ); 43 | 44 | // NEWS 45 | 46 | router.post('/news', favoritesController.addFavNews, (req, res) => { 47 | res.status(200).send(res.locals.message); 48 | }); 49 | 50 | /* saved favorite news outdated...? 51 | router.get('/news', favoritesController.updateFavNews, (req, res) => { 52 | res.status(200).json({ message: 'would we ever need to update news?' }); 53 | }); 54 | */ 55 | 56 | router.delete('/news/', favoritesController.deleteFavNews, (req, res) => { 57 | res.status(200).send(res.locals.message); 58 | }); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /server/models/db_postgres_create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Users ( 2 | _id SERIAL PRIMARY KEY, 3 | -- username VARCHAR NOT NULL UNIQUE, 4 | first_name VARCHAR NOT NULL, 5 | last_name VARCHAR NOT NULL, 6 | email VARCHAR NOT NULL UNIQUE, 7 | password VARCHAR NOT NULL, 8 | location VARCHAR DEFAULT 'NYC' 9 | ) 10 | 11 | -- activities, using yelp id as _id, client side need to send these data in the post request 12 | CREATE TABLE Businesses ( 13 | _id VARCHAR PRIMARY KEY, 14 | name VARCHAR NOT NULL, 15 | rating INT, 16 | review VARCHAR, 17 | location VARCHAR, 18 | image_url VARCHAR, 19 | url VARCHAR, 20 | category VARCHAR 21 | ) 22 | 23 | CREATE TABLE News ( 24 | _id SERIAL PRIMARY KEY, 25 | url VARCHAR NOT NULL, 26 | urlToImage VARCHAR, 27 | title VARCHAR NOT NULL, 28 | source_name VARCHAR NOT NULL, 29 | category VARCHAR 30 | ) 31 | 32 | CREATE TABLE user_fav_businesses ( 33 | _id SERIAL PRIMARY KEY, 34 | user_id INT NOT NULL, 35 | business_id VARCHAR NOT NULL, 36 | saved_date DATE DEFAULT CURRENT_DATE, 37 | FOREIGN KEY (user_id) REFERENCES users (_id), 38 | FOREIGN KEY (business_id) REFERENCES businesses (_id) 39 | ) 40 | 41 | CREATE TABLE user_fav_news ( 42 | _id SERIAL PRIMARY KEY, 43 | user_id INT NOT NULL, 44 | news_id INT NOT NULL, 45 | saved_date DATE DEFAULT CURRENT_DATE, 46 | FOREIGN KEY (user_id) REFERENCES Users (_id), 47 | FOREIGN KEY (news_id) REFERENCES News (_id) 48 | ) 49 | 50 | CREATE TABLE user_business_categories ( 51 | _id SERIAL PRIMARY KEY, 52 | user_id INT NOT NULL, 53 | business_category VARCHAR NOT NULL, 54 | FOREIGN KEY (user_id) REFERENCES Users (_id) 55 | ) 56 | 57 | 58 | CREATE TABLE user_news_categories ( 59 | _id SERIAL PRIMARY KEY, 60 | user_id INT NOT NULL, 61 | news_category VARCHAR NOT NULL, 62 | FOREIGN KEY (user_id) REFERENCES Users (_id) 63 | ) 64 | 65 | 66 | -- CREATE TABLE user_fav_events ( 67 | -- _id SERIAL PRIMARY KEY, 68 | -- user_id INT NOT NULL, 69 | -- event_id INT NOT NULL, 70 | -- saved_date DATE NOT NULL, 71 | -- FOREIGN KEY (user_id) REFERENCES Users (_id), 72 | -- FOREIGN KEY (event_id) REFERENCES Events (_id) 73 | -- ) 74 | 75 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); 4 | 5 | module.exports = { 6 | entry: path.resolve(__dirname, 'client/index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/i, // Allows to transpile JSX 15 | exclude: /(node_modules)/, // Exclude them from compilation because they are already in JS 16 | use: { 17 | loader: 'babel-loader', // package name 18 | options: { 19 | presets: ['@babel/preset-env', '@babel/preset-react'], 20 | }, 21 | }, 22 | }, 23 | { 24 | test: /\.html$/, 25 | use: [ 26 | { 27 | loader: 'html-loader', 28 | }, 29 | ], 30 | }, 31 | { 32 | test: /.(css|scss)$/, 33 | exclude: /node_modules/, 34 | use: ['style-loader', 'css-loader', 'sass-loader'], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new HtmlWebPackPlugin({ 40 | template: path.resolve(__dirname, 'client/template.html'), 41 | filename: 'index.html', 42 | alwaysWriteToDisk: true, 43 | }), 44 | new HtmlWebpackHarddiskPlugin(), 45 | ], 46 | devServer: { 47 | host: 'localhost', 48 | port: 8080, 49 | contentBase: path.join(__dirname, 'build'), 50 | publicPath: '/', 51 | hot: true, 52 | historyApiFallback: true, 53 | headers: { 54 | 'Access-Control-Allow-Origin': '*', 55 | }, 56 | proxy: { 57 | '/businesses/**': { 58 | target: 'http://localhost:5000', 59 | secure: false, 60 | }, 61 | '/location/**': { 62 | target: 'http://localhost:5000', 63 | secure: false, 64 | }, 65 | '/news/**': { 66 | target: 'http://localhost:5000', 67 | secure: false, 68 | }, 69 | '/weather/**': { 70 | target: 'http://localhost:5000', 71 | secure: false, 72 | }, 73 | }, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from 'react-bootstrap/Form'; 3 | import Button from 'react-bootstrap/Button'; 4 | import Tabs from 'react-bootstrap/Tabs'; 5 | import Tab from 'react-bootstrap/Tab'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | function Login() { 9 | return ( 10 |
11 |

Welcome!

12 | 13 | 14 |
15 | 16 | First Name 17 | 18 | 19 | 20 | 21 | Last Name 22 | 23 | 24 | 25 | 26 | Email address 27 | 28 | 29 | We'll never share your email with anyone else. 30 | 31 | 32 | 33 | 34 | Password 35 | 36 | 37 | 40 |
41 |
42 | 43 |
44 | 45 | Email address 46 | 47 | 48 | 49 | 50 | Password 51 | 52 | 53 | 56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export default Login; 64 | -------------------------------------------------------------------------------- /API-Data-Examples/Yelp_Businesses_Reviews.json: -------------------------------------------------------------------------------- 1 | // GET https://api.yelp.com/v3/businesses/{id}/reviews 2 | { 3 | "reviews": [ 4 | { 5 | "id": "xAG4O7l-t1ubbwVAlPnDKg", 6 | "rating": 5, 7 | "user": { 8 | "id": "W8UK02IDdRS2GL_66fuq6w", 9 | "profile_url": "https://www.yelp.com/user_details?userid=W8UK02IDdRS2GL_66fuq6w", 10 | "image_url": "https://s3-media3.fl.yelpcdn.com/photo/iwoAD12zkONZxJ94ChAaMg/o.jpg", 11 | "name": "Ella A." 12 | }, 13 | "text": "Went back again to this place since the last time i visited the bay area 5 months ago, and nothing has changed. Still the sketchy Mission, Still the cashier...", 14 | "time_created": "2016-08-29 00:41:13", 15 | "url": "https://www.yelp.com/biz/la-palma-mexicatessen-san-francisco?hrid=hp8hAJ-AnlpqxCCu7kyCWA&adjust_creative=0sidDfoTIHle5vvHEBvF0w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_reviews&utm_source=0sidDfoTIHle5vvHEBvF0w" 16 | }, 17 | { 18 | "id": "1JNmYjJXr9ZbsfZUAgkeXQ", 19 | "rating": 4, 20 | "user": { 21 | "id": "rk-MwIUejOj6LWFkBwZ98Q", 22 | "profile_url": "https://www.yelp.com/user_details?userid=rk-MwIUejOj6LWFkBwZ98Q", 23 | "image_url": null, 24 | "name": "Yanni L." 25 | }, 26 | "text": "The \"restaurant\" is inside a small deli so there is no sit down area. Just grab and go.\n\nInside, they sell individually packaged ingredients so that you can...", 27 | "time_created": "2016-09-28 08:55:29", 28 | "url": "https://www.yelp.com/biz/la-palma-mexicatessen-san-francisco?hrid=fj87uymFDJbq0Cy5hXTHIA&adjust_creative=0sidDfoTIHle5vvHEBvF0w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_reviews&utm_source=0sidDfoTIHle5vvHEBvF0w" 29 | }, 30 | { 31 | "id": "SIoiwwVRH6R2s2ipFfs4Ww", 32 | "rating": 4, 33 | "user": { 34 | "id": "rpOyqD_893cqmDAtJLbdog", 35 | "profile_url": "https://www.yelp.com/user_details?userid=rpOyqD_893cqmDAtJLbdog", 36 | "image_url": null, 37 | "name": "Suavecito M." 38 | }, 39 | "text": "Dear Mission District,\n\nI miss you and your many delicious late night food establishments and vibrant atmosphere. I miss the way you sound and smell on a...", 40 | "time_created": "2016-08-10 07:56:44", 41 | "url": "https://www.yelp.com/biz/la-palma-mexicatessen-san-francisco?hrid=m_tnQox9jqWeIrU87sN-IQ&adjust_creative=0sidDfoTIHle5vvHEBvF0w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_reviews&utm_source=0sidDfoTIHle5vvHEBvF0w" 42 | } 43 | ], 44 | "total": 3, 45 | "possible_languages": ["en"] 46 | } -------------------------------------------------------------------------------- /client/components/DetailedWeather.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import Card from 'react-bootstrap/Card'; 5 | import CardDeck from 'react-bootstrap/CardDeck'; 6 | // import CardGroup from 'react-bootstrap/CardGroup'; 7 | import * as actions from '../actions/actions.js' 8 | 9 | // const mapDispatchToProps = dispatch => ({ 10 | // addCity(data) { dispatch(actions.addCity(data)) } 11 | // }); 12 | 13 | const mapStateToProps = ({ 14 | informationReducer: { weatherDays } 15 | }) => ({ weatherDays }); 16 | 17 | const DetailedWeather = props => { 18 | const WEATHER_API_URI = '#'; 19 | const weatherInfo = { 20 | dayName: 'Monday', 21 | description: 'Clear Sky', 22 | imgURL: 'http://openweathermap.org/img/wn/01d@2x.png', 23 | currentTemp: '75', 24 | hiTemp: '80', 25 | loTemp: '65', 26 | humidity: '100', 27 | windSpeed: '10', 28 | sunRise: '06:30', 29 | sunSet: '19:00' 30 | } 31 | 32 | const arrayOfDays = [weatherInfo, weatherInfo, 33 | weatherInfo, weatherInfo, weatherInfo, weatherInfo, weatherInfo]; 34 | const daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; 35 | const convertKtoF = (K) => Math.round((((K - 273.15) * 9) / 5) + 32); 36 | 37 | const slicedWeatherDays = props.weatherDays.slice(0, props.weatherDays.length - 1); 38 | const weatherArr = slicedWeatherDays.map((day, index) => { 39 | // TODO: dynamically generate day of week :) 40 | console.log('Day: ', day.dt); 41 | const imgCode = day.weather[0].icon; 42 | const dayOfWeek = daysOfWeek[index]; 43 | 44 | console.log(dayOfWeek); 45 | 46 | return ( 47 | 48 | {dayOfWeek} 49 | 50 | 51 | {/* Current Temp: {day.currentTemp}°F */} 52 | Hi: {convertKtoF(day.temp.max)}°F 53 | Lo: {convertKtoF(day.temp.min)}°F 54 | Humidity: {day.humidity}% 55 | Wind Speed: {day.wind_speed} MPH 56 | 57 | 58 | ) 59 | }); 60 | return ( 61 |
62 |

Detailed Weather Information

63 | 64 | {weatherArr} 65 | 66 |
67 | ); 68 | } 69 | export default connect(mapStateToProps, null)(DetailedWeather); 70 | 71 | -------------------------------------------------------------------------------- /client/components/FrontPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/actions'; 4 | import { Link } from 'react-router-dom'; 5 | import WeatherView from './WeatherView.jsx'; 6 | import NewsView from './NewsView.jsx'; 7 | import ActivitiesView from './ActivitiesView.jsx'; 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | addCity(data) { dispatch(actions.addCity(data)) } 11 | }); 12 | 13 | const mapStateToProps = ({ 14 | informationReducer: { lat, long, countryCode, city } 15 | }) => ({ lat, long, countryCode, city }); 16 | 17 | const Search = (props) => { 18 | const [searchValue, setSearchValue] = useState(''); 19 | const [city, setCity] = useState(''); 20 | 21 | const handleChange = (event) => { 22 | setSearchValue(event.target.value); 23 | } 24 | 25 | const handleSubmit = (event) => { 26 | event.preventDefault(); 27 | if (!searchValue) return alert('Please type in a city'); 28 | setCity(sendLocation(searchValue)); 29 | event.preventDefault(); 30 | } 31 | 32 | const sendLocation = (location) => { 33 | fetch(`/location/${location}`, { 34 | method: 'GET', 35 | headers: { 36 | "Content-Type": "Application/JSON" 37 | } 38 | }) 39 | .then(res => res.json()) 40 | .then(data => { 41 | setCity(searchValue); 42 | props.addCity({ 43 | ...data, 44 | city: searchValue 45 | }) 46 | }) 47 | .then(data => { 48 | setCity(searchValue); 49 | props.addCity({ 50 | ...data, 51 | city: searchValue 52 | }) 53 | }) 54 | .catch(err => console.log('Location fetch ERROR: ', err)); 55 | return location; 56 | } 57 | 58 | return ( 59 |
60 |
61 |
62 | 63 | 64 | {/* */} 65 | 66 | 67 |
68 |
69 |

Find the best your city has to offer

70 |
71 | 72 | 73 |
74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 | ); 82 | } 83 | 84 | export default connect(mapStateToProps, mapDispatchToProps)(Search); 85 | -------------------------------------------------------------------------------- /server/controllers/businessesController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const businessesController = {}; 4 | 5 | const API_KEY = process.env.BUSINESSES_API_KEY; 6 | 7 | businessesController.getBusinesses = (req, res, next) => { 8 | const { category } = req.params; 9 | const { lat, lon } = req.query; 10 | 11 | console.log('from getBusiness', category, lat, lon); 12 | // log error if latitude or longitude are undefined 13 | if (lat === undefined || lon === undefined) { 14 | return next({ 15 | log: 16 | 'businessController.getBusinesses: ERROR: lat and/or lon are undefined', 17 | message: { 18 | err: 19 | 'businessesController.getBusinesses: ERROR: Check server logs for details', 20 | }, 21 | }); 22 | } 23 | 24 | const limit = 5; // we can make this dynamic later 25 | const url = `https://api.yelp.com/v3/businesses/search?latitude=${lat}&longitude=${lon}&categories=${category}&limit=${limit}`; 26 | 27 | fetch(url, { 28 | headers: { 29 | Authorization: `Bearer ${API_KEY}`, 30 | }, 31 | }) 32 | .then((data) => data.json()) 33 | .then(({ businesses }) => { 34 | // console.log(businesses); 35 | res.locals.businesses = businesses; 36 | return next(); 37 | }) 38 | .catch((err) => next(err)); 39 | }; 40 | 41 | businessesController.getBusinessesByPrice = (req, res, next) => { 42 | const { category, priceLevel } = req.params; 43 | const { lat, lon } = req.query; 44 | 45 | // log error if latitude or longitude are undefined 46 | if (lat === undefined || lon === undefined) { 47 | return next({ 48 | log: 49 | 'businessController.getBusinesses: ERROR: lat and/or lon are undefined', 50 | message: { 51 | err: 52 | 'businessesController.getBusinesses: ERROR: Check server logs for details', 53 | }, 54 | }); 55 | } 56 | 57 | // log error if priceLevel is an invalid input 58 | if (priceLevel < 1 || priceLevel > 4) { 59 | return next({ 60 | log: 61 | 'businessesController.getBusinessesByPrice: ERROR: priceLevel is invalid. priceLevel must be: 1 <= priceLevel <= 4.', 62 | message: { 63 | err: 64 | 'businessesController.getBusinessesByPrice: ERROR: Check server logs for details', 65 | }, 66 | }); 67 | } 68 | 69 | const limit = 5; // we can make this dynamic later 70 | const url = `https://api.yelp.com/v3/businesses/search?latitude=${lat}&longitude=${lon}&categories=${category}&price=${priceLevel}&limit=${limit}`; 71 | 72 | fetch(url, { 73 | headers: { 74 | Authorization: `Bearer ${API_KEY}`, 75 | }, 76 | }) 77 | .then((data) => data.json()) 78 | .then(({ businesses }) => { 79 | res.locals.businesses = businesses; 80 | return next(); 81 | }) 82 | .catch((err) => next(err)); 83 | }; 84 | 85 | module.exports = businessesController; 86 | -------------------------------------------------------------------------------- /client/components/WeatherView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import * as actions from '../actions/actions.js'; 5 | 6 | const mapStateToProps = ({ informationReducer: { lat, long } }) => ({ 7 | lat, 8 | long, 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | addWeather(data) { 13 | dispatch(actions.addWeather(data)); 14 | }, 15 | }); 16 | 17 | const WeatherView = (props) => { 18 | const [weatherData, setWeatherData] = useState([]); 19 | const [fetchedData, setFetchedData] = useState(false); 20 | 21 | const fetchData = () => { 22 | fetch(`/weather/?latitude=${props.lat}&longitude=${props.long}`, { 23 | method: 'GET', 24 | headers: { 25 | 'Content-Type': 'Application/JSON', 26 | }, 27 | }) 28 | .then((res) => res.json()) 29 | .then((data) => { 30 | setWeatherData([data.weather]); 31 | setFetchedData(true); 32 | props.addWeather(data.weather.daily); 33 | console.log('WeatherView Data: ', data.weather.daily); 34 | }) 35 | .catch((err) => console.log('Weather fetch ERROR: ', err)); 36 | }; 37 | 38 | const convertKtoF = (K) => Math.round(((K - 273.15) * 9) / 5 + 32); 39 | 40 | const dayOfWeek = (dayNum) => { 41 | switch (dayNum) { 42 | case 0: 43 | return 'Sunday'; 44 | case 1: 45 | return 'Monday'; 46 | case 2: 47 | return 'Tuesday'; 48 | case 3: 49 | return 'Wednesday'; 50 | case 4: 51 | return 'Thursday'; 52 | case 5: 53 | return 'Friday'; 54 | case 6: 55 | return 'Saturday'; 56 | default: 57 | return 'Invalid input'; 58 | } 59 | }; 60 | 61 | const createWeatherBoxes = (data) => { 62 | const dayNum = new Date().getDay(); 63 | return data.map((day, i) => { 64 | return ( 65 |
66 | 67 |
{dayOfWeek(dayNum)}
68 |
69 | 72 |
73 |

{convertKtoF(day.daily[0].temp.max)}°F

74 |

{convertKtoF(day.daily[0].temp.min)}°F

75 |
76 |
77 | ); 78 | }); 79 | }; 80 | 81 | useEffect(() => { 82 | if (!fetchedData) fetchData(); 83 | }, []); 84 | 85 | useEffect(() => { 86 | fetchData(); 87 | }, [props.city]); 88 | 89 | if (fetchedData) { 90 | const weatherDivs = createWeatherBoxes(weatherData); 91 | return ( 92 |
93 | {weatherDivs} 94 |
95 | ); 96 | } else { 97 | return
Fetching weather info
; 98 | } 99 | }; 100 | 101 | export default connect(mapStateToProps, mapDispatchToProps)(WeatherView); 102 | 103 | /*TODO: 104 | get more days for weather 105 | fix search 106 | link up redux 107 | more info weather 108 | */ 109 | -------------------------------------------------------------------------------- /API-Data-Examples/Yelp_Businesses_id.json: -------------------------------------------------------------------------------- 1 | // GET https://api.yelp.com/v3/businesses/{id} 2 | // https://www.yelp.com/developers/documentation/v3/business 3 | 4 | { 5 | "id": "WavvLdfdP6g8aZTtbBQHTw", 6 | "alias": "gary-danko-san-francisco", 7 | "name": "Gary Danko", 8 | "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/CPc91bGzKBe95aM5edjhhQ/o.jpg", 9 | "is_claimed": true, 10 | "is_closed": false, 11 | "url": "https://www.yelp.com/biz/gary-danko-san-francisco?adjust_creative=wpr6gw4FnptTrk1CeT8POg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_lookup&utm_source=wpr6gw4FnptTrk1CeT8POg", 12 | "phone": "+14157492060", 13 | "display_phone": "(415) 749-2060", 14 | "review_count": 5296, 15 | "categories": [ 16 | { 17 | "alias": "newamerican", 18 | "title": "American (New)" 19 | }, 20 | { 21 | "alias": "french", 22 | "title": "French" 23 | }, 24 | { 25 | "alias": "wine_bars", 26 | "title": "Wine Bars" 27 | } 28 | ], 29 | "rating": 4.5, 30 | "location": { 31 | "address1": "800 N Point St", 32 | "address2": "", 33 | "address3": "", 34 | "city": "San Francisco", 35 | "zip_code": "94109", 36 | "country": "US", 37 | "state": "CA", 38 | "display_address": [ 39 | "800 N Point St", 40 | "San Francisco, CA 94109" 41 | ], 42 | "cross_streets": "" 43 | }, 44 | "coordinates": { 45 | "latitude": 37.80587, 46 | "longitude": -122.42058 47 | }, 48 | "photos": [ 49 | "https://s3-media2.fl.yelpcdn.com/bphoto/CPc91bGzKBe95aM5edjhhQ/o.jpg", 50 | "https://s3-media4.fl.yelpcdn.com/bphoto/FmXn6cYO1Mm03UNO5cbOqw/o.jpg", 51 | "https://s3-media4.fl.yelpcdn.com/bphoto/HZVDyYaghwPl2kVbvHuHjA/o.jpg" 52 | ], 53 | "price": "$$$$", 54 | "hours": [ 55 | { 56 | "open": [ 57 | { 58 | "is_overnight": false, 59 | "start": "1730", 60 | "end": "2200", 61 | "day": 0 62 | }, 63 | { 64 | "is_overnight": false, 65 | "start": "1730", 66 | "end": "2200", 67 | "day": 1 68 | }, 69 | { 70 | "is_overnight": false, 71 | "start": "1730", 72 | "end": "2200", 73 | "day": 2 74 | }, 75 | { 76 | "is_overnight": false, 77 | "start": "1730", 78 | "end": "2200", 79 | "day": 3 80 | }, 81 | { 82 | "is_overnight": false, 83 | "start": "1730", 84 | "end": "2200", 85 | "day": 4 86 | }, 87 | { 88 | "is_overnight": false, 89 | "start": "1730", 90 | "end": "2200", 91 | "day": 5 92 | }, 93 | { 94 | "is_overnight": false, 95 | "start": "1730", 96 | "end": "2200", 97 | "day": 6 98 | } 99 | ], 100 | "hours_type": "REGULAR", 101 | "is_open_now": false 102 | } 103 | ], 104 | "transactions": [], 105 | "special_hours": [ 106 | { 107 | "date": "2019-02-07", 108 | "is_closed": null, 109 | "start": "1600", 110 | "end": "2000", 111 | "is_overnight": false 112 | } 113 | ] 114 | } -------------------------------------------------------------------------------- /Weather API Example.txt: -------------------------------------------------------------------------------- 1 | { 2 | "lat": 40.12, 3 | "lon": -96.66, 4 | "timezone": "America/Chicago", 5 | "timezone_offset": -18000, 6 | "current": { 7 | "dt": 1595243443, 8 | "sunrise": 1595243663, 9 | "sunset": 1595296278, 10 | "temp": 293.28, 11 | "feels_like": 293.82, 12 | "pressure": 1016, 13 | "humidity": 100, 14 | "dew_point": 293.28, 15 | "uvi": 10.64, 16 | "clouds": 90, 17 | "visibility": 10000, 18 | "wind_speed": 4.6, 19 | "wind_deg": 310, 20 | "weather": [ 21 | { 22 | "id": 501, 23 | "main": "Rain", 24 | "description": "moderate rain", 25 | "icon": "10n" 26 | }, 27 | { 28 | "id": 201, 29 | "main": "Thunderstorm", 30 | "description": "thunderstorm with rain", 31 | "icon": "11n" 32 | } 33 | ], 34 | "rain": { 35 | "1h": 2.93 36 | } 37 | }, 38 | "minutely": [ 39 | { 40 | "dt": 1595243460, 41 | "precipitation": 2.928 42 | }, 43 | ... 44 | }, 45 | "hourly": [ 46 | { 47 | "dt": 1595242800, 48 | "temp": 293.28, 49 | "feels_like": 293.82, 50 | "pressure": 1016, 51 | "humidity": 100, 52 | "dew_point": 293.28, 53 | "clouds": 90, 54 | "visibility": 10000, 55 | "wind_speed": 4.6, 56 | "wind_deg": 123, 57 | "weather": [ 58 | { 59 | "id": 501, 60 | "main": "Rain", 61 | "description": "moderate rain", 62 | "icon": "10n" 63 | } 64 | ], 65 | "pop": 0.99, 66 | "rain": { 67 | "1h": 2.46 68 | } 69 | }, 70 | ... 71 | } 72 | "daily": [ 73 | { 74 | "dt": 1595268000, 75 | "sunrise": 1595243663, 76 | "sunset": 1595296278, 77 | "temp": { 78 | "day": 298.82, 79 | "min": 293.25, 80 | "max": 301.9, 81 | "night": 293.25, 82 | "eve": 299.72, 83 | "morn": 293.48 84 | }, 85 | "feels_like": { 86 | "day": 300.06, 87 | "night": 292.46, 88 | "eve": 300.87, 89 | "morn": 293.75 90 | }, 91 | "pressure": 1014, 92 | "humidity": 82, 93 | "dew_point": 295.52, 94 | "wind_speed": 5.22, 95 | "wind_deg": 146, 96 | "weather": [ 97 | { 98 | "id": 502, 99 | "main": "Rain", 100 | "description": "heavy intensity rain", 101 | "icon": "10d" 102 | } 103 | ], 104 | "clouds": 97, 105 | "pop": 1, 106 | "rain": 12.57, 107 | "uvi": 10.64 108 | }, 109 | ... 110 | }, 111 | "alerts": [ 112 | { 113 | "sender_name": "NWS Tulsa (Eastern Oklahoma)", 114 | "event": "Heat Advisory", 115 | "start": 1597341600, 116 | "end": 1597366800, 117 | "description": "...HEAT ADVISORY REMAINS IN EFFECT FROM 1 PM THIS AFTERNOON TO\n8 PM CDT THIS EVENING...\n* WHAT...Heat index values of 105 to 109 degrees expected.\n* WHERE...Creek, Okfuskee, Okmulgee, McIntosh, Pittsburg,\nLatimer, Pushmataha, and Choctaw Counties.\n* WHEN...From 1 PM to 8 PM CDT Thursday.\n* IMPACTS...The combination of hot temperatures and high\nhumidity will combine to create a dangerous situation in which\nheat illnesses are possible." 118 | }, 119 | ... 120 | ]\ 121 | 122 | 123 | -------------------------------------------------------------------------------- /API-Data-Examples/Weather%20API%20Example.txt: -------------------------------------------------------------------------------- 1 | { 2 | "lat": 40.12, 3 | "lon": -96.66, 4 | "timezone": "America/Chicago", 5 | "timezone_offset": -18000, 6 | "current": { 7 | "dt": 1595243443, 8 | "sunrise": 1595243663, 9 | "sunset": 1595296278, 10 | "temp": 293.28, 11 | "feels_like": 293.82, 12 | "pressure": 1016, 13 | "humidity": 100, 14 | "dew_point": 293.28, 15 | "uvi": 10.64, 16 | "clouds": 90, 17 | "visibility": 10000, 18 | "wind_speed": 4.6, 19 | "wind_deg": 310, 20 | "weather": [ 21 | { 22 | "id": 501, 23 | "main": "Rain", 24 | "description": "moderate rain", 25 | "icon": "10n" 26 | }, 27 | { 28 | "id": 201, 29 | "main": "Thunderstorm", 30 | "description": "thunderstorm with rain", 31 | "icon": "11n" 32 | } 33 | ], 34 | "rain": { 35 | "1h": 2.93 36 | } 37 | }, 38 | "minutely": [ 39 | { 40 | "dt": 1595243460, 41 | "precipitation": 2.928 42 | }, 43 | ... 44 | }, 45 | "hourly": [ 46 | { 47 | "dt": 1595242800, 48 | "temp": 293.28, 49 | "feels_like": 293.82, 50 | "pressure": 1016, 51 | "humidity": 100, 52 | "dew_point": 293.28, 53 | "clouds": 90, 54 | "visibility": 10000, 55 | "wind_speed": 4.6, 56 | "wind_deg": 123, 57 | "weather": [ 58 | { 59 | "id": 501, 60 | "main": "Rain", 61 | "description": "moderate rain", 62 | "icon": "10n" 63 | } 64 | ], 65 | "pop": 0.99, 66 | "rain": { 67 | "1h": 2.46 68 | } 69 | }, 70 | ... 71 | } 72 | "daily": [ 73 | { 74 | "dt": 1595268000, 75 | "sunrise": 1595243663, 76 | "sunset": 1595296278, 77 | "temp": { 78 | "day": 298.82, 79 | "min": 293.25, 80 | "max": 301.9, 81 | "night": 293.25, 82 | "eve": 299.72, 83 | "morn": 293.48 84 | }, 85 | "feels_like": { 86 | "day": 300.06, 87 | "night": 292.46, 88 | "eve": 300.87, 89 | "morn": 293.75 90 | }, 91 | "pressure": 1014, 92 | "humidity": 82, 93 | "dew_point": 295.52, 94 | "wind_speed": 5.22, 95 | "wind_deg": 146, 96 | "weather": [ 97 | { 98 | "id": 502, 99 | "main": "Rain", 100 | "description": "heavy intensity rain", 101 | "icon": "10d" 102 | } 103 | ], 104 | "clouds": 97, 105 | "pop": 1, 106 | "rain": 12.57, 107 | "uvi": 10.64 108 | }, 109 | ... 110 | }, 111 | "alerts": [ 112 | { 113 | "sender_name": "NWS Tulsa (Eastern Oklahoma)", 114 | "event": "Heat Advisory", 115 | "start": 1597341600, 116 | "end": 1597366800, 117 | "description": "...HEAT ADVISORY REMAINS IN EFFECT FROM 1 PM THIS AFTERNOON TO\n8 PM CDT THIS EVENING...\n* WHAT...Heat index values of 105 to 109 degrees expected.\n* WHERE...Creek, Okfuskee, Okmulgee, McIntosh, Pittsburg,\nLatimer, Pushmataha, and Choctaw Counties.\n* WHEN...From 1 PM to 8 PM CDT Thursday.\n* IMPACTS...The combination of hot temperatures and high\nhumidity will combine to create a dangerous situation in which\nheat illnesses are possible." 118 | }, 119 | ... 120 | ]\ 121 | 122 | 123 | -------------------------------------------------------------------------------- /client/components/NewsView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Card from 'react-bootstrap/Card'; 4 | import CardDeck from 'react-bootstrap/CardDeck'; 5 | import Button from 'react-bootstrap/Button'; 6 | 7 | const mapStateToProps = ({ 8 | informationReducer: { lat, long, countryCode }, 9 | }) => ({ lat, long, countryCode }); 10 | 11 | const NewsView = (props) => { 12 | const [newsData, setNewsData] = useState([]); 13 | const [fetchedData, setFetchedData] = useState(false); 14 | const [currentArticles, setCurrentArticles] = useState([]); 15 | 16 | const DEFAULT_IMG = 17 | 'https://joebalestrino.com/wp-content/uploads/2019/02/Marketplace-Lending-News.jpg'; 18 | 19 | const createNewsArticles = (newsObject, category = 'business') => { 20 | return newsObject[category].map((newsInfo, i) => { 21 | return ( 22 | // TODO: transfer in-line styles to styles.css 23 | 24 |
25 | 26 | 31 | 32 |
33 | 34 | {newsInfo.title} 35 | {newsInfo.source.name} 36 | 37 |
38 | ); 39 | }); 40 | }; 41 | 42 | const fetchData = (category = 'business') => { 43 | fetch(`/news/${props.countryCode}?category=${category}`, { 44 | method: 'GET', 45 | headers: { 46 | 'Content-Type': 'Application/JSON', 47 | }, 48 | }) 49 | .then((res) => res.json()) 50 | .then((data) => { 51 | console.log(`/news/${props.countryCode}?category=${category}`); 52 | console.log(data.news); 53 | setNewsData(data.news); 54 | setFetchedData(true); 55 | setCurrentArticles(createNewsArticles(data.news)); 56 | }) 57 | .catch((err) => console.log('News fetch ERROR: ', err)); 58 | }; 59 | 60 | const changeCategory = (category) => { 61 | return () => { 62 | setCurrentArticles(createNewsArticles(newsData, category)); 63 | }; 64 | }; 65 | 66 | useEffect(() => { 67 | if (!fetchedData) fetchData(); 68 | }, []); 69 | 70 | useEffect(() => { 71 | fetchData(); 72 | }, [props.city]); 73 | 74 | useEffect(() => { 75 | fetchData(); 76 | }, [props.city]); 77 | 78 | if (!newsData) return null; 79 | 80 | if (fetchedData) { 81 | const CATEGORIES = [ 82 | 'business', 83 | 'entertainment', 84 | 'general', 85 | 'health', 86 | 'science', 87 | 'sports', 88 | 'technology', 89 | ]; 90 | const buttonsArray = []; 91 | 92 | for (let i = 0; i < CATEGORIES.length; i += 1) { 93 | buttonsArray.push( 94 | 102 | ); 103 | } 104 | 105 | return ( 106 |
107 |

Local News Information

108 | {buttonsArray} 109 |
110 | {currentArticles} 111 |
112 |
113 | ); 114 | } else { 115 | return

Fetching from database

; 116 | } 117 | }; 118 | export default connect(mapStateToProps, null)(NewsView); 119 | -------------------------------------------------------------------------------- /client/components/ActivitiesView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Card from 'react-bootstrap/Card'; 4 | import CardDeck from 'react-bootstrap/CardDeck'; 5 | import Button from 'react-bootstrap/Button'; 6 | 7 | const mapStateToProps = ({ 8 | informationReducer: { lat, long, countryCode }, 9 | }) => ({ lat, long, countryCode }); 10 | 11 | const ActivitiesView = (props) => { 12 | const [activitiesData, setActivitiesData] = useState([]); 13 | const [fetchedData, setFetchedData] = useState(false); 14 | const [currentActivities, setCurrentActivities] = useState([]); // DISCUSS 15 | 16 | const countryCode = 'US'; 17 | const DEFAULT_IMG = 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=1000&q=80'; 18 | 19 | const createActivities = (activitiesObject, category) => { 20 | return activitiesObject.map((activitiesInfo, i) => { 21 | return ( 22 | 23 |
24 | 25 |
26 | 27 | {activitiesInfo.name} 28 | 29 | Rating: {activitiesInfo.rating} 30 | 31 | 32 | Reviews: {activitiesInfo.review} 33 | 34 | 35 | Location: {activitiesInfo.location.address1} 36 | 37 | 38 |
39 | ); 40 | }); 41 | }; 42 | 43 | const fetchData = (category = 'bars') => { 44 | fetch(`/businesses/${category}?lat=${props.lat}&lon=${props.long}`, { 45 | method: 'GET', 46 | headers: { 47 | "Content-Type": "Application/JSON", 48 | }, 49 | }) 50 | .then((res) => (res.json())) 51 | .then((data) => { 52 | setActivitiesData(data); 53 | setFetchedData(true); 54 | setCurrentActivities(createActivities(data)); 55 | }) 56 | .catch((err) => console.log('Activities fetch ERROR: ', err)); 57 | }; 58 | 59 | const changeCategory = (category) => { 60 | return () => { 61 | fetchData(category); 62 | // setCurrentActivities(createActivities(activitiesData, category)); // DISCUSS 63 | }; 64 | }; 65 | 66 | useEffect(() => { 67 | if (!fetchedData) fetchData(); 68 | }, []); 69 | 70 | useEffect(() => { 71 | fetchData(); 72 | }, [props.city]) 73 | 74 | if (!activitiesData) return null; 75 | 76 | if (fetchedData) { 77 | const CATEGORIES = ['restaurants', 'bars', 'climbing', 'health', 'bowling', 'fitness']; 78 | const buttonsArray = []; 79 | 80 | for (let i = 0; i < CATEGORIES.length; i += 1) { 81 | buttonsArray.push( 82 | , 91 | ); 92 | } 93 | 94 | return ( 95 |
96 |

Local Activities Information

97 |
98 | {buttonsArray} 99 |
100 |
101 | 102 | {currentActivities} 103 | 104 |
105 |
106 | ); 107 | } else { 108 | return ( 109 |

Fetching from database

110 | ); 111 | } 112 | }; 113 | 114 | export default connect(mapStateToProps, null)(ActivitiesView); 115 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin:0; 4 | padding:0; 5 | text-decoration: none; 6 | } 7 | 8 | .main-content { 9 | width: 100%; 10 | padding: 30px; 11 | 12 | } 13 | 14 | .news-container { 15 | margin: 50px 0px; 16 | } 17 | 18 | .item-wrapper { 19 | /* height: auto; 20 | width: auto; */ 21 | display: flex; 22 | /* border: 1px solid black; */ 23 | /* margin: 10px; */ 24 | /* flex: 1; */ 25 | box-shadow: 2px 2px #444444; 26 | 27 | height: 250px; 28 | width: 18%; 29 | min-width: 150px; 30 | border: 1px solid black; 31 | margin: 10px; 32 | font-size: 12px; 33 | margin-top: 5px; 34 | box-shadow: 2px 2px #444444; 35 | 36 | } 37 | 38 | .item-wrapper img{ 39 | height: 150px; 40 | width: 100%; 41 | display: flex; 42 | border: 1px solid black; 43 | } 44 | 45 | .item-wrapper p, strong{ 46 | margin: 0; 47 | font-size: 11px; 48 | height: auto; 49 | width: 100%; 50 | display: flex; 51 | flex-direction: column; 52 | } 53 | 54 | .weather-container { 55 | display: flex; 56 | width: fit-content; 57 | align-self: flex-end; 58 | } 59 | 60 | .weather-wrapper { 61 | display: flex; 62 | flex-direction: column; 63 | border: 1px solid black; 64 | border-radius: 2%; 65 | box-shadow: 2px 2px #444444; 66 | margin-right: 5px; 67 | background-color: rgba(255, 255, 255, 0.8); 68 | } 69 | 70 | .weather-wrapper img{ 71 | margin: 0px; 72 | align-self: center; 73 | } 74 | 75 | .weather-wrapper p{ 76 | width: auto; 77 | justify-content: center; 78 | } 79 | 80 | .weather-wrapper strong{ 81 | margin: 5px 0px 0px 0px; 82 | } 83 | 84 | .temp-wrapper { 85 | display: flex; 86 | justify-content: space-between; 87 | } 88 | 89 | .temp-wrapper p{ 90 | margin: 0px 5px 5px 5px; 91 | } 92 | 93 | .detailed-weather-container { 94 | display: flex; 95 | } 96 | 97 | .detailed-weather-wrapper { 98 | display: flex; 99 | flex-direction: column; 100 | border: 1px solid black; 101 | flex: 1; 102 | align-items: center; 103 | } 104 | 105 | .banner { 106 | width: 100%; 107 | } 108 | 109 | .hero-container { 110 | background-image: url('https://www.developmentguild.com/assets/nyc-banner-night.jpg'); 111 | /* background-size: auto; */ 112 | display: flex; 113 | flex-direction: column; 114 | height: 120%; 115 | min-height: 400px; 116 | justify-content: space-between; 117 | align-items: center; 118 | background-repeat: no-repeat; 119 | background-size: cover; 120 | } 121 | 122 | .search-input { 123 | width: 80%; 124 | min-width:300px; 125 | height: 30px; 126 | margin-bottom: 10px; 127 | } 128 | 129 | .search-btn { 130 | width: 50px; 131 | height: 30px; 132 | margin-bottom: 10px; 133 | font-size:13px; 134 | font-weight: bold; 135 | } 136 | 137 | .search-wrapper h1{ 138 | font-size: 27px; 139 | margin-bottom: 10px; 140 | } 141 | 142 | .search-wrapper { 143 | background: rgba(255, 255, 255, 0.7); 144 | padding-top: 15px; 145 | border-radius: 3px; 146 | width:24%; 147 | min-width:450px; 148 | display: flex; 149 | flex-direction: column; 150 | align-items: center; 151 | } 152 | 153 | .signup-login-container { 154 | width: 600px; 155 | } 156 | 157 | .signup-login-form { 158 | padding: 20px 0px; 159 | } 160 | 161 | .card-img-container { 162 | height: 250px; 163 | display: flex; 164 | flex-direction: column; 165 | justify-content: center; 166 | align-items: center; 167 | overflow: hidden; 168 | background: #dfdfdf; 169 | } 170 | 171 | .card-img { 172 | height: 100%; 173 | } 174 | 175 | #title { 176 | font-size: 27px; 177 | } 178 | 179 | .loginButton { 180 | align-self: flex-start; 181 | } 182 | 183 | #loginButton { 184 | margin:5px; 185 | width: 50px; 186 | height: 30px; 187 | margin-bottom: 10px; 188 | font-size:13px; 189 | font-weight: bold; 190 | background-color: Transparent; 191 | transition: color, background-color; 192 | border-radius:5px; 193 | } 194 | 195 | #loginButton:hover { 196 | color:white; 197 | background-color:gray; 198 | } 199 | 200 | .top-container { 201 | display: flex; 202 | justify-content: space-between; 203 | width: 100%; 204 | } 205 | /* 206 | .activity-card { 207 | width: 18%; 208 | min-width: 150px; 209 | } */ 210 | -------------------------------------------------------------------------------- /server/controllers/favoritesController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/models'); 2 | const fetch = require('node-fetch'); 3 | const { ResolvePlugin } = require('webpack'); 4 | 5 | const favoritesController = {}; 6 | 7 | const YELP_API_KEY = process.env.Businesses_API_KEY; 8 | 9 | favoritesController.getFavBusinesses = (req, res, next) => { 10 | // const { user_id } = req.query; 11 | // if using this middleware when loggin/signing in - should be somewhere in the res.locals.user 12 | const { _id } = res.locals.user; 13 | 14 | const queryStr = ` 15 | SELECT b._id AS id, b.name, b.rating, b.review, b.location, b.image_url, b.url 16 | FROM businesses AS b 17 | INNER JOIN user_fav_businesses 18 | ON b._id = user_fav_businesses.business_id 19 | WHERE user_fav_businesses.user_id = $1`; 20 | 21 | db.query(queryStr, [_id]) 22 | .then((data) => { 23 | if (!data.rows[0]) { 24 | return next({ 25 | message: `database query from getFavBusiness returned undefined`, 26 | }); 27 | } 28 | // organize data into to an object 29 | // { 30 | // business_id (yelp id): {businessObj} 31 | // } 32 | // businessObj = {_id, name, url, ...} 33 | // data.rows is an array of businessObj 34 | res.locals.favBusinesses = data.rows.reduce((obj, businessObj) => { 35 | businessObj.location = JSON.parse(businessObj.location); 36 | obj[businessObj._id] = businessObj; 37 | return obj; 38 | }, {}); 39 | console.log('favBusiness is an obj of objs', res.locals.favBusinesses); 40 | return next(); 41 | }) 42 | .catch((error) => { 43 | return next({ 44 | message: `Error in favoritesController.getLocationData; ERROR: ${JSON.stringify( 45 | error 46 | )}`, 47 | }); 48 | }); 49 | }; 50 | 51 | favoritesController.addFavBusiness = (req, res, next) => { 52 | const { user_id, business_id } = req.query; 53 | console.log( 54 | 'from addFav, user_id and business_id with reqBody', 55 | user_id, 56 | business_id, 57 | req.body 58 | ); 59 | const { id, name, url, rating, review, location, image_url } = req.body; 60 | 61 | // insert business into Businesses table 62 | // what happens if value is undefined? 63 | let queryStr = ` 64 | INSERT INTO Businesses (_id, name, url, rating, review, location, image_url) 65 | VALUES ( $1, $2, $3, $4, $5, $6, $7) 66 | RETURNING _id`; 67 | 68 | let values = [ 69 | id, 70 | name, 71 | url, 72 | rating, 73 | review, 74 | JSON.stringify(location), 75 | image_url, 76 | ]; 77 | 78 | db.query(queryStr, values) 79 | .then((data) => { 80 | if (!data.rows[0]) { 81 | return next({ 82 | message: `database insertion from addFavBusiness1 returned undefined`, 83 | }); 84 | } 85 | // now add to user_fav_businesses 86 | queryStr = ` 87 | INSERT INTO user_fav_businesses (user_id, business_id) 88 | VALUES ($1, $2) 89 | RETURNING _id`; 90 | 91 | values = [user_id, business_id]; 92 | db.query(queryStr, values).then((data) => { 93 | if (!data.rows[0]) { 94 | return next({ 95 | message: `database insertion from addFavBusiness2 returned undefined`, 96 | }); 97 | } 98 | return next(); 99 | }); 100 | }) 101 | .catch((error) => { 102 | return next({ 103 | message: `Error in favoritesController.addFavBusiness; ERROR: ${JSON.stringify( 104 | error 105 | )}`, 106 | }); 107 | }); 108 | }; 109 | 110 | favoritesController.updateFavBusiness = (req, res, next) => { 111 | const { user_id, business_id } = req.query; 112 | 113 | // make an api request to yelp 114 | const url = `https://api.yelp.com/v3/businesses/${business_id}`; 115 | 116 | fetch(url, { 117 | headers: { 118 | Authorization: `Bearer ${YELP_API_KEY}`, 119 | }, 120 | }) 121 | .then((data) => data.json()) 122 | .then((data) => { 123 | // data is a BusinessObj 124 | const { 125 | id, 126 | name, 127 | image_url, 128 | url, 129 | review_count, 130 | rating, 131 | categories, 132 | location, 133 | } = data; 134 | res.locals.businessInfo = { 135 | id, 136 | name, 137 | image_url, 138 | url, 139 | review: review_count, 140 | rating, 141 | location, 142 | }.catch((err) => next(err)); 143 | // update the businesses table 144 | 145 | let queryStr = ` 146 | INSERT INTO Businesses (_id, name, image_url, url, review, rating, location) 147 | VALUES ($1, $2, $3, $4, $5, $6, $7)`; 148 | db.query(queryStr, [ 149 | id, 150 | name, 151 | image_url, 152 | url, 153 | review_count, 154 | rating, 155 | JSON.stringify(location), 156 | ]) 157 | .then((data) => { 158 | // update the user_fav_businesses table 159 | let queryStr = ` 160 | INSERT INTO user_fav_businesses (user_id, business_id) 161 | VALUES ($1, $2) 162 | RETURNING _id`; 163 | db.query(queryStr, [user_id, business_id]) 164 | .then((data) => { 165 | console.log('user_fav_businesses id', data.row[0]); 166 | return next(); 167 | }) 168 | .catch((err) => next(err)); 169 | }) 170 | .catch((err) => next(err)); 171 | }); 172 | return next(); 173 | }; 174 | 175 | favoritesController.deleteFavBusiness = (req, res, next) => { 176 | const { user_id, business_id } = req.query; 177 | 178 | const queryStr = ` 179 | DELETE FROM user_fav_businesses 180 | WHERE user_id = $1 AND business_id = $2 181 | RETURNING _id;`; 182 | 183 | const values = [user_id, business_id]; 184 | db.query(queryStr, values) 185 | .then((data) => { 186 | console.log(data.rows.length + 'entries deleted'); 187 | res.locals.message = 'deletion success'; 188 | return next(); 189 | }) 190 | .catch((error) => { 191 | return next({ 192 | message: `Error in favoritesController.deleteFavBusiness; ERROR: ${JSON.stringify( 193 | error 194 | )}`, 195 | }); 196 | }); 197 | }; 198 | 199 | favoritesController.getFavNews = (req, res, next) => { 200 | const { _id } = res.locals.user; 201 | 202 | const queryStr = ` 203 | SELECT News._id AS id, News.url, News.urlToImage, News.title, News.source_name 204 | FROM News 205 | INNER JOIN user_fav_news 206 | ON news._id = user_fav_news.news_id 207 | WHERE user_fav_news.user_id = $1`; 208 | 209 | db.query(queryStr, [_id]) 210 | .then((data) => { 211 | if (!data.rows[0]) { 212 | return next({ 213 | message: `database query from getFavBusiness returned undefined`, 214 | }); 215 | } 216 | 217 | res.locals.favNews = data.rows.reduce((obj, newsObj) => { 218 | obj[newsObj._id] = newsObj; 219 | return obj; 220 | }, {}); 221 | console.log('favNews is an obj of objs', res.locals.favNews); 222 | return next(); 223 | }) 224 | .catch((error) => { 225 | return next({ 226 | message: `Error in favoritesController.getFavNews; ERROR: ${JSON.stringify( 227 | error 228 | )}`, 229 | }); 230 | }); 231 | }; 232 | 233 | favoritesController.addFavNews = (req, res, next) => { 234 | return next(); 235 | }; 236 | 237 | favoritesController.deleteFavNews = (req, res, next) => { 238 | const { user_id, news_id } = req.query; 239 | 240 | const queryStr = ` 241 | DELETE FROM user_fav_businesses 242 | WHERE user_id = $1 AND business_id = $2 243 | RETURNING _id;`; 244 | 245 | const values = [user_id, news_id]; 246 | db.query(queryStr, values) 247 | .then((data) => { 248 | console.log(data.rows.length + 'entries deleted'); 249 | res.locals.message = 'deletion success'; 250 | return next(); 251 | }) 252 | .catch((error) => { 253 | return next({ 254 | message: `Error in favoritesController.deleteFavNews; ERROR: ${JSON.stringify( 255 | error 256 | )}`, 257 | }); 258 | }); 259 | }; 260 | 261 | module.exports = favoritesController; 262 | -------------------------------------------------------------------------------- /API-Data-Examples/NewsApi.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "ok", 3 | "totalResults": 3129, 4 | -"articles": [ 5 | -{ 6 | -"source": { 7 | "id": "business-insider", 8 | "name": "Business Insider" 9 | }, 10 | "author": "Melia Russell", 11 | "title": "An incident known as 'bathroomgate' left some Coinbase employees feeling 'targeted,' say former workers. It's the kind of fight CEO Brian Armstrong wants to avoid.", 12 | "description": "Summary List Placement\n\n \nThe signs were unmissable. They hung next to the doors of office bathrooms at Coinbase and said, \"Coinbase recognizes that gender is not binary. You are free and safe to use whichever bathroom is most comfortable for you.\"\nWhen emplo…", 13 | "url": "https://www.businessinsider.com/bathroomgate-incident-is-one-example-of-political-drama-at-coinbase-2020-10", 14 | "urlToImage": "https://i.insider.com/5f77aa162400440019129cc5?width=1200&format=jpeg", 15 | "publishedAt": "2020-10-02T22:38:40Z", 16 | "content": "The signs were unmissable. They hung next to the doors of office bathrooms at Coinbase and said, \"Coinbase recognizes that gender is not binary. You are free and safe to use whichever bathroom is mos… [+3939 chars]" 17 | }, 18 | -{ 19 | -"source": { 20 | "id": null, 21 | "name": "Customer Think" 22 | }, 23 | "author": "Ved Raj", 24 | "title": "Top 5 Thought-Provoking Use Cases Of Blockchain In Banking & Finance Sector", 25 | "description": "Image Source: yourstory.com Blockchain technology has a huge crowd around it that cherishes and admires its capabilities. Though, banking and finance industry experts share a great devotion to this technology. By 2018, 90% of European and U.S financial and ba…", 26 | "url": "https://customerthink.com/top-5-thought-provoking-use-cases-of-blockchain-in-banking-finance-sector/", 27 | "urlToImage": "https://customerthink.com/wp-content/uploads/block-chain-3145392_1280-pixabay-blockchain-tech.jpg", 28 | "publishedAt": "2020-10-02T21:44:57Z", 29 | "content": "Image Source: yourstory.com\r\nBlockchain technology has a huge crowd around it that cherishes and admires its capabilities. Though, banking and finance industry experts share a great devotion to this … [+9929 chars]" 30 | }, 31 | -{ 32 | -"source": { 33 | "id": null, 34 | "name": "Cointelegraph" 35 | }, 36 | "author": "Cointelegraph By Marcel Pechman", 37 | "title": "What BitMEX scandal? Bitcoin futures data shows traders focused on $12K", 38 | "description": "Bitcoin futures and options sentiment held steady despite the BitMEX and Kucoin news, signaling a $12K bull run is near.", 39 | "url": "https://cointelegraph.com/news/what-bitmex-scandal-bitcoin-futures-data-shows-traders-focused-on-12k", 40 | "urlToImage": "https://s3.cointelegraph.com/storage/uploads/view/3f13fa828b3b1ff0043e516d46630874.jpg", 41 | "publishedAt": "2020-10-02T21:35:00Z", 42 | "content": "BitMEX used to be the indisputable leader of Bitcoin (BTC) futures trading and if something similar to yesterday's civil enforcement action were to happen back in 2015-2018 the crypto markets would h… [+5789 chars]" 43 | }, 44 | -{ 45 | -"source": { 46 | "id": null, 47 | "name": "Cointelegraph" 48 | }, 49 | "author": "Cointelegraph By Kollen Post", 50 | "title": "CFTC promises to protect 'the burgeoning markets for digital assets such as Bitcoin'", 51 | "description": "The CFTC continues its roll of crypto announcements this week in a recent fraud bust-up.", 52 | "url": "https://cointelegraph.com/news/cftc-promises-to-protect-the-burgeoning-markets-for-digital-assets-such-as-bitcoin", 53 | "urlToImage": "https://s3.eu-central-1.amazonaws.com/s3.cointelegraph.com/uploads/2020-10/a61a3562-f818-4603-a7ae-d8731b6e242a.jpg", 54 | "publishedAt": "2020-10-02T21:28:55Z", 55 | "content": "On Friday, the Commodity Futures Trading Commission made a fraudster pay back $7.4 million to investors while vowing to protect the Bitcoin market.\r\nPer the CFTC's announcement, James McDonald, Direc… [+800 chars]" 56 | }, 57 | -{ 58 | -"source": { 59 | "id": null, 60 | "name": "Monevator.com" 61 | }, 62 | "author": "The Investor", 63 | "title": "Weekend reading: Gold fingered", 64 | "description": "The real cost of partaking in the gold rally, plus the rest of the week's good reads…", 65 | "url": "https://monevator.com/weekend-reading-gold-fingered/", 66 | "urlToImage": null, 67 | "publishedAt": "2020-10-02T21:12:05Z", 68 | "content": "What caught my eye this week.\r\nI was fortunate to come into 2020 with some gold ETFs and a couple of miners in my portfolio.\r\nPlease don’t say I’d ‘got religion’. (We know what happens to people who … [+7158 chars]" 69 | }, 70 | -{ 71 | -"source": { 72 | "id": null, 73 | "name": "Yahoo Entertainment" 74 | }, 75 | "author": "Daniel Cawrey", 76 | "title": "Market Wrap: Bitcoin Rebounds to $10.5K; Stablecoin Market Cap ‘Goes Parabolic’", 77 | "description": "Bitcoin price has proved resilient in the face of bad news but traders expect crypto volatility ahead.", 78 | "url": "https://finance.yahoo.com/news/market-wrap-bitcoin-rebounds-10-203640114.html", 79 | "urlToImage": "https://s.yimg.com/ny/api/res/1.2/oQCafdEH5sPBn985OmgBog--/YXBwaWQ9aGlnaGxhbmRlcjt3PTEyODA7aD04MjIuNA--/https://s.yimg.com/uu/api/res/1.2/1DNOnj4WyYzW27ms5iaBSg--~B/aD0xNjQ1O3c9MjU2MDtzbT0xO2FwcGlkPXl0YWNoeW9u/https://media.zenfs.com/en/coindesk_75/256da594a611901e54a8d60f7bb058d0", 80 | "publishedAt": "2020-10-02T20:36:40Z", 81 | "content": "Bitcoin has performed well in the face of a bleak news cycle while stablecoin assets in the crypto ecosystem continue to grow.\r\n