├── .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 |
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 |
38 | Sign Up!
39 |
40 |
41 |
42 |
43 |
45 | Email address
46 |
47 |
48 |
49 |
50 | Password
51 |
52 |
53 |
54 | Log In
55 |
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 | Login
64 | {/* */}
65 |
66 |
67 |
68 |
69 |
Find the best your city has to offer
70 |
74 |
75 |
76 |
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 |
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 |
100 | {CATEGORIES[i]}
101 |
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 |
89 | {CATEGORIES[i]}
90 | ,
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\nBitcoin (BTC) trading around $10,515 as of 20:00 UTC (4 p.m. ET… [+4895 chars]"
82 | },
83 | -{
84 | -"source": {
85 | "id": null,
86 | "name": "CoinDesk"
87 | },
88 | "author": "Daniel Cawrey",
89 | "title": "Market Wrap: Bitcoin Rebounds to $10.5K; Stablecoin Market Cap ‘Goes Parabolic’",
90 | "description": "Bitcoin price has proved resilient in the face of bad news but traders expect crypto volatility ahead.",
91 | "url": "https://www.coindesk.com/market-wrap-bitcoin-rebounds-stablecoin-parabolic",
92 | "urlToImage": "https://static.coindesk.com/wp-content/uploads/2020/10/cdbpioct2-1200x628.jpg",
93 | "publishedAt": "2020-10-02T20:36:40Z",
94 | "content": "Bitcoin has performed well in the face of a bleak news cycle while stablecoin assets in the crypto ecosystem continue to grow.\r\nBitcoin (BTC) trading around $10,515 as of 20:00 UTC (4 p.m. ET… [+4739 chars]"
95 | },
96 | -{
97 | -"source": {
98 | "id": null,
99 | "name": "Yahoo Entertainment"
100 | },
101 | "author": null,
102 | "title": "U.S. Immigration and Customs Enforcement wants to automate its accounting — and that includes transactions in bitcoin",
103 | "description": "ICE has put out a request for information for software that the agency can use for financial management — including its dealings in bitcoin.The post U.S. Immigration and Customs Enforcement wants to automate its accounting — and that includes transactions in …",
104 | "url": "https://consent.yahoo.com/v2/collectConsent?sessionId=1_cc-session_e919b02d-3a4b-498e-b3d6-3e7b2a4ad33f",
105 | "urlToImage": null,
106 | "publishedAt": "2020-10-02T20:32:39Z",
107 | "content": "Yahoo fait partie de Verizon Media. Nos partenaires et nous-mêmes stockerons et/ou utiliserons des informations concernant votre appareil, par lintermédiaire de cookies et de technologies similaires,… [+879 chars]"
108 | },
109 | -{
110 | -"source": {
111 | "id": "business-insider",
112 | "name": "Business Insider"
113 | },
114 | "author": "insider@insider.com (Jean Folger)",
115 | "title": "A self-directed IRA gives you control over a greater choice of investment options, but it also means more responsibility and risks",
116 | "description": "A self-directed IRA (SDIRA) can invest in assets that are off-limits to regular IRAs. You directly manage the account, held by a special custodian.",
117 | "url": "https://www.businessinsider.com/what-is-a-self-directed-ira",
118 | "urlToImage": "https://i.insider.com/5f778b662400440019129c64?width=1200&format=jpeg",
119 | "publishedAt": "2020-10-02T20:21:53Z",
120 | "content": "When it comes to IRA investments, stocks, bonds, and mutual funds/exchange-traded funds (ETFs) are traditionally the assets of choice. After all, these securities are easy to buy and sell, and in the… [+7731 chars]"
121 | },
122 | -{
123 | -"source": {
124 | "id": null,
125 | "name": "CoinDesk"
126 | },
127 | "author": "Muyao Shen",
128 | "title": "Binance, Gemini, Kraken So Far the Winners From BitMEX’s Legal Woes",
129 | "description": "Binance, Gemini, and Kraken have become the biggest winners since US regulators' charges against BitMEX on Thursday.",
130 | "url": "https://www.coindesk.com/bitmex-bitcoin-outflows-binance-gemini-kraken",
131 | "urlToImage": "https://static.coindesk.com/wp-content/uploads/2014/10/shutterstock_99647405-1200x628.jpg",
132 | "publishedAt": "2020-10-02T20:15:23Z",
133 | "content": "U.S. regulatory authorities on Thursday brought a series of civil and criminal charges against BitMEX. Since then more than 41,000 bitcoin were withdrawn from the Seychelles-based crypto exchange. Wh… [+1367 chars]"
134 | },
135 | -{
136 | -"source": {
137 | "id": null,
138 | "name": "FXStreet"
139 | },
140 | "author": "Lorenzo Stroe",
141 | "title": "Bitcoin’s CME gap at $9,600 could be filled soon after renewed selling pressure",
142 | "description": "On October 1, one of the biggest cryptocurrency exchanges, BitMEX, was charged by the CFTC for illegally operating a cryptocurrency derivatives tradin",
143 | "url": "https://www.fxstreet.com/cryptocurrencies/news/bitcoins-cme-gap-at-9-600-could-be-filled-soon-after-renewed-selling-pressure-202010022010",
144 | "urlToImage": "https://editorial.fxstreet.com/images/Markets/Currencies/Digital Currencies/Bitcoin/bitcoins-33758372_Large.jpg",
145 | "publishedAt": "2020-10-02T20:10:22Z",
146 | "content": "Bitcoin is currently trading at $10,560 after a notable price rejection from $10,800. News about BitMEX getting charged by the CFTC had a major impact on the price of BTC. On… [+3130 chars]"
147 | },
148 | -{
149 | -"source": {
150 | "id": null,
151 | "name": "Cointelegraph"
152 | },
153 | "author": "Cointelegraph By Benjamin Pirus",
154 | "title": "Technology itself is deflationary, Diginex CEO says",
155 | "description": "Answers on rationale for money printing, and why markets have risen amid a pandemic.",
156 | "url": "https://cointelegraph.com/news/technology-itself-is-deflationary-diginex-ceo-says",
157 | "urlToImage": "https://s3.eu-central-1.amazonaws.com/s3.cointelegraph.com/uploads/2020-09/bcb051fd-1aa2-4f1f-b50f-c67888a27e75.jpg",
158 | "publishedAt": "2020-10-02T20:04:40Z",
159 | "content": "Over the years, technology has improved by leaps and bounds, therefore making life more cost-effective and efficient. Such technological improvements, however, may not save citizens money as intended… [+4022 chars]"
160 | },
161 | -{
162 | -"source": {
163 | "id": "techcrunch",
164 | "name": "TechCrunch"
165 | },
166 | "author": "Megan Rose Dickey",
167 | "title": "Human Capital: Coinbase and Clubhouse aside, Ethel’s Club founder wants to take us ‘Somewhere Good’",
168 | "description": "Welcome back to Human Capital, a weekly digest about diversity, inclusion and the human labor that powers tech. This week, we’re looking at a number of topics because a lot went down. Coinbase CEO Brian Armstrong took a controversial stance on social, Clubhou…",
169 | "url": "http://techcrunch.com/2020/10/02/human-capital-coinbase-and-clubhouse-aside-ethels-club-founder-wants-to-take-us-somewhere-good/",
170 | "urlToImage": "https://techcrunch.com/wp-content/uploads/2020/09/naj-headshot-june-2020-1.jpeg?w=533",
171 | "publishedAt": "2020-10-02T20:00:49Z",
172 | "content": "Welcome back to Human Capital, a weekly digest about diversity, inclusion and the human labor that powers tech.\r\nThis week, we’re looking at a number of topics because a lot went down. Coinbase CEO B… [+11181 chars]"
173 | },
174 | -{
175 | -"source": {
176 | "id": null,
177 | "name": "Bitcoinist"
178 | },
179 | "author": "Cole Petersen",
180 | "title": "Ethereum to See Further Losses as Crypto Market Becomes “Beyond Bearish”",
181 | "description": "Ethereum and the aggregated crypto market have been caught in the throes of a strong downtrend throughout the past 24-hours This has come about due to multiple macro developments, including news about President Trump’s health as well as the government’s decis…",
182 | "url": "https://bitcoinist.com/ethereum-to-see-further-losses-as-crypto-market-becomes-beyond-bearish/",
183 | "urlToImage": "https://bitcoinist.com/wp-content/uploads/2020/10/erwan-hesry-IqB5MPcQp6k-unsplash-1920x1280.jpg",
184 | "publishedAt": "2020-10-02T20:00:06Z",
185 | "content": "Ethereum and the aggregated crypto market have been caught in the throes of a strong downtrend throughout the past 24-hours This has come about due to multiple macro developments, inc… [+2383 chars]"
186 | },
187 | -{
188 | -"source": {
189 | "id": null,
190 | "name": "newsBTC"
191 | },
192 | "author": "Tony Spilotro",
193 | "title": "DeFi Token Yearn.Finance (YFI) Breaks Massive Pattern Neckline, What’s Next?",
194 | "description": "Yearn.Finance (YFI) has stolen the attention away from most other DeFi tokens, Ethereum, and Bitcoin thanks to an amazing run where tens of thousands per coin in value were added. But the Ethereum-based ERC20 token has recently broken below and is now retesti…",
195 | "url": "https://www.newsbtc.com/2020/10/02/defi-token-yearn-finance-yfi-breaks-massive-pattern-neckline-whats-next/",
196 | "urlToImage": "https://www.newsbtc.com/wp-content/uploads/2020/10/yearn.finance-yfi-Depositphotos_27059155_xl-2015-scaled.jpg",
197 | "publishedAt": "2020-10-02T20:00:05Z",
198 | "content": "Yearn.Finance (YFI) has stolen the attention away from most other DeFi tokens, Ethereum, and Bitcoin thanks to an amazing run where tens of thousands per coin in value were added.\r\nBut the Ethereum-b… [+2574 chars]"
199 | },
200 | -{
201 | -"source": {
202 | "id": null,
203 | "name": "Cointelegraph"
204 | },
205 | "author": "Cointelegraph By Kollen Post",
206 | "title": "Law Decoded: The year of the Crypto Futures Trading Commission, Sept. 25–Oct. 2",
207 | "description": "The end of the U.S. federal government's fiscal year brought a cascade of major announcements from agencies.",
208 | "url": "https://cointelegraph.com/news/law-decoded-the-year-of-the-crypto-futures-trading-commission-sept-25-oct-2",
209 | "urlToImage": "https://s3.eu-central-1.amazonaws.com/s3.cointelegraph.com/uploads/2020-10/0b4fa1c2-7d02-42bb-82fb-a6a096f77b2b.jpg",
210 | "publishedAt": "2020-10-02T19:58:00Z",
211 | "content": "Every Friday, Law Decoded delivers analysis on the weeks critical stories in the realms of policy, regulation and law.\r\nEditor's note\r\nIn a tweet late last night, President Trump said that he and Mel… [+6480 chars]"
212 | },
213 | -{
214 | -"source": {
215 | "id": null,
216 | "name": "CoinDesk"
217 | },
218 | "author": "Zack Voell",
219 | "title": "Bitcoin Miners Saw 11% Revenue Drop in September",
220 | "description": "Miners generated an estimated $328 million in September.",
221 | "url": "https://www.coindesk.com/bitcoin-mining-revenue-september",
222 | "urlToImage": "https://static.coindesk.com/wp-content/uploads/2020/10/september-miner-rev-1200x628.png",
223 | "publishedAt": "2020-10-02T19:42:26Z",
224 | "content": "Bitcoin miners generated an estimated $328 million in revenue in September, down 11% from August, according to Coin Metrics data analyzed by CoinDesk.\r\nThe moderate decrease in revenue came a… [+633 chars]"
225 | },
226 | -{
227 | -"source": {
228 | "id": null,
229 | "name": "CoinDesk"
230 | },
231 | "author": "Nathaniel Whittemore",
232 | "title": "‘Good Reason to Worry’: What the BitMEX Indictment Means for DeFi and Bitcoin, Feat. Stephen Palley and Preston Byrne",
233 | "description": "Crypto legal experts join to discuss the U.S. government’s case against BitMEX and its implications for the broader ecosystem.",
234 | "url": "https://www.coindesk.com/bitmex-indictment-defi-palley-byrne",
235 | "urlToImage": "https://static.coindesk.com/wp-content/uploads/2020/10/Breakdown-10.2-1200x628.jpg",
236 | "publishedAt": "2020-10-02T19:00:00Z",
237 | "content": "Crypto legal experts join to discuss the U.S. governments case against BitMEX and its implications for the broader ecosystem. \r\nFor more episodes and free early access before our regular 3 p.m. Easte… [+1474 chars]"
238 | },
239 | -{
240 | -"source": {
241 | "id": null,
242 | "name": "Yahoo Entertainment"
243 | },
244 | "author": null,
245 | "title": "‘Good Reason to Worry’: What the BitMEX Indictment Means for DeFi and Bitcoin, Feat. Stephen Palley and Preston Byrne",
246 | "description": "Crypto legal experts join to discuss the U.S. government’s case against BitMEX and its implications for the broader ecosystem.",
247 | "url": "https://consent.yahoo.com/v2/collectConsent?sessionId=1_cc-session_51415517-b043-42bf-a8d7-3052369edb9e",
248 | "urlToImage": null,
249 | "publishedAt": "2020-10-02T19:00:00Z",
250 | "content": "Yahoo fait partie de Verizon Media. Nos partenaires et nous-mêmes stockerons et/ou utiliserons des informations concernant votre appareil, par lintermédiaire de cookies et de technologies similaires,… [+879 chars]"
251 | },
252 | -{
253 | -"source": {
254 | "id": null,
255 | "name": "Yahoo Entertainment"
256 | },
257 | "author": null,
258 | "title": "Bitcoin’s Rising Correlation With Stocks Debunks Haven Narrative",
259 | "description": "(Bloomberg) -- Stocks were jolted Friday following news President Donald Trump tested positive for coronavirus. But in a move counter to the often-touted narrative that the dominant cryptocurrency acts as a haven, Bitcoin also retreated.That’s because the cor…",
260 | "url": "https://consent.yahoo.com/v2/collectConsent?sessionId=1_cc-session_8771f325-531d-4a4f-99d0-1088259be336",
261 | "urlToImage": null,
262 | "publishedAt": "2020-10-02T18:57:51Z",
263 | "content": "Yahoo fait partie de Verizon Media. Nos partenaires et nous-mêmes stockerons et/ou utiliserons des informations concernant votre appareil, par lintermédiaire de cookies et de technologies similaires,… [+879 chars]"
264 | }
265 | ]
266 | }
--------------------------------------------------------------------------------