├── .babelrc ├── .gitignore ├── README.md ├── client ├── App.jsx ├── actions │ └── actions.js ├── assets │ ├── background.gif │ ├── background_new.gif │ ├── db_schema.png │ ├── logo.png │ └── wave-starting-point.png ├── components │ ├── Login.jsx │ ├── Logout.jsx │ ├── MainContainer.jsx │ ├── Navbar.jsx │ ├── ProfileIcon.jsx │ ├── ResultCard.jsx │ ├── ResultsContainer.jsx │ ├── Sidebar.jsx │ └── Signup.jsx ├── constants │ └── actionTypes.js ├── index.html ├── index.js ├── reducers │ ├── index.js │ └── mainReducer.js ├── store.js └── style.scss ├── dist ├── 1ba3cd3495ab9b506bf2.gif ├── bundle.js ├── bundle.js.LICENSE.txt └── index.html ├── package.json ├── server ├── controllers │ ├── controller.js │ └── userController.js ├── db │ └── users.sql ├── middleware │ └── authMiddleware.js ├── models │ ├── dbModel.js │ └── responseModel.js ├── routes │ ├── auth.js │ └── router.js ├── secrets │ ├── secrets.js │ └── secrets_blank.js └── server.js ├── test ├── controllerTest.js ├── react_test.js └── responseModel.test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | server/secrets/secrets.js 3 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Frollic 2 | 3 | An app for finding accessible venues 4 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './components/Navbar.jsx'; 3 | import MainContainer from './components/MainContainer.jsx'; 4 | 5 | const App = () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | 12 | export default App; -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | import axios from 'axios'; 3 | 4 | export const getResults = (location, radius, categories, attributes) => (dispatch) => { 5 | 6 | axios({ 7 | method: 'POST', 8 | url: `/api/search`, 9 | headers: { 'Content-Type': 'application/JSON' }, 10 | data: { 11 | location: location, 12 | radius: radius, 13 | categories: categories, 14 | attributes: attributes, 15 | } 16 | }) 17 | .then((response) => { 18 | console.log(response.data) 19 | dispatch({ 20 | type: types.GET_RESULTS, 21 | payload: response.data, 22 | }); 23 | }); 24 | }; 25 | 26 | export const addFav = (businessID) => (dispatch) => { 27 | // modify to send favorite request to database 28 | console.log('ADDING TO FAVORITES, BUSINESS ID: ', businessID) 29 | return axios({ 30 | method: 'POST', 31 | // sync endpoint with backend 32 | url: `/api/addfavorite`, 33 | headers: { 'Content-Type': 'application/JSON' }, 34 | // what type of data to send 35 | data: {businessId: businessID} 36 | }) 37 | .then((response) => { 38 | console.log('ADDED TO FAVORITES:', response.data) 39 | dispatch({ 40 | type: types.ADD_FAV, 41 | payload: response.data, 42 | }); 43 | }); 44 | }; 45 | 46 | export const getFav = () => (dispatch) => { 47 | // modify to send favorite request to database 48 | console.log('RETRIEVING FAVORITES') 49 | axios({ 50 | method: 'GET', 51 | // sync endpoint with backend 52 | url: `/api/getfavorites`, 53 | headers: { 'Content-Type': 'application/JSON' }, 54 | }) 55 | .then((response) => { 56 | console.log('RETRIEVING FAVORITES:', response.data) 57 | dispatch({ 58 | type: types.GET_FAV, 59 | payload: response.data, 60 | }); 61 | }); 62 | }; 63 | 64 | export const toggleFavsPage = () => ({ 65 | type: types.TOGGLE_FAVS_PAGE, 66 | }); 67 | 68 | export const addComment = (number, comment) => ({ 69 | type: types.ADD_COMMENT, 70 | payload: { number, comment } 71 | }); 72 | 73 | export const toggleComments = () => ({ 74 | type: types.TOGGLE_COMMENTS, 75 | }); 76 | 77 | export const toggleLogin = () => ({ 78 | type: types.TOGGLE_LOGIN, 79 | }); 80 | 81 | export const toggleAuth = () => ({ 82 | type: types.TOGGLE_AUTH, 83 | }); 84 | 85 | export const validateUsername = (username) => (dispatch) => { 86 | 87 | axios({ 88 | method: 'GET', 89 | url: `/auth/validateusername/${username}`, 90 | headers: { 'Content-Type': 'application/JSON' }, 91 | // params: { 92 | // username: username, 93 | // } 94 | }) 95 | .then((response) => { 96 | dispatch({ 97 | type: types.VALIDATE_USERNAME, 98 | // {username: req.params.username, found: true/false } 99 | payload: response.data.found, 100 | }); 101 | }); 102 | }; 103 | 104 | export const userLogin = (username, password) => (dispatch) => { 105 | 106 | return axios({ 107 | method: 'POST', 108 | url: `/auth/login`, 109 | headers: { 'Content-Type': 'application/JSON' }, 110 | data: {auth: 111 | { 112 | username: username, 113 | password: password 114 | } 115 | } 116 | }) 117 | .then((response) => { 118 | console.log('LOGIN RESPONSE:', response.data) 119 | dispatch({ 120 | type: types.USER_LOGIN, 121 | // determine response from backend using status code 122 | payload: response.data, 123 | }); 124 | }); 125 | }; 126 | 127 | export const userLogout = () => (dispatch) => { 128 | axios({ 129 | method:'POST', 130 | url: `/auth/logout`, 131 | headers: { 'Content-Type': 'application/JSON' }, 132 | data: {} 133 | }) 134 | .then((response) => { 135 | dispatch({ 136 | type: types.USER_LOGOUT, 137 | payload: response.data 138 | }) 139 | }) 140 | }; 141 | 142 | export const userSignup = (username, password) => (dispatch) => { 143 | 144 | axios({ 145 | method: 'POST', 146 | url: `/auth/signup`, 147 | headers: { 'Content-Type': 'application/JSON' }, 148 | data: {auth: 149 | { 150 | username: username, 151 | password: password 152 | } 153 | } 154 | }) 155 | .then((response) => { 156 | dispatch({ 157 | type: types.USER_SIGNUP, 158 | // determine response from backend using status code 159 | payload: response.data, 160 | }); 161 | }); 162 | }; 163 | 164 | export const userLoginAndGetFav = (username, password) => (dispatch) => { 165 | 166 | dispatch(userLogin(username, password)) 167 | .then(() => dispatch(getFav())) 168 | }; 169 | 170 | export const addFavAndGetFav = (businessID) => (dispatch) => { 171 | 172 | dispatch(addFav(businessID)) 173 | .then(() => dispatch(getFav())) 174 | }; -------------------------------------------------------------------------------- /client/assets/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/client/assets/background.gif -------------------------------------------------------------------------------- /client/assets/background_new.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/client/assets/background_new.gif -------------------------------------------------------------------------------- /client/assets/db_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/client/assets/db_schema.png -------------------------------------------------------------------------------- /client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/client/assets/logo.png -------------------------------------------------------------------------------- /client/assets/wave-starting-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/client/assets/wave-starting-point.png -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Login = (props) => { 4 | const usernameText = React.createRef(); 5 | const passwordText = React.createRef(); 6 | 7 | return ( 8 |
9 |
10 |
{ 11 | e.preventDefault(); 12 | // props.loginUser(usernameText.current.value, passwordText.current.value); 13 | props.loginUserAndGetFav(usernameText.current.value, passwordText.current.value) 14 | }} > 15 |
Login
16 | 19 | 22 |
{props.loginError}
23 | 24 |
25 | Create an Account 26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export default Login; 33 | -------------------------------------------------------------------------------- /client/components/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Logout= (props) => { 4 | return ( 5 |
6 | 8 |
9 | ); 10 | } 11 | 12 | export default Logout; 13 | -------------------------------------------------------------------------------- /client/components/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from './Sidebar.jsx'; 3 | import ResultsContainer from './ResultsContainer.jsx'; 4 | 5 | const MainContainer = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | export default MainContainer; -------------------------------------------------------------------------------- /client/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/actions'; 4 | import ProfileIcon from './ProfileIcon.jsx'; 5 | import Login from './Login.jsx'; 6 | import Signup from './Signup.jsx'; 7 | import Logout from './Logout.jsx'; 8 | 9 | const mapStateToProps = (state) => ({ 10 | authState: state.search.authState, 11 | loggedIn: state.search.loggedIn, 12 | loginState: state.search.loginState, 13 | usernameExists: state.search.usernameExists, 14 | signupError: state.search.signupError, 15 | loginError: state.search.loginError, 16 | }); 17 | 18 | const mapDispatchToProps = (dispatch) => ({ 19 | toggleFavsPage: () => { 20 | dispatch(actions.toggleFavsPage()); 21 | }, 22 | toggleLogin: () => { 23 | dispatch(actions.toggleLogin()); 24 | }, 25 | toggleAuth: () => { 26 | dispatch(actions.toggleAuth()); 27 | }, 28 | loginUser: (username, password) => { 29 | dispatch(actions.userLogin(username, password)); 30 | }, 31 | logoutUser: () => { 32 | dispatch(actions.userLogout()); 33 | }, 34 | signupUser: (username, password) =>{ 35 | dispatch(actions.userSignup(username, password)) 36 | }, 37 | loginUserAndGetFav: (username, password) =>{ 38 | dispatch(actions.userLoginAndGetFav(username, password)) 39 | }, 40 | }); 41 | 42 | const Navbar = (props) => { 43 | return ( 44 | 62 | ) 63 | } 64 | 65 | export default connect(mapStateToProps, mapDispatchToProps)(Navbar); 66 | -------------------------------------------------------------------------------- /client/components/ProfileIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ProfileIcon= (props) => { 4 | return ( 5 |
6 | 7 | {/* */} 10 |
11 | ); 12 | } 13 | 14 | export default ProfileIcon; 15 | -------------------------------------------------------------------------------- /client/components/ResultCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {CopyToClipboard} from 'react-copy-to-clipboard'; 3 | 4 | const ResultCard = (props) => { 5 | return ( 6 |
7 |
8 | 9 |
10 |

{props.distance}

11 |

{props.name}

12 |

{props.price}Rating: {props.rating}

13 |

{props.address}

14 |

{props.phone}

15 | 16 |
17 |
18 | {props.loggedIn ? 19 |
20 | 21 | 22 | 23 | 24 | 25 |
: 26 |
27 | 28 | 29 | 30 |
} 31 |
32 | ); 33 | } 34 | 35 | export default ResultCard; -------------------------------------------------------------------------------- /client/components/ResultsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ResultCard from './ResultCard.jsx' 4 | import * as actions from '../actions/actions'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | searchResults: state.search.searchResults, 8 | firstRender: state.search.firstRender, 9 | loggedIn: state.search.loggedIn, 10 | }); 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | addFav: (businessID) => { 14 | dispatch(actions.addFav(businessID)); 15 | }, 16 | addFavAndGetFav: (businessID) => { 17 | dispatch(actions.addFavAndGetFav(businessID)); 18 | }, 19 | addComment: (comment) => { 20 | dispatch(actions.addComment(comment)); 21 | } 22 | }); 23 | 24 | const ResultsContainer = (props) => { 25 | 26 | if (!props.searchResults.length && !props.firstRender) { 27 | return ( 28 |
29 |

Sorry, no results found matching your query.
Try expanding your search radius.

30 |
31 | ) 32 | } else if (!props.searchResults.length) { 33 | return ( 34 |
35 |

fun with frollic

36 |
37 | ) 38 | } 39 | 40 | const resultCards = props.searchResults.map((resultObj, index) => { 41 | return 59 | }); 60 | 61 | return ( 62 |
63 |

Results:

64 | {resultCards} 65 |
66 | ); 67 | } 68 | 69 | export default connect(mapStateToProps, mapDispatchToProps)(ResultsContainer); -------------------------------------------------------------------------------- /client/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/actions'; 4 | 5 | const mapDispatchToProps = (dispatch) => ({ 6 | getResults: (location, radius, categories, attributes) => { 7 | dispatch(actions.getResults(location, radius, categories, attributes)); 8 | } 9 | }); 10 | 11 | const Sidebar = (props) => { 12 | const handleClick = (e) => { 13 | e.preventDefault(); 14 | 15 | //possibly refactor? 16 | //run a test and console log to better understand 17 | const location = document.querySelector('input[name="location"]').value; 18 | const radius = document.querySelector('select[name="radius"]').value; 19 | 20 | const checkboxes = document.querySelectorAll('input[class=categories]:checked'); 21 | let categories = ''; 22 | checkboxes.forEach((el) => categories += ',' + el.name); 23 | categories = categories.slice(1); 24 | 25 | const accessible = document.querySelectorAll('input[class=accessibility]:checked'); 26 | let attributes = ''; 27 | accessible.forEach((el)=> attributes += ',' + el.name); 28 | attributes = attributes.slice(1); 29 | 30 | props.getResults(location, radius, categories, attributes); 31 | } 32 | // onSubmit={() => {return false}} 33 | return ( 34 | 144 | ) 145 | }; 146 | 147 | export default connect(null, mapDispatchToProps)(Sidebar); -------------------------------------------------------------------------------- /client/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Signup = (props) =>{ 4 | const usernameText = React.createRef(); 5 | const passwordText = React.createRef(); 6 | 7 | return ( 8 |
9 |
10 |
{ 11 | e.preventDefault(); 12 | props.signupUser(usernameText.current.value, passwordText.current.value); 13 | }}> 14 |
Signup
15 | 16 | 17 |
{props.signupError}
18 | 19 |
20 | Login to Account 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default Signup; -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_RESULTS = 'GET_RESULTS'; 2 | export const ADD_FAV = 'ADD_FAV'; 3 | export const GET_FAV = 'GET_FAV'; 4 | export const TOGGLE_FAVS_PAGE = 'TOGGLE_FAVS_PAGE'; 5 | export const ADD_COMMENT = 'ADD_COMMENT'; 6 | export const TOGGLE_COMMENTS = 'TOGGLE_COMMENTS'; 7 | export const VALIDATE_USERNAME = 'VALIDATE_USERNAME'; 8 | export const USER_LOGIN = 'USER_LOGIN'; 9 | export const USER_LOGOUT = 'USER_LOGOUT'; 10 | export const USER_SIGNUP = 'USER_SIGNUP'; 11 | export const TOGGLE_LOGIN = 'TOGGLE_LOGIN'; 12 | export const TOGGLE_AUTH = 'TOGGLE_AUTH'; 13 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Frollic 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './App.jsx'; 5 | import store from './store'; 6 | import style from './style.scss'; 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import mainReducer from './mainReducer'; 4 | 5 | const reducers = combineReducers({ 6 | search: mainReducer, 7 | }); 8 | 9 | export default reducers; -------------------------------------------------------------------------------- /client/reducers/mainReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | searchResults: [], 5 | favorites: [], 6 | savedResults: [], 7 | favsPageOn: false, 8 | firstRender: true, 9 | comments: [], 10 | username: '', 11 | password: '', 12 | loggedIn: false, 13 | authState: true, 14 | loginState: true, 15 | signupState: false, 16 | usernameExists: false, 17 | signupError: '', 18 | loginError: '', 19 | }; 20 | 21 | const mainReducer = (state = initialState, action) => { 22 | 23 | switch (action.type) { 24 | case types.GET_RESULTS: 25 | return { 26 | ...state, 27 | firstRender: false, 28 | searchResults: action.payload, 29 | } 30 | case types.ADD_FAV: 31 | // doing a deep copy using the slice method 32 | return { 33 | ...state, 34 | } 35 | case types.GET_FAV: 36 | console.log('RETRIEVING FAVORITES') 37 | return { 38 | ...state, 39 | favorites: action.payload.data, 40 | } 41 | case types.TOGGLE_FAVS_PAGE: 42 | if (!state.favsPageOn) { 43 | 44 | return { 45 | ...state, 46 | savedResults: state.searchResults, 47 | searchResults: state.favorites, 48 | favsPageOn: true, 49 | } 50 | } 51 | return { 52 | ...state, 53 | searchResults: state.savedResults, 54 | saveResults: [], 55 | favsPageOn: false, 56 | } 57 | case types.ADD_COMMENT: 58 | const newComments = state.comments.slice(); 59 | newComments.push(action.payload); 60 | 61 | return { 62 | ...state, 63 | comments: state.newComments, 64 | } 65 | case types.TOGGLE_COMMENTS: 66 | return { 67 | ...state, 68 | } 69 | case types.TOGGLE_LOGIN: 70 | if (!state.loginState) { 71 | return { 72 | ...state, 73 | loginState: true, 74 | } 75 | } 76 | return { 77 | ...state, 78 | loginState: false, 79 | } 80 | case types.TOGGLE_AUTH: 81 | if (!state.authState) { 82 | return { 83 | ...state, 84 | authState: true, 85 | } 86 | } 87 | return { 88 | ...state, 89 | authState: false, 90 | } 91 | case types.VALIDATE_USERNAME: 92 | // Assuming response object from POST request is in the form of {userExists: true} 93 | console.log('USERNAME EXISTS:', action.payload) 94 | return { 95 | ...state, 96 | usernameExists: action.payload, 97 | } 98 | case types.USER_LOGIN: 99 | // Assuming response object from POST request is in the form of {userExists: true} 100 | console.log('USER HAS LOGGED ON: ', action.payload) 101 | if (!action.payload.actionSuccess){ 102 | return { 103 | ...state, 104 | loginError: action.payload.message 105 | } 106 | } 107 | return { 108 | ...state, 109 | loggedIn: true, 110 | authState: true, 111 | loginError: action.payload.message, 112 | } 113 | case types.USER_LOGOUT: 114 | console.log('USER HAS LOGGED OUT', action.payload) 115 | return { 116 | ...state, 117 | loggedIn: false, 118 | } 119 | case types.USER_SIGNUP: 120 | // Assuming response object from POST request is in the form of {userExists: true} 121 | console.log('USER HAS SIGNED UP: ', action.payload) 122 | if (!action.payload.actionSuccess){ 123 | return { 124 | ...state, 125 | signupError: action.payload.message 126 | } 127 | } 128 | return { 129 | ...state, 130 | signupError: action.payload.message, 131 | loginState: true, 132 | } 133 | default: 134 | return state; 135 | } 136 | }; 137 | 138 | export default mainReducer; 139 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducers from './reducers/index'; 4 | 5 | const store = createStore( 6 | reducers, 7 | applyMiddleware(thunk) 8 | ); 9 | 10 | export default store; -------------------------------------------------------------------------------- /client/style.scss: -------------------------------------------------------------------------------- 1 | $teal: #4fcaf0; 2 | $gold: #fde454; 3 | $white: #FFFCF5; 4 | $gray: #D0D7DD; 5 | $black: #2A343A; 6 | $shadow: rgb(42, 52, 58, .10); 7 | 8 | html, body { 9 | width: 100%; 10 | height: 100%; 11 | margin: 0; 12 | overflow-x: hidden; 13 | max-height:100vh; 14 | color: $black; 15 | font-family: 'Roboto', sans-serif; 16 | background-color: $white; 17 | } 18 | 19 | h1 { 20 | color: $white; 21 | background-color: $black; 22 | font-size: 48px; 23 | text-align: center; 24 | padding: 75px 50px; 25 | } 26 | 27 | h2 { 28 | color: $white; 29 | background-color: $black; 30 | text-align: center; 31 | padding: 75px 50px; 32 | line-height: 35px; 33 | } 34 | 35 | .result-word { 36 | align-items: left; 37 | } 38 | 39 | p { 40 | color: $black; 41 | margin: 5px 0; 42 | } 43 | 44 | .loginPopup, .signupPopup { 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: center; 48 | position: absolute; 49 | top: 0; 50 | min-width: 100vw; 51 | 52 | .transparentBg{ 53 | position: absolute; 54 | min-width: 100vw; 55 | min-height: 100vh; 56 | z-index: 2; 57 | background-color: rgba(0, 0, 0, 0.425); 58 | top: 0; 59 | float: left; 60 | } 61 | } 62 | 63 | .loginForm, .signupForm { 64 | display: flex; 65 | align-self: center; 66 | flex-direction: column; 67 | position: absolute; 68 | // min-width: 200px; 69 | width: 220px; 70 | min-height: 200px; 71 | border: 1px solid #fde454;; 72 | box-shadow: 5px 5px 3px rgba(48, 48, 48, 0.788); 73 | border-radius: 10px; 74 | background-color:#4fcaf0; 75 | z-index: 2; 76 | top: 75px; 77 | right: 75px; 78 | padding: 15px; 79 | 80 | .loginText, .signupText { 81 | text-align: center; 82 | font-size: 24px; 83 | color: white; 84 | font-weight: bold; 85 | } 86 | 87 | .loginButton, .signupButton { 88 | width: 100%; 89 | padding: 12px 10px; 90 | margin: 5px 0 10px; 91 | border: 1px solid $black; 92 | color: $black; 93 | border-radius: 5px; 94 | font-size: 14px; 95 | } 96 | } 97 | 98 | 99 | #profile-container { 100 | z-index: 3; 101 | } 102 | 103 | #nav { 104 | width: 100%; 105 | height: 75px; 106 | color: $white; 107 | background-color: $teal; 108 | display: flex; 109 | align-items: center; 110 | justify-content: space-between; 111 | position: fixed; 112 | box-shadow: -2px 3px 3px 2px $shadow; 113 | z-index: 2; 114 | } 115 | 116 | #logo { 117 | margin-top: 4px; 118 | margin-left: 40.2px; 119 | width: auto; 120 | height: 52.5px; 121 | } 122 | 123 | #profile-icon { 124 | margin-top: 4px; 125 | margin-right: 32px; 126 | width: 45px; 127 | height: 45px; 128 | border-radius: 50%; 129 | color: $white; 130 | background-color: $white; 131 | border: 1px solid $white; 132 | z-index: auto; 133 | box-shadow: -.5px 1.5px 2px 2px $shadow; 134 | } 135 | 136 | button { 137 | z-index: auto; 138 | } 139 | 140 | button:hover { 141 | cursor: pointer; 142 | } 143 | 144 | a { 145 | z-index: auto; 146 | } 147 | 148 | a:hover { 149 | cursor: pointer; 150 | } 151 | 152 | #main-container { 153 | height: 100%; 154 | } 155 | 156 | aside { 157 | position: fixed; 158 | margin-top: 75px; 159 | padding: 40px; 160 | height: 100%; 161 | width: 330px; 162 | background-color:#FFFCF5; 163 | display: flex; 164 | align-items: flex-start; 165 | } 166 | 167 | .side-header { 168 | font-weight: 500; 169 | font-size: 1rem; 170 | margin-bottom: 10px; 171 | } 172 | 173 | input[type=text], select[name=radius], input[type=password] { 174 | width: 90%; 175 | padding: 12px 10px; 176 | margin: 10px 0 20px; 177 | border: 1px solid $black; 178 | color: $black; 179 | border-radius: 5px; 180 | font-size: 14px; 181 | } 182 | 183 | select[name=radius] { 184 | width: 97%; 185 | padding: 11px 10px; 186 | font-size: 14px; 187 | } 188 | 189 | input[type=checkbox]{ 190 | width: 30px; 191 | height: 30px; 192 | vertical-align: middle; 193 | margin-right: 10px; 194 | } 195 | 196 | .checkbox { 197 | display: flex; 198 | align-items: center; 199 | margin: 5px 0; 200 | 201 | .checkbox label { 202 | padding-left: 5px; 203 | font-size: 10px; 204 | } 205 | } 206 | 207 | .checkboxes { 208 | display: grid; 209 | grid-template-columns: 1fr 1fr; 210 | } 211 | 212 | .access { 213 | display: flex; 214 | } 215 | 216 | #search { 217 | background-color: $gold; 218 | color: $black; 219 | font-weight: bold; 220 | font-size: 16px; 221 | border: 1px solid $gold; 222 | border-radius: 5px; 223 | width: 50%; 224 | height: 45px; 225 | z-index: 1; 226 | box-shadow: 0px 2px 2px $shadow; 227 | margin: 30px 0 0; 228 | } 229 | 230 | #results-container { 231 | margin-left: 385px; 232 | display: flex; 233 | flex-direction: column; 234 | flex-basis: 300px; 235 | width: auto; 236 | padding: 93px 40px 40px 40px; 237 | } 238 | 239 | #splash { 240 | margin-left: 385px; 241 | display: flex; 242 | flex-direction: column; 243 | align-content: center; 244 | justify-content: center; 245 | align-items: center; 246 | background: url("./assets/background_new.gif") $gold repeat center; 247 | height: 100vh; 248 | padding: 40px; 249 | } 250 | 251 | article { 252 | display: flex; 253 | justify-content: start; 254 | margin-bottom: 20px; 255 | width: 100%; 256 | } 257 | 258 | .business { 259 | display: flex; 260 | flex-direction: row; 261 | align-self: stretch; 262 | justify-content: flex-end; 263 | width: 100%; 264 | min-width: 500px; 265 | border: 1px solid $shadow; 266 | box-shadow: 0 2px 5px 2px $shadow; 267 | } 268 | 269 | .businessImg { 270 | width: 200px; 271 | height: 200px; 272 | object-fit: cover; 273 | flex-shrink: 0; 274 | } 275 | 276 | .businessDetails { 277 | display: flex; 278 | flex-direction: column; 279 | justify-content: center; 280 | width: 100%; 281 | padding: 12px 20px 12px 22px; 282 | } 283 | 284 | .buttonContainer { 285 | display: flex; 286 | flex-direction: column; 287 | margin-left: 10px; 288 | vertical-align: middle; 289 | } 290 | 291 | .addFav { 292 | background-color: $teal; 293 | color:black; 294 | border-radius: 5px; 295 | border: 1px solid $teal; 296 | width: 125px; 297 | padding: 10px; 298 | margin: 0 0 10px; 299 | box-shadow: 0px 2px 2px $shadow; 300 | } 301 | 302 | .comment { 303 | background-color: $gold; 304 | border: 1px solid $gold; 305 | color: $black; 306 | border-radius: 5px; 307 | width: 125px; 308 | padding: 10px; 309 | box-shadow: 0px 2px 2px $shadow; 310 | } 311 | 312 | .share { 313 | background-color: $gold; 314 | border: 1px solid $gold; 315 | color: $black; 316 | border-radius: 5px; 317 | width: 125px; 318 | padding: 10px; 319 | margin: 10px 0 0; 320 | box-shadow: 0px 2px 2px $shadow; 321 | } 322 | 323 | 324 | .name { 325 | color: rgb(3, 104, 104); 326 | font-size: 16px; 327 | font-weight: bold; 328 | text-decoration: none; 329 | } 330 | 331 | .name:hover { 332 | color: $gold; 333 | text-decoration: underline; 334 | } 335 | 336 | .distance { 337 | padding-top: 8px; 338 | margin: 0; 339 | color: $black; 340 | font-size: 14px; 341 | } 342 | 343 | .rating { 344 | color: $black; 345 | margin-left: 5px; 346 | font-weight: 700; 347 | font-size: 14px; 348 | } 349 | 350 | .price { 351 | color: $black; 352 | margin-left: 0; 353 | padding-right: 2px; 354 | font-weight: 700; 355 | font-size: 14px; 356 | } 357 | 358 | .Address { 359 | margin: 5px 0 0 0; 360 | } 361 | 362 | .phone { 363 | margin-top: 4px; 364 | } 365 | 366 | .add-comment { 367 | padding: 12px 0 0 1px; 368 | margin: 0; 369 | color: teal; 370 | align-self: flex-start; 371 | background: none; 372 | font-size: 14px; 373 | border: none; 374 | } 375 | 376 | .add-comment:hover { 377 | color: $gold; 378 | } 379 | 380 | @media screen and (max-width: 1060px) and (min-width: 500px) { 381 | #main-container { 382 | display: flex; 383 | flex-direction: column; 384 | justify-content: start; 385 | width: 100%; 386 | height: 100vh; 387 | } 388 | 389 | #results-container { 390 | margin-left: 0; 391 | display: flex; 392 | flex-direction: column; 393 | width: auto; 394 | padding: 40px 40px 40px 40px; 395 | } 396 | 397 | #splash { 398 | margin-left: 0; 399 | display: flex; 400 | flex-direction: column; 401 | align-content: center; 402 | justify-content: center; 403 | align-items: center; 404 | padding: 40px; 405 | } 406 | 407 | aside { 408 | position: relative; 409 | padding: 40px 0; 410 | width: 100%; 411 | justify-content: center; 412 | } 413 | 414 | aside > * { 415 | padding: 0 40px; 416 | } 417 | 418 | form { 419 | display: grid; 420 | grid-template-columns: 1fr 1fr; 421 | grid-template-areas: 422 | "left right" 423 | "bottom right"; 424 | gap: 20px; 425 | } 426 | 427 | .filters { 428 | grid-area: right; 429 | } 430 | 431 | .checkboxes { 432 | margin-top: 20px; 433 | } 434 | 435 | .location-and-radius { 436 | grid-area: left; 437 | } 438 | 439 | #search { 440 | grid-area: bottom; 441 | width: 97%; 442 | padding: 0; 443 | margin-top: -5px; 444 | } 445 | 446 | .side-header { 447 | margin-top: 0; 448 | } 449 | 450 | } 451 | 452 | @media screen and (max-width: 500px) { 453 | #main-container { 454 | display: flex; 455 | flex-direction: column; 456 | justify-content: start; 457 | } 458 | 459 | #results-container { 460 | margin-left: 0; 461 | display: flex; 462 | flex-direction: column; 463 | width: auto; 464 | padding: 40px; 465 | } 466 | 467 | #splash { 468 | margin-left: 0; 469 | display: flex; 470 | flex-direction: column; 471 | align-content: center; 472 | justify-content: center; 473 | align-items: center; 474 | padding: 40px; 475 | } 476 | 477 | aside { 478 | position: relative; 479 | padding: 40px; 480 | width: auto; 481 | height: 100%; 482 | justify-content: center; 483 | } 484 | 485 | aside > * { 486 | padding: 0 20px; 487 | } 488 | 489 | form { 490 | display: block; 491 | } 492 | 493 | #search { 494 | margin-top: 30px; 495 | } 496 | 497 | article { 498 | display: flex; 499 | flex-direction: column; 500 | justify-content: start; 501 | margin-bottom: 20px; 502 | width: 100%; 503 | } 504 | 505 | .business { 506 | display: flex; 507 | flex-direction: column; 508 | width: 100%; 509 | min-width: 300px; 510 | box-shadow: -2px 5px 5px 2px $shadow; 511 | } 512 | 513 | .businessImg { 514 | width: auto; 515 | object-fit: cover; 516 | } 517 | 518 | .businessDetails { 519 | // display: flex; 520 | // flex-direction: column; 521 | // justify-content: center; 522 | // border: 1px solid $shadow; 523 | // width: auto; 524 | // height: auto; 525 | // padding: 20px; 526 | // box-shadow: 0px 2px 2px $shadow; 527 | } 528 | 529 | .buttonContainer { 530 | display: flex; 531 | flex-direction: row; 532 | margin-left: 0; 533 | align-items: center; 534 | justify-content: center; 535 | min-width: 300px; 536 | } 537 | 538 | .addFav { 539 | background-color: $teal; 540 | color:black; 541 | border-radius: 5px; 542 | border: 1px solid $teal; 543 | width: 125px; 544 | padding: 10px; 545 | margin: 10px 5px; 546 | box-shadow: 0px 2px 2px $shadow; 547 | } 548 | 549 | .comment { 550 | background-color: $gold; 551 | border: 1px solid $gold; 552 | color: $black; 553 | border-radius: 5px; 554 | width: 125px; 555 | padding: 10px; 556 | margin: 10px 5px; 557 | box-shadow: 0px 2px 2px $shadow; 558 | } 559 | 560 | .share { 561 | background-color: $gold; 562 | border: 1px solid $gold; 563 | color: $black; 564 | border-radius: 5px; 565 | width: 125px; 566 | padding: 10px; 567 | margin: 10px 5px; 568 | box-shadow: 0px 2px 2px $shadow; 569 | } 570 | } 571 | 572 | -------------------------------------------------------------------------------- /dist/1ba3cd3495ab9b506bf2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IterAlt/frollic/c3c96cf504ed530a419afeb822d848aff9e48adb/dist/1ba3cd3495ab9b506bf2.gif -------------------------------------------------------------------------------- /dist/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.20.2 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-is.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v17.0.2 26 | * react-dom.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v17.0.2 35 | * react-is.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** @license React v17.0.2 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Frollic
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "access", 3 | "version": "1.0.0", 4 | "description": "An app to help plan accessible events", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production nodemon server/server.js", 8 | "build": "cross-env NODE_ENV=production webpack", 9 | "dev": "concurrently \"cross-env NODE_ENV=development nodemon server/server.js\" \"cross-env NODE_ENV=development webpack serve --open\"", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Star-Nosed/Moles.git" 15 | }, 16 | "keywords": [ 17 | "accessibility", 18 | "social", 19 | "entertainment" 20 | ], 21 | "author": "Aram Paparian, Katrina Henderson, Wirapa May Boonyasurat, Megan Nadkarni", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/Star-Nosed/Moles/issues" 25 | }, 26 | "homepage": "https://github.com/Star-Nosed/Moles#readme", 27 | "dependencies": { 28 | "axios": "^0.25.0", 29 | "bcrypt": "^5.0.1", 30 | "bootstrap": "^5.1.3", 31 | "cookie-parser": "^1.4.6", 32 | "copy-to-clipboard": "^3.3.1", 33 | "css-loader": "^6.5.1", 34 | "express": "^4.17.2", 35 | "file-loader": "^6.2.0", 36 | "html-webpack-plugin": "^5.5.0", 37 | "install": "^0.13.0", 38 | "jest": "^27.4.7", 39 | "nodemon": "^2.0.15", 40 | "npm": "^8.4.0", 41 | "pg": "^8.7.1", 42 | "react": "^17.0.2", 43 | "react-copy-to-clipboard": "^5.0.4", 44 | "react-dom": "^17.0.2", 45 | "react-redux": "^7.2.6", 46 | "react-router-dom": "^6.2.1", 47 | "redux": "^4.1.2", 48 | "redux-devtools-extension": "^2.13.9", 49 | "redux-thunk": "^2.4.1", 50 | "save": "^2.4.0", 51 | "scss-loader": "^0.0.1", 52 | "style-loader": "^3.3.1", 53 | "url-loader": "^4.1.1", 54 | "uuid": "^8.3.2" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.16.12", 58 | "@babel/preset-env": "^7.16.11", 59 | "@babel/preset-react": "^7.16.7", 60 | "@testing-library/react": "^12.1.2", 61 | "babel-loader": "^8.2.3", 62 | "concurrently": "^7.0.0", 63 | "cross-env": "^7.0.3", 64 | "sass": "^1.49.0", 65 | "sass-loader": "^12.4.0", 66 | "webpack": "^5.67.0", 67 | "webpack-cli": "^4.9.2", 68 | "webpack-dev-server": "^4.7.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/controllers/controller.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const YELP_KEY = 'Bearer kDH3a4z30wEffzgX0iyn8OTTyqvuVU4zUw9vPFzfFi9p8pFJxtEeyWyoDH1hYi2jpNNUnYmhrtu1OrwI1Q_mOMZwQbTY95bKtm8IN-xynKO82AHLjd_CS2fjfdr5YXYx'; 3 | 4 | const controller = {}; 5 | 6 | controller.getResults = (req, res, next) => { 7 | 8 | const radius = Math.round((req.body.radius || 5) * 1600); 9 | const location = req.body.location || 10109; 10 | const categories = req.body.categories || []; 11 | const attributes = req.body.attributes || 'wheelchair_accessible'; 12 | 13 | axios({ 14 | method: 'GET', 15 | url: 'https://api.yelp.com/v3/businesses/search', 16 | params: { 17 | 'attributes': attributes, 18 | 'radius': radius, 19 | 'location': location, 20 | 'categories': categories, 21 | }, 22 | headers: { 'Authorization' : YELP_KEY }, 23 | }) 24 | .then(response => { 25 | const data = response.data.businesses.map(business => { 26 | return { 27 | businessID : business.id, 28 | name : business.name, 29 | alias: business.alias, 30 | image : business.image_url, 31 | url : business.url, 32 | reviews: business.review_count, 33 | address : `${business.location.address1}, ${business.location.city}, ${business.location.state} ${business.location.zip_code}`, 34 | phone : `(${business.phone.slice(2, 5)}) ${business.phone.slice(5, 8)}-${business.phone.slice(8)}`, 35 | rating : business.rating, 36 | price : business.price, 37 | coordinates: business.coordinates, 38 | distance :`${Math.round(business.distance / 1000 / 1.6 * 100) / 100} mi` 39 | }; 40 | }); 41 | res.locals.searchResults = data; 42 | return next(); 43 | }) 44 | .catch(err => next(err)); 45 | } 46 | 47 | // const test = false; 48 | // const fakeRes = {locals: {}}; 49 | // const fakeNext = () => console.log(fakeRes.locals.userFavorites[0]); 50 | controller.getBusinessInfo = (req, res, next) => { 51 | let favorites; 52 | favorites = res.locals.userFavorites; 53 | favorites = favorites.map(el => 54 | axios({ 55 | method: 'GET', 56 | url: `https://api.yelp.com/v3/businesses/${el.places_id}`, 57 | headers: { 'Authorization' : YELP_KEY }, 58 | }) 59 | .then(response => { 60 | console.log('response is', response); 61 | const { id, name, alias, image_url, url, review_count, location, phone, rating, price, coordinates, distance } = response.data; 62 | return { 63 | businessID : id, 64 | name : name, 65 | alias: alias, 66 | image : image_url, 67 | url : url, 68 | reviews: review_count, 69 | address : `${location.address1}, ${location.city}, ${location.state} ${location.zip_code}`, 70 | phone : `(${phone.slice(2, 5)}) ${phone.slice(5, 8)}-${phone.slice(8)}`, 71 | rating : rating, 72 | price : price, 73 | coordinates: coordinates, 74 | distance :`N/A` 75 | }; 76 | }) 77 | .catch(err => null) 78 | ); 79 | Promise.all(favorites) 80 | .then(result => { 81 | res.locals.userFavorites = result; 82 | return next(); 83 | }) 84 | .catch(err => next(err)); 85 | } 86 | 87 | //console.log(controller.getBusinessInfo(fakeRes, fakeRes, fakeNext)); 88 | 89 | module.exports = controller; 90 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/dbModel'); 2 | const { createResponse } = require('../models/responseModel'); 3 | const controller = require('./controller'); 4 | 5 | const userController = {}; 6 | 7 | userController.addFavorite = (req, res, next) => { 8 | let userId, businessId; 9 | 10 | if (!res.locals.authInfo || !res.locals.authInfo.authenticated) { 11 | return res.status(200).json(createResponse(false, 400, 'You must be logged in to store favorites')); 12 | } else { 13 | userId = res.locals.authInfo.user_id; 14 | } 15 | if (req.body.businessId) { 16 | businessId = req.body.businessId; 17 | } else { 18 | return res.status(200).json(createResponse(false, 400, 'You must specify a business id')); 19 | } 20 | 21 | const queryString = 'INSERT INTO favorites (user_id, places_id) VALUES ($1, $2);'; 22 | const queryParams = [userId, businessId]; 23 | console.log(queryParams); 24 | 25 | db.query(queryString, queryParams) 26 | .then(result => { 27 | return next(); 28 | }) 29 | .catch(err => next(err)); 30 | 31 | } 32 | 33 | userController.getFavorites = (req, res, next) => { 34 | 35 | console.log('inside usercontroller get favorites') 36 | 37 | if (!res.locals.authInfo || !res.locals.authInfo.authenticated) { 38 | return res.status(200).json(createResponse(false, 400, 'You must be logged in to view favorites')); 39 | } else { 40 | userId = res.locals.authInfo.user_id; 41 | } 42 | 43 | const queryString = 'SELECT places_id FROM favorites WHERE user_id = $1;'; 44 | const queryParams = [userId]; 45 | 46 | db.query(queryString, queryParams) 47 | .then(result => { 48 | res.locals.userFavorites = result.rows; 49 | return next(); 50 | }) 51 | .catch(err => next(err)); 52 | 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | module.exports = userController; -------------------------------------------------------------------------------- /server/db/users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | _id SERIAL PRIMARY KEY NOT NULL, 3 | username VARCHAR(255) UNIQUE NOT NULL, 4 | pwd VARCHAR(500) NOT NULL, 5 | created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP 6 | ); 7 | 8 | CREATE TABLE session_log ( 9 | _id SERIAL PRIMARY KEY NOT NULL, 10 | user_id BIGINT REFERENCES users (_id), 11 | ssid VARCHAR(500) NOT NULL UNIQUE, 12 | isActive BOOLEAN, 13 | created_time TIMESTAMPTZ NOT NULL DEFAULT NOW (), 14 | expiration_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '1 month', 15 | creation_ip CIDR 16 | ); 17 | 18 | CREATE TABLE favorites ( 19 | _id SERIAL PRIMARY KEY NOT NULL, 20 | user_id BIGINT REFERENCES users (_id), 21 | places_id VARCHAR(500) NOT NULL 22 | ); 23 | 24 | CREATE TABLE comments ( 25 | _id SERIAL PRIMARY KEY NOT NULL, 26 | user_id BIGINT REFERENCES users (_id), 27 | places_id BIGINT REFERENCES places (_id), 28 | content VARCHAR(1000) NOT NULL, 29 | created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP 30 | ); 31 | -------------------------------------------------------------------------------- /server/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid'); 2 | const bcrypt = require('bcrypt'); 3 | const db = require('../models/dbModel'); 4 | const { createResponse } = require('../models/responseModel'); 5 | 6 | function genCookieParams() { 7 | const ONE_MONTH = 1000 * 60 * 60 * 24 * 30; 8 | return { maxAge: ONE_MONTH, httpOnly: true, path: '/' }; 9 | } 10 | 11 | // Place this in app.use so it is used globally 12 | function globalAuthMiddleware(req, res, next) { 13 | 14 | console.log('inside global auth middleware') 15 | 16 | // set default values for res.locals.authInfo object 17 | // authInfo object model 18 | res.locals.authInfo = { 19 | authenticated: false, 20 | ssid: null, 21 | username: null, 22 | user_id: null, 23 | message: 'No SSID cookie found' 24 | }; 25 | 26 | // if there is no ssid cookie or falsy value 27 | if (!req.cookies.ssid) return next(); 28 | 29 | const unverifiedSsid = req.cookies.ssid; 30 | if (!uuid.validate(unverifiedSsid)) return next(); 31 | // check ssid against the database table session_log 32 | const queryString = ` 33 | SELECT l.user_id, u.username, l.ssid 34 | FROM session_log l 35 | INNER JOIN users u 36 | ON l.user_id = u._id 37 | WHERE l.isActive = true 38 | AND CURRENT_TIMESTAMP < l.expiration_time 39 | AND l.ssid = $1 40 | `; 41 | const input = [unverifiedSsid]; 42 | 43 | db.query(queryString, input) 44 | .then(data => { 45 | console.log(data.rows); 46 | if (!data.rows.length) { 47 | res.locals.authInfo.message = 'Invalid SSID cookie'; 48 | return next(); 49 | } 50 | const { user_id, username, ssid } = data.rows[0]; 51 | res.locals.authInfo = { authenticated: true, user_id, username, ssid, message: 'Success' }; 52 | console.log('about to call next'); 53 | return next(); 54 | }) 55 | .catch(err => { 56 | console.log('error querying db in global auth middleware') 57 | return next(err); 58 | } ); 59 | } 60 | 61 | 62 | function signupUser(req, res, next) { 63 | 64 | // validate body of request 65 | 66 | if (!req.body.auth) return res.status(200).json(createResponse(false, 400, 'Error: please specify username and password')); 67 | const { username, password } = req.body.auth; 68 | if (!username || !password) return res.status(200).json(createResponse(false, 400, 'Error: please specify username and password')); 69 | 70 | 71 | bcrypt.hash(password, 10) 72 | .then(hashedPwd => { 73 | // create user in database 74 | const createUserQuery = ` 75 | INSERT INTO users (username, pwd) 76 | VALUES ($1, $2) 77 | RETURNING _id, username; 78 | `; 79 | const vars = [username, hashedPwd]; 80 | return db.query(createUserQuery, vars); 81 | }) 82 | .then(result => { 83 | // createSession object template 84 | res.locals.createSession = { 85 | userId: result.rows[0]._id, 86 | username: result.rows[0].username, 87 | valid: true 88 | }; 89 | return next(); 90 | }) 91 | .catch(err => { 92 | if (err.constraint === 'users_username_key') { 93 | return res.status(200).json(createResponse(false, 400, 'Error: user already exists')); 94 | } 95 | return next(err); 96 | }) 97 | } 98 | 99 | function loginUser(req, res, next) { 100 | if (!req.body.auth) return res.status(200).json(createResponse(false, 400, 'Error: please specify username and password')); 101 | const { username, password } = req.body.auth; 102 | if (!username || !password) return res.status(200).json(createResponse(false, 400, 'Error: please specify username and password')); 103 | 104 | const dbQuery = 'SELECT _id, pwd FROM users WHERE username = $1'; 105 | const vars = [username]; 106 | db.query(dbQuery, vars) 107 | .then(result => { 108 | if (!result.rows.length) return res.status(200).json(createResponse(false, 400, 'Error: incorrect username and/or password')); 109 | const { _id, pwd } = result.rows[0]; 110 | res.locals.createSession = { 111 | userId: _id, 112 | username: username 113 | }; 114 | return bcrypt.compare(password, pwd); 115 | }) 116 | .then(result => { 117 | if (typeof result !== 'boolean') return; 118 | if (result === false) { 119 | return res.status(200).json(createResponse(false, 400, 'Error: incorrect username and/or password')); 120 | } else { 121 | res.locals.createSession.valid = true; 122 | return next(); 123 | } 124 | }) 125 | .catch(err => next(err)); 126 | } 127 | 128 | function logoutUser(req, res, next) { 129 | const session = req.cookies.ssid; 130 | if (!session) return res.status(200).json(createResponse(false, 400, 'Error: you are not logged in')); 131 | res.clearCookie('ssid', genCookieParams()); 132 | const dbQuery = 'UPDATE session_log SET isactive = false WHERE ssid = $1'; 133 | const vars = [session]; 134 | db.query(dbQuery, vars) 135 | .then(result => next()) 136 | .catch(err => next(err)); 137 | } 138 | 139 | function protectPage(req, res, next) { 140 | if (!res.locals.authInfo.authenticated) return res.status(200).json(createResponse(false, 400, 'Error: you must be logged in to view this content')); 141 | return next(); 142 | } 143 | 144 | function validateUsername(req, res, next) { 145 | const { username } = req.params; 146 | const dbQuery = 'SELECT username FROM users WHERE username = $1'; 147 | const vars = [username]; 148 | db.query(dbQuery, vars) 149 | .then(result => { 150 | if(!result.rows.length) return res.status(200).json(createResponse(false, 400, 'Username not found')); 151 | else return next(); 152 | }) 153 | .catch(err => next(err)); 154 | } 155 | 156 | function createSession(req, res, next) { 157 | if (!res.locals.createSession || !res.locals.createSession.valid) { 158 | console.log('Error: Cannot create session'); 159 | return next(err); 160 | } 161 | const { userId, username } = res.locals.createSession; 162 | const createSessionQuery = `INSERT INTO session_log (user_id, ssid) VALUES ($1, $2);`; 163 | const ssid = uuid.v4(); 164 | const vars = [userId, ssid]; 165 | db.query(createSessionQuery, vars) 166 | .then(result => { 167 | res.cookie('ssid', ssid, genCookieParams()); 168 | // update authInfo object 169 | res.locals.authInfo = { 170 | authenticated: true, 171 | ssid: ssid, 172 | username: username, 173 | user_id: userId, 174 | message: 'Success' 175 | }; 176 | return next(); 177 | }) 178 | .catch(err => next(err)); 179 | } 180 | 181 | module.exports = { globalAuthMiddleware, signupUser, loginUser, logoutUser, createSession, protectPage, validateUsername }; 182 | -------------------------------------------------------------------------------- /server/models/dbModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const { DB_URL } = require('../secrets/secrets.js'); 4 | 5 | const PG_URI = DB_URL; 6 | 7 | 8 | const pool = new Pool({ 9 | connectionString: PG_URI, 10 | ssl: { rejectUnauthorized: false } 11 | }); 12 | 13 | 14 | module.exports = { 15 | query: (text, params, callback) => { 16 | console.log('executed query', text); 17 | return pool.query(text, params, callback); 18 | } 19 | }; -------------------------------------------------------------------------------- /server/models/responseModel.js: -------------------------------------------------------------------------------- 1 | function createResponse(actionSuccess, statusCode = 200, message = '', data = {}) { 2 | return { 3 | actionSuccess, // boolean 4 | statusCode, // HTTP code 5 | message, // string 6 | data // object 7 | }; 8 | } 9 | 10 | // function createError(log, status, message) { 11 | 12 | // if (!log) log = 'Express error handler caught unknown middleware error'; 13 | // if (!status) status = 500; 14 | // if (!message) message = { err: 'An error occurred' }; 15 | // else message = { err: message }; 16 | 17 | // return { log, status, message }; 18 | // } 19 | 20 | module.exports = { createResponse }; -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const controller = require('../controllers/controller.js'); 3 | const { createResponse } = require('../models/responseModel'); 4 | const { signupUser, loginUser, logoutUser, createSession, validateUsername } = require('../middleware/authMiddleware.js'); 5 | const authRouter = express.Router(); 6 | 7 | 8 | authRouter.get('/validateusername/:username', validateUsername, (req, res) => { 9 | return res.status(200).json(createResponse(true, 200, 'Username found')); 10 | }); 11 | 12 | authRouter.post('/login', loginUser, createSession, (req, res) => { 13 | return res.status(200).json(createResponse(true, 200, 'Login successful')); 14 | }); 15 | 16 | authRouter.post('/logout', logoutUser, (req, res) => { 17 | return res.status(200).json(createResponse(true, 200, 'Logout successful')); 18 | }); 19 | 20 | authRouter.post('/signup', signupUser, createSession, (req, res) => { 21 | return res.status(200).json(createResponse(true, 200, 'Signup successful')); 22 | }); 23 | 24 | module.exports = authRouter; 25 | -------------------------------------------------------------------------------- /server/routes/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const controller = require('../controllers/controller.js'); 3 | const userController = require('../controllers/userController.js'); 4 | const { createResponse } = require('../models/responseModel'); 5 | const router = express.Router(); 6 | 7 | 8 | router.post('/search', controller.getResults, (req, res) => { 9 | res.status(200).json(res.locals.searchResults); 10 | }); 11 | 12 | router.get('/getfavorites', userController.getFavorites, controller.getBusinessInfo, (req, res) => { 13 | return res.status(200).json(createResponse(true, 200, 'Favorites found', res.locals.userFavorites)); 14 | }); 15 | 16 | router.post('/addfavorite', userController.addFavorite, (req, res) => { 17 | return res.status(200).json(createResponse(true, 200, 'Added to favorites')); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /server/secrets/secrets.js: -------------------------------------------------------------------------------- 1 | const YELP_KEY = 'Bearer kDH3a4z30wEffzgX0iyn8OTTyqvuVU4zUw9vPFzfFi9p8pFJxtEeyWyoDH1hYi2jpNNUnYmhrtu1OrwI1Q_mOMZwQbTY95bKtm8IN-xynKO82AHLjd_CS2fjfdr5YXYx'; 2 | 3 | const DB_URL = 'postgresql://noodle:BGothCYGUeNmAZ4f@db-postgresql-nyc1-82145-do-user-10576934-0.b.db.ondigitalocean.com:25061/noodledb-pool1'; 4 | 5 | module.exports = { YELP_KEY, DB_URL }; -------------------------------------------------------------------------------- /server/secrets/secrets_blank.js: -------------------------------------------------------------------------------- 1 | const YELP_KEY = 'yelp API key goes here'; 2 | 3 | const DB_URL = 'db connection string goes here'; 4 | 5 | module.exports = { YELP_KEY, DB_URL }; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const router = require('./routes/router'); 4 | const auth = require('./routes/auth'); 5 | const app = express(); 6 | const cookieParser = require('cookie-parser'); 7 | const { globalAuthMiddleware } = require('./middleware/authMiddleware'); 8 | const port = process.env.PORT || 3000; 9 | 10 | app.use(express.json()); 11 | app.use(express.urlencoded({ extended: true })); 12 | app.use(cookieParser()); 13 | 14 | app.use(globalAuthMiddleware); 15 | 16 | app.use('/api', router); 17 | app.use('/auth', auth); 18 | 19 | app.use('/assets', express.static(path.join(__dirname, '../client/assets'))); 20 | 21 | app.use((req, res) => { 22 | console.log('Error: page not found') 23 | res.status(404).send('Error: page not found'); 24 | }); 25 | 26 | app.use((err, req, res, next) => { 27 | const defaultErr = { 28 | log: 'Express error handler caught unknown middleware error', 29 | status: 500, 30 | message: { err: 'An error occurred' }, 31 | }; 32 | const errorObj = Object.assign({}, defaultErr, err); 33 | console.log(errorObj.log); 34 | return res.status(errorObj.status).json(errorObj.message); 35 | }); 36 | 37 | module.exports = app.listen(port, () => console.log(`Listening on port ${port}`)); 38 | -------------------------------------------------------------------------------- /test/controllerTest.js: -------------------------------------------------------------------------------- 1 | const controller = require('../server/controllers/controller'); 2 | 3 | describe('controller unit test', () => { 4 | beforeEach((done) => { 5 | const testReq = {}; 6 | const testRes; 7 | testRes.locals.userFavorites = [{places_id : '97-KvAlzGxT14XqIGQFECQ'}]; 8 | const testNext = jest.fn(); 9 | done(); 10 | }); 11 | 12 | describe('testing controller.getBusinessInfo method', () => { 13 | it('', () => { 14 | const result = controller.getBusinessInfo(req, res, next); 15 | }) 16 | }) 17 | }) -------------------------------------------------------------------------------- /test/react_test.js: -------------------------------------------------------------------------------- 1 | import React from 'React'; 2 | import { render, screen, waitFor } from '@testing-library/react' 3 | 4 | import App from '../client/App' 5 | import Sidebar from '../client/components/Sidebar' 6 | 7 | describe('Unit testing React components', () => { 8 | 9 | describe('Sidebar', () => { 10 | 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/responseModel.test.js: -------------------------------------------------------------------------------- 1 | const { createResponse } = require('../server/models/responseModel'); 2 | 3 | const defaultResponse = { actionSuccess: true, statusCode: 200, message: '', data: {} }; 4 | 5 | test('returns a default response', () => { 6 | expect(createResponse(true)).toBe(defaultResponse); 7 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const process = require('process'); 5 | 6 | module.exports = { 7 | // entry:[path.join(__dirname, 'client', 'index.js'), path.join(__dirname, 'client', 'style.scss')], 8 | mode: process.env.NODE_ENV, 9 | entry: {src: './client/index.js'}, 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, 'dist'), 13 | // publicPath: '/', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['@babel/preset-env', '@babel/preset-react'], 24 | }, 25 | } 26 | }, 27 | { 28 | test: /.(css|scss)$/, 29 | use: ['style-loader', 'css-loader', 'sass-loader'], 30 | }, 31 | { 32 | test: /\.(png|jp(e*)g|svg|gif)$/, 33 | type: 'asset/resource', 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | title: 'Development', 40 | template: './client/index.html', 41 | }) 42 | ], 43 | devServer: { 44 | static: { 45 | publicPath: '/dist', 46 | directory: path.resolve(__dirname, 'dist'), 47 | }, 48 | proxy: { 49 | '/api': 'http://localhost:3000', 50 | '/auth': 'http://localhost:3000', 51 | '/assets' : 'http://localhost:3000' 52 | } 53 | } 54 | } --------------------------------------------------------------------------------