├── .gitignore
├── .DS_Store
├── .babelrc
├── README.md
├── client
├── assets
│ ├── icon.png
│ ├── icon2.png
│ ├── option1.png
│ ├── search.png
│ └── background.jpg
├── components
│ ├── .DS_Store
│ ├── Review.jsx
│ ├── Reviews.jsx
│ ├── Signup.jsx
│ ├── addReview.jsx
│ ├── Login.jsx
│ ├── Workspace.jsx
│ └── addWorkspace.jsx
├── containers
│ ├── .DS_Store
│ ├── WorkspaceContainer.jsx
│ ├── NavBarContainer.jsx
│ └── HomePage.jsx
├── index.js
├── store.js
├── App.jsx
└── stylesheets
│ └── styles.scss
├── dist
├── 487884a8601198ead17c.jpg
├── index.html
└── bundle.js.LICENSE.txt
├── server
├── index.js
├── models
│ ├── sqlModels.js
│ └── dbModels.js
├── routes
│ ├── sqlUserRouter.js
│ ├── userRouter.js
│ ├── workspaceRouter.js
│ ├── sqlReviewRouter.js
│ └── sqlWorkspaceRouter.js
├── sqlServer.js
├── controllers
│ ├── sqlUserController.js
│ ├── sqlReviewController.js
│ ├── sqlWorkspaceController.js
│ ├── UserController.js
│ └── WorkspaceController.js
└── server.js
├── index.html
├── __tests__
├── react.js
├── supertest.js
└── db.js
├── webpack.config.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["@babel/preset-env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CafeQuery
2 | Open source project to find the best cafes for study or work
3 |
--------------------------------------------------------------------------------
/client/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/assets/icon.png
--------------------------------------------------------------------------------
/client/assets/icon2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/assets/icon2.png
--------------------------------------------------------------------------------
/client/assets/option1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/assets/option1.png
--------------------------------------------------------------------------------
/client/assets/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/assets/search.png
--------------------------------------------------------------------------------
/client/components/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/components/.DS_Store
--------------------------------------------------------------------------------
/client/containers/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/containers/.DS_Store
--------------------------------------------------------------------------------
/client/assets/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/client/assets/background.jpg
--------------------------------------------------------------------------------
/dist/487884a8601198ead17c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Super-Panda-Whale/CafeQuery/HEAD/dist/487884a8601198ead17c.jpg
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const app = require('./sqlServer');
2 |
3 | app.listen(3000, () => {
4 | console.log(`Server listening on port: ${PORT}...`);
5 | });
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App.jsx';
4 |
5 | import styles from './stylesheets/styles.scss';
6 |
7 | render(
8 | ,
9 | document.getElementById('root')
10 | );
11 |
--------------------------------------------------------------------------------
/server/models/sqlModels.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 |
3 | const PG_URI =
4 | 'postgres://pnpqhovz:xmn6RTTNcTruASv9i7J3ySrFkFTpQ84H@castor.db.elephantsql.com/pnpqhovz';
5 |
6 | const pool = new Pool({
7 | connectionString: PG_URI,
8 | });
9 |
10 | module.exports = {
11 | query: (text, params, callback) => {
12 | console.log('executed query: ', text);
13 | return pool.query(text, params, callback);
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/client/components/Review.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from 'axios';
3 |
4 | function Review(props) {
5 | const { username, rating, review } = props;
6 |
7 | return (
8 |
9 |
{username}
10 |
{rating}
11 |
{review}
12 |
13 | )
14 | }
15 |
16 | export default Review;
17 |
--------------------------------------------------------------------------------
/client/components/Reviews.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Review from './Review.jsx';
3 |
4 | const Reviews = ({reviews}) => {
5 | const reviewArr = [];
6 | for (let i = 0; i < reviews.length; i++) {
7 | reviewArr.push()
8 | }
9 |
10 | return (
11 |
12 | {reviewArr}
13 |
14 | )
15 | }
16 |
17 | export default Reviews;
18 |
--------------------------------------------------------------------------------
/server/routes/sqlUserRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const sqlUserController = require('../controllers/sqlUserController');
3 | const router = express.Router();
4 |
5 | router.post('/signup', sqlUserController.signUp, (req, res) => {
6 | return res.status(201).json(res.locals.newUser);
7 | });
8 |
9 | router.post('/login', sqlUserController.verifyUser, (req, res) => {
10 | return res.status(200).json('User has signed in!').redirect('/');
11 | });
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/client/containers/WorkspaceContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Workspace from '../components/Workspace.jsx';
3 |
4 |
5 | const WorkspaceContainer = (props) => {
6 | const { workspaces } = props;
7 |
8 | const locationArray = [];
9 | for (let i = 0; i < workspaces.length; i++){
10 | //pass in response body into as props to display spaces
11 | locationArray.push( );
12 | }
13 | //render search bar for zip code search and then resuls of the zip code search
14 | return(
15 |
16 | {locationArray}
17 |
18 | );
19 | }
20 |
21 | export default WorkspaceContainer;
22 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 | CafeQuery
--------------------------------------------------------------------------------
/client/store.js:
--------------------------------------------------------------------------------
1 |
2 | import { createStore, applyMiddleware} from 'redux';
3 | import thunkMiddleware from 'redux-thunk';
4 | import { composeWithDevTools } from 'redux-devtools-extension';
5 | import reducers from './reducers/index';
6 | // adding middleware and specifically built in thunk to make
7 | // calls to make redux asynchrounous.
8 | const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))
9 |
10 |
11 | // We now have access to the index file
12 | // the function will trigger and run all of the reducers
13 | // we are adding composeWithDevTools here to get easy access to the Redux dev tools
14 | const store = createStore(
15 | // invoke store and pass createstore
16 | // gathers results to a single state object
17 | reducers,
18 | composedEnhancer
19 | );
20 |
21 | export default store;
22 |
--------------------------------------------------------------------------------
/server/routes/userRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const UserController = require('../controllers/UserController');
5 |
6 | // Create a user in the database
7 | router.post('/', UserController.createUser, (req, res) =>
8 | res.status(201).json(res.locals.newUser)
9 | );
10 |
11 | // Gets a user from the database
12 | router.get('/:username', UserController.getUser, (req, res) => {
13 | res.status(200).json(res.locals.user);
14 | });
15 |
16 | // Adds a workspace to the user favorites
17 | router.patch('/:username', UserController.addFavorite, (req, res) =>
18 | res.status(200).json(res.locals.updatedUser)
19 | );
20 |
21 | // Deletes a user from the database
22 | router.delete('/:username', UserController.deleteUser, (req, res) =>
23 | res.status(200).json(res.locals.deletedUser)
24 | );
25 |
26 | module.exports = router;
27 |
--------------------------------------------------------------------------------
/server/routes/workspaceRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const WorkspaceController = require('../controllers/WorkspaceController');
5 |
6 | // Create a workspace in the database
7 | router.post('/', WorkspaceController.createWorkspace,
8 | (req, res) => res.status(201).json(res.locals.newWorkspace));
9 |
10 | // Gets a workspace from the database
11 | // router.get(['/:workspace_id', '/:zipcodeSearch'], WorkspaceController.getWorkspaceByZip,
12 | // (req, res) => res.status(200).json(res.locals.workspace));
13 |
14 | //Gets a workspace from a zipcode search
15 | router.get('/:zipcodeSearch', WorkspaceController.getWorkspaceByZip,
16 | (req, res) => res.status(200).json(res.locals.workspace));
17 |
18 | // Deletes a workspace from the database
19 | router.delete('/:workspace_id', WorkspaceController.deleteWorkspace,
20 | (req, res) => res.status(200).json(res.locals.deletedWorkspace));
21 |
22 |
23 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/sqlReviewRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const sqlReviewController = require('../controllers/sqlReviewController');
3 | const router = express.Router();
4 |
5 | router.get('/', sqlReviewController.getReviewsForOne, (req, res) => {
6 | return res.status(200).json(res.locals.review);
7 | });
8 |
9 | //create route to handle get requests, use getReviews middleware to get all reviews for a particular id
10 | router.get('/:workspaceid', sqlReviewController.getReviews, (req, res) => {
11 | return res.status(200).json(res.locals.reviews);
12 | });
13 |
14 | //create route to handle get requests, use getReviews middleware to get all reviews for a particular
15 | router.post('/:workspaceid', sqlReviewController.createReview, (req, res) => {
16 | return res.status(201).json(res.locals.review);
17 | });
18 |
19 | // router.delete('/', (req, res) => {
20 | // console.log('delete this');
21 | // return res.status(200).json('we did it!');
22 | // });
23 |
24 | module.exports = router;
25 |
--------------------------------------------------------------------------------
/server/routes/sqlWorkspaceRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const sqlWorkSpaceController = require('../controllers/sqlWorkspaceController');
3 | const router = express.Router();
4 |
5 | //get requests are handled by getWorkspace middleware
6 | router.get('/', sqlWorkSpaceController.getWorkspace, (req, res) => {
7 | //send back a list of all workspaces with the zipcode from the query string
8 | return res.status(200).json(res.locals.workspaces);
9 | });
10 |
11 | //handles get requests for a single workspace by ID
12 | router.get(
13 | '/id/:workspaceid',
14 | sqlWorkSpaceController.getOneWorkspace,
15 | (req, res) => {
16 | return res.status(200).json(res.locals.workspace);
17 | }
18 | );
19 |
20 | //post requests to / route are handled by createWorkspace middleware
21 | router.post('/', sqlWorkSpaceController.createWorkspace, (req, res) => {
22 | //send back the new workspace
23 | return res.status(201).json(res.locals.newWorkspace);
24 | });
25 |
26 | module.exports = router;
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | CafeQuery
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/containers/NavBarContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Navbar, Nav} from 'react-bootstrap';
3 | import {LinkContainer} from 'react-router-bootstrap'
4 | import option1 from '../assets/option1.png';
5 |
6 | function NavBarContainer() {
7 | const [isLoggedIn, setLogin] = useState(false);
8 |
9 |
10 |
11 | return (
12 |
13 |
14 |
15 | CafeQuery
16 |
17 |
18 |
19 |
20 |
21 | Find A Location
22 |
23 |
24 | Add a Workspace
25 |
26 |
27 | Log In
28 |
29 |
30 | Sign Up
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default NavBarContainer;
39 |
--------------------------------------------------------------------------------
/client/components/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from 'axios';
3 |
4 | const Signup = () => {
5 | const [username, setUsername] = useState('');
6 | const [password, setPassword] = useState('');
7 |
8 | // event handler for signup button
9 | const handleSignup = (event) => {
10 | // prevent page reload
11 | event.preventDefault();
12 |
13 | const userInputObj = {
14 | username: username,
15 | password: password,
16 | };
17 | // request to server
18 | axios
19 | .post('/user/signup', userInputObj)
20 | .then((res) => console.log(res))
21 | .catch((err) => console.log(err));
22 | };
23 |
24 | return (
25 |
26 |
44 |
45 | );
46 | };
47 |
48 | export default Signup;
49 |
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter, Routes, Route } from "react-router-dom";
3 | import 'bootstrap';
4 | import 'bootstrap/dist/css/bootstrap.min.css';
5 | import AddWorkspace from './components/addWorkspace.jsx';
6 | import HomePage from './containers/HomePage.jsx';
7 | import Login from './components/Login.jsx';
8 | import Signup from './components/Signup.jsx';
9 | import NavBar from './containers/NavBarContainer.jsx'
10 |
11 | function App() {
12 | return (
13 |
14 |
15 |
16 |
17 | }>
18 | }>
19 | }>
20 | }>
21 | }>
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | /*
29 | User Stories
30 | TO TEST:
31 | - adds a location
32 | - adds reviews
33 | shows all reviews for a workspace
34 | clicking on a review shows a single review for that workspace
35 | - shows all locations for a zipcode
36 | clicking on a location shows a single workspace
37 | - login & signup & logout (check for redirects too)
38 | */
39 |
40 | export default App;
41 |
--------------------------------------------------------------------------------
/__tests__/react.js:
--------------------------------------------------------------------------------
1 | import React from 'React';
2 | import userEvent from '@testing-library/user-event'
3 | import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4 | import regeneratorRuntime from 'regenerator-runtime';
5 |
6 | import Review from '../client/components/Review';
7 | import AddReview from '../client/components/addReview';
8 |
9 | describe('Unit testing React components', () => {
10 |
11 | describe('Review Component', () =>{
12 | let review;
13 | const props = {
14 | username: 'best reviewer na',
15 | rating: 5,
16 | review: 'This good mane'
17 | };
18 |
19 | beforeAll(() => {
20 | review = render( )
21 | });
22 |
23 | test('Renders the passed down props', () => {
24 | expect(review.getByText('best reviewer na')).toBeTruthy();
25 | expect(review.getByText(5)).toBeTruthy();
26 | expect(review.getByText('This good mane')).toBeTruthy();
27 | });
28 |
29 | });
30 |
31 | describe('addReview Component', () =>{
32 | let reviews;
33 | const props = {
34 | workspaceid: 1
35 | };
36 |
37 | beforeAll(() =>{
38 | reviews = render( )
39 | });
40 |
41 | test('It should contain a button for adding a review', async () => {
42 | const buttons = await screen.findAllByRole('button');
43 | expect(buttons.length).toBe(1);
44 | expect(buttons[0]).toHaveTextContent('Submit');
45 | });
46 | });
47 |
48 | });
--------------------------------------------------------------------------------
/server/models/dbModels.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | // set up a Schema for 'users' collection
6 | const userSchema = new Schema({
7 | username: {
8 | type: String,
9 | required: true
10 | },
11 | password: {
12 | type: String,
13 | required: true
14 | },
15 | zipcode: {
16 | type: String,
17 | required: true
18 | },
19 | // birthday: {
20 | // type: Date,
21 | // required: true
22 | // },
23 | cookie: String,
24 | favorites: Array
25 | });
26 |
27 | // Schema for 'workspaces' collection
28 | const workspaceSchema = new Schema({
29 | workspaceName: {
30 | type: String,
31 | required: true
32 | },
33 | zipcode: {
34 | type: String,
35 | required: true
36 | },
37 | address: {
38 | type: String,
39 | },
40 | rating: {
41 | type: Number,
42 | },
43 | wifi: {
44 | type: String,
45 | },
46 | type: {
47 | type: String,
48 | },
49 | quiet: String,
50 | outlets: String,
51 | timeLimit: Number,
52 | laptopRestrictions: Boolean,
53 | crowded: String,
54 | outdoorSeating: Boolean,
55 | petFriendly: Boolean,
56 | url: String,
57 | foodRating: Number,
58 | coffeeRating: Number,
59 | seating: String,
60 | other: String,
61 | });
62 |
63 | // creates models for collections to export
64 | const User = mongoose.model('user', userSchema);
65 | const Workspace = mongoose.model('workspace', workspaceSchema);
66 |
67 | // exports all models in an object to be used in the controller
68 | module.exports = {
69 | User,
70 | Workspace
71 | }
--------------------------------------------------------------------------------
/server/sqlServer.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const app = express();
4 | const sqlWorkspaceRouter = require('./routes/sqlWorkspaceRouter');
5 | const sqlReviewRouter = require('./routes/sqlReviewRouter');
6 | const sqlUserRouter = require('./routes/sqlUserRouter');
7 |
8 | const cors = require('cors');
9 | const PORT = 3000;
10 |
11 | //parse incoming requests for json
12 | app.use(express.json());
13 | app.use(express.urlencoded({ extended: true }));
14 |
15 | //route to corect routes based on url endpoints
16 | app.use('/reviews', sqlReviewRouter);
17 | app.use('/workspace', sqlWorkspaceRouter);
18 | app.use('/user', sqlUserRouter);
19 |
20 | //unknown route handler
21 | app.use((req, res) => res.sendStatus(404));
22 |
23 | //serve the index.html if in production
24 | if (process.env.NODE_ENV === 'production') {
25 | // statically serve everything in the dist folder on the route '/dist'
26 | app.use('/dist', express.static(path.join(__dirname, '../dist')));
27 | // serve index.html on the route '/'
28 | app.get('/', (req, res) => {
29 | return res.status(200).sendFile(path.join(__dirname, '../index.html'));
30 | });
31 | }
32 |
33 | //global error handler
34 | app.use((err, req, res, next) => {
35 | const defaultErr = {
36 | log: 'Express error handler caught unknown middleware error',
37 | status: 500,
38 | message: { err: 'An error occurred' },
39 | };
40 | const errorObj = Object.assign({}, defaultErr, err);
41 | console.log(errorObj.log);
42 | return res.status(errorObj.status).json(errorObj.message);
43 | });
44 |
45 |
46 | app.listen(3000, () => {
47 | console.log(`Server listening on port: ${PORT}...`);
48 | });
49 |
50 |
51 |
--------------------------------------------------------------------------------
/client/components/addReview.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { useState } from 'react';
3 |
4 | function AddReview({workspaceid}) {
5 |
6 | const [username, setUsername] = useState('');
7 | const [rating, setNumberRating] = useState('');
8 | const [review, setReviewText] = useState('');
9 |
10 | // function to handle button click for add Space
11 | const handleAddReview = (event) => {
12 | // we want to pass all of the input values to an object to pass to the db
13 | event.preventDefault();
14 |
15 | const inputObj = {
16 | 'username': username,
17 | 'rating': rating,
18 | 'review': review,
19 | };
20 |
21 | // TODO: edge cases to check if required fields aren't entered
22 | if (username === '') {
23 | alert('Please enter a valid name.');
24 | };
25 |
26 | console.log('handleAddReview', inputObj);
27 |
28 | // send POST request to server with new workspace info in body
29 | axios.post(`/reviews/${workspaceid}`, inputObj)
30 | .then(res => {
31 | // panda whale - need something to respond so we know it successfully posted
32 | console.log({res});
33 | })
34 | .catch(err => {
35 | console.log(err);
36 | });
37 | };
38 |
39 | return (
40 |
41 |
47 |
48 | )
49 | }
50 |
51 | export default AddReview;
52 |
--------------------------------------------------------------------------------
/server/controllers/sqlUserController.js:
--------------------------------------------------------------------------------
1 | const { query } = require('express');
2 | const sqlDB = require('../models/sqlModels.js');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const sqlUserController = {};
6 |
7 | sqlUserController.signUp = async function (req, res, next) {
8 | const { username, password } = req.body;
9 | const hashedPw = await bcrypt.hash(password, 5);
10 | const queryString = `INSERT INTO users(username, password) VALUES ('${username}', '${hashedPw}') RETURNING *`;
11 | try {
12 | const result = await sqlDB.query(queryString);
13 | res.locals.newUser = result.rows;
14 | return next();
15 | } catch (err) {
16 | console.log('error in signUp: ', err);
17 | return next({ err });
18 | }
19 | };
20 |
21 | sqlUserController.verifyUser = async function (req, res, next) {
22 | const { username, password } = req.body;
23 | const queryString = `SELECT * FROM users WHERE username = '${username}'`;
24 | try {
25 | const result = await sqlDB.query(queryString);
26 | console.log(result);
27 | if (!result.rows.length) {
28 | return next({
29 | log: 'INVALID CREDENTIALS: ' + username,
30 | status: 401,
31 | message: { err: 'USER NOT FOUND' },
32 | });
33 | }
34 | const hashedPw = result.rows[0].password;
35 | const isValidCreds = await bcrypt.compare(password, hashedPw);
36 | if (!isValidCreds) {
37 | return next({
38 | log: 'INVALID CREDENTIALS: ' + username,
39 | status: 401,
40 | message: { err: 'Password does not match Username' },
41 | });
42 | }
43 | res.locals.isValid = isValidCreds;
44 | return next();
45 | } catch (err) {
46 | console.log('error in verifyingUser: ', err);
47 | return next({ err });
48 | }
49 | };
50 |
51 | module.exports = sqlUserController;
52 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV,
6 |
7 | entry: './client/index.js',
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | filename: 'bundle.js',
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | use: {
17 | loader: 'babel-loader',
18 | options: {
19 | presets: ['@babel/preset-env', '@babel/preset-react'],
20 | },
21 | },
22 | exclude: /node_modules/,
23 | },
24 | {
25 | test: /\.css$/,
26 | use: ['style-loader', 'css-loader'],
27 | exclude: /\.module\.css$/,
28 | },
29 | {
30 | test: /\.png$/,
31 | use: [
32 | {
33 | loader: 'url-loader',
34 | options: {
35 | mimetype: 'image/png',
36 | },
37 | },
38 | ],
39 | },
40 | {
41 | test: /\.scss$/,
42 | use: ['style-loader', 'css-loader', 'sass-loader'],
43 | },
44 | {
45 | test: /\.css$/,
46 | use: [
47 | 'style-loader',
48 | {
49 | loader: 'css-loader',
50 | options: {
51 | importLoaders: 1,
52 | modules: true,
53 | },
54 | },
55 | ],
56 | include: /\.module\.css$/,
57 | },
58 | ],
59 | },
60 | devServer: {
61 | static: {
62 | directory: path.join(__dirname, 'dist'),
63 | publicPath: '/',
64 | },
65 | proxy: {
66 | '/user': 'http://localhost:3000',
67 | '/reviews': 'http://localhost:3000',
68 | '/workspace': 'http://localhost:3000',
69 | },
70 | },
71 | plugins: [
72 | new HtmlWebpackPlugin({
73 | title: 'Development',
74 | template: './index.html',
75 | }),
76 | ],
77 | };
78 |
--------------------------------------------------------------------------------
/server/controllers/sqlReviewController.js:
--------------------------------------------------------------------------------
1 | const { query } = require('express');
2 | const sqlDB = require('../models/sqlModels.js');
3 |
4 | const sqlReviewController = {};
5 |
6 | //create middleware to get all reviews by workspaceid
7 | sqlReviewController.getReviews = async function (req, res, next) {
8 | const { workspaceid } = req.params;
9 | const queryString = `SELECT * FROM reviews WHERE workspaceid = ${workspaceid}`;
10 | try {
11 | const result = await sqlDB.query(queryString);
12 | res.locals.reviews = result.rows;
13 | return next();
14 | } catch (err) {
15 | console.log('err in getReviews middleware: ', err);
16 | return next({
17 | err,
18 | });
19 | }
20 | };
21 |
22 | //create middleware to create a review for a certain workspace, given by id
23 | sqlReviewController.createReview = async function (req, res, next) {
24 | const { workspaceid } = req.params;
25 | const { username, rating, review } = req.body;
26 | const queryString = `INSERT INTO reviews (username, rating, review, workspaceid)
27 | VALUES ('${username}', '${rating}', '${review}', '${workspaceid}') RETURNING *`;
28 | try {
29 | const result = await sqlDB.query(queryString);
30 | res.locals.review = result.rows;
31 | return next();
32 | } catch (err) {
33 | console.log('err in getReviews middleware: ', err);
34 | return next({
35 | err,
36 | });
37 | }
38 | };
39 | // middleware to get one review by review id
40 | sqlReviewController.getReviewsForOne = async function (req, res, next) {
41 | const { id } = req.query;
42 | const queryString = `SELECT * FROM reviews WHERE reviewid = ${id}`;
43 | try {
44 | const result = await sqlDB.query(queryString);
45 | res.locals.review = result.rows;
46 | return next();
47 | } catch (err) {
48 | console.log(err, ' Error in getReviewsForOne middleware');
49 | return next({ err });
50 | }
51 | };
52 |
53 | module.exports = sqlReviewController;
54 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const mongoose = require('mongoose');
3 | const path = require('path');;
4 | const fetch = require("node-fetch");
5 | const webpackProcess = require('../webpack.config');
6 |
7 |
8 | const app = express();
9 |
10 | const userRouter = require('./routes/userRouter');
11 | const workspaceRouter = require('./routes/workspaceRouter');
12 |
13 | const PORT = 3000;
14 |
15 | // Connect to MongoDB
16 | mongoose.connect(mongoURI, {
17 | useNewUrlParser: true,
18 | useUnifiedTopology: true,
19 | dbName: 'db'
20 | })
21 | .then(()=>console.log('Connected to Mongo DB'))
22 | .catch(err=>console.log(`Error connecting to MongoDB: ${err}`));
23 |
24 | // need to determine how we are parsing data
25 | app.use(express.json());
26 | app.use(express.urlencoded({ extended: true }));
27 |
28 | // set userRouter with /user endpoint
29 | app.use('/user', userRouter);
30 | app.use('/workspace', workspaceRouter);
31 |
32 | // unknown route handler
33 | app.use((req, res) => res.sendStatus(404));
34 |
35 | // Global Error Handler
36 | app.use((err, req, res, next) => {
37 | const defaultErr = {
38 | log: 'Express error handler caught unknown middleware error',
39 | status: 400,
40 | message: { err: 'An error occured'}
41 | };
42 | const errorObj = Object.assign({}, defaultErr, err);
43 | console.log(errorObj);
44 | return res.status(errorObj.status).json(errorObj.message);
45 | });
46 |
47 |
48 | if (process.env.NODE_ENV === 'production'){
49 | console.log('call to dist in production')
50 | // statically serve everything in the dist folder on the route '/dist'
51 | app.use('/dist', express.static(path.join(__dirname, '../dist')));
52 | // serve index.html on the route '/'
53 | app.get('/', (req, res) => {
54 | return res.status(200).sendFile(path.join(__dirname, '../index.html'));
55 | });
56 | }
57 |
58 | // Start server
59 | app.listen(PORT, () => {
60 | console.log(`Server listening on PORT ${PORT}`);
61 | })
62 |
63 |
--------------------------------------------------------------------------------
/client/containers/HomePage.jsx:
--------------------------------------------------------------------------------
1 | // moved to containers section, per definition this will be the stateful component passing props to the components - Lyam
2 | import React, { useState } from 'react';
3 | import axios from 'axios';
4 | import WorkspaceContainer from './WorkspaceContainer.jsx';
5 | import searchIcon from '../assets/search.png';
6 |
7 | const HomePage = () => {
8 | const [zipcode, setZipcode] = useState('');
9 | const [workspaces, setWorkspaces] = useState('');
10 |
11 | const handleZipcodeSearch = () => {
12 | axios
13 | .get(`/workspace?zipcode=${zipcode}`) // should be a POST request??
14 | .then((res) => {
15 | console.log(res);
16 | setWorkspaces(res.data);
17 | })
18 | .catch((error) => {
19 | console.error(
20 | `Couldn\'t fetch workspaces handleZipcodeSearch in HomePage, error: ${error}`
21 | );
22 | });
23 | };
24 |
25 | return (
26 | <>
27 |
28 |
setZipcode(e.target.value)}
33 | />
34 |
39 |
40 |
41 |
42 |
43 |
44 | Looking for a place to work or study remotely?
45 | Use CafeQuery to search for a specific cafe, restaurant, or
46 | bar to see reviews from other remote workers.
47 | You can also look up your zipcode to find workspaces near
48 | you!
49 |
50 |
51 |
52 | {/* removing this to place into workspace endpoint */}
53 |
54 | >
55 | );
56 | };
57 |
58 | export default HomePage;
59 |
--------------------------------------------------------------------------------
/__tests__/supertest.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const server = 'http://localhost:3000';
3 |
4 | describe('Route Integration', () => {
5 | const dummyData = {
6 | workspaceName: 'Cafe Boulud',
7 | zipcode: 12345,
8 | address: 'addy',
9 | wifi: 'High speed and reliable',
10 | type: 'Cafe',
11 | quiet: 'No',
12 | outlets: 'Many and accessible',
13 | laptopRestrictions: false,
14 | crowded: 'Very busy',
15 | outdoorSeating: true,
16 | petFriendly: false,
17 | url: 'joey.com',
18 | seating: '0-10',
19 | other: 'Hello I dont like coffee'
20 | };
21 |
22 | describe('testing workspace routes', () => {
23 |
24 | it('returns a status code 200 if server is connected', async () => {
25 | const res = await request(server).get('/workspace/id/1')
26 | expect(res.statusCode).toBe(200);
27 | });
28 |
29 | it('returns a status code 201 if workspace is created', async () => {
30 | const res = await request(server).post('/workspace').send(dummyData);
31 | expect(res.statusCode).toBe(201);
32 | });
33 |
34 | it('responds with the created workspace entry', async () => {
35 | const res = await request(server).post('/workspace').send(dummyData);
36 | expect(Object.values(res.body[0]).slice(1)).toEqual(Object.values(dummyData));
37 | });
38 |
39 | it('responds with workspace entries that match the query zipcode', async () => {
40 | const res = await request(server).get('/workspace?zipcode=12345');
41 | expect(res.body.length).not.toEqual(0);
42 | });
43 |
44 | it('responds with workspace that matched workspace ID', async () => {
45 | const id = 4;
46 | const res = await request(server).get(`/workspace/id/${id}`);
47 | expect(res.body[0].workspaceid).toEqual(id)
48 | });
49 |
50 | });
51 |
52 | describe('testing review routes', () => {
53 |
54 | it('returns a status code 200 if server is connected', async () => {
55 | const res = await request(server).get('/reviews/1')
56 | expect(res.statusCode).toBe(200);
57 | });
58 |
59 | });
60 |
61 | });
62 |
63 |
--------------------------------------------------------------------------------
/client/components/Login.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React, { useState } from 'react';
3 |
4 | const Login = () => {
5 | const [username, setUsername] = useState('');
6 | const [password, setPassword] = useState('');
7 | const [isLoggedIn, setLogin] = useState(false);
8 |
9 | // onclick function to send post request to server
10 | // if login is successful, do we need to add a cookie to local storage?
11 | // on signup, do we need to create and store a cookie for the user?
12 | // add axios call to check user credentials against db on click of submit
13 | // prevent page reload
14 | const handleLogin = (event) => {
15 | event.preventDefault();
16 |
17 | // boolean to see if user is found
18 | // let found = false;
19 |
20 | const loginObj = {
21 | username: username,
22 | password: password,
23 | };
24 |
25 | // should be a POST to endpoint to check SQL server if username exists
26 | axios.post(`/user/login`, loginObj)
27 | .then((data) => {
28 | console.log(data);
29 | })
30 | .catch((err) => {
31 | console.log(err);
32 | });
33 |
34 | // conditional login/logout button
35 | // console.log(isLoggedIn)
36 | setLogin(!isLoggedIn)
37 | // console.log(isLoggedIn)
38 |
39 | if (isLoggedIn) {
40 | document.querySelector('#nav-login').innerText = 'Logout'
41 | } else {
42 | document.querySelector('#nav-login').innerText = 'Login'
43 | }
44 | };
45 |
46 | return (
47 | <>
48 |
66 | >
67 | );
68 | }
69 |
70 | export default Login;
71 |
--------------------------------------------------------------------------------
/dist/bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * Bootstrap v5.1.3 (https://getbootstrap.com/)
9 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
10 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
11 | */
12 |
13 | /*!
14 | Copyright (c) 2018 Jed Watson.
15 | Licensed under the MIT License (MIT), see
16 | http://jedwatson.github.io/classnames
17 | */
18 |
19 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
20 |
21 | /**
22 | * React Router DOM v6.3.0
23 | *
24 | * Copyright (c) Remix Software Inc.
25 | *
26 | * This source code is licensed under the MIT license found in the
27 | * LICENSE.md file in the root directory of this source tree.
28 | *
29 | * @license MIT
30 | */
31 |
32 | /**
33 | * React Router v6.3.0
34 | *
35 | * Copyright (c) Remix Software Inc.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE.md file in the root directory of this source tree.
39 | *
40 | * @license MIT
41 | */
42 |
43 | /** @license React v0.20.2
44 | * scheduler.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 |
52 | /** @license React v17.0.2
53 | * react-dom.production.min.js
54 | *
55 | * Copyright (c) Facebook, Inc. and its affiliates.
56 | *
57 | * This source code is licensed under the MIT license found in the
58 | * LICENSE file in the root directory of this source tree.
59 | */
60 |
61 | /** @license React v17.0.2
62 | * react-jsx-runtime.production.min.js
63 | *
64 | * Copyright (c) Facebook, Inc. and its affiliates.
65 | *
66 | * This source code is licensed under the MIT license found in the
67 | * LICENSE file in the root directory of this source tree.
68 | */
69 |
70 | /** @license React v17.0.2
71 | * react.production.min.js
72 | *
73 | * Copyright (c) Facebook, Inc. and its affiliates.
74 | *
75 | * This source code is licensed under the MIT license found in the
76 | * LICENSE file in the root directory of this source tree.
77 | */
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cafe-query",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "ISC",
9 | "scripts": {
10 | "clean": "rm dist/bundle.js",
11 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve --open\" \"cross-env NODE_ENV=development nodemon server/sqlServer.js\"",
12 | "start": "cross-env NODE_ENV=production nodemon server/sqlServer.js",
13 | "build": "webpack",
14 | "test": "jest --verbose"
15 | },
16 | "jest": {
17 | "testEnvironment": "jest-environment-jsdom",
18 | "setupFilesAfterEnv": [
19 | "@testing-library/jest-dom/extend-expect"
20 | ]
21 | },
22 | "dependencies": {
23 | "@faker-js/faker": "^7.3.0",
24 | "@testing-library/jest-dom": "^5.16.4",
25 | "@testing-library/react": "^12.1.2",
26 | "@testing-library/user-event": "^14.3.0",
27 | "axios": "^0.27.2",
28 | "bcryptjs": "^2.4.3",
29 | "bootstrap": "^5.1.3",
30 | "express": "^4.18.1",
31 | "file-loader": "^6.2.0",
32 | "jest-environment-jsdom": "^28.1.3",
33 | "jest-puppeteer": "^6.1.1",
34 | "jquery": "^3.6.0",
35 | "node-fetch": "^2.3.0",
36 | "nodemon": "^2.0.19",
37 | "pg": "^8.7.3",
38 | "popper.js": "^1.16.1",
39 | "postgres": "^3.2.4",
40 | "puppeteer": "^15.5.0",
41 | "react": "^17.0.2",
42 | "react-bootstrap": "^2.4.0",
43 | "react-dom": "^17.0.2",
44 | "react-redux": "^8.0.2",
45 | "react-router": "^6.3.0",
46 | "react-router-bootstrap": "^0.26.2",
47 | "react-router-dom": "^6.3.0",
48 | "redux": "^4.2.0",
49 | "redux-devtools-extension": "^2.13.9",
50 | "redux-thunk": "^2.4.1",
51 | "regenerator-runtime": "^0.13.9",
52 | "request": "^2.88.2",
53 | "sass": "^1.53.0",
54 | "supertest": "^6.2.4"
55 | },
56 | "devDependencies": {
57 | "@babel/core": "^7.18.6",
58 | "@babel/preset-env": "^7.18.6",
59 | "@babel/preset-react": "^7.18.6",
60 | "babel-jest": "^28.1.3",
61 | "babel-loader": "^8.2.5",
62 | "concurrently": "^7.3.0",
63 | "cors": "^2.8.5",
64 | "cross-env": "^7.0.3",
65 | "css-loader": "^6.7.1",
66 | "html-webpack-plugin": "^5.5.0",
67 | "jest": "^28.1.3",
68 | "mongodb": "^4.8.0",
69 | "mongoose": "^5.11.8",
70 | "node-sass": "^7.0.1",
71 | "sass-loader": "^13.0.2",
72 | "style-loader": "^3.3.1",
73 | "url-loader": "^4.1.1",
74 | "webpack": "^5.73.0",
75 | "webpack-cli": "^4.10.0",
76 | "webpack-dev-server": "^4.9.3"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/server/controllers/sqlWorkspaceController.js:
--------------------------------------------------------------------------------
1 | const { query } = require('express');
2 | const sqlDB = require('../models/sqlModels.js');
3 |
4 | const sqlWorkspaceController = {};
5 |
6 | //middleware to get all workspaces by zipcode
7 | sqlWorkspaceController.getWorkspace = async function (req, res, next) {
8 | //destructure zipcode from query string
9 | const { zipcode } = req.query;
10 | const queryString = `SELECT * FROM workspaces WHERE zipcode = ${zipcode}`;
11 | try {
12 | const workspaces = await sqlDB.query(queryString);
13 | //send through res.locals all relevant workspaces
14 | res.locals.workspaces = workspaces.rows;
15 | return next();
16 | } catch (err) {
17 | console.log(err);
18 | next({
19 | log: err + ' error in the getWorkspace Middleware',
20 | status: 404,
21 | message: { err: 'You have a stupid error: ', err },
22 | });
23 | }
24 | };
25 |
26 | //middleware to get a specific workspace by ID
27 | sqlWorkspaceController.getOneWorkspace = async function (req, res, next) {
28 | const { workspaceid } = req.params;
29 | const queryString = `SELECT * FROM workspaces WHERE workspaceid = ${workspaceid}`;
30 | try {
31 | const workspace = await sqlDB.query(queryString);
32 | //send through res.locals the retrieved workspace
33 | res.locals.workspace = workspace.rows;
34 | return next();
35 | } catch (err) {
36 | console.log(err);
37 | next({
38 | log: err + ' error in the getOneWorkspace Middleware',
39 | status: 404,
40 | message: { err: 'You have a stupid error: ', err },
41 | });
42 | }
43 | };
44 |
45 | //middleware to create a new workspace
46 | sqlWorkspaceController.createWorkspace = async function (req, res, next) {
47 | //destructure from request body all relevant information to create a new workspace
48 | const {
49 | workspaceName,
50 | zipcode,
51 | address,
52 | wifi,
53 | type,
54 | quiet,
55 | outlets,
56 | laptopRestrictions,
57 | crowded,
58 | outdoorSeating,
59 | petFriendly,
60 | url,
61 | seating,
62 | other,
63 | } = req.body;
64 | const queryString = `
65 | INSERT INTO workspaces (WorkspaceName, Zipcode, Address, Wifi, Type, Quiet, Outlets, LaptopRestrictions, Crowded, OutdoorSeating, PetFriendly, URL, Seating, Other)
66 | VALUES('${workspaceName}', '${zipcode}', '${address}', '${wifi}', '${type}', '${quiet}', '${outlets}', '${laptopRestrictions}', '${crowded}', '${outdoorSeating}', '${petFriendly}', '${url}', '${seating}', '${other}') RETURNING *`;
67 | try {
68 | const result = await sqlDB.query(queryString);
69 | //send back the new workspace through res.locals
70 | res.locals.newWorkspace = result.rows;
71 | return next();
72 | } catch (err) {
73 | console.log(err);
74 | next({
75 | log: err + ' error in the createWorkspace Middleware',
76 | status: 404,
77 | message: { err: 'You have a stupid error: ', err },
78 | });
79 | }
80 | };
81 |
82 | module.exports = sqlWorkspaceController;
83 |
--------------------------------------------------------------------------------
/client/components/Workspace.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Reviews from './Reviews.jsx';
3 | import AddReview from './addReview.jsx';
4 | import axios from 'axios';
5 |
6 | const Workspace = (props) => {
7 | //display information received in the result body
8 | //from the databse query to locations
9 | const { workspacename, address, wifi, type, quiet, outlets, timeLimit, laptopRestrictions, crowded,
10 | outdoorSeating, petFriendly, seating, workspaceid } = props.resultObject;
11 | const [reviews, setReviews] = useState([]);
12 |
13 | const [ isClicked, setIsClicked ] = useState(false);
14 | const [ reviewClicked, setReviewClicked ] = useState(false);
15 |
16 | // making a fetch to workspace/id - redirect or popup to review container?
17 | const handleWorkspaceView = async () => {
18 | console.log('handleWorkspaceView Clicked');
19 | try {
20 | // workspace/:id
21 | const response = await axios.get(`/workspace/${workspaceid}`)
22 | console.log('Data received from workspace: ', response.data)
23 | } catch (err) {
24 | console.log('Error viewing workspace: ', err);
25 | }
26 | }
27 |
28 | // where the joining of a single workspace to reviews table is happening
29 | // one (workspace) to many (reviews) relationship
30 | // implmenet error handling using try/catch block later
31 |
32 | const reviewObj = {};
33 |
34 | async function getReviews() {
35 | const response = await axios.get(`/reviews/${workspaceid}/`);
36 | setReviews(response.data);
37 | console.log(response.data)
38 |
39 | // return
40 | };
41 |
42 | // getReviews();
43 |
44 | console.log('reviewsData: ', reviews)
45 |
46 | return(
47 | <>
48 |
49 |
Name: {workspacename}
50 |
Address: {address}
51 |
Wifi: {wifi}
52 |
Type: {type}
53 |
Noise level: {quiet}
54 |
Outlets: {outlets}
55 |
Time limit: {timeLimit}
56 |
Laptop Restrictions: {laptopRestrictions}
57 |
Busy: {crowded}
58 |
Outdoor Seating: {outdoorSeating}
59 |
Pet friendly: {petFriendly}
60 |
Seating: {seating}
61 |
setIsClicked(!isClicked)}>Add a review Add a review
62 | { isClicked &&
}
63 |
View View
64 |
{
65 | getReviews();
66 | setReviewClicked(!reviewClicked);
67 | }}>Get Reviews Get Reviews
68 | { reviewClicked &&
}
69 |
70 | >
71 | );
72 | };
73 |
74 | export default Workspace;
--------------------------------------------------------------------------------
/__tests__/db.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg'); // should create an instance of a new db (?)
2 | const faker = require('@faker-js/faker'); // mock data to test our db functionality
3 |
4 | xdescribe ('database testing', () => {
5 | let pgPool; // inits a db connection
6 | beforeAll(async () => {
7 | pgPool = new Pool({
8 | connectionString: 'postgres://pnpqhovz:xmn6RTTNcTruASv9i7J3ySrFkFTpQ84H@castor.db.elephantsql.com/pnpqhovz'
9 | });
10 | console.log('Connected to db server.');
11 | });
12 |
13 | // testing for table creation
14 | describe('user testing', () => {
15 |
16 | beforeEach(async () => {
17 | let createTableSQL =
18 | "CREATE TABLE 'testusers' ( 'id' INT(2) NOT NULL AUTO_INCREMENT, 'name' VARCHAR(100) NOT NULL, 'password' VARCHAR(50) NOT NULL, PRIMARY KEY ('id'))";
19 | await pgPool.query(createTableSQL);
20 | });
21 |
22 | afterEach(async () => {
23 | let dropTableSQL = "DROP TABLE IF EXISTS 'testusers';";
24 | await pgPool.query(dropTableSQL);
25 | await pgPool.end();
26 | });
27 |
28 | it('check testusers -> id is created (first 5)', async () => {
29 | const client = await pgPool.connect();
30 | await client.query('BEGIN');
31 |
32 | console.log(1)
33 | const total_test_users = 5;
34 | let insertQueries = [];
35 | for (let i = 0; i < total_test_users; i++) {
36 | let insertSQL = `INSERT INTO users (name, email) VALUES ('${faker.name.findName()}', '${faker.internet.password()}');`;
37 | insertQueries.push(client.query(insertSQL))
38 | };
39 | console.log({insertQueries});
40 | console.log(2)
41 | await Promise.all(insertQueries);
42 | const data = client.query('SELECT * FROM testusers');
43 | expect(data.rows.length).toBe(total_test_users)
44 | await client.query('ROLLBACK');
45 | });
46 | });
47 |
48 |
49 | // it('should test', async () => {
50 | // const client = await pgPool.connect();
51 | // try {
52 | // await client.query('BEGIN');
53 |
54 | // const { rows } = await client.query('SELECT AS "result"');
55 | // console.log(rows)
56 | // expect(rows[0]["result"]).toBe(1);
57 |
58 | // await client.query('ROLLBACK');
59 | // } catch(err) {
60 | // throw err;
61 | // } finally {
62 | // client.release();
63 | // }
64 | // })
65 |
66 | // describe('tests workspace db table queries', () => {
67 |
68 | // beforeEach(async () => {
69 | // let createTableSQL =
70 | // "CREATE TABLE `testworkspace` ( `id` INT(2) NOT NULL AUTO_INCREMENT , `name` VARCHAR(100) NOT NULL , `email` VARCHAR(50) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;";
71 | // await pgPool.query(createTableSQL);
72 | // })
73 |
74 |
75 | // })
76 |
77 | // describe('tests review db table queries', () => {
78 |
79 | // beforeEach(async () => {
80 | // let createTableSQL =
81 | // "CREATE TABLE `testreview` ( `id` INT(2) NOT NULL AUTO_INCREMENT , `name` VARCHAR(100) NOT NULL , `email` VARCHAR(50) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;";
82 | // await pgPool.query(createTableSQL);
83 | // })
84 |
85 |
86 | // })
87 |
88 | });
--------------------------------------------------------------------------------
/server/controllers/UserController.js:
--------------------------------------------------------------------------------
1 | const { request } = require('express');
2 | const { User } = require('../models/dbModels');
3 |
4 | const UserController = {
5 |
6 | // Create a new user in the database
7 | // information will be sent in the request body
8 | createUser(req, res, next) {
9 | // deconstruct the req body (excluding favorites since not part of signup)
10 | const { username, password, zipcode, birthday, cookie } = req.body;
11 |
12 | // add user to the database
13 | User.create( { username, password, zipcode, birthday, cookie } )
14 | .then(data => {
15 | res.locals.newUser = data;
16 | console.log('New User: ',data);
17 | return next();
18 | })
19 | .catch(err => {
20 | return next({
21 | log: `Error occurred in createUser method of UserController : ${err}`,
22 | status: 400,
23 | message: { err : 'An error occurred while creating a new user'}
24 | });
25 | });
26 | },
27 |
28 | // Grab user information from the database
29 | // username will be the parameter
30 | getUser(req, res, next) {
31 | // deconstruct the username that will be sent in the request parameter
32 | const { username } = req.params;
33 |
34 | User.findOne({username: username})
35 | .then(data => {
36 | res.locals.user = data;
37 | console.log('Found user: ',data);
38 | return next();
39 | })
40 | .catch(err => {
41 | return next({
42 | log: `Error occured in getUser method of UserController : ${err}`,
43 | status: 400,
44 | message: { err: 'An error occured while trying to get user'}
45 | });
46 | });
47 | },
48 |
49 | // Adds a favorite workspace to the user favorites list
50 | // username will be the parameter and the workspace_id will be in the body
51 | addFavorite(req, res, next) {
52 | const { username } = req.params;
53 | // unsure if should use workspace name or wor
54 | const { workspace_id } = req.body;
55 |
56 | // find based on username param
57 | // push the workspace_id to the favorites array
58 | User.findOneAndUpdate({ username: username }, { "$push": { favorites: workspace_id }})
59 | .then(data => {
60 | res.locals.updatedUser = data;
61 | console.log('Updated user: ', data);
62 | return next();
63 | })
64 | .catch(err => {
65 | return next({
66 | log: `Error caught in addFavorite method of UserController : ${err}`,
67 | status: 400,
68 | message: { err: 'An error occured when trying to add a new favorite'}
69 | })
70 | });
71 | },
72 |
73 | // Deletes the user from the database
74 | // username will be the parameter
75 | deleteUser(req, res, next) {
76 | // deconstruct the username from params
77 | const { username } = req.params;
78 |
79 | // find user based on user params and delete
80 | // will return the deleted username - don't need to do anything with it
81 | User.findOneAndDelete({username: username})
82 | .then(data => {
83 | res.locals.deletedUser = data;
84 | console.log('Deleted user: ', data);
85 | return next();
86 | })
87 | .catch(err => {
88 | return next({
89 | log: `Error caught in the deleteUser method of UserController : ${err}`,
90 | status: 400,
91 | message: { err : 'An error occured when trying to delete a user'}
92 | })
93 | });
94 | }
95 | }
96 |
97 | module.exports = UserController;
98 |
--------------------------------------------------------------------------------
/server/controllers/WorkspaceController.js:
--------------------------------------------------------------------------------
1 | const { request } = require('express');
2 | const { Workspace } = require('../models/dbModels');
3 |
4 | const WorkspaceController = {
5 |
6 | // Create a new workspace in the database
7 | // information will be sent in the request body
8 | createWorkspace(req, res, next) {
9 | // deconstruct the req body
10 | const { workspaceName, zipcode, address, rating, wifi, type, quiet, outlets, timeLimit, laptopRestrictions,
11 | crowded, outdoorSeating, petFriendly, url, foodRating, coffeeRating, seating, other } = req.body;
12 |
13 | // adds a workspace to the database
14 | Workspace.create( { workspaceName, zipcode, address, rating, wifi, type, quiet, outlets, timeLimit, laptopRestrictions,
15 | crowded, outdoorSeating, petFriendly, url, foodRating, coffeeRating, seating, other })
16 | .then(data => {
17 | res.locals.newWorkspace = data;
18 | console.log('New workspace: ',data);
19 | return next();
20 | })
21 | .catch(err => {
22 | return next({
23 | log: `Error occurred in createWorkspace method of WorkspaceController : ${err}`,
24 | status: 400,
25 | message: { err : 'An error occurred while creating a new workspace'}
26 | });
27 | });
28 | },
29 |
30 | // Grabs a workspace from the database
31 | // workspace_id will be the parameter
32 | getWorkspace(req, res, next) {
33 | // deconstruct the username that will be sent in the request parameter
34 | const { workspace_id } = req.params;
35 |
36 | // finds workspace from the database
37 | Workspace.findOne({_id: workspace_id})
38 | .then(data => {
39 | res.locals.workspace = data;
40 | console.log('Found workspace: ',data);
41 | return next();
42 | })
43 | .catch(err => {
44 | return next({
45 | log: `Error occured in getWorkspace method of WorkspaceController : ${err}`,
46 | status: 400,
47 | message: { err: 'An error occured while trying to get workspace'}
48 | });
49 | });
50 | },
51 |
52 | getWorkspaceByZip(req, res, next) {
53 | // deconstruct the username that will be sent in the request parameter
54 | console.log('Reached the get workspace by zip middleware.');
55 |
56 | const { zipcodeSearch } = req.params;
57 |
58 | if (typeof zipcodeSearch !== 'string' || zipcodeSearch.length !== 5){
59 | return next({
60 | log: `User input error: entered input was less than 5 digits`,
61 | status: 400,
62 | message: {err: 'Please enter a 5 digit zipcode'}})
63 | };
64 |
65 | // finds workspace from the database
66 | Workspace.find({zipcode: zipcodeSearch})
67 | .then(data => {
68 | if (data.length > 0) {
69 | res.locals.workspace = data;
70 | // console.log('Found workspace:', res.locals.workspace);
71 | return next();
72 | }
73 | else {
74 | return next({
75 | log: `No locations found in that zip code.`,
76 | status: 400,
77 | message: {err: 'No locations found in that zip code.'}
78 | })
79 | }
80 | })
81 | .catch(err => {
82 | return next({
83 | log: `Error occured in getWorkspaceByZip method of WorkspaceController : ${err}`,
84 | status: 400,
85 | message: { err: 'An error occured while trying to get workspace'}
86 | });
87 | });
88 | },
89 |
90 | // Deletes the workspace from the database
91 | // workspace_id will be the parameter
92 | deleteWorkspace(req, res, next) {
93 | // deconstruct the workspace_id from params
94 | const { workspace_id } = req.params;
95 |
96 | // find user based on user params and delete
97 | // will return the deleted username - don't need to do anything with it
98 | Workspace.findOneAndDelete({_id: workspace_id})
99 | .then(data => {
100 | res.locals.deletedWorkspace = data;
101 | console.log('Deleted workspace: ', data);
102 | return next();
103 | })
104 | .catch(err => {
105 | return next({
106 | log: `Error caught in the deleteWorkspace method of WorkspaceController : ${err}`,
107 | status: 400,
108 | message: { err : 'An error occured when trying to delete a workspace'}
109 | })
110 | });
111 | }
112 | }
113 |
114 | module.exports = WorkspaceController;
--------------------------------------------------------------------------------
/client/components/addWorkspace.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from 'axios';
3 | import '../stylesheets/styles.scss';
4 | const AddWorkspace = () => {
5 | const [workspaceName, setName] = useState('');
6 | const [address, setAddress] = useState('');
7 | const [zipCode, setZipCode] = useState('');
8 | const [wifi, setWifi] = useState('');
9 | const [type, setType] = useState('');
10 | const [noise, setNoise] = useState('');
11 | const [outlets, setOutlets] = useState('');
12 | const [time, setTime] = useState('');
13 | const [laptopChecked, setLaptop] = useState(false);
14 | const [busy, setBusy] = useState('');
15 | const [outdoorChecked, setOutdoor] = useState(false);
16 | const [petChecked, setPetFriendly] = useState(false);
17 | const [url, setUrlAddress] = useState('');
18 | const [seating, setSeating] = useState('');
19 | const [additional, setAdditional] = useState('');
20 |
21 | // function to handle button click for add Space
22 | const handleSubmit = (event) => {
23 | // we want to pass all of the input values to an object to pass to the db
24 | event.preventDefault();
25 |
26 | const inputObj = {
27 | 'workspaceName': workspaceName,
28 | 'zipcode': zipCode,
29 | 'address': address,
30 | 'wifi': wifi,
31 | 'type': type,
32 | 'quiet': noise,
33 | 'outlets': outlets,
34 | 'timeLimit': time,
35 | 'laptopRestrictions': laptopChecked,
36 | 'crowded': busy,
37 | 'outdoorSeating': outdoorChecked,
38 | 'petFriendly': petChecked,
39 | 'url': url,
40 | 'seating': seating,
41 | 'other': additional
42 | };
43 |
44 | // TODO: edge cases to check if required fields aren't entered
45 | if (workspaceName === '') {
46 | alert('Please enter a valid workspace name.');
47 | }
48 |
49 | // send POST request to server with new workspace info in body
50 | axios
51 | .post('/workspace', inputObj)
52 | .then((res) => {
53 | // panda whale - need something to respond so we know it successfully posted
54 | console.log(res);
55 | })
56 | .catch((err) => {
57 | console.log(err);
58 | });
59 | };
60 |
61 | return (
62 |
63 |
Add a Workspace!
64 |
141 |
142 | );
143 | };
144 |
145 | export default AddWorkspace;
146 |
--------------------------------------------------------------------------------
/client/stylesheets/styles.scss:
--------------------------------------------------------------------------------
1 | $primary-color: rgb(223, 133, 133);
2 | $bg: black;
3 | $peach: #f6d78d;
4 |
5 | @import url('https://fonts.googleapis.com/css2?family=Twinkle+Star&display=swap');
6 |
7 | h1 {
8 | color: $primary-color;
9 | font-weight: bold;
10 | display: flex;
11 | justify-content: center;
12 | }
13 |
14 |
15 |
16 | ::-webkit-scrollbar {
17 | width: .5vw;
18 | background-color: transparent;
19 | border-radius: 5px;
20 | overflow: auto;
21 | }
22 |
23 | ::-webkit-scrollbar-thumb {
24 | box-shadow: inset 0 0 1px #e65100;
25 | background-color: #ffb74d;
26 | border-radius: 5px;
27 | }
28 |
29 | ::-webkit-scrollbar-thumb:hover {
30 | background-color: #faa627;
31 | }
32 |
33 | ::-webkit-scrollbar-track:hover {
34 | background-color: #cecece;
35 | }
36 |
37 |
38 |
39 | .submit_btn1 {
40 | position: fixed;
41 | left: 30%;
42 | top: 10%;
43 | }
44 |
45 | .signup {
46 | text-align: center;
47 | display: flex;
48 | justify-content: center;
49 | }
50 |
51 | .navbar {
52 | background-color: $peach;
53 | }
54 |
55 | .navbar-brand {
56 | font-family: 'Twinkle Star';
57 | font-size: 35px;
58 | }
59 |
60 | body {
61 | background-image: url('../assets/background.jpg');
62 | // background-size: cover;
63 | background-repeat: no-repeat;
64 | background-size: 100%;
65 | }
66 |
67 | .searchContainer {
68 | margin: 200px auto;
69 | width: 500px;
70 | height: 60px;
71 | }
72 |
73 | .searchForm {
74 | height: 100%;
75 | width: 100%;
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | }
80 |
81 | .search-field {
82 | width: 35%;
83 | padding: 10px 35px 10px 15px;
84 | border: none;
85 | border-radius: 100px;
86 | outline: none;
87 | display: flex;
88 | align-items: center;
89 | justify-content: center;
90 | margin-top: 300px;
91 | font-family: 'Twinkle Star';
92 | }
93 |
94 | .search-button {
95 | background: transparent;
96 | border: none;
97 | outline: none;
98 | margin-left: -33px;
99 | margin-top: 300px;
100 | }
101 |
102 | .search-button img {
103 | width: 20px;
104 | height: 20px;
105 | object-fit: cover;
106 | }
107 |
108 | .appDescription {
109 | display: flex;
110 | align-items: center;
111 | justify-content: center;
112 | width: 100%;
113 | margin-top: 3%;
114 | color: $peach;
115 | text-align: center;
116 | font-size: 15px;
117 | }
118 |
119 | .icon {
120 | padding-right: 10px;
121 | padding-left: 10px;
122 | }
123 |
124 | .LocationDisplay {
125 | color: white;
126 | border: 1px solid white;
127 | border-color: white;
128 | border-width: 5px;
129 | text-align: center;
130 | margin-top: 30%;
131 | margin-left: 35%;
132 | margin-right: 35%;
133 | margin-bottom: 30%;
134 | }
135 |
136 | h4{
137 | font-size: 20px;
138 | }
139 |
140 | .location_submission{
141 | display: flex;
142 | flex-direction: column;
143 | align-items: center;
144 | input {
145 | max-width: 75%;
146 | }
147 | }
148 |
149 | .workspace {
150 | color: white;
151 | border: 1px solid white;
152 | border-color: $peach;
153 | border-width: 5px;
154 | text-align: center;
155 | margin-top: 10%;
156 | margin-left: 30%;
157 | margin-right: 30%;
158 | margin-bottom: 30%;
159 | font-size: 25px !important;
160 | padding: 50px 40px 50px 40px !important;
161 | }
162 |
163 | .login_form{
164 | display: flex;
165 | align-items: center;
166 | justify-content: center;
167 | margin-top: 5%;
168 | margin-right: 20px;
169 | margin-left: 20px;
170 | padding: 10px 35px 10px 15px !important;
171 | }
172 |
173 | h7{
174 | color: $peach;
175 | font-family: 'Twinkle Star';
176 | display: flex;
177 | align-items: center;
178 | justify-content: center;
179 | margin-top: 20%;
180 | font-size: 40px;
181 | }
182 |
183 | .btn-87,
184 | .btn-87 *,
185 | .btn-87 :after,
186 | .btn-87 :before,
187 | .btn-87:after,
188 | .btn-87:before {
189 | border: 0 solid;
190 | box-sizing: border-box;
191 | }
192 | .btn-87 {
193 | -webkit-tap-highlight-color: transparent;
194 | -webkit-appearance: button;
195 | background-color: #000;
196 | background-image: none;
197 | color: #fff;
198 | cursor: pointer;
199 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
200 | Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
201 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
202 | font-size: 100%;
203 | line-height: 1.5;
204 | margin: 0;
205 | -webkit-mask-image: -webkit-radial-gradient(#000, #fff);
206 | padding: 0;
207 | }
208 | .btn-87:disabled {
209 | cursor: default;
210 | }
211 | .btn-87:-moz-focusring {
212 | outline: auto;
213 | }
214 | .btn-87 svg {
215 | display: block;
216 | vertical-align: middle;
217 | }
218 | .btn-87 [hidden] {
219 | display: none;
220 | }
221 | .btn-87 {
222 | background: none;
223 | border-radius: 999px;
224 | box-sizing: border-box;
225 | display: block;
226 | font-size: 20px;
227 | font-weight: 900;
228 | height: 110px;
229 | position: relative;
230 | text-transform: uppercase;
231 | width: 110px;
232 | }
233 | .btn-87 span {
234 | mix-blend-mode: difference;
235 | }
236 | .btn-87:before {
237 | background: #fff;
238 | border-radius: 50%;
239 | content: "";
240 | inset: 0;
241 | opacity: 0;
242 | position: absolute;
243 | transition: opacity 0.2s linear;
244 | z-index: -1;
245 | }
246 | .btn-87:hover:before {
247 | opacity: 1;
248 | transition: opacity 0.2s linear 1s;
249 | }
250 | .btn-87 svg {
251 | fill: none;
252 | stroke: currentcolor;
253 | stroke-width: 4px;
254 | stroke-dasharray: 450;
255 | stroke-dashoffset: 450;
256 | height: 105%;
257 | left: -5px;
258 | pointer-events: none;
259 | position: absolute;
260 | top: -5px;
261 | transition: stroke-dashoffset 0.4s ease-in-out;
262 | width: 105%;
263 | }
264 | .btn-87 circle {
265 | cx: 52%;
266 | cy: 52%;
267 | r: 45%;
268 | }
269 | .btn-87:hover svg {
270 | stroke-dashoffset: 120;
271 | transition: stroke-dashoffset 1s ease-in-out;
272 | }
273 |
274 | .btn-33,
275 | .btn-33 *,
276 | .btn-33 :after,
277 | .btn-33 :before,
278 | .btn-33:after,
279 | .btn-33:before {
280 | border: 0 solid;
281 | box-sizing: border-box;
282 | }
283 | .btn-33 {
284 | -webkit-tap-highlight-color: transparent;
285 | -webkit-appearance: button;
286 | background-color: #000;
287 | background-image: none;
288 | color: #fff;
289 | cursor: pointer;
290 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
291 | Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
292 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
293 | font-size: 100%;
294 | font-weight: 900;
295 | line-height: 1.5;
296 | margin: 0;
297 | -webkit-mask-image: -webkit-radial-gradient(#000, #fff);
298 | padding: 0;
299 | text-transform: uppercase;
300 | }
301 | .btn-33:disabled {
302 | cursor: default;
303 | }
304 | .btn-33:-moz-focusring {
305 | outline: auto;
306 | }
307 | .btn-33 svg {
308 | display: block;
309 | vertical-align: middle;
310 | }
311 | .btn-33 [hidden] {
312 | display: none;
313 | }
314 | .btn-33 {
315 | border-radius: 99rem;
316 | border-width: 2px;
317 | overflow: hidden;
318 | padding: 0.8rem 3rem;
319 | position: relative;
320 | }
321 | .btn-33 span {
322 | display: grid;
323 | inset: 0;
324 | place-items: center;
325 | position: absolute;
326 | transition: opacity 0.2s ease;
327 | }
328 | .btn-33 .marquee {
329 | --spacing: 5em;
330 | --start: 0em;
331 | --end: 5em;
332 | -webkit-animation: marquee 0.4s linear infinite;
333 | animation: marquee 0.4s linear infinite;
334 | -webkit-animation-play-state: paused;
335 | animation-play-state: paused;
336 | opacity: 0;
337 | position: relative;
338 | text-shadow: #fff 0 var(--spacing), #fff 0 calc(var(--spacing) * -1),
339 | #fff 0 calc(var(--spacing) * -2);
340 | }
341 | .btn-33:hover .marquee {
342 | -webkit-animation-play-state: running;
343 | animation-play-state: running;
344 | opacity: 1;
345 | }
346 | .btn-33:hover .text {
347 | opacity: 0;
348 | }
349 | @-webkit-keyframes marquee {
350 | 0% {
351 | transform: translateY(var(--start));
352 | }
353 | to {
354 | transform: translateY(var(--end));
355 | }
356 | }
357 | @keyframes marquee {
358 | 0% {
359 | transform: translateY(var(--start));
360 | }
361 | to {
362 | transform: translateY(var(--end));
363 | }
364 | }
365 |
366 | .submit_btn,
367 | .submit_btn *,
368 | .submit_btn :after,
369 | .submit_btn :before,
370 | .submit_btn:after,
371 | .submit_btn:before {
372 | border: 0 solid;
373 | box-sizing: border-box;
374 | }
375 | .submit_btn {
376 | -webkit-tap-highlight-color: transparent;
377 | -webkit-appearance: button;
378 | background-color: #000;
379 | background-image: none;
380 | color: #fff;
381 | cursor: pointer;
382 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
383 | Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
384 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
385 | font-size: 100%;
386 | line-height: 1.5;
387 | margin: 0;
388 | -webkit-mask-image: -webkit-radial-gradient(#000, #fff);
389 | padding: 0;
390 | }
391 | .submit_btn:disabled {
392 | cursor: default;
393 | }
394 | .submit_btn:-moz-focusring {
395 | outline: auto;
396 | }
397 | .submit_btn svg {
398 | display: block;
399 | vertical-align: middle;
400 | }
401 | .submit_btn [hidden] {
402 | display: none;
403 | }
404 | .submit_btn {
405 | -webkit-animation: pulse 2s infinite;
406 | animation: pulse 2s infinite;
407 | border: 1px solid;
408 | border-radius: 999px;
409 | box-shadow: 0 0 0 2em transparent;
410 | box-sizing: border-box;
411 | display: block;
412 | font-weight: 900;
413 | -webkit-mask-image: none;
414 | overflow: hidden;
415 | padding: 1.2rem 3rem;
416 | position: relative;
417 | text-transform: uppercase;
418 | }
419 | @-webkit-keyframes pulse {
420 | 0% {
421 | box-shadow: 0 0 0 0 #fff;
422 | }
423 | }
424 | @keyframes pulse {
425 | 0% {
426 | box-shadow: 0 0 0 0 #fff;
427 | }
428 | }
--------------------------------------------------------------------------------