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

Signup Here

28 | setUsername(e.target.value)} 33 | /> 34 | setPassword(e.target.value)} 39 | /> 40 | 43 |
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 |
42 | {setUsername(e.target.value)}} placeholder='Name' value = {username} /> 43 | {setNumberRating(e.target.value)}} placeholder='number rating' value = {rating} /> 44 | {setReviewText(e.target.value)}} placeholder='review text' value = {review} /> 45 | 46 |
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 | 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 |
49 |
Log In
50 |
51 | setUsername(e.target.value)} 56 | /> 57 | setPassword(e.target.value)} 62 | /> 63 | 64 |
65 |
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 | 62 | { isClicked && } 63 | 64 | 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 |
65 | setName(e.target.value)} /> 66 | setAddress(e.target.value)} /> 67 | setZipCode(e.target.value)} /> 68 | 78 | 87 | 96 | 105 | 109 | 119 | 123 | 127 | setUrlAddress(e.target.value)} /> 128 | 138 | setAdditional(e.target.value)} /> 139 | 140 |
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 | } --------------------------------------------------------------------------------