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

46 | {props.authState ? null : props.loginState ?
47 |
:
52 | }
57 |
58 | {props.loggedIn ?
59 | :
60 | null}
61 |
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 |
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 | }
--------------------------------------------------------------------------------