├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ ├── deploy-dev.yml │ ├── deploy-prod.yml │ └── integrate.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Dockerfile-dev ├── README.md ├── client ├── .DS_Store ├── assets │ ├── constants.js │ ├── roomier.png │ ├── roomier.svg │ ├── roomier_banner.png │ └── roomier_loading.gif ├── components │ ├── App.jsx │ ├── ApplicationModal.jsx │ ├── Autocomplete.jsx │ ├── ContainerApplication.jsx │ ├── ContainerFeed.jsx │ ├── CreatePost.jsx │ ├── DisplayCard.jsx │ ├── EditCard.jsx │ ├── Home.jsx │ ├── HomeFeed.jsx │ ├── ImageGallery.jsx │ ├── Login.jsx │ ├── NavBar.jsx │ ├── Phrases.jsx │ ├── PostModal.jsx │ ├── Profile.jsx │ ├── ProfileFeed.jsx │ ├── Signup.jsx │ ├── Slider.jsx │ ├── ToggleLightDark.jsx │ ├── charts │ │ └── Scatter.jsx │ ├── context │ │ ├── ColorModeContext.js │ │ └── Context.js │ ├── styles │ │ └── googleMaps.js │ ├── tests │ │ ├── App.test.js │ │ ├── ApplicationModal.test.js │ │ ├── ContainerApplication.test.js │ │ ├── ContainerFeed.test.js │ │ ├── DisplayCard.test.js │ │ ├── EditCard.test.js │ │ ├── EditCardActions.test.js │ │ ├── HomeFeed.test.js │ │ ├── Login.test.js │ │ ├── NavBar.test.js │ │ ├── Phrases.test.js │ │ ├── PostModal.test.js │ │ ├── Profile.test.js │ │ ├── ProfileCardActions.test.js │ │ ├── ProfileFeed.test.js │ │ ├── Signup.test.js │ │ ├── UserCardActions.test.js │ │ └── __mocks__ │ │ │ └── mockContext.js │ ├── utils │ │ ├── SentryRoutes.jsx │ │ └── firebase.js │ └── views │ │ ├── EditCardActions.jsx │ │ ├── ProfileCardActions.jsx │ │ └── UserCardActions.jsx ├── constants │ └── home.js ├── hooks │ ├── posts │ │ ├── useGeocode.js │ │ ├── useImageUpload.js │ │ └── usePosts.js │ └── session │ │ └── useVerifySession.js ├── index.js ├── stores │ ├── app.js │ ├── home.js │ └── post-form.js ├── stylesheets │ ├── card.scss │ ├── containerApplication.scss │ ├── containerFeed.scss │ ├── createPost.scss │ ├── gallery.scss │ ├── globalVariables.scss │ ├── homeFeed.scss │ ├── imageGallery.scss │ ├── login.scss │ ├── navbar.scss │ ├── profileFeed.scss │ └── styles.css └── utils │ ├── isNonNumberString.js │ ├── isNum.js │ ├── isPassword.js │ ├── isUsername.js │ ├── isZipcode.js │ └── logger.js ├── config ├── default.js ├── development.js └── production.js ├── docker-compose-dev.yml ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── server ├── __tests__ │ └── supertest.test.js ├── config │ └── passport.js ├── controllers │ ├── cookie │ │ ├── deleteCookie.js │ │ ├── deleteCookie.test.js │ │ ├── getCookie.js │ │ ├── getCookie.test.js │ │ ├── index.js │ │ ├── setSSIDCookie.js │ │ └── setSSIDCookie.test.js │ ├── metaData │ │ ├── createPost.js │ │ └── index.js │ ├── post │ │ ├── createPost.js │ │ ├── createPost.test.js │ │ ├── deletePost.js │ │ ├── deletePost.test.js │ │ ├── filterPostsByDistance.js │ │ ├── filterPostsByDistance.test.js │ │ ├── getAllPosts.js │ │ ├── getAllPosts.test.js │ │ ├── getProfilePosts.js │ │ ├── getProfilePosts.test.js │ │ ├── index.js │ │ ├── removeImage.js │ │ ├── removeImage.test.js │ │ ├── updateApplicationPost.js │ │ ├── updateApplicationPost.test.js │ │ ├── updatePost.js │ │ └── updatePost.test.js │ ├── session │ │ ├── deleteSession.js │ │ ├── deleteSession.test.js │ │ ├── findSession.js │ │ ├── findSession.test.js │ │ ├── index.js │ │ ├── startSession.js │ │ └── startSession.test.js │ └── user │ │ ├── createUser.js │ │ ├── createUser.test.js │ │ ├── findUser.js │ │ ├── findUser.test.js │ │ ├── index.js │ │ ├── verifyUser.js │ │ └── verifyUser.test.js ├── db │ ├── db.js │ ├── metadata.js │ ├── postModel.js │ ├── sessionModel.js │ └── userModel.js ├── routes │ ├── index.js │ ├── oauth.js │ ├── user.js │ └── user.test.js ├── server.js ├── server.test.js └── util │ └── logger.js └── webpack ├── common.webpack.js ├── development.webpack.js └── production.webpack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "jest/globals": true 7 | }, 8 | "extends": [ 9 | "plugin:react/recommended", 10 | "airbnb", 11 | "prettier/prettier", 12 | "plugin:prettier/recommended" 13 | ], 14 | "overrides": [ 15 | ], 16 | "parserOptions": { 17 | "ecmaVersion": "latest" 18 | }, 19 | "plugins": [ 20 | "import", 21 | "react", 22 | "react-hooks", 23 | "jest", 24 | "prettier" 25 | ], 26 | "rules": { 27 | "react-hooks/rules-of-hooks": "error", 28 | "react-hooks/exhaustive-deps": ["warn", { 29 | "additionalHooks": "useMemo,useEffect,useCallback" 30 | }], 31 | "react/prop-types": 0, 32 | "react/no-array-index-key": 0, 33 | "no-underscore-dangle": 0, 34 | "react/jsx-props-no-spreading": 0, 35 | "import/no-extraneous-dependencies": 0, 36 | "react/jsx-filename-extension": 0, 37 | "consistent-return": 0, 38 | "array-callback-return": 0, 39 | "import/prefer-default-export": 0, 40 | "import/order": [ 41 | "warn", 42 | { 43 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], 44 | "newlines-between": "always" 45 | } 46 | ], 47 | "semi": ["warn", "always"], 48 | "newline-before-return": "warn", 49 | "no-console": "error", 50 | "no-unused-vars": "warn", 51 | "no-undef": "error", 52 | "no-multiple-empty-lines": ["warn", { "max": 2 }], 53 | "indent": ["warn", 2], 54 | "max-len": ["warn", { "code": 120 }], 55 | "no-var": "error", 56 | "prefer-const": "error", 57 | "no-else-return": "warn", 58 | "no-param-reassign": "error", 59 | "no-use-before-define": "error", 60 | "camelcase": "warn", 61 | "react/function-component-definition": 0, 62 | "prettier/prettier": "error" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Roomier on AWS Elastic Beanstalk 2 | 3 | env: 4 | APP_NAME: Roomier-dev 5 | S3_BUCKET: roomier-dev-s3-deploy 6 | AWS_REGION: us-east-1 7 | AWS_PLATFORM: Docker 8 | PIPELINE_ID: ${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER} 9 | 10 | on: 11 | pull_request: 12 | branches: 13 | - test 14 | 15 | jobs: 16 | create_eb_version: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Configure AWS credentials 21 | uses: aws-actions/configure-aws-credentials@v1 22 | with: 23 | aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} 24 | aws-secret-access-key: ${{ secrets.SECRET_ACCESS_KEY }} 25 | aws-region: ${{ env.AWS_REGION }} 26 | - run: | 27 | AWS_VERSION_LABEL=${{env.APP_NAME}}-${{env.PIPELINE_ID}} 28 | 29 | echo "Creating Source Bundle" 30 | zip -r ${{env.APP_NAME}}.zip ./ 31 | S3_KEY="$AWS_VERSION_LABEL.zip" 32 | 33 | echo "Uploading Source Bundle to S3" 34 | aws s3 cp ${{env.APP_NAME}}.zip s3://${{env.S3_BUCKET}}/${S3_KEY} --region ${{env.AWS_REGION}} 35 | 36 | echo "Creating Elastic Beanstalk version" 37 | aws elasticbeanstalk create-application-version --application-name ${{env.APP_NAME}} --version-label $AWS_VERSION_LABEL --region ${{env.AWS_REGION}} --source-bundle S3Bucket=${{env.S3_BUCKET}},S3Key=${S3_KEY} --auto-create-application 38 | 39 | echo "Deploy new ElasticBeanstalk Application Version" 40 | aws elasticbeanstalk update-environment --environment-name Roomierdev-env --version-label $AWS_VERSION_LABEL 41 | 42 | # deploy_aws: 43 | # needs: [create_eb_version] 44 | # runs-on: ubuntu-latest 45 | # steps: 46 | # - uses: actions/checkout@v2 47 | # - name: Set up Python 3.6 (needed for eb cli) 48 | # uses: actions/setup-python@v4 49 | # with: 50 | # python-version: "3.6" 51 | # - name: Configure AWS credentials 52 | # uses: aws-actions/configure-aws-credentials@v1 53 | # with: 54 | # aws-id: ${{ secrets.AWS_ID }} 55 | # aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} 56 | # aws-secret-access-key: ${{ secrets.SECRET_ACCESS_KEY }} 57 | # aws-region: ${{ env.AWS_REGION }} 58 | # - run: | 59 | # AWS_VERSION_LABEL=${{env.APP_NAME}}-${{env.PIPELINE_ID}} 60 | 61 | # echo "Installing Elastic Beanstalk Cli" 62 | # python -m pip install --upgrade pip 63 | # pip install awsebcli --upgrade 64 | # eb --version 65 | 66 | # echo "Deploy init" 67 | # eb init -i ${{env.APP_NAME}} -p ${{env.AWS_PLATFORM}} -k ${{secrets.AWS_ID}} --region ${{env.AWS_REGION}} 68 | # eb deploy ${{env.APP_NAME}} --version ${AWS_VERSION_LABEL} 69 | # echo "Deploy finished" -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Prod Deploy to EB 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - test 7 | 8 | jobs: 9 | prod_deploy: 10 | name: Deploy 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | # Zip file package to deploy to S3 bucket 18 | - name: Create ZIP 19 | # run: git archive -v -o roomier-deploy.zip --format=zip HEAD 20 | run: zip -r roomier-deploy.zip ./ 21 | 22 | # Access AWS using GitHub mounted secrets 23 | - name: Configure AWS Credentials 24 | uses: aws-actions/configure-aws-credentials@v1 25 | with: 26 | aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} 27 | aws-secret-access-key: ${{ secrets.SECRET_ACCESS_KEY }} 28 | aws-region: "us-east-1" 29 | 30 | # Copy zip file contents to S3 bucket 31 | - name: Upload package to S3 bucket 32 | run: aws s3 cp roomier-deploy.zip s3://roomier-s3-deploy/ 33 | 34 | # Deploy to Prod 35 | - name: Create new ElasticBeanstalk Application Version 36 | run: | 37 | aws elasticbeanstalk create-application-version \ 38 | --application-name Roomier \ 39 | --source-bundle S3Bucket="roomier-s3-deploy",S3Key="roomier-deploy.zip" \ 40 | --version-label "ver-${{ github.sha }}" \ 41 | --description "commit-sha-${{ github.sha }}" 42 | 43 | - name: Deploy new ElasticBeanstalk Application Version 44 | run: aws elasticbeanstalk update-environment --environment-name roomier --version-label "ver-${{ github.sha }}" -------------------------------------------------------------------------------- /.github/workflows/integrate.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: ["main", "dev"] 9 | pull_request: 10 | branches: ["main", "dev"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{matrix.os}} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: "npm" 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - name: Running Tests 32 | run: npm run test 33 | 34 | analyze: 35 | name: CodeQL Analysis 36 | runs-on: ubuntu-latest 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | language: 41 | - javascript 42 | 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v2 46 | 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v2 54 | 55 | - name: Perform CodeQL Analysis 56 | uses: github/codeql-action/analyze@v2 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .env 4 | coverage 5 | dist 6 | .DS_Store 7 | roomier.zip 8 | logs 9 | roomier-deploy.zip 10 | 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.validate": ["javascript"], 6 | "javascript.suggest.imports": true, 7 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3000 7 | ENTRYPOINT ["node", "./server/server.js"] -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | WORKDIR /usr/src/app 3 | RUN npm install --save-dev webpack 4 | COPY package*.json /usr/src/app 5 | RUN npm install 6 | EXPOSE 3000 7 | ENTRYPOINT ["node", "./server/server.js"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Contributors 6 | GitHub All Releases 7 | GitHub last commit 8 | GitHub Repo stars 9 | 10 |

11 | 12 | # Overview 13 | 14 | Roomier is a platform designed to connect you with your next roommate. Sign up for an account and begin creating posts to advertise your current or future housing accommodations, or browse the number of open requests for roommates from other users like you. If you find something you like, apply directly in app and other users will be sent your name, email and which location post you're applying to. It's as simple as that! 15 | 16 | # Getting started 17 | 18 | 1. Fork this repo and install dependencies by running command: 19 | 20 | ```sh 21 | npm install 22 | ``` 23 | 24 | 2. To run the application, run commands: 25 | 26 | ```sh 27 | npm run build 28 | ``` 29 | 30 | ```sh 31 | npm run start 32 | ``` 33 | 34 | 3. Navigate to `localhost:3000` to view the application build 35 | 36 | NOTE: Proper environment variables are necessary to run the application properly. These include MongoDB database and Google Client Oauth tokens. 37 | 38 | # Future Work 39 | 40 | The `production` deployment of this repo is hosted [here](https://roomier.onrender.com) and `development` can be accessed [here](https://roomier-dev.onrender.com). 41 | 42 | Currently, the team is hard at work on updating the user interface and including a search functionality for existing posts. We're experimenting with visualizing useful key metrics to help end users make informative decisions. We're also doing research on connecting different user social profiles like Instagram, Facebook, etc. for a more open social experience. 43 | 44 | # Authors 45 | 46 | - **Adithya Chandrashekar** [Github](https://github.com/addychandrashekar) | [Linkedin](https://www.linkedin.com/in/addyc/) 47 | - **Brennan Lee** [Github](https://github.com/blee3395) | [Linkedin](https://www.linkedin.com/in/brennan-lee/) 48 | - **Michael Reyes** [Github](https://github.com/Michaelr499) | [Linkedin](https://www.linkedin.com/in/michael-reyes-b4319216b) 49 | - **Walter Li** [Github](https://github.com/findwalle) | [Linkedin](https://www.linkedin.com/in/li-walter/) 50 | - **Huu (John) Le** [Github](https://github.com/JohnLeGit) | [Linkedin](https://www.linkedin.com/in/huu-le/) 51 | -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabRitz/roomier/525a62b1a5c7fb997bd2d2c844d890f05c3bf11b/client/.DS_Store -------------------------------------------------------------------------------- /client/assets/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | export const DEFAULT_IMAGE = 'https://mindfuldesignconsulting.com/wp-content/uploads/2017/07/Fast-Food-Restaurant-Branding-with-Interior-Design.jpg'; 3 | -------------------------------------------------------------------------------- /client/assets/roomier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabRitz/roomier/525a62b1a5c7fb997bd2d2c844d890f05c3bf11b/client/assets/roomier.png -------------------------------------------------------------------------------- /client/assets/roomier_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabRitz/roomier/525a62b1a5c7fb997bd2d2c844d890f05c3bf11b/client/assets/roomier_banner.png -------------------------------------------------------------------------------- /client/assets/roomier_loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabRitz/roomier/525a62b1a5c7fb997bd2d2c844d890f05c3bf11b/client/assets/roomier_loading.gif -------------------------------------------------------------------------------- /client/components/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import { styled } from '@mui/material/styles'; 3 | import React, { 4 | useMemo, useCallback, useState, useEffect, 5 | } from 'react'; 6 | import { 7 | BrowserRouter as Router, 8 | Route, 9 | } from 'react-router-dom'; 10 | import Alert from '@mui/material/Alert'; 11 | import AlertTitle from '@mui/material/AlertTitle'; 12 | import Stack from '@mui/material/Stack'; 13 | 14 | import { useVerifySession } from '../hooks/session/useVerifySession'; 15 | 16 | import { SentryRoutes } from './utils/SentryRoutes'; 17 | import Login from './Login'; 18 | import Signup from './Signup'; 19 | import Home from './Home'; 20 | import CreatePost from './CreatePost'; 21 | import Profile from './Profile'; 22 | import NavBar from './NavBar'; 23 | import Context from './context/Context'; 24 | 25 | export const App = () => { 26 | const [userInfo, setUserInfo] = useState(''); 27 | const [alert, setAlert] = useState([]); 28 | 29 | const { query: useVerifySessionQuery } = useVerifySession(setAlert); 30 | 31 | const isLoading = useMemo(() => { 32 | return useVerifySessionQuery.isLoading; 33 | }, [useVerifySessionQuery.isLoading]); 34 | 35 | const hasSession = useMemo(() => { 36 | if (isLoading) return; 37 | 38 | return useVerifySessionQuery.data; 39 | }, [isLoading, useVerifySessionQuery.data]); 40 | 41 | const handleDismiss = useCallback((i) => { 42 | setAlert((prev) => [...prev.slice(0, i), ...prev.slice(i + 1)]); 43 | }, []); 44 | 45 | const contextValue = useMemo( 46 | () => ({ userInfo, setUserInfo, setAlert }), 47 | [userInfo, setUserInfo, setAlert], 48 | ); 49 | 50 | useEffect(() => { 51 | if (hasSession) { 52 | return setUserInfo(hasSession); 53 | } 54 | }, [hasSession]); 55 | 56 | return ( 57 | 58 | {isLoading ? ( 59 | 60 | Loading... 65 | 66 | ) : userInfo === '' ? ( 67 | 68 | 69 | } /> 70 | } /> 71 | 72 | 73 | ) : ( 74 | 75 | 76 | 77 | } /> 78 | } /> 79 | } /> 80 | 81 | 82 | )} 83 | 84 | {alert.map((element, i) => ( 85 | handleDismiss(i)} 89 | > 90 | 91 | {element.severity.charAt(0).toUpperCase() 92 | + element.severity.slice(1)} 93 | 94 | {element.message} 95 | 96 | ))} 97 | 98 | 99 | ); 100 | }; 101 | 102 | const LoadingWrapper = styled('div')(() => ({ 103 | background: '#98C1D9', 104 | width: '100vw', 105 | height: '100vh', 106 | display: 'flex', 107 | justifyContent: 'center', 108 | alignContent: 'center', 109 | })); 110 | 111 | const AlertStackWrapper = styled(Stack)(() => ({ 112 | position: 'fixed', 113 | bottom: '12px', 114 | left: '12px', 115 | zIndex: 10, 116 | width: 'auto', 117 | maxWidth: '400px', 118 | })); 119 | 120 | const AlertWrapper = styled(Alert)(() => ({ 121 | boxShadow: '0px 4px 12px rgba(115, 115, 115, 0.5)', 122 | })); 123 | -------------------------------------------------------------------------------- /client/components/ApplicationModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from '@mui/material/Modal'; 3 | import Box from '@mui/material/Box'; 4 | import List from '@mui/material/List'; 5 | import ListItem from '@mui/material/ListItem'; 6 | import Divider from '@mui/material/Divider'; 7 | import ListItemText from '@mui/material/ListItemText'; 8 | import ListItemAvatar from '@mui/material/ListItemAvatar'; 9 | import Avatar from '@mui/material/Avatar'; 10 | import Typography from '@mui/material/Typography'; 11 | 12 | const style = { 13 | position: 'absolute', 14 | top: '50%', 15 | left: '50%', 16 | transform: 'translate(-50%, -50%)', 17 | width: '70%', 18 | maxWidth: '500px', 19 | maxHeight: '50%', 20 | bgcolor: 'background.paper', 21 | boxShadow: 24, 22 | p: 1, 23 | }; 24 | 25 | function ApplicationModal({ applications, closeApps, open }) { 26 | return ( 27 | 34 | 35 | 36 | {applications.map((app, i) => ( 37 | <> 38 | 39 | 40 | 41 | 42 | 51 | {app.username} 52 | 53 | )} 54 | /> 55 | 56 | 57 | 58 | ))} 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default ApplicationModal; 66 | -------------------------------------------------------------------------------- /client/components/Autocomplete.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import PlacesAutocomplete from 'react-places-autocomplete'; 3 | import { styled } from '@mui/material/styles'; 4 | import TextField from '@mui/material/TextField'; 5 | import Tooltip from '@mui/material/Tooltip'; 6 | 7 | 8 | export const Autocomplete = ({ 9 | label, tooltip, value, placeholder, onChange, onSelect, searchOptions, required = false, 10 | }) => { 11 | const labelContent = useMemo(() => { 12 | if (tooltip) { 13 | return ( 14 | 15 | {label} 16 | 17 | ); 18 | } 19 | 20 | return label; 21 | }, [label, tooltip]); 22 | 23 | return ( 24 | 30 | {({ 31 | getInputProps, suggestions, getSuggestionItemProps, loading, 32 | }) => ( 33 | 34 | 46 |
47 | {loading &&
Loading...
} 48 | {suggestions.map((suggestion) => { 49 | const className = suggestion.active ? 'suggestion-item--active' : 'suggestion-item'; 50 | 51 | return ( 52 |
53 | {suggestion.description} 54 |
55 | ); 56 | })} 57 |
58 |
59 | )} 60 |
61 | ); 62 | }; 63 | 64 | const AutocompleteWrapper = styled('div')(() => ({ 65 | position: 'relative', 66 | '.MuiTextField-root': { 67 | marginRight: '4px', 68 | width: '100%', 69 | '.location-search-input': { 70 | fontSize: 14, 71 | }, 72 | }, 73 | '.autocomplete-dropdown-container': { 74 | width: '100%', 75 | position: 'absolute', 76 | zIndex: 5, 77 | '.suggestion-loading': { 78 | fontSize: '12px', 79 | backgroundColor: 'white', 80 | }, 81 | '.suggestion-item--active': { 82 | width: '100%', 83 | backgroundColor: '#e1e4e6', 84 | color: '#293241', 85 | fontWeight: '500', 86 | cursor: 'pointer', 87 | '> span': { 88 | fontSize: '12px', 89 | overflowX: 'hidden', 90 | }, 91 | }, 92 | '.suggestion-item': { 93 | width: '100%', 94 | backgroundColor: '#ffffff', 95 | cursor: 'pointer', 96 | '> span': { 97 | fontSize: '12px', 98 | overflowX: 'hidden', 99 | }, 100 | }, 101 | }, 102 | })); 103 | -------------------------------------------------------------------------------- /client/components/ContainerApplication.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import ContainerFeed from './ContainerFeed'; 4 | import Context from './context/Context'; 5 | 6 | import '../stylesheets/containerApplication.scss'; 7 | 8 | function ContainerApplications({ 9 | getProfilePosts, postInfo, setPostInfo, setEditMode, 10 | }) { 11 | const { setAlert } = useContext(Context); 12 | 13 | const handleUpdate = () => { 14 | setPostInfo(postInfo); 15 | setEditMode(true); 16 | }; 17 | 18 | const handleDelete = async () => { 19 | try { 20 | const response = await fetch(`/posts/${postInfo._id}`, { 21 | method: 'DELETE', 22 | headers: { 'Content-Type': 'application/json' }, 23 | }); 24 | const data = await response.json(); 25 | if (data.deletedCount === 1) { 26 | setAlert((alerts) => [...alerts, { severity: 'success', message: 'Post successfully removed' }]); 27 | getProfilePosts(); 28 | } else setAlert((alerts) => [...alerts, { severity: 'error', message: 'Unable to delete post' }]); 29 | } catch (err) { 30 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in delete post' }]); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | 44 |
45 | ); 46 | } 47 | 48 | export default ContainerApplications; 49 | -------------------------------------------------------------------------------- /client/components/ContainerFeed.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, useEffect, useContext, Suspense, useMemo, 3 | } from 'react'; 4 | import Card from '@mui/material/Card'; 5 | import CardActions from '@mui/material/CardActions'; 6 | import CardContent from '@mui/material/CardContent'; 7 | import CardMedia from '@mui/material/CardMedia'; 8 | import Typography from '@mui/material/Typography'; 9 | import { motion } from 'framer-motion'; 10 | 11 | import { DEFAULT_IMAGE } from '../assets/constants'; 12 | 13 | import Context from './context/Context'; 14 | 15 | import '../stylesheets/containerFeed.scss'; 16 | 17 | const UserCardActions = React.lazy(() => import('./views/UserCardActions')); 18 | const ProfileCardActions = React.lazy(() => import('./views/ProfileCardActions')); 19 | const ApplicationModal = React.lazy(() => import('./ApplicationModal')); 20 | 21 | const ContainerFeed = ({ 22 | post, handleOpen, setPostInfo, view, handleUpdate, handleDelete, setEditMode, 23 | }) => { 24 | const { userInfo, setAlert } = useContext(Context); 25 | const { 26 | address, description, rent, images, applicantData, 27 | } = post; 28 | 29 | const [application, setApplication] = useState(!applicantData ? [] : applicantData); 30 | const [hasApplied, setHasApplied] = useState(false); 31 | 32 | // Profile view applications 33 | const [open, setOpen] = useState(false); 34 | const openApps = () => setOpen(true); 35 | const closeApps = () => setOpen(false); 36 | 37 | const displayImage = useMemo(() => { 38 | if (!images[0]) return DEFAULT_IMAGE; 39 | 40 | if (images[0].imgUrl === undefined) { 41 | return Object.keys(images[0])[0]; 42 | } 43 | 44 | return images[0].imgUrl; 45 | }, [images]); 46 | 47 | const handleApply = async (e) => { 48 | try { 49 | const reqBody = { 50 | firstName: userInfo.firstName, 51 | lastName: userInfo.lastName, 52 | username: userInfo.username, 53 | }; 54 | const response = await fetch(`/home/${post._id}`, { 55 | method: 'PATCH', 56 | headers: { 'Content-Type': 'application/json' }, 57 | body: JSON.stringify(reqBody), 58 | }); 59 | const data = await response.json(); 60 | if (data) { // only update if patch request is true/not error 61 | setApplication([...application, reqBody]); 62 | setHasApplied(true); 63 | setAlert((alerts) => [...alerts, { severity: 'success', message: 'Successfully submitted application' }]); 64 | } 65 | } catch (err) { 66 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in applying to post' }]); 67 | } 68 | }; 69 | 70 | const handleClick = () => { 71 | setPostInfo(post); 72 | if (view === 'profile') { 73 | setEditMode(false); 74 | } else if (view === 'user') { 75 | handleOpen(); 76 | } 77 | }; 78 | 79 | useEffect(() => { 80 | setApplication(applicantData); 81 | }, [applicantData]); 82 | 83 | useEffect(() => { 84 | if (userInfo !== '') { 85 | const applied = applicantData.reduce((acc, curr) => { 86 | return (curr.username === userInfo.username); 87 | }, false); 88 | 89 | return setHasApplied(applied); 90 | } 91 | }, [applicantData, application, userInfo]); 92 | 93 | return ( 94 | 104 | 105 | 106 | 110 | 111 | {`$${rent}/mo`} 112 | 113 | 114 | {`${address.street1} ${address.street2}`} 115 | 116 | 117 | {`${description.BR}BR | ${description.BA}BA | ${description.sqFt} sqft`} 118 | 119 | 120 | Loading...}> 121 | {(view === 'user') 122 | ? ( 123 | 124 | 129 | 130 | ) 131 | : ( 132 | 133 | 139 | 140 | )} 141 | 142 | 143 | Loading...}> 144 | {(view === 'profile') && } 145 | 146 | 147 | ); 148 | }; 149 | 150 | export default ContainerFeed; 151 | -------------------------------------------------------------------------------- /client/components/DisplayCard.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import React, { useState, useEffect, useContext } from 'react'; 3 | import { useTheme } from '@mui/material/styles'; 4 | import { GoogleMap, Marker } from '@react-google-maps/api'; 5 | 6 | import CardActions from '@mui/material/CardActions'; 7 | import Paper from '@mui/material/Paper'; 8 | import CardMedia from '@mui/material/CardMedia'; 9 | import IconButton from '@mui/material/IconButton'; 10 | import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; 11 | import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; 12 | import Tooltip from '@mui/material/Tooltip'; 13 | import Button from '@mui/material/Button'; 14 | import Typography from '@mui/material/Typography'; 15 | 16 | import Context from './context/Context'; 17 | import { mapDisplayCardStyle, darkModeStyle, lightModeStyle } from './styles/googleMaps'; 18 | 19 | const defaultImg = 'https://mindfuldesignconsulting.com/wp-content/uploads/2017/07/Fast-Food-Restaurant-Branding-with-Interior-Design.jpg'; 20 | 21 | function DisplayCard({ postInfo }) { 22 | const { 23 | address, roommate, description, moveInDate, utilities, 24 | rent, bio, images, geoData, applicantData, 25 | } = postInfo; 26 | 27 | const theme = useTheme(); 28 | const { userInfo, setAlert } = useContext(Context); 29 | 30 | const [index, setIndex] = useState(0); // Index for gallery image 31 | const [hasApplied, setHasApplied] = useState(false); 32 | 33 | const handleClick = (dir) => { 34 | if (index + dir < 0) setIndex(images.length - 1); 35 | else if (index + dir > images.length - 1) setIndex(0); 36 | else setIndex(index + dir); 37 | }; 38 | 39 | const handleApply = async () => { 40 | try { 41 | const reqBody = { 42 | firstName: userInfo.firstName, 43 | lastName: userInfo.lastName, 44 | username: userInfo.username, 45 | }; 46 | const response = await fetch(`/home/${postInfo._id}`, { 47 | method: 'PATCH', 48 | headers: { 'Content-Type': 'application/json' }, 49 | body: JSON.stringify(reqBody), 50 | }); 51 | await response.json(); 52 | setHasApplied(true); 53 | } catch (err) { 54 | console.log('Error applying to post: ', err); 55 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in applying to post' }]); 56 | } 57 | }; 58 | 59 | useEffect(() => { 60 | if (userInfo !== '') { 61 | let applied = false; 62 | applicantData.map((applicant) => { 63 | if (applicant.username === userInfo.username) applied = true; 64 | }); 65 | setHasApplied(applied); 66 | } 67 | }, []); 68 | 69 | return ( 70 |
74 |
78 | 84 | 91 | 92 | handleClick(-1)}> 93 | 94 | 95 | { hasApplied 96 | ? ( 97 | 102 | ) 103 | : ( 104 | 105 | 110 | 111 | )} 112 | handleClick(1)}> 113 | 114 | 115 | 116 | 117 | 123 | 124 | {`${address.street1} ${address.street2}`} 125 | 126 | 127 | {`${address.city}, ${address.state} ${address.zipCode}`} 128 | 129 | 130 | {`$${rent}/mo`} 131 | 132 | 133 | {`${description.BR}BR | ${description.BA}BA | ${description.sqFt} sqft`} 134 | 135 | 136 | {`Available: ${new Date(moveInDate).toLocaleDateString()}`} 137 | 138 | 139 | {`Looking for: ${roommate.gender}`} 140 | 141 | 153 | {bio} 154 | 155 | 156 |
157 | 167 | 168 | 169 |
170 | ); 171 | } 172 | 173 | export default DisplayCard; 174 | -------------------------------------------------------------------------------- /client/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, useState, useEffect, useContext, useMemo, 3 | } from 'react'; 4 | import { useTheme } from '@mui/material/styles'; 5 | import { Circle, GoogleMap, Marker } from '@react-google-maps/api'; 6 | import Geocode from 'react-geocode'; 7 | 8 | import { homeStore } from '../stores/home'; 9 | import { useGeocode } from '../hooks/posts/useGeocode'; 10 | import { usePosts } from '../hooks/posts/usePosts'; 11 | 12 | import HomeFeed from './HomeFeed'; 13 | import Context from './context/Context'; 14 | import { 15 | mapContainerStyle, darkModeStyle, darkDotStyle, darkBoundaryStyle, 16 | lightBoundaryStyle, lightModeStyle, lightDotStyle, 17 | } from './styles/googleMaps'; 18 | 19 | const GoogleMapsAPIKey = 'AIzaSyAdo3_P6D0eBnk6Xj6fmQ4b1pO-HHvEfOM'; 20 | Geocode.setApiKey(GoogleMapsAPIKey); 21 | 22 | const Home = () => { 23 | const { 24 | center, 25 | distance, 26 | priceRange, 27 | sqftRange, 28 | br, 29 | ba, 30 | filters, 31 | setCenter, 32 | } = homeStore((state) => ({ 33 | center: state.center, 34 | distance: state.distance, 35 | priceRange: state.priceRange, 36 | sqftRange: state.sqftRange, 37 | br: state.br, 38 | ba: state.ba, 39 | filters: state.filters, 40 | setCenter: state.setCenter, 41 | })); 42 | 43 | const theme = useTheme(); 44 | const { userInfo, setAlert } = useContext(Context); 45 | 46 | // Google Maps 47 | const [mapref, setMapRef] = useState(null); 48 | const handleOnLoad = (map) => { setMapRef(map); }; 49 | const handleCenterChanged = () => { 50 | if (mapref) { 51 | const newCenter = mapref.getCenter(); 52 | const lat = newCenter.lat(); 53 | const lng = newCenter.lng(); 54 | 55 | setCenter({ lat, lng }); 56 | } 57 | }; 58 | 59 | const { query: useGeocodeQuery } = useGeocode(setAlert); 60 | const isGeocodeLoading = useMemo(() => useGeocodeQuery.isLoading, [useGeocodeQuery.isLoading]); 61 | const geocode = useMemo(() => { 62 | if (isGeocodeLoading) return { lat: 0, lng: 0 }; 63 | 64 | return useGeocodeQuery.data; 65 | }, [isGeocodeLoading, useGeocodeQuery.data]); 66 | 67 | useEffect(() => { 68 | setCenter(geocode); 69 | }, [geocode, setCenter]); 70 | 71 | const { query: usePostsQuery } = usePosts(userInfo, setAlert); 72 | const isPostsLoading = useMemo(() => usePostsQuery.isLoading, [usePostsQuery.isLoading]); 73 | const posts = useMemo(() => { 74 | if (isPostsLoading) return []; 75 | 76 | return usePostsQuery.data ?? []; 77 | }, [isPostsLoading, usePostsQuery.data]); 78 | 79 | const isLoading = useMemo(() => isGeocodeLoading || isPostsLoading, [isGeocodeLoading, isPostsLoading]); 80 | 81 | // Lambda function to determine if values meet range or min criteria 82 | const isInRange = useCallback((value, range) => { 83 | if (Array.isArray(range)) { 84 | return value >= range[0] && value <= range[1]; 85 | } 86 | 87 | return value >= range; 88 | }, []); 89 | 90 | // Filter posts based on user criteria 91 | const filterPosts = useMemo(() => { 92 | if (posts.length === 0) return []; 93 | 94 | const results = posts.filter((post) => { 95 | if (isInRange(post.rent, priceRange) 96 | && isInRange(post.description.sqFt, sqftRange) 97 | && isInRange(post.description.BR, br) 98 | && isInRange(post.description.BA, ba)) { 99 | if (filters.length !== 0) { 100 | filters.map((filter) => { 101 | if (post.description[filter.toLowerCase()]) { 102 | return true; 103 | } 104 | }); 105 | } else { 106 | return true; 107 | } 108 | } else { 109 | return false; 110 | } 111 | }); 112 | 113 | return results; 114 | }, [posts, isInRange, priceRange, sqftRange, br, ba, filters]); 115 | 116 | // Convert posts to markers on Maps 117 | const markers = useMemo(() => { 118 | if (filterPosts.length === 0) return []; 119 | 120 | return filterPosts.map((post, i) => { 121 | if (!post.geoData) return; 122 | 123 | const posObj = { 124 | lng: post.geoData.coordinates[0], 125 | lat: post.geoData.coordinates[1], 126 | }; 127 | 128 | return ; 129 | }); 130 | }, [filterPosts]); 131 | 132 | return ( 133 |
134 |
135 | 148 | 152 | 160 | {markers} 161 | 162 |
163 | 164 |
165 | ); 166 | }; 167 | 168 | export default Home; 169 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import Context from './context/Context'; 5 | import Phrases from './Phrases'; 6 | import { error, info } from '../utils/logger'; 7 | 8 | import '../stylesheets/login.scss'; 9 | 10 | function Login() { 11 | const { setUserInfo, setAlert } = useContext(Context); 12 | 13 | const handleLogin = async () => { 14 | const reqBody = { 15 | username: document.getElementById('username').value, 16 | password: document.getElementById('password').value, 17 | }; 18 | try { 19 | const res = await fetch('/', { 20 | method: 'POST', 21 | headers: { 'Content-Type': 'application/json' }, 22 | body: JSON.stringify(reqBody), 23 | }); 24 | 25 | if (res.status === 400) return setAlert((alerts) => [...alerts, { severity: 'warning', message: 'Enter a correct username and/or password' }]); 26 | if (res.status === 500) return setAlert((alerts) => [...alerts, { severity: 'error', message: 'Uh oh... the server is currently down :(' }]); 27 | if (res.status === 200) { 28 | const data = await res.json(); 29 | if (data === null || data.err) return setAlert((alerts) => [...alerts, { severity: 'warning', message: 'Cannot find user with that username and password' }]); 30 | 31 | info(`Successfully logged in: ${data.username}`); 32 | setUserInfo(data); 33 | document.getElementById('username').value = ''; 34 | document.getElementById('password').value = ''; 35 | return setAlert((alerts) => [...alerts, { severity: 'success', message: 'Good job! Welcome to Roomier :)' }]); 36 | } 37 | } catch (err) { 38 | error(err); 39 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in attempting to login' }]); 40 | } 41 | }; 42 | 43 | return ( 44 |
45 |
46 | Roomier Logo 47 |

a LabRitz thing

48 |
looking for a Zillow corporate sponsorship
49 |
50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 | Google Login 58 | 59 | 60 | 61 | 62 | Sign up 63 |
64 |
65 | ); 66 | } 67 | 68 | export default Login; 69 | -------------------------------------------------------------------------------- /client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useTheme } from '@mui/material/styles'; 4 | 5 | import Brightness4Icon from '@mui/icons-material/Brightness4'; 6 | import NightlightIcon from '@mui/icons-material/Nightlight'; 7 | import SpeedDial from '@mui/material/SpeedDial'; 8 | import SpeedDialAction from '@mui/material/SpeedDialAction'; 9 | import AppBar from '@mui/material/AppBar'; 10 | import Box from '@mui/material/Box'; 11 | import Toolbar from '@mui/material/Toolbar'; 12 | import IconButton from '@mui/material/IconButton'; 13 | import Typography from '@mui/material/Typography'; 14 | import Menu from '@mui/material/Menu'; 15 | import MenuIcon from '@mui/icons-material/Menu'; 16 | import Container from '@mui/material/Container'; 17 | import Avatar from '@mui/material/Avatar'; 18 | import Tooltip from '@mui/material/Tooltip'; 19 | import MenuItem from '@mui/material/MenuItem'; 20 | import Fab from '@mui/material/Fab'; 21 | import EditIcon from '@mui/icons-material/Edit'; 22 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 23 | import LogoutIcon from '@mui/icons-material/Logout'; 24 | 25 | import Context from './context/Context'; 26 | import ColorModeContext from './context/ColorModeContext'; 27 | import Phrases from './Phrases'; 28 | import '../stylesheets/navbar.scss'; 29 | 30 | const pages = [ 31 | { title: 'Home', nav: '/' }, 32 | { title: 'Create Post', nav: '/createPost' }, 33 | ]; 34 | 35 | function NavBar() { 36 | const theme = useTheme(); 37 | const navigate = useNavigate(); 38 | const { userInfo, setUserInfo, setAlert } = useContext(Context); 39 | const { toggleColorMode } = useContext(ColorModeContext); 40 | 41 | const [anchorElNav, setAnchorElNav] = useState(null); 42 | const [anchorElUser, setAnchorElUser] = useState(false); 43 | 44 | const handleOpenNavMenu = (event) => { 45 | setAnchorElNav(event.currentTarget); 46 | }; 47 | 48 | const toggleUserMenu = () => { 49 | setAnchorElUser(!anchorElUser); 50 | }; 51 | 52 | const handleCloseNavMenu = (page) => { 53 | setAnchorElNav(null); 54 | if (page.nav) navigate(page.nav); 55 | }; 56 | 57 | const handleCloseUserMenu = (setting) => { 58 | setAnchorElUser(null); 59 | if (setting.action) setting.action(); 60 | }; 61 | 62 | const handleSignout = async () => { 63 | try { 64 | const res = await fetch('/signout'); 65 | if (res.status === 204) { 66 | setUserInfo(''); 67 | navigate('/'); 68 | } 69 | } catch (err) { 70 | console.log('ERROR: Cannot sign user out'); 71 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error occurred attempting to sign user out' }]); 72 | } 73 | }; 74 | 75 | const settings = [ 76 | { 77 | icon: , 78 | name: 'Profile', 79 | action: () => navigate('/profile'), 80 | }, 81 | { 82 | icon: (theme.palette.mode === 'dark') ? : , 83 | name: (theme.palette.mode === 'dark') ? 'Light mode' : 'Dark mode', 84 | action: () => { 85 | toggleColorMode(); 86 | setAnchorElNav(null); 87 | }, 88 | }, 89 | { 90 | icon: , 91 | name: 'Signout', 92 | action: () => handleSignout(), 93 | }, 94 | ]; 95 | 96 | return ( 97 | <> 98 | 99 | 100 | 101 | navigate('/')} 104 | alt={userInfo.firstName} 105 | sx={{ 106 | width: 112, 107 | bgcolor: 'transparent', 108 | display: { xs: 'none', sm: 'none', md: 'flex' }, 109 | mr: 1, 110 | }} 111 | variant="rounded" 112 | > 113 | Roomier logo 114 | 115 | 116 | 124 | 125 | 126 | 144 | {pages.map((page) => ( 145 | handleCloseNavMenu(page)}> 146 | {page.title} 147 | 148 | ))} 149 | 150 | 151 | 152 | navigate('/')} alt={userInfo.firstName} sx={{ width: 112, bgcolor: 'transparent' }} variant="rounded"> 153 | Roomier logo 154 | 155 | 156 | 157 | 158 | 159 | 160 | 165 | 166 | 167 | 168 | 169 | )} 170 | onClick={toggleUserMenu} 171 | open={anchorElUser} 172 | direction="down" 173 | > 174 | {settings.map((setting, i) => ( 175 | handleCloseUserMenu(setting)} 181 | /> 182 | ))} 183 | 184 | 185 | 186 | 187 | 188 | navigate('/createPost')}> 189 | 190 | 191 | 192 | ); 193 | } 194 | 195 | export default NavBar; 196 | -------------------------------------------------------------------------------- /client/components/Phrases.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Typography from '@mui/material/Typography'; 3 | 4 | function Phrases() { 5 | const [phrase, setPhrase] = useState('roommate'); 6 | const phrases = ['roommate', 'future', 'life', 'friend', 'bed']; 7 | 8 | useEffect(() => { 9 | const index = phrases.indexOf(phrase); 10 | const newPhrase = (index === phrases.length - 1) ? phrases[0] : phrases[index + 1]; 11 | setTimeout(() => { 12 | setPhrase(newPhrase); 13 | }, 5000); 14 | }, [phrase]); 15 | 16 | return ( 17 | 21 | find a 22 | {' '} 23 | {phrase} 24 | ... 25 | 26 | ); 27 | } 28 | 29 | export default Phrases; 30 | -------------------------------------------------------------------------------- /client/components/PostModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from '@mui/material/Modal'; 3 | import Box from '@mui/material/Box'; 4 | 5 | import DisplayCard from './DisplayCard'; 6 | 7 | const style = { 8 | position: 'absolute', 9 | top: '50%', 10 | left: '50%', 11 | transform: 'translate(-50%, -50%)', 12 | width: '70%', 13 | minWidth: '500px', 14 | height: '60%', 15 | bgcolor: 'background.paper', 16 | boxShadow: 24, 17 | p: 1, 18 | borderRadius: '4px', 19 | }; 20 | 21 | function PostModal({ postInfo, open, handleClose }) { 22 | return ( 23 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export default PostModal; 38 | -------------------------------------------------------------------------------- /client/components/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | 3 | import Skeleton from '@mui/material/Skeleton'; 4 | 5 | import ProfileFeed from './ProfileFeed'; 6 | import Context from './context/Context'; 7 | 8 | function Profile() { 9 | const { userInfo, setAlert } = useContext(Context); 10 | 11 | const [posts, setPosts] = useState([]); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const getProfilePosts = async () => { 15 | try { 16 | setIsLoading(true); 17 | const res = await fetch(`/profile/${userInfo.username}`); 18 | const data = await res.json(); 19 | if (data.length === 0) { 20 | setAlert((alerts) => [...alerts, { severity: 'info', message: 'There are no posts to display' }]); 21 | } 22 | setPosts(data); 23 | setIsLoading(false); 24 | } catch (err) { 25 | console.log('ERROR: Cannot get profile posts', err); 26 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in attempting to get user posts' }]); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | getProfilePosts(); 32 | }, []); 33 | 34 | return (isLoading 35 | ? ( 36 |
51 |
61 | 62 |
63 |
64 |
65 | {Array(4).fill(0).map((post, i) => ( 66 |
67 | 68 |
72 |
73 | 74 | 75 |
79 | 80 | 81 |
82 |
83 |
84 |
85 | ))} 86 |
87 |
88 |
89 | ) 90 | : ( 91 |
92 | 93 |
94 | ) 95 | ); 96 | } 97 | 98 | export default Profile; 99 | -------------------------------------------------------------------------------- /client/components/ProfileFeed.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import Paper from '@mui/material/Paper'; 4 | 5 | import DisplayCard from './DisplayCard'; 6 | import ContainerApplication from './ContainerApplication'; 7 | import EditCard from './EditCard'; 8 | 9 | import '../stylesheets/profileFeed.scss'; 10 | 11 | const style = { 12 | marginTop: '70px', 13 | height: `${parseInt(window.innerHeight, 10) - 70}px`, 14 | paddingLeft: '12px', 15 | paddingRight: '12px', 16 | display: 'grid', 17 | gridTemplateColumns: '1fr', 18 | gridTemplateRows: '55% 45%', 19 | rowGap: '12px', 20 | justifyItems: 'center', 21 | alignItems: 'center', 22 | }; 23 | 24 | function ProfileFeed({ posts, getProfilePosts }) { 25 | const [editMode, setEditMode] = useState(false); 26 | const [postInfo, setPostInfo] = useState({ 27 | address: '', 28 | roommate: { 29 | gender: '', 30 | }, 31 | description: { 32 | condition: '', 33 | BA: 0, 34 | BR: 0, 35 | sqFt: 0, 36 | pets: false, 37 | smoking: false, 38 | parking: false, 39 | }, 40 | moveInDate: '', 41 | utilities: '', 42 | rent: '', 43 | bio: '', 44 | images: { key: 'value' }, 45 | applicantData: [], 46 | }); 47 | 48 | useEffect(() => { 49 | if (posts.length > 0) setPostInfo(posts[0]); 50 | }, [posts]); 51 | 52 | return ( 53 |
54 | 66 | {(editMode) ? : } 67 | 68 |
69 | {posts.map((post, i) => ( 70 | 77 | ))} 78 |
79 |
80 | ); 81 | } 82 | 83 | export default ProfileFeed; 84 | -------------------------------------------------------------------------------- /client/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import TextField from '@mui/material/TextField'; 5 | import Button from '@mui/material/Button'; 6 | import FormControl from '@mui/material/FormControl'; 7 | 8 | import Context from './context/Context'; 9 | import Phrases from './Phrases'; 10 | import '../stylesheets/login.scss'; 11 | import { isPassword } from '../utils/isPassword'; 12 | import { isUsername } from '../utils/isUsername'; 13 | import { isNonNumberString } from '../utils/isNonNumberString'; 14 | import { isZipcode } from '../utils/isZipcode'; 15 | import { styled } from '@mui/material'; 16 | 17 | const Signup = () => { 18 | const navigate = useNavigate(); 19 | const { setAlert } = useContext(Context); 20 | 21 | const [firstName, setFirstName] = useState(''); 22 | const [lastName, setLastName] = useState(''); 23 | const [username, setUsername] = useState(''); 24 | const [password, setPassword] = useState(''); 25 | const [zipcode, setZipcode] = useState(''); 26 | 27 | const handleSignUp = async (e) => { 28 | e.preventDefault(); 29 | 30 | if (firstName.length === 0 || !isNonNumberString(firstName) 31 | || lastName.length === 0 || !isNonNumberString(lastName) 32 | || !isUsername(username) || !isPassword(password) 33 | || !isZipcode(zipcode)) { 34 | return setAlert((alerts) => [...alerts, { severity: 'warn', message: 'Please fill out the required fields' }]); 35 | } 36 | 37 | try { 38 | const reqBody = { 39 | firstName, 40 | lastName, 41 | username, 42 | password, 43 | zipCode: zipcode, 44 | }; 45 | 46 | const res = await fetch('/signup', { 47 | method: 'POST', 48 | headers: { 'Content-Type': 'application/json' }, 49 | body: JSON.stringify(reqBody), 50 | }); 51 | const data = await res.json(); 52 | if (data !== null) { 53 | navigate('/'); 54 | } else { 55 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'User already exists in the database' }]); 56 | } 57 | } catch (err) { 58 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error occurred while creating new user' }]); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 |
65 | Roomier Logo 66 |

a LabRitz thing

67 |
looking for a Zillow corporate sponsorship
68 |
69 |
70 | 71 | 72 | setFirstName(e.target.value)} 80 | /> 81 | setLastName(e.target.value)} 89 | /> 90 | setUsername(e.target.value)} 98 | /> 99 | { setPassword(e.target.value); }} 107 | /> 108 | setZipcode(e.target.value)} 116 | /> 117 | 118 | 119 | Signup 120 | navigate('/')}>Back 121 | 122 |
123 |
124 | ); 125 | } 126 | 127 | export default Signup; 128 | 129 | const FormWrapper = styled(FormControl)(() => ({ 130 | display: 'flex', 131 | flexDirection: 'column', 132 | alignItems: 'center', 133 | margin: '4px', 134 | })) 135 | 136 | const TextFieldWrapper = styled(TextField)(() => ({ 137 | margin: '4px', 138 | 'label': { 139 | fontSize: '14px', 140 | }, 141 | 'input': { 142 | fontSize: '14px', 143 | }, 144 | })) 145 | 146 | const ButtonWrapper = styled(Button)(() => ({ 147 | margin: '4px', 148 | width: '100px', 149 | })) 150 | -------------------------------------------------------------------------------- /client/components/Slider.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | import Slider from '@mui/material/Slider'; 3 | 4 | const PrettoSlider = styled(Slider)({ 5 | height: 6, 6 | '& .MuiSlider-track': { 7 | border: 'none', 8 | }, 9 | '& .MuiSlider-thumb': { 10 | height: 18, 11 | width: 18, 12 | backgroundColor: '#fff', 13 | border: '2px solid currentColor', 14 | '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': { 15 | boxShadow: 'inherit', 16 | }, 17 | '&:before': { 18 | display: 'none', 19 | }, 20 | }, 21 | '& .MuiSlider-valueLabel': { 22 | lineHeight: 1.2, 23 | fontSize: 12, 24 | background: 'unset', 25 | padding: 0, 26 | width: 32, 27 | height: 32, 28 | borderRadius: '50% 50% 50% 0', 29 | backgroundColor: '#3D5A80', 30 | transformOrigin: 'bottom left', 31 | transform: 'translate(50%, -80%) rotate(-45deg) scale(0)', 32 | '&:before': { display: 'none' }, 33 | '&.MuiSlider-valueLabelOpen': { 34 | transform: 'translate(50%, -80%) rotate(-45deg) scale(1)', 35 | }, 36 | '& > *': { 37 | transform: 'rotate(45deg)', 38 | }, 39 | }, 40 | }); 41 | 42 | export default PrettoSlider; 43 | -------------------------------------------------------------------------------- /client/components/ToggleLightDark.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | 5 | import { App } from './App'; 6 | import ColorModeContext from './context/ColorModeContext'; 7 | 8 | const queryClient = new QueryClient(); 9 | 10 | export const ToggleLightDark = () => { 11 | const [mode, setMode] = useState('light'); 12 | const colorMode = useMemo(() => ({ 13 | toggleColorMode: () => { 14 | setMode((prevMode) => { 15 | document.body.style.background = (prevMode === 'light') 16 | ? '#293241' 17 | : 'linear-gradient(180deg,rgb(214, 214, 214) 85%,rgba(169, 169, 169, 0.903)95%, rgba(140, 140, 140, 0.937) 98%, rgba(0, 25, 46, 0.585))'; 18 | 19 | return (prevMode === 'light' ? 'dark' : 'light'); 20 | }); 21 | }, 22 | }), []); 23 | 24 | const getDesignTokens = (version) => ({ 25 | palette: { 26 | mode: version, 27 | ...(version === 'light' 28 | ? { 29 | primary: { 30 | main: '#3D5A80', 31 | }, 32 | secondary: { 33 | main: '#fff', 34 | }, 35 | background: { 36 | default: '#3D5A80', 37 | paper: '#fff', 38 | }, 39 | text: { 40 | primary: '#293241', 41 | secondary: '#3D5A80', 42 | ternary: '#5f6266', 43 | darkBlue: '#293241', 44 | midBlue: '#3D5A80', 45 | lightBlue: '#98C1D9', 46 | grey: '#7a7a7a', 47 | }, 48 | } 49 | : { 50 | primary: { 51 | main: '#3D5A80', 52 | }, 53 | secondary: { 54 | main: '#fff', 55 | }, 56 | background: { 57 | default: '#293241', 58 | paper: '#3D5A80', 59 | }, 60 | text: { 61 | primary: '#e8e6e6', 62 | secondary: '#E0FBFC', 63 | ternary: '#98C1D9', 64 | darkBlue: '#293241', 65 | midBlue: '#3D5A80', 66 | lightBlue: '#98C1D9', 67 | }, 68 | }), 69 | }, 70 | typography: { 71 | fontFamily: 'DM Sans', 72 | }, 73 | }); 74 | 75 | const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]); 76 | 77 | return ( 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /client/components/context/ColorModeContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ColorModeContext = React.createContext({ toggleColorMode: () => {} }); 4 | 5 | export default ColorModeContext; 6 | -------------------------------------------------------------------------------- /client/components/context/Context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Context = React.createContext(); 4 | 5 | export default Context; 6 | -------------------------------------------------------------------------------- /client/components/tests/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from '../App.jsx'; 4 | 5 | jest.mock('../Login.jsx', () => function () { 6 | return
Login
; 7 | }); 8 | 9 | jest.mock('../Signup.jsx', () => function () { 10 | return
Signup
; 11 | }); 12 | 13 | jest.mock('../Home.jsx', () => function () { 14 | return
Home
; 15 | }); 16 | 17 | jest.mock('../CreatePost.jsx', () => function () { 18 | return
CreatePost
; 19 | }); 20 | 21 | jest.mock('../Profile.jsx', () => function () { 22 | return
Profile
; 23 | }); 24 | 25 | jest.mock('../NavBar.jsx', () => function () { 26 | return
NavBar
; 27 | }); 28 | 29 | describe('App.jsx', () => { 30 | let originalFetch; 31 | 32 | beforeEach(() => { 33 | originalFetch = global.fetch; 34 | global.fetch = jest.fn(() => Promise.resolve({ 35 | // json: () => Promise.resolve({ 36 | // _id: new ObjectId("123), 37 | // firstName: 'Johhn', 38 | // lastName: 'Smith', 39 | // username: 'jsmith@gmail.com', 40 | // password: '$HG8ujfsfghds', 41 | // zipCode: '10000', 42 | // __v: 0 43 | // }) 44 | json: () => Promise.resolve(false), 45 | })); 46 | }); 47 | 48 | it('renders Login component when no user session', async () => { 49 | render(); 50 | expect(await screen.getByTestId('Login')).toBeTruthy(); 51 | }); 52 | 53 | xit('renders Home, CreatePost, and Profile components with NavBar when user has session', async () => { 54 | render(); 55 | 56 | expect(await screen.getByTestId('Home')).toBeTruthy(); 57 | expect(await screen.getByTestId('CreatePost')).toBeTruthy(); 58 | expect(await screen.getByTestId('Profile')).toBeTruthy(); 59 | expect(await screen.getByTestId('NavBar')).toBeTruthy(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /client/components/tests/ApplicationModal.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, fireEvent } from "@testing-library/react"; 3 | import ApplicationModal from "../ApplicationModal.jsx"; 4 | 5 | const applications = [ 6 | { 7 | firstName: 'John', 8 | lastName: 'Smith', 9 | username: 'test@gmail.com' 10 | }, 11 | { 12 | firstName: 'Jimmy', 13 | lastName: 'Johns', 14 | username: 'jj@gmail.com' 15 | }, 16 | { 17 | firstName: 'Jenga', 18 | lastName: 'Manual', 19 | username: 'jm@gmail.com' 20 | } 21 | ] 22 | 23 | describe("ApplicationModal.jsx", () => { 24 | 25 | it("Renders open ApplicationModal", async () => { 26 | render(); 27 | 28 | expect(await screen.getByText("John Smith")).toBeTruthy(); 29 | expect(await screen.getByText("test@gmail.com")).toBeTruthy(); 30 | }); 31 | 32 | it("Renders open ApplicationModal", async () => { 33 | render(); 34 | 35 | expect(await screen.getAllByTestId("listItemModal").length).toEqual(3); 36 | expect(await screen.getAllByTestId("dividerModal").length).toEqual(3); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /client/components/tests/ContainerApplication.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { screen } from '@testing-library/react'; 3 | import ContainerApplication from '../ContainerApplication'; 4 | import mockContext from './__mocks__/mockContext'; 5 | 6 | jest.mock('../ContainerFeed.jsx', () => function () { 7 | return
; 8 | }); 9 | 10 | const userInfo = { 11 | firstName: 'John', 12 | lastName: 'Smith', 13 | username: 'test@gmail.com', 14 | }; 15 | 16 | describe('ContainerApplication.jsx', () => { 17 | let providerProps; 18 | beforeEach(() => { 19 | providerProps = { 20 | userInfo, 21 | }; 22 | }); 23 | 24 | it('Renders ContainerApplication component', async () => { 25 | mockContext(, { providerProps }); 26 | 27 | expect(await screen.getByTestId('ContainerFeed')).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /client/components/tests/ContainerFeed.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { screen, waitFor } from '@testing-library/react'; 4 | 5 | import mockContext from './__mocks__/mockContext.js'; 6 | import ContainerFeed from '../ContainerFeed.jsx'; 7 | 8 | jest.mock('../views/UserCardActions.jsx', () => function () { 9 | return
; 10 | }); 11 | 12 | jest.mock('../views/ProfileCardActions.jsx', () => function () { 13 | return
; 14 | }); 15 | 16 | const userInfo = { 17 | firstName: 'John', 18 | lastName: 'Smith', 19 | username: 'test@gmail.com', 20 | }; 21 | 22 | const postInfo = { 23 | address: { 24 | street1: 'Street 1', 25 | street2: 'Street 2', 26 | city: 'City', 27 | state: 'State', 28 | zipCode: '12345', 29 | }, 30 | roommate: { gender: 'male' }, 31 | description: { 32 | BR: 1, 33 | BA: 1, 34 | sqFt: 100, 35 | parking: true, 36 | smoking: true, 37 | pets: true, 38 | }, 39 | moveInDate: Date.now(), 40 | rent: 100, 41 | bio: 'Description', 42 | images: { 43 | imgUrl: '', 44 | imgPath: '', 45 | }, 46 | applicantData: [], 47 | currUser: { 48 | firstName: 'John', 49 | lastName: 'Smith', 50 | username: 'test@gmail.com', 51 | }, 52 | }; 53 | 54 | describe('ContainerFeed.jsx', () => { 55 | let providerProps; 56 | beforeEach(() => { 57 | providerProps = { 58 | userInfo, 59 | }; 60 | }); 61 | 62 | it('Renders ContainerFeed component', () => { 63 | mockContext(, { providerProps }); 64 | 65 | expect(screen.getByText('$100/mo')).toBeTruthy(); 66 | expect(screen.getByText('Street 1 Street 2')).toBeTruthy(); 67 | expect(screen.getByText('1BR | 1BA | 100 sqft')).toBeTruthy(); 68 | expect(screen.getByText('Loading...')).toBeTruthy(); 69 | }); 70 | 71 | it('Renders ContainerFeed component in profile view', async () => { 72 | mockContext(, { providerProps }); 73 | await waitFor(() => expect(screen.getByTestId('ProfileCardActions')).toBeTruthy()); 74 | }); 75 | 76 | it('Renders ContainerFeed component in user view', async () => { 77 | mockContext(, { providerProps }); 78 | await waitFor(() => expect(screen.getByTestId('UserCardActions')).toBeTruthy()); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /client/components/tests/DisplayCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { screen, fireEvent } from '@testing-library/react'; 4 | 5 | import mockContext from './__mocks__/mockContext'; 6 | import DisplayCard from '../DisplayCard'; 7 | 8 | jest.mock('@react-google-maps/api', () => function () { 9 | return { GoogleMap:
}; 10 | }); 11 | 12 | const userInfo = { 13 | firstName: 'John', 14 | lastName: 'Smith', 15 | username: 'test@gmail.com', 16 | }; 17 | 18 | const postInfo = { 19 | address: { 20 | street1: 'Street 1', 21 | street2: 'Street 2', 22 | city: 'City', 23 | state: 'State', 24 | zipCode: '12345', 25 | }, 26 | roommate: { gender: 'male' }, 27 | description: { 28 | BR: 1, 29 | BA: 1, 30 | sqFt: 100, 31 | parking: true, 32 | smoking: true, 33 | pets: true, 34 | }, 35 | moveInDate: Date.now(), 36 | rent: 100, 37 | bio: 'Description', 38 | images: { 39 | imgUrl: '', 40 | imgPath: '', 41 | }, 42 | currUser: { 43 | firstName: 'John', 44 | lastName: 'Smith', 45 | username: 'test@gmail.com', 46 | }, 47 | }; 48 | 49 | xdescribe('DisplayCard.jsx', () => { 50 | let originalFetch; 51 | let providerProps; 52 | beforeEach(() => { 53 | originalFetch = global.fetch; 54 | global.fetch = jest.fn(() => Promise.resolve({ 55 | json: () => Promise.resolve(false), 56 | })); 57 | providerProps = { 58 | userInfo, 59 | }; 60 | }); 61 | 62 | const date = Date.now(); 63 | 64 | it('Renders Login component', async () => { 65 | mockContext(, { providerProps }); 66 | 67 | expect(await screen.getByText('Street 1 Street 2')).toBeTruthy(); 68 | expect(await screen.getByText('City, State 12345')).toBeTruthy(); 69 | expect(await screen.getByText('Description')).toBeTruthy(); 70 | expect(await screen.getByText('$100/mo')).toBeTruthy(); 71 | expect(await screen.getByText('1BR | 1BA | 100 sqft')).toBeTruthy(); 72 | expect(await screen.getByText(`Available: ${new Date(date).toLocaleDateString()}`)).toBeTruthy(); 73 | expect(await screen.getByText('Looking for: male')).toBeTruthy(); 74 | }); 75 | 76 | it('Cycles images', async () => { 77 | mockContext(, { providerProps }); 78 | 79 | const leftImg = await screen.getByTestId('leftImg'); 80 | const rightImg = await screen.getByTestId('rightImg'); 81 | 82 | fireEvent.click(leftImg); 83 | fireEvent.click(rightImg); 84 | }); 85 | 86 | it('Populates user view', async () => { 87 | mockContext(, { providerProps }); 88 | 89 | expect(await screen.getByText('Apply')).toBeTruthy(); 90 | }); 91 | 92 | /** 93 | * NEED TO TEST ABSENCE OF APPLY BUTTON 94 | */ 95 | xit('Populates profile view', async () => { 96 | mockContext(, { providerProps }); 97 | 98 | expect(await screen.getByText('Apply')).toBeFalsy(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /client/components/tests/EditCard.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { screen, fireEvent } from "@testing-library/react"; 4 | 5 | import mockContext from "./__mocks__/mockContext.js"; 6 | import EditCard from "../EditCard.jsx"; 7 | 8 | jest.mock('../ImageGallery.jsx', () => { 9 | return () =>
10 | }); 11 | 12 | const userInfo = { 13 | firstName: 'John', 14 | lastName: 'Smith', 15 | username: 'test@gmail.com' 16 | } 17 | 18 | const postInfo = { 19 | address: { 20 | street1: 'Street 1', 21 | street2: 'Street 2', 22 | city: 'City', 23 | state: 'State', 24 | zipCode: '12345' 25 | }, 26 | roommate: {gender: 'male'}, 27 | description: { 28 | BR: 1, 29 | BA: 1, 30 | sqFt: 100, 31 | parking: true, 32 | smoking: true, 33 | pets: true 34 | }, 35 | moveInDate: Date.now(), 36 | rent: 100, 37 | bio: 'Description', 38 | images: { 39 | imgUrl: '', 40 | imgPath: '' 41 | }, 42 | currUser: { 43 | firstName: 'John', 44 | lastName: 'Smith', 45 | username: 'test@gmail.com' 46 | } 47 | } 48 | 49 | const getProfilePosts = () => { 50 | return Promise.resolve() 51 | } 52 | 53 | describe("EditCard.jsx", () => { 54 | let originalFetch; 55 | let providerProps; 56 | beforeEach(() => { 57 | originalFetch = global.fetch; 58 | global.fetch = jest.fn(() => Promise.resolve({ 59 | json: () => Promise.resolve(false) 60 | })); 61 | providerProps = { 62 | userInfo: userInfo 63 | } 64 | }); 65 | 66 | xit("Renders EditCard component", async () => { 67 | mockContext(, { providerProps }) 68 | 69 | expect(await screen.getByText("Upload Image")).toBeTruthy(); 70 | expect(await screen.getByText("Remove Image")).toBeTruthy(); 71 | 72 | }); 73 | 74 | xit("Populates values in state", async () => { 75 | mockContext(, { providerProps }) 76 | 77 | const cityInput = await screen.getByPlaceholderText('cityInput'); 78 | 79 | fireEvent.change(cityInput, { target: { value: 'New York' } }); 80 | 81 | expect(cityInput.value).toBe('New York') 82 | 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /client/components/tests/EditCardActions.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import EditCardActions from "../views/EditCardActions.jsx"; 4 | 5 | describe("EditCardActions.jsx", () => { 6 | 7 | it("Renders EditCardActions component", async () => { 8 | render(); 9 | 10 | expect(await screen.getByLabelText('Restore changes')).toBeTruthy(); 11 | expect(await screen.getByLabelText('Save changes')).toBeTruthy(); 12 | }); 13 | 14 | }); -------------------------------------------------------------------------------- /client/components/tests/HomeFeed.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { screen, fireEvent } from '@testing-library/react'; 4 | 5 | import mockContext from './__mocks__/mockContext'; 6 | import HomeFeed from '../HomeFeed'; 7 | 8 | jest.mock('../ContainerFeed.jsx', () => function () { 9 | return
; 10 | }); 11 | 12 | jest.mock('../PostModal.jsx', () => function () { 13 | return
; 14 | }); 15 | 16 | jest.mock('../charts/Scatter.jsx', () => function () { 17 | return
; 18 | }); 19 | const userInfo = { 20 | firstName: 'John', 21 | lastName: 'Smith', 22 | username: 'test@gmail.com', 23 | }; 24 | 25 | const posts = [{ 26 | address: { 27 | street1: 'Street 1', 28 | street2: 'Street 2', 29 | city: 'City', 30 | state: 'State', 31 | zipCode: '12345', 32 | }, 33 | roommate: { gender: 'male' }, 34 | description: { 35 | BR: 1, 36 | BA: 1, 37 | sqFt: 100, 38 | parking: true, 39 | smoking: true, 40 | pets: true, 41 | }, 42 | moveInDate: Date.now(), 43 | rent: 100, 44 | bio: 'Description', 45 | images: { 46 | imgUrl: '', 47 | imgPath: '', 48 | }, 49 | currUser: { 50 | firstName: 'John', 51 | lastName: 'Smith', 52 | username: 'test@gmail.com', 53 | }, 54 | }]; 55 | 56 | describe('HomeFeed.jsx', () => { 57 | let originalFetch; 58 | let providerProps; 59 | beforeEach(() => { 60 | originalFetch = global.fetch; 61 | global.fetch = jest.fn(() => Promise.resolve({ 62 | json: () => Promise.resolve(false), 63 | })); 64 | providerProps = { 65 | userInfo, 66 | }; 67 | }); 68 | 69 | it('Renders HomeFeed component', async () => { 70 | mockContext(, { providerProps }); 80 | 81 | expect(await screen.getByTestId('homeFeed')).toBeTruthy(); 82 | expect(await screen.getByLabelText('Zipcode')).toBeTruthy(); 83 | expect(await screen.getByLabelText('Distance (mi)')).toBeTruthy(); 84 | // expect(await screen.getByLabelText('# of Posts')).toBeTruthy(); 85 | }); 86 | 87 | it('Populates props input', async () => { 88 | mockContext(, { providerProps }); 98 | }); 99 | 100 | it('Open filter options will lazy load', async () => { 101 | mockContext(, { providerProps }); 111 | 112 | const toggleFilterBtn = await screen.getByTestId('toggleFilter'); 113 | fireEvent.click(toggleFilterBtn); 114 | 115 | expect(await screen.getByTestId('loadingFilter')).toBeTruthy(); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /client/components/tests/Login.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 4 | import { screen, fireEvent } from "@testing-library/react"; 5 | 6 | import mockContext from "./__mocks__/mockContext.js"; 7 | import Login from "../Login.jsx"; 8 | 9 | jest.mock('../Phrases.jsx', () => { 10 | return () =>
11 | }); 12 | 13 | const userInfo = { 14 | firstName: 'John', 15 | lastName: 'Smith', 16 | username: 'test@gmail.com' 17 | } 18 | 19 | describe("Login.jsx", () => { 20 | 21 | let originalFetch; 22 | 23 | beforeEach(() => { 24 | originalFetch = global.fetch; 25 | global.fetch = jest.fn(() => Promise.resolve({ 26 | json: () => Promise.resolve(false) 27 | })); 28 | }); 29 | 30 | it("Renders Login component", async () => { 31 | const providerProps = { userInfo: '' } 32 | mockContext( 33 | 34 | 35 | } /> 36 | 37 | 38 | , { providerProps }) 39 | expect(await screen.getByText("a LabRitz thing")).toBeTruthy(); 40 | expect(await screen.getByText("looking for a Zillow corporate sponsorship")).toBeTruthy(); 41 | expect(await screen.getByPlaceholderText("Enter your email address")).toBeTruthy(); 42 | expect(await screen.getByPlaceholderText("Enter your password")).toBeTruthy(); 43 | expect(await screen.getByText("Login")).toBeTruthy(); 44 | expect(await screen.getByText("Google Login")).toBeTruthy(); 45 | expect(await screen.getByText("Sign up")).toBeTruthy(); 46 | expect(await screen.getByTestId('phrases')).toBeTruthy();; 47 | 48 | }); 49 | 50 | it("Renders Signup component when clicked", async () => { 51 | const providerProps = { userInfo: '' } 52 | mockContext( 53 | 54 | 55 | } /> 56 | Signup
} /> 57 | 58 | 59 | , { providerProps }) 60 | 61 | const signupButton = screen.getByText('Sign up'); 62 | fireEvent.click(signupButton); 63 | 64 | expect(await screen.getByTestId("Signup")).toBeTruthy(); 65 | }); 66 | 67 | /** 68 | * FIRST SHOULD CHANGE LOGIN WINDOW ALERT TO TOAST ALERT 69 | * THEN TEST SHOULD LISTEN FOR TOAST 70 | */ 71 | xit("Attempt to login with bad credentials", async () => { 72 | render( 73 | 74 | 75 | } /> 76 | 77 | 78 | ); 79 | 80 | const loginButton = screen.getByText('Login'); 81 | fireEvent.click(loginButton); 82 | 83 | expect(await screen.getByTestId("Signup")).toBeTruthy(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /client/components/tests/NavBar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 4 | import { screen, fireEvent } from "@testing-library/react"; 5 | 6 | import mockContext from "./__mocks__/mockContext.js"; 7 | import NavBar from "../NavBar.jsx"; 8 | 9 | jest.mock('../Phrases.jsx', () => { 10 | return () =>
11 | }); 12 | 13 | const setUserInfo = jest.fn() 14 | 15 | const userInfo = { 16 | firstName: 'John', 17 | lastName: 'Smith', 18 | username: 'test123@gmail.com', 19 | zipCode: '12345' 20 | } 21 | 22 | describe("Signup.jsx", () => { 23 | 24 | let originalFetch; 25 | let providerProps; 26 | beforeEach(() => { 27 | originalFetch = global.fetch; 28 | global.fetch = jest.fn(() => Promise.resolve({ 29 | json: () => Promise.resolve(false) 30 | })); 31 | providerProps = { 32 | userInfo: userInfo 33 | } 34 | }); 35 | 36 | it("Renders NavBar component", async () => { 37 | mockContext( 38 | 39 | 40 | 41 | Home
} /> 42 | CreatePost
} /> 43 | Profile
} /> 44 | 45 | 46 | , { providerProps }) 47 | 48 | expect(await screen.getByTestId("phrases")).toBeTruthy(); 49 | expect(await screen.getByTestId("Home")).toBeTruthy(); 50 | }); 51 | 52 | it("Navigate to CreatePost", async () => { 53 | mockContext( 54 | 55 | 56 | 57 | Home
} /> 58 | CreatePost
} /> 59 | Profile
} /> 60 | 61 | 62 | , { providerProps }) 63 | 64 | const createButton = await screen.getByTestId('createPostBtn'); 65 | fireEvent.click(createButton) 66 | expect(await screen.getByTestId("CreatePost")).toBeTruthy(); 67 | }); 68 | 69 | it("Navigate to Home", async () => { 70 | mockContext( 71 | 72 | 73 | 74 | Home
} /> 75 | CreatePost
} /> 76 | Profile
} /> 77 | 78 | 79 | , { providerProps }) 80 | 81 | const createButton = await screen.getByTestId('createPostBtn'); 82 | fireEvent.click(createButton) 83 | 84 | const homeButton = await screen.getByTestId('homeBtn'); 85 | fireEvent.click(homeButton) 86 | expect(await screen.getByTestId("Home")).toBeTruthy(); 87 | }); 88 | 89 | it("Navigate to Home in Menu", async () => { 90 | mockContext( 91 | 92 | 93 | 94 | Home
} /> 95 | CreatePost} /> 96 | Profile} /> 97 | 98 | 99 | , { providerProps }) 100 | 101 | const createButton = await screen.getByTestId('createPostBtn'); 102 | fireEvent.click(createButton) 103 | 104 | const homeButton = await screen.getByTestId('menuHomeBtn'); 105 | fireEvent.click(homeButton) 106 | expect(await screen.getByTestId("Home")).toBeTruthy(); 107 | }); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /client/components/tests/Phrases.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import Phrases from "../Phrases.jsx"; 4 | 5 | describe("Phrases.jsx", () => { 6 | 7 | it("Renders Phrases component", async () => { 8 | render(); 9 | 10 | expect(await screen.getByText("find a roommate...")).toBeTruthy(); 11 | }); 12 | 13 | it("Phrase changes after 5s", async () => { 14 | render(); 15 | 16 | expect(await screen.getByText("find a roommate...")).toBeTruthy(); 17 | 18 | setTimeout(async () => { 19 | expect(await screen.getByText("find a future...")).toBeTruthy(); 20 | }, 5500) 21 | }); 22 | 23 | it("Phrase restarts after 25s", async () => { 24 | render(); 25 | 26 | setTimeout(async () => { 27 | expect(await screen.getByText("find a bed...")).toBeTruthy(); 28 | }, 20500) 29 | 30 | setTimeout(async () => { 31 | expect(await screen.getByText("find a roommate...")).toBeTruthy(); 32 | }, 25500) 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /client/components/tests/PostModal.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import PostModal from "../PostModal.jsx"; 4 | 5 | jest.mock('../DisplayCard.jsx', () => { 6 | return () =>
7 | }); 8 | 9 | describe("PostModal.jsx", () => { 10 | 11 | it("Renders PostModal component", async () => { 12 | render(); 13 | 14 | expect(await screen.getByTestId('DisplayCard')).toBeTruthy(); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /client/components/tests/Profile.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { screen } from '@testing-library/react'; 3 | 4 | import mockContext from './__mocks__/mockContext'; 5 | import Profile from '../Profile'; 6 | 7 | jest.mock('../ProfileFeed', () => function () { 8 | return
; 9 | }); 10 | 11 | const userInfo = { 12 | firstName: 'John', 13 | lastName: 'Smith', 14 | username: 'test@gmail.com', 15 | }; 16 | 17 | const setAlert = () => {}; 18 | 19 | describe('Profile.jsx', () => { 20 | let providerProps; 21 | beforeEach(() => { 22 | providerProps = { 23 | userInfo, 24 | setAlert, 25 | }; 26 | }); 27 | 28 | it('Renders Profile component', async () => { 29 | mockContext(, { providerProps }); 30 | 31 | expect(await screen.getByTestId('ProfileFeed')).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /client/components/tests/ProfileCardActions.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import ProfileCardActions from "../views/ProfileCardActions.jsx"; 4 | 5 | const application = [] 6 | 7 | describe("ProfileCardActions.jsx", () => { 8 | 9 | it("Renders ProfileCardActions component", async () => { 10 | render(); 11 | 12 | expect(await screen.getByLabelText('Edit post')).toBeTruthy(); 13 | expect(await screen.getByLabelText('Remove post')).toBeTruthy(); 14 | expect(await screen.getByText('0 in review')).toBeTruthy(); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /client/components/tests/ProfileFeed.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { screen } from '@testing-library/react'; 4 | 5 | import mockContext from './__mocks__/mockContext'; 6 | import ProfileFeed from '../ProfileFeed'; 7 | 8 | jest.mock('../ContainerApplication.jsx', () => function () { 9 | return
; 10 | }); 11 | 12 | jest.mock('../DisplayCard.jsx', () => function () { 13 | return
; 14 | }); 15 | 16 | jest.mock('../ImageGallery.jsx', () => function () { 17 | return
; 18 | }); 19 | 20 | const userInfo = { 21 | firstName: 'John', 22 | lastName: 'Smith', 23 | username: 'test@gmail.com', 24 | }; 25 | 26 | const posts = [{ 27 | address: { 28 | street1: 'Street 1', 29 | street2: 'Street 2', 30 | city: 'City', 31 | state: 'State', 32 | zipCode: '12345', 33 | }, 34 | roommate: { gender: 'male' }, 35 | description: { 36 | BR: 1, 37 | BA: 1, 38 | sqFt: 100, 39 | parking: true, 40 | smoking: true, 41 | pets: true, 42 | }, 43 | moveInDate: Date.now(), 44 | rent: 100, 45 | bio: 'Description', 46 | images: { 47 | imgUrl: '', 48 | imgPath: '', 49 | }, 50 | currUser: { 51 | firstName: 'John', 52 | lastName: 'Smith', 53 | username: 'test@gmail.com', 54 | }, 55 | }]; 56 | 57 | describe('ProfileFeed.jsx', () => { 58 | let providerProps; 59 | beforeEach(() => { 60 | providerProps = { 61 | userInfo, 62 | }; 63 | }); 64 | 65 | it('Renders ProfileFeed component', async () => { 66 | mockContext(, { providerProps }); 67 | 68 | expect(await screen.getByTestId('ContainerApplication')).toBeTruthy(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /client/components/tests/Signup.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 3 | import { screen, fireEvent } from '@testing-library/react'; 4 | import Signup from '../Signup'; 5 | import mockContext from './__mocks__/mockContext'; 6 | 7 | jest.mock('../Phrases.jsx', () => function () { 8 | return
; 9 | }); 10 | 11 | const userInfo = { 12 | firstName: 'John', 13 | lastName: 'Smith', 14 | username: 'test@gmail.com', 15 | }; 16 | 17 | describe('Signup.jsx', () => { 18 | let originalFetch; 19 | let providerProps; 20 | beforeEach(() => { 21 | originalFetch = global.fetch; 22 | global.fetch = jest.fn(() => Promise.resolve({ 23 | json: () => Promise.resolve(false), 24 | })); 25 | providerProps = { 26 | userInfo, 27 | }; 28 | }); 29 | 30 | it('Renders Signup component', async () => { 31 | mockContext( 32 | 33 | 34 | } /> 35 | 36 | , 37 | { providerProps }, 38 | ); 39 | expect(await screen.getByText('a LabRitz thing')).toBeTruthy(); 40 | expect(await screen.getByText('looking for a Zillow corporate sponsorship')).toBeTruthy(); 41 | expect(await screen.getByPlaceholderText('Rich')).toBeTruthy(); 42 | expect(await screen.getByPlaceholderText('Barton')).toBeTruthy(); 43 | expect(await screen.getByPlaceholderText('r.barton@zillow.com')).toBeTruthy(); 44 | expect(await screen.getByPlaceholderText('ForbesCEO2021')).toBeTruthy(); 45 | expect(await screen.getByPlaceholderText('10001')).toBeTruthy(); 46 | expect(await screen.getByText('Signup')).toBeTruthy(); 47 | expect(await screen.getByText('Back')).toBeTruthy(); 48 | }); 49 | 50 | it('Populates values in state', async () => { 51 | mockContext( 52 | 53 | 54 | } /> 55 | 56 | , 57 | { providerProps }, 58 | ); 59 | 60 | const firstNameInput = await screen.getByPlaceholderText('Rich'); 61 | const lastNameInput = await screen.getByPlaceholderText('Barton'); 62 | const usernameInput = await screen.getByPlaceholderText('r.barton@zillow.com'); 63 | const passwordInput = await screen.getByPlaceholderText('ForbesCEO2021'); 64 | const zipcodeInput = await screen.getByPlaceholderText('10001'); 65 | 66 | fireEvent.change(firstNameInput, { target: { value: 'John' } }); 67 | fireEvent.change(lastNameInput, { target: { value: 'Smith' } }); 68 | fireEvent.change(usernameInput, { target: { value: 'test@example.com' } }); 69 | fireEvent.change(passwordInput, { target: { value: 'abc123' } }); 70 | fireEvent.change(zipcodeInput, { target: { value: '12345' } }); 71 | 72 | expect(firstNameInput.value).toBe('John'); 73 | expect(lastNameInput.value).toBe('Smith'); 74 | expect(usernameInput.value).toBe('test@example.com'); 75 | expect(passwordInput.value).toBe('abc123'); 76 | expect(zipcodeInput.value).toBe('12345'); 77 | }); 78 | 79 | /** 80 | * FIRST SHOULD CHANGE SIGNUP WINDOW ALERT TO TOAST ALERT 81 | * THEN TEST SHOULD LISTEN FOR TOAST 82 | */ 83 | xit('Attempt to login with bad credentials', async () => { 84 | mockContext( 85 | 86 | 87 | } /> 88 | 89 | , 90 | { providerProps }, 91 | ); 92 | 93 | const loginButton = screen.getByText('Login'); 94 | fireEvent.click(loginButton); 95 | 96 | expect(await screen.getByTestId('Signup')).toBeTruthy(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /client/components/tests/UserCardActions.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import UserCardActions from "../views/UserCardActions.jsx"; 4 | 5 | const application = [] 6 | 7 | describe("UserCardActions.jsx", () => { 8 | 9 | it("Renders UserCardActions component when user has applied", async () => { 10 | render(); 11 | 12 | expect(await screen.getByText('Applied')).toBeTruthy(); 13 | expect(await screen.getByText('0 in review')).toBeTruthy(); 14 | }); 15 | 16 | it("Renders UserCardActions component when user has not applied", async () => { 17 | render(); 18 | 19 | expect(await screen.getByText('Apply')).toBeTruthy(); 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /client/components/tests/__mocks__/mockContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from "@testing-library/react"; 3 | 4 | import Context from '../../context/Context.js' 5 | 6 | const mockContext = (children, { providerProps, ...renderOptions }) => { 7 | return render( 8 | { 9 | children} 10 | , 11 | renderOptions 12 | ); 13 | } 14 | 15 | export default mockContext; 16 | -------------------------------------------------------------------------------- /client/components/utils/SentryRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, 3 | } from 'react'; 4 | import { 5 | Routes, 6 | useLocation, 7 | useNavigationType, 8 | createRoutesFromChildren, 9 | matchRoutes, 10 | } from 'react-router-dom'; 11 | import * as Sentry from '@sentry/react'; 12 | import { BrowserTracing } from '@sentry/tracing'; 13 | 14 | Sentry.init({ 15 | dsn: 'https://917e39262caa4db0bab41139f4c8ddbd@o4504696226447360.ingest.sentry.io/4504696228675584', 16 | integrations: [ 17 | new BrowserTracing({ 18 | routingInstrumentation: Sentry.reactRouterV6Instrumentation( 19 | useEffect, 20 | useLocation, 21 | useNavigationType, 22 | createRoutesFromChildren, 23 | matchRoutes, 24 | ), 25 | }), 26 | ], 27 | tracesSampleRate: 0.2, // Lower sampling rate for prod 28 | }); 29 | 30 | export const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); 31 | 32 | -------------------------------------------------------------------------------- /client/components/utils/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app'; 2 | import { getStorage } from 'firebase/storage'; 3 | 4 | const firebaseConfig = { 5 | apiKey: 'AIzaSyBOnee-KSyXhLZu8BtDO9VPbCPpAYUHK_I', 6 | authDomain: 'roomier-1cc68.firebaseapp.com', 7 | projectId: 'roomier-1cc68', 8 | storageBucket: 'roomier-1cc68.appspot.com', 9 | messagingSenderId: '498568617132', 10 | appId: '1:498568617132:web:15275ea991c04a880c5309', 11 | measurementId: 'G-H9WYGT2GCD', 12 | }; 13 | 14 | const app = initializeApp(firebaseConfig); 15 | export default getStorage(app); 16 | -------------------------------------------------------------------------------- /client/components/views/EditCardActions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import Tooltip from '@mui/material/Tooltip'; 4 | import SaveIcon from '@mui/icons-material/Save'; 5 | import RestoreIcon from '@mui/icons-material/Restore'; 6 | 7 | function EditCardActions({ handleSave, handleUndo }) { 8 | return ( 9 |
13 | 14 | 17 | 18 | 19 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default EditCardActions; 28 | -------------------------------------------------------------------------------- /client/components/views/ProfileCardActions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import Tooltip from '@mui/material/Tooltip'; 4 | import Typography from '@mui/material/Typography'; 5 | import TuneIcon from '@mui/icons-material/Tune'; 6 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; 7 | 8 | function ProfileCardActions({ 9 | application, handleUpdate, handleDelete, openApps, 10 | }) { 11 | return ( 12 |
16 | 17 | 20 | 21 | 22 | {application.length} 23 | {' '} 24 | in review 25 | 26 | 27 | 30 | 31 |
32 | ); 33 | } 34 | 35 | export default ProfileCardActions; 36 | -------------------------------------------------------------------------------- /client/components/views/UserCardActions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import Tooltip from '@mui/material/Tooltip'; 4 | import Typography from '@mui/material/Typography'; 5 | 6 | function UserCardActions({ application, handleApply, hasApplied }) { 7 | return ( 8 |
9 | { hasApplied 10 | ? ( 11 | 16 | ) 17 | : ( 18 | 19 | 24 | 25 | )} 26 | 27 | {application.length} 28 | {' '} 29 | in review 30 | 31 |
32 | ); 33 | } 34 | 35 | export default UserCardActions; 36 | -------------------------------------------------------------------------------- /client/constants/home.js: -------------------------------------------------------------------------------- 1 | export const filterOptions = ['Pets', 'Smoking', 'Parking']; 2 | export const distances = [1, 2, 5, 10, 25, 50]; 3 | export const postsPerPage = [2, 4, 6, 12, 24]; 4 | export const bedrooms = [0, 1, 2, 3, 4]; 5 | export const bathrooms = [1, 2, 3, 4]; 6 | export const priceGap = 100; 7 | export const sqftGap = 50; -------------------------------------------------------------------------------- /client/hooks/posts/useGeocode.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import Geocode from 'react-geocode'; 3 | 4 | import { homeStore } from '../../stores/home'; 5 | 6 | export const useGeocode = (setAlert) => { 7 | const { zipcode } = homeStore((state) => ({ 8 | zipcode: state.zipcode, 9 | })); 10 | 11 | const sendRequest = async () => { 12 | try { 13 | const geocode = await Geocode.fromAddress(zipcode); 14 | if (geocode.status === 'OK') { 15 | const { lng, lat } = geocode.results[0].geometry.location; 16 | 17 | return { lat, lng }; 18 | } 19 | setAlert((alerts) => [...alerts, { severity: 'warn', message: 'Cannot find zip code' }]); 20 | } catch (err) { 21 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in identifying zip code' }]); 22 | } 23 | }; 24 | 25 | const queryKey = ['GEOCODE', zipcode]; 26 | const queryConfig = { 27 | queryKey, 28 | queryFn: sendRequest, 29 | }; 30 | 31 | const query = useQuery(queryConfig); 32 | 33 | return { 34 | queryKey, 35 | queryConfig, 36 | query, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /client/hooks/posts/useImageUpload.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { useCallback, useContext, useMemo } from 'react'; 3 | import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; 4 | 5 | import storage from '../../components/utils/firebase'; 6 | import Context from '../../components/context/Context'; 7 | import { error } from '../../utils/logger'; 8 | 9 | export const useImageUpload = ({ imageUpload, imgLoad }) => { 10 | const { userInfo, setAlert } = useContext(Context); 11 | 12 | const imgPath = useMemo(() => { 13 | if (!imageUpload) return null; 14 | 15 | return `images/${userInfo.username}/${imageUpload.name}`; 16 | }, [imageUpload, userInfo.username]); 17 | 18 | const imgRef = useMemo(() => { 19 | if (!imgPath) return null; 20 | 21 | return ref(storage, imgPath); 22 | }, [imgPath]); 23 | 24 | const sendRequest = useCallback(async () => { 25 | if (!imageUpload) return false; 26 | 27 | try { 28 | await uploadBytes(imgRef, imageUpload); 29 | const imgUrl = await getDownloadURL(imgRef); 30 | 31 | return { 32 | imgUrl, 33 | imgPath, 34 | }; 35 | } catch (err) { 36 | error(err); 37 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error occurred while uploading image' }]); 38 | } 39 | }, [imageUpload, imgPath, imgRef, setAlert]); 40 | 41 | const queryKey = useMemo(() => ['IMAGE', imageUpload], [imageUpload]); 42 | const queryConfig = useMemo(() => ({ 43 | queryKey, 44 | queryFn: sendRequest, 45 | enabled: (!!imageUpload && !!imgLoad), 46 | }), [imageUpload, imgLoad, queryKey, sendRequest]); 47 | 48 | const query = useQuery(queryConfig); 49 | 50 | return { 51 | queryKey, 52 | queryConfig, 53 | query, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /client/hooks/posts/usePosts.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { homeStore } from '../../stores/home'; 4 | import { error, info } from '../../utils/logger'; 5 | 6 | export const usePosts = (userInfo, setAlert) => { 7 | const { center, distance } = homeStore((state) => ({ 8 | center: state.center, 9 | distance: state.distance, 10 | })); 11 | 12 | const sendRequest = async () => { 13 | if (!center) return []; 14 | 15 | try { 16 | info(`Getting posts for ${userInfo.username}`); 17 | const res = await fetch(`/home/${userInfo.username}`, { 18 | method: 'POST', 19 | headers: { 'Content-Type': 'application/json' }, 20 | body: JSON.stringify({ 21 | lng: center.lng, 22 | lat: center.lat, 23 | minDistance: 0, 24 | maxDistance: distance, 25 | }), 26 | }); 27 | const postsArr = await res.json(); 28 | info(`Received ${postsArr.length} posts for ${userInfo.username}`); 29 | 30 | return postsArr; 31 | } catch (err) { 32 | error(err); 33 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Error in getting posts at location' }]); 34 | } 35 | }; 36 | 37 | const queryKey = ['POSTS', center, userInfo]; 38 | const queryConfig = { 39 | queryKey, 40 | queryFn: sendRequest, 41 | }; 42 | 43 | const query = useQuery(queryConfig); 44 | 45 | return { 46 | queryKey, 47 | queryConfig, 48 | query, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /client/hooks/session/useVerifySession.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { error, info } from '../../utils/logger'; 4 | 5 | export const useVerifySession = (setAlert) => { 6 | const sendRequest = async () => { 7 | try { 8 | const res = await fetch('/currentSession'); 9 | if (res.status === 500) { 10 | info('Server returned 500 trying to retrieve current session'); 11 | setAlert((alerts) => [...alerts, { severity: 'error', message: 'Uh oh... the server is currently down :(' }]); 12 | } 13 | 14 | const hasSession = await res.json(); 15 | if (hasSession) { 16 | info(`User session found: ${hasSession.firstName} ${hasSession.lastName} ${hasSession.username}`); 17 | } 18 | 19 | return hasSession; 20 | } catch (err) { 21 | error(err); 22 | } 23 | }; 24 | 25 | const queryKey = ['SESSION']; 26 | const queryConfig = { 27 | queryKey, 28 | queryFn: sendRequest, 29 | }; 30 | 31 | const query = useQuery(queryConfig); 32 | 33 | return { 34 | queryKey, 35 | queryConfig, 36 | query, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { ToggleLightDark } from './components/ToggleLightDark'; 5 | 6 | import './stylesheets/styles.css'; 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container); 10 | root.render( 11 | , 12 | ); 13 | -------------------------------------------------------------------------------- /client/stores/app.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | export const appStore = create((set) => ({ 4 | // DUMMY VALUES 5 | // TODO: set up userinfo in app store 6 | setting: true, 7 | setSetting: (val) => { 8 | set({ setting: val }); 9 | }, 10 | })); 11 | -------------------------------------------------------------------------------- /client/stores/home.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export const homeStore = create((set) => ({ 4 | zipcode: 10016, 5 | setZipcode: (val) => { 6 | set({ zipcode: val }); 7 | }, 8 | center: null, 9 | setCenter: (val) => { 10 | set({ center: val }); 11 | }, 12 | distance: 1609.344 * 2, 13 | setDistance: (val) => { 14 | set({ distance: val }); 15 | }, 16 | priceRange: [3000, 8000], 17 | setPriceRange: (val) => { 18 | set({ priceRange: val }); 19 | }, 20 | sqftRange: [200, 1500], 21 | setSqftRange: (val) => { 22 | set({ sqftRange: val }); 23 | }, 24 | br: 0, 25 | setBR: (val) => { 26 | set({ br: val }); 27 | }, 28 | ba: 1, 29 | setBA: (val) => { 30 | set({ ba: val }); 31 | }, 32 | filters: [], 33 | setFilters: (val) => { 34 | set({ filters: val }); 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /client/stores/post-form.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export const defaultFormState = { 4 | location: { 5 | street1: '', 6 | street2: '', 7 | city: '', 8 | state: '', 9 | zipCode: '', 10 | }, 11 | price: 0, 12 | utilities: 0, 13 | br: 0, 14 | ba: 0, 15 | sqft: 0, 16 | date: Date.now(), 17 | gender: '', 18 | description: '', 19 | filters: [], 20 | condition: '', 21 | images: [], 22 | }; 23 | 24 | export const usePostStore = create((set) => ({ 25 | postFormState: defaultFormState, 26 | setPostFormState: (form) => { 27 | set({ postFormState: form }); 28 | }, 29 | })); 30 | -------------------------------------------------------------------------------- /client/stylesheets/card.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;600&display=swap'); 3 | 4 | .card { 5 | margin-left: 5%; 6 | 7 | display: grid; 8 | grid-template-rows: 3fr 2fr; 9 | row-gap: 4%; 10 | padding: 1%; 11 | .header { 12 | 13 | display: flex; 14 | flex-direction: row; 15 | height: 100%; 16 | width: 100%; 17 | .img { 18 | padding: 1%; 19 | width: 50%; 20 | height: 100%; 21 | .preview { 22 | position: relative; 23 | width: 100%; 24 | height: 100%; 25 | #imgBtns { 26 | position: relative; 27 | width: auto; 28 | height: 100%; 29 | z-index: 2; 30 | display: flex; 31 | flex-direction: row; 32 | button { 33 | svg { 34 | filter: invert(100%) sepia(0%) saturate(7481%) hue-rotate(191deg) brightness(105%) contrast(107%) drop-shadow(3px 5px 2px rgb(0, 0, 0)); 35 | } 36 | &#imgLeft { 37 | border-top-left-radius: 32px; 38 | border-bottom-left-radius: 32px; 39 | } 40 | &#imgRight { 41 | margin-left: auto; 42 | border-top-right-radius: 32px; 43 | border-bottom-right-radius: 32px; 44 | } 45 | } 46 | } 47 | 48 | img { 49 | object-fit: cover; 50 | position: absolute; 51 | z-index: 0; 52 | width: 100%; 53 | height: 100%; 54 | top: 0%; 55 | border-radius: 32px; 56 | box-shadow: 0px 4px 8px gray; 57 | } 58 | } 59 | } 60 | .data { 61 | display: flex; 62 | flex-direction: column; 63 | width: 50%; 64 | padding-left: 2%; 65 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 66 | 67 | p { 68 | margin-block-start: 0px; 69 | margin-block-end: 0px; 70 | font-size: 125%; 71 | 72 | span { 73 | font-weight: bold; 74 | } 75 | 76 | &.address { 77 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 78 | font-size: 150%; 79 | font-weight: 300; 80 | } 81 | 82 | &.address.cityState { 83 | margin-bottom: 10px; 84 | } 85 | } 86 | } 87 | } 88 | .info { 89 | label, p { 90 | font-size: 75%; 91 | font-family: 'Red Hat Display', sans-serif; 92 | font-weight: 500; 93 | color: rgb(85, 85, 85); 94 | margin-block-start: 0px; 95 | margin-block-end: 0px; 96 | margin-top:1%; 97 | .bold { 98 | font-weight: 700; 99 | font-size: 150%; 100 | } 101 | } 102 | .bio { 103 | height: 80px; 104 | overflow-y: scroll; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/stylesheets/containerApplication.scss: -------------------------------------------------------------------------------- 1 | // @import './globalVariables.scss'; 2 | // @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;600&display=swap'); 3 | 4 | // .applications { 5 | // display: flex; 6 | // flex-direction: row; 7 | // align-items: center; 8 | // padding: 2%; 9 | // border-top-left-radius: 8px; 10 | // border-bottom-right-radius: 8px; 11 | // border-top-right-radius: 48px; 12 | // border-bottom-left-radius: 48px; 13 | // box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.512); 14 | // width: 100%; 15 | // background: linear-gradient(180deg, rgb(232, 232, 232) 80%, rgba(209, 209, 209, 0.82) 90%, rgba(210, 210, 210, 0.82) 95%); 16 | // height: 100%; 17 | // .apply { 18 | // display: flex; 19 | // flex-direction: column; 20 | // justify-content: center; 21 | // .buttons { 22 | // display: flex; 23 | // flex-direction: row; 24 | // justify-content: space-around; 25 | 26 | // button { 27 | // border: none; 28 | // background: none; 29 | // } 30 | // } 31 | 32 | // .dropdown { 33 | // display: inline-block; 34 | // position: relative; 35 | // text-align: center; 36 | // margin-top: 2%; 37 | // button { 38 | // border: none; 39 | // background: none; 40 | // font-size: 50%; 41 | // color: rgb(58, 131, 200); 42 | // &:hover { 43 | // transform: scale(1.05); 44 | // filter: brightness(1.1); 45 | // } 46 | // } 47 | 48 | // .dropdown-applicants { 49 | // background: rgb(232, 232, 232); 50 | // display: none; 51 | // position: absolute; 52 | // width: auto; 53 | // box-shadow: 0px 4px 8px rgb(84, 84, 84); 54 | // border-bottom-left-radius: 16px; 55 | // border-bottom-right-radius: 4px; 56 | // border-top-right-radius: 16px; 57 | 58 | // transition-duration: 300ms; 59 | // padding-top: 5%; 60 | // padding-bottom: 5%; 61 | // padding-right: 10%; 62 | // padding-left: 15%; 63 | 64 | // margin-top: 4%; 65 | 66 | // li { 67 | // font-size: 50%; 68 | // text-align: left; 69 | // &:hover { 70 | // cursor: pointer; 71 | // color: rgb(58, 131, 200); 72 | // transition-duration: 300ms; 73 | // } 74 | 75 | // &:not(:hover) { 76 | // transition-duration: 600ms; 77 | // } 78 | 79 | // } 80 | 81 | // } 82 | 83 | // } 84 | 85 | // } 86 | 87 | // } 88 | 89 | -------------------------------------------------------------------------------- /client/stylesheets/containerFeed.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;600&display=swap'); 3 | 4 | 5 | // div:hover { 6 | // border: 1px solid black; 7 | // } 8 | 9 | .feed { 10 | display: grid; 11 | grid-template-columns: 9fr 1fr; 12 | align-items: center; 13 | padding: 2%; 14 | border-radius: 12px; 15 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.512); 16 | width: 90%; 17 | background: linear-gradient(180deg, rgb(232, 232, 232) 80%, rgba(209, 209, 209, 0.82) 90%, rgba(210, 210, 210, 0.82) 95%); 18 | 19 | .apply { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | button { 24 | align-items: center; 25 | border-radius: 15px; 26 | padding: 4%; 27 | box-shadow: 0px 2px 2px $light-blue; 28 | 29 | &:hover { 30 | background: $light-blue; 31 | transition-duration: 300ms; 32 | } 33 | &:not(:hover) { 34 | transition-duration: 600ms; 35 | } 36 | 37 | } 38 | p { 39 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 40 | font-size: 12px; 41 | font-weight: 100; 42 | } 43 | } 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /client/stylesheets/createPost.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | 3 | .createPost { 4 | margin-top: 140px; 5 | margin-block-end: 0px; 6 | 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | height: 70%; 12 | min-width: 600px; 13 | 14 | @media screen and (min-width:1536px){ 15 | max-width:1356px; 16 | margin-left: auto; 17 | margin-right: auto; 18 | } 19 | 20 | .postForm { 21 | display: flex; 22 | flex-direction:row; 23 | align-content: center; 24 | justify-content: center; 25 | min-Width:300px; 26 | max-width:1000px; 27 | height:100%; 28 | } 29 | 30 | } 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/stylesheets/gallery.scss: -------------------------------------------------------------------------------- 1 | 2 | // .img { 3 | // width: 60%; 4 | .preview { 5 | position: absolute; 6 | // top: 10%; 7 | // left: 10%; 8 | width: 30%; 9 | height: 30%; 10 | // border: 1px solid black; 11 | 12 | // display: flex; 13 | // flex-direction: row; 14 | // justify-content: center; 15 | // align-items: center; 16 | #imgBtns { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | z-index: 2; 21 | // padding-top: 30%; 22 | display: flex; 23 | flex-direction: row; 24 | 25 | button { 26 | 27 | svg { 28 | filter: invert(100%) sepia(0%) saturate(7481%) hue-rotate(191deg) brightness(105%) contrast(107%) drop-shadow(3px 5px 2px rgb(0, 0, 0)); 29 | } 30 | &#imgLeft { 31 | border-top-left-radius: 32px; 32 | border-bottom-left-radius: 32px; 33 | // &:hover { 34 | // background:linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0) 10%, rgba(255, 255, 255, 0.2) 30%, rgba(255, 255, 255, 0.2) 70%, rgba(255, 255, 255, 0) 90%); 35 | // } 36 | // &:not(:hover) { 37 | // transition-duration: 600ms; 38 | // } 39 | } 40 | &#imgRight { 41 | margin-left: auto; 42 | border-top-right-radius: 32px; 43 | border-bottom-right-radius: 32px; 44 | // &:hover { 45 | // background:linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0) 10%, rgba(255, 255, 255, 0.2) 30%, rgba(255, 255, 255, 0.2) 70%, rgba(255, 255, 255, 0) 90%); 46 | // } 47 | // &:not(:hover) { 48 | // transition-duration: 600ms; 49 | // } 50 | } 51 | } 52 | } 53 | 54 | img { 55 | position: absolute; 56 | z-index: 0; 57 | width: 100%; 58 | height: 100%; 59 | top: 0%; 60 | left: 0%; 61 | border-radius: 32px; 62 | box-shadow: 0px 4px 8px gray; 63 | } 64 | } 65 | // } -------------------------------------------------------------------------------- /client/stylesheets/globalVariables.scss: -------------------------------------------------------------------------------- 1 | $mid-blue: #3D5A80; 2 | $light-blue: #98C1D9; 3 | $sky-blue: #E0FBFC; 4 | $dark-blue: #293241; -------------------------------------------------------------------------------- /client/stylesheets/homeFeed.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | 3 | 4 | .home { 5 | display: grid; 6 | grid-template-columns: 4fr 5fr; 7 | column-gap: 24px; 8 | // align-items: center; 9 | width: 95%; 10 | height: 90vh; 11 | padding-top: 80px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | 15 | @media screen and (min-width:1536px){ 16 | max-width:1356px; 17 | margin-left: auto; 18 | margin-right: auto; 19 | } 20 | 21 | .homeFeed { 22 | padding-top: 12px; 23 | height: 100%; 24 | overflow-y: scroll; 25 | display: flex; 26 | flex-direction: column; 27 | z-index: 2; 28 | 29 | } 30 | #googleMapDiv { 31 | width: 100%; 32 | height: 100%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/stylesheets/imageGallery.scss: -------------------------------------------------------------------------------- 1 | .imageUpload { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: center; 6 | input { 7 | width:50% 8 | } 9 | button { 10 | margin-left: 0%; 11 | margin-top: 2%; 12 | } 13 | } 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/stylesheets/login.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap'); 3 | 4 | 5 | .router { 6 | display: grid; 7 | grid-template-columns: 1fr 1fr; 8 | justify-items: center; 9 | align-items: center; 10 | height: 100vh; 11 | .logo { 12 | background: $light-blue; 13 | height: 100%; 14 | width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | box-shadow: 0px 0px 12px $mid-blue; 20 | img { 21 | width:80% 22 | } 23 | h4, h6 { 24 | color: $dark-blue; 25 | margin-block-end: 0px; 26 | } 27 | h6 { 28 | margin-block-start: 0px; 29 | } 30 | } 31 | 32 | .login { 33 | font-family: 'DM Sans', sans-serif; 34 | font-weight: 700; 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | width: 100%; 39 | 40 | button { 41 | font-family: 'DM Sans', sans-serif; 42 | font-weight: 700; 43 | font-size: 20px; 44 | &:hover { 45 | color:rgb(116, 60, 227); 46 | transform: scale(1.05) translateY(-2px); 47 | transition-duration: 300ms; 48 | } 49 | &:not(:hover) { 50 | transition-duration: 600ms; 51 | } 52 | } 53 | 54 | .homeLink { 55 | visibility: hidden; 56 | font-family: 'DM Sans', sans-serif; 57 | font-weight: 700; 58 | &:hover { 59 | color:rgb(116, 60, 227); 60 | transform: scale(1.05) translateY(-2px); 61 | transition-duration: 300ms; 62 | } 63 | &:not(:hover) { 64 | transition-duration: 600ms; 65 | } 66 | } 67 | 68 | input { 69 | font-family: 'DM Sans', sans-serif; 70 | font-weight: 400; 71 | border: none; 72 | margin-bottom: 4%; 73 | height: 24px; 74 | padding-left: 10px; 75 | border-radius: 15px; 76 | width: 50%; 77 | &:focus { 78 | outline: none; 79 | box-shadow: 0px 2px 4px gray; 80 | border-bottom: 1px gray; 81 | transition-duration: 300ms; 82 | } 83 | 84 | &:not(:focus) { 85 | transition-duration: 600ms; 86 | } 87 | } 88 | 89 | a { 90 | 91 | svg { 92 | margin-left: 4px; 93 | margin-top: 2px; 94 | } 95 | 96 | &:visited { 97 | color:$dark-blue; 98 | } 99 | &:hover { 100 | color:rgb(116, 60, 227); 101 | transform: scale(1.05) translateY(-2px); 102 | transition-duration: 300ms; 103 | } 104 | &:not(:hover) { 105 | transition-duration: 600ms; 106 | } 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | -------------------------------------------------------------------------------- /client/stylesheets/navbar.scss: -------------------------------------------------------------------------------- 1 | @import 'globalVariables.scss'; 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;600&display=swap'); 4 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@700&display=swap'); 5 | 6 | .navBarLogo { 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | h1, button { 12 | border: none; 13 | background: none; 14 | 15 | &:hover { 16 | transform: scale(1.1); 17 | cursor: pointer; 18 | transition-duration: 300ms; 19 | } 20 | 21 | &:not(:hover) { 22 | transition-duration: 750ms; 23 | } 24 | 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | display: flex; 30 | } 31 | 32 | .nav { 33 | position: fixed; 34 | background: linear-gradient(180deg, #658495 0%, #81a9c1 30%, #98C1D9 80%); 35 | width: 100%; 36 | height: 66px; 37 | 38 | z-index: 5; 39 | 40 | display: flex; 41 | flex-direction: row; 42 | justify-content: space-between; 43 | align-items: center; 44 | 45 | box-shadow: 0px 0px 24px $mid-blue; 46 | 47 | .leftBtn { 48 | 49 | padding-left: 2.5%; 50 | display: flex; 51 | flex-direction: row; 52 | align-items: center; 53 | width: 50%; 54 | a { 55 | margin-left: 2%; 56 | height: 100%; 57 | img { 58 | height: 120px; 59 | &:hover { 60 | transform: scale(1.1); 61 | transition-duration: 300ms; 62 | } 63 | &:not(:hover) { 64 | transition-duration: 600ms; 65 | } 66 | } 67 | } 68 | 69 | h1 { 70 | font-family: 'DM Sans', sans-serif; 71 | font-size: 36px; 72 | margin-left: 4%; 73 | color: #293241; 74 | width:350px; 75 | margin-block-end: 0px; 76 | margin-block-start: 0px; 77 | &:hover { 78 | transform: scale(1.05) translateX(4px) 79 | } 80 | } 81 | // a > img { 82 | // height: 120px; 83 | // // width:300px; 84 | // filter: invert(16%) sepia(8%) saturate(1991%) hue-rotate(179deg) brightness(94%) contrast(89%); 85 | // } 86 | } 87 | 88 | 89 | .rightBtn { 90 | padding-right: 3%; 91 | width: 30%; 92 | display: flex; 93 | justify-content: end; 94 | gap: 2.5%; 95 | 96 | .post { 97 | margin-right: 4%; 98 | } 99 | } 100 | 101 | } 102 | 103 | -------------------------------------------------------------------------------- /client/stylesheets/profileFeed.scss: -------------------------------------------------------------------------------- 1 | @import './globalVariables.scss'; 2 | 3 | 4 | .profile { 5 | // height: 100vh; 6 | @media screen and (min-width:1536px){ 7 | max-width:1356px; 8 | margin-left: auto; 9 | margin-right: auto; 10 | } 11 | 12 | .profileFeed { 13 | overflow-x:scroll; 14 | padding: 12px; 15 | width: 80%; 16 | height: 100%; 17 | display: flex; 18 | flex-direction: row; 19 | justify-content: flex-start; 20 | align-items: center; 21 | 22 | @media screen and (min-width:1536px){ 23 | max-width:1356px; 24 | margin-left: auto; 25 | margin-right: auto; 26 | } 27 | 28 | .applications { 29 | min-width: '300px'; 30 | &:hover { 31 | transform: scale(1.05); 32 | transition-duration: 500ms; 33 | } 34 | &:not(:hover) { 35 | transition-duration: 500ms; 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /client/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@500;700&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap'); 3 | 4 | body { 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, 6 | Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | background: linear-gradient(180deg,rgb(214, 214, 214) 85%,rgba(169, 169, 169, 0.903)95%, rgba(140, 140, 140, 0.937) 98%, rgba(0, 25, 46, 0.585)); 8 | background-attachment: fixed; 9 | font-family: 'DM Sans', sans-serif; 10 | margin: 0px; 11 | height: 90%; 12 | } -------------------------------------------------------------------------------- /client/utils/isNonNumberString.js: -------------------------------------------------------------------------------- 1 | export const isNonNumberString = (val) => { 2 | return /^([a-zA-Z_\-\.]+)/.test(val) 3 | } -------------------------------------------------------------------------------- /client/utils/isNum.js: -------------------------------------------------------------------------------- 1 | // Validate whether input is number 2 | export const isNum = (val) => { 3 | const regex = /^[0-9\b]+$/; 4 | 5 | return (val === '' || regex.test(val)); 6 | }; 7 | -------------------------------------------------------------------------------- /client/utils/isPassword.js: -------------------------------------------------------------------------------- 1 | export const isPassword = (val) => { 2 | return /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*_=+-]).{8,12}$/.test(val) 3 | } -------------------------------------------------------------------------------- /client/utils/isUsername.js: -------------------------------------------------------------------------------- 1 | export const isUsername = (val) => { 2 | return /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/.test(val) 3 | } -------------------------------------------------------------------------------- /client/utils/isZipcode.js: -------------------------------------------------------------------------------- 1 | export const isZipcode = (val) => { 2 | return /[0-9]{5}/.test(val) 3 | } -------------------------------------------------------------------------------- /client/utils/logger.js: -------------------------------------------------------------------------------- 1 | import { 2 | addBreadcrumb, captureException, captureEvent, captureMessage, 3 | } from '@sentry/react'; 4 | 5 | export const breadCrumb = (category, message, level, timestamp = Date.now(), data = null) => { 6 | addBreadcrumb({ 7 | category, 8 | message, 9 | level, 10 | timestamp, 11 | data, 12 | }); 13 | }; 14 | 15 | export const error = (err) => { 16 | captureException(err); 17 | }; 18 | 19 | export const event = (message, form, ...options) => { 20 | captureEvent({ 21 | message, 22 | extra: { 23 | form, 24 | ...options, 25 | }, 26 | }); 27 | }; 28 | 29 | export const info = (message) => { 30 | captureMessage(message); 31 | }; 32 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DOMAIN: process.env.DOMAIN, 3 | PORT: process.env.PORT, 4 | PROTOCOL: 'https://', 5 | SERVER_PORT: process.env.SERVER_PORT, 6 | DATABASE: { 7 | URI: process.env.ATLAS_URI, 8 | dbName: process.env.DB_TABLE, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /config/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DOMAIN: 'localhost', 3 | PORT: 8080, 4 | PROTOCOL: 'http://', 5 | SERVER_PORT: 3000, 6 | }; 7 | -------------------------------------------------------------------------------- /config/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DOMAIN: process.env.DOMAIN, 3 | PORT: process.env.PORT, 4 | PROTOCOL: 'https://', 5 | SERVER_PORT: process.env.SERVER_PORT, 6 | DATABASE: { 7 | URI: process.env.ATLAS_URI, 8 | dbName: process.env.DB_TABLE, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | dev: 4 | image: "roomier-dev:latest" 5 | container_name: "roomier-dev" 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - ".:/usr/src/app" 10 | - "node_modules:/usr/src/app/node_modules" 11 | command: "npm run dev" 12 | volumes: 13 | node_modules: -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Roomier: Find a Roommate 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | verbose: true, 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | '**/client/**', 6 | '**/server/**', 7 | // Ignore the below files 8 | '!**/server.js', 9 | '!**/server.test.js', 10 | '!**/controllers/metaData/**.js', 11 | '!**/db/**.js', 12 | '!**/__tests__/**.js', 13 | '!**/firebase.js', 14 | '!**/index.js', 15 | '!**/passport.js', 16 | ], 17 | moduleNameMapper: { 18 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 19 | '/__mocks__/fileMock.js', 20 | '\\.(scss|css|less)$': 'identity-obj-proxy', 21 | }, 22 | testEnvironment: 'jsdom', 23 | testMatch: ['**.test.js'], 24 | testPathIgnorePatterns: [ 25 | './server/server.test.js', 26 | './__tests__/', 27 | './config/', 28 | './node_modules/', 29 | ], 30 | transform: { 31 | '^.+\\.(js|jsx)$': 'babel-jest', 32 | }, 33 | }; 34 | 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roomier", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server/server.js", 8 | "build": "NODE_ENV=production webpack --config=./webpack/production.webpack.js", 9 | "dev": "cross-env NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack serve --config=./webpack/development.webpack.js --open ", 10 | "dev:server": "NODE_ENV=development nodemon server/server.js", 11 | "dev:client": "NODE_ENV=development webpack serve --config=./webpack/development.webpack.js", 12 | "test": "NODE_ENV=development jest" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/labritz/roomier.git" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/labritz/roomier/issues" 23 | }, 24 | "homepage": "https://github.com/labritz/roomier#readme", 25 | "dependencies": { 26 | "@emotion/react": "^11.10.5", 27 | "@emotion/styled": "^11.10.5", 28 | "@mui/icons-material": "^5.11.0", 29 | "@mui/material": "^5.11.3", 30 | "@mui/x-date-pickers": "^5.0.15", 31 | "@react-google-maps/api": "^2.12.1", 32 | "@sentry/react": "^7.38.0", 33 | "@sentry/tracing": "^7.38.0", 34 | "@tanstack/react-query": "^4.29.14", 35 | "@visx/event": "^3.0.0", 36 | "@visx/gradient": "^3.0.0", 37 | "@visx/group": "^3.0.0", 38 | "@visx/mock-data": "^3.0.0", 39 | "@visx/react-spring": "^3.0.0", 40 | "@visx/responsive": "^3.0.0", 41 | "@visx/scale": "^3.0.0", 42 | "@visx/shape": "^3.0.0", 43 | "@visx/tooltip": "^3.0.0", 44 | "@visx/voronoi": "^3.0.0", 45 | "bcryptjs": "^2.4.3", 46 | "config": "^3.3.8", 47 | "cookie-parser": "^1.4.6", 48 | "cors": "^2.8.5", 49 | "cross-env": "^7.0.3", 50 | "dotenv": "^16.0.1", 51 | "express": "^4.18.1", 52 | "express-session": "^1.17.3", 53 | "firebase": "^9.9.0", 54 | "framer-motion": "^8.4.6", 55 | "lodash": "^4.17.21", 56 | "moment": "^2.29.4", 57 | "mongoose": "^6.4.4", 58 | "node-fetch": "^2.3.0", 59 | "passport": "^0.6.0", 60 | "passport-google-oauth20": "^2.0.0", 61 | "passport-local": "^1.0.0", 62 | "react": "^18.2.0", 63 | "react-dom": "^18.2.0", 64 | "react-geocode": "^0.2.3", 65 | "react-places-autocomplete": "^7.3.0", 66 | "react-router": "^5.1.0", 67 | "react-router-dom": "^6.6.1", 68 | "sass": "^1.53.0", 69 | "sass-loader": "^13.0.2", 70 | "winston": "^3.8.2", 71 | "zustand": "^4.3.7" 72 | }, 73 | "devDependencies": { 74 | "@babel/core": "^7.1.2", 75 | "@babel/preset-env": "^7.1.0", 76 | "@babel/preset-react": "^7.0.0", 77 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 78 | "@testing-library/react": "^13.4.0", 79 | "babel-loader": "^8.2.3", 80 | "css-loader": "^6.5.1", 81 | "dotenv-webpack": "^8.0.1", 82 | "eslint": "^8.34.0", 83 | "eslint-config-airbnb": "^19.0.4", 84 | "eslint-config-airbnb-base": "^15.0.0", 85 | "eslint-config-prettier": "^8.8.0", 86 | "eslint-plugin-import": "^2.27.5", 87 | "eslint-plugin-jest": "^27.2.1", 88 | "eslint-plugin-jsx-a11y": "^6.7.1", 89 | "eslint-plugin-react": "^7.32.2", 90 | "eslint-plugin-react-hooks": "^4.6.0", 91 | "html-webpack-plugin": "^5.5.0", 92 | "identity-obj-proxy": "^3.0.0", 93 | "jest": "^29.1.2", 94 | "jest-environment-jsdom": "^29.3.1", 95 | "nodemon": "^1.18.9", 96 | "passport": "^0.6.0", 97 | "passport-google-oauth20": "^2.0.0", 98 | "passport-local": "^1.0.0", 99 | "prettier": "^2.8.8", 100 | "style-loader": "^3.3.1", 101 | "supertest": "^6.2.4", 102 | "webpack": "^5.64.1", 103 | "webpack-bundle-analyzer": "^4.7.0", 104 | "webpack-cli": "^4.9.1", 105 | "webpack-dev-server": "^4.5.0" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/__tests__/supertest.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | const request = require('supertest'); 4 | 5 | const app = require('../server.js') 6 | 7 | describe('API Test', () => { 8 | 9 | test('GET /', (done) => { 10 | request('http://localhost:3000') 11 | .get('/') 12 | .expect(200) 13 | .expect("Content-Type", /html/) 14 | .end(function(err, res) { 15 | if (err) throw err; 16 | return done() 17 | }) 18 | }); 19 | 20 | test('GET /home/:username', (done) => { 21 | request('http://localhost:3000/') 22 | .get('/home/test123@gmail.com') 23 | .expect(404) 24 | .end(function(err, res) { 25 | if (err) throw err; 26 | return done() 27 | }) 28 | }); 29 | 30 | test('POST /', (done) => { 31 | request('http://localhost:3000') 32 | .post('/') 33 | .expect("Content-Type", /json/) 34 | .send({ 35 | username: 'test123@gmail.com', 36 | password: '123' 37 | }) 38 | .expect(200) 39 | .expect(res => { 40 | res.body.firstName = 'Brennan', 41 | res.body.lastName = 'Lee', 42 | username = 'test123@gmail.com', 43 | password = '$2a$10$kD4jLpxYBgqPP3b0AAECAe5W9GLc97BTR4hlz85/4aAAexz0Hp6DO' 44 | }) 45 | .end(function(err, res) { 46 | if (err) throw err; 47 | return done() 48 | }) 49 | }); 50 | 51 | test('POST /signup', (done) => { 52 | request('http://localhost:3000') 53 | .post('/signup') 54 | .expect("Content-Type", /json/) 55 | .send({ 56 | firstName: 'Test', 57 | lastName: 'Test', 58 | username: 'test@email.com', 59 | password: '123', 60 | zipCode: '00000' 61 | }) 62 | .expect(200) 63 | .expect(res => { 64 | res.body.firstName = 'Test', 65 | res.body.lastName = 'Test', 66 | username = 'test@email.com', 67 | zipCode = '00000' 68 | }) 69 | .end(function(err, res) { 70 | if (err) throw err; 71 | return done() 72 | }) 73 | }); 74 | 75 | 76 | 77 | 78 | }) -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | require('dotenv').config(); 3 | const GoogleStrategy = require('passport-google-oauth20').Strategy; 4 | const fetch = require('node-fetch'); 5 | 6 | const { DOMAIN, PROTOCOL, SERVER_PORT } = require('config'); 7 | const User = require('../db/userModel'); 8 | 9 | function googleAuth(passport) { 10 | passport.use(new GoogleStrategy( 11 | { 12 | clientID: process.env.GOOGLE_CLIENT_ID, 13 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 14 | callbackURL: '/login/auth/google/callback', 15 | }, 16 | async (accessToken, refreshToken, profile, done) => { 17 | const { given_name, family_name, email } = profile._json; 18 | const { sub } = profile._json; 19 | 20 | const userBody = { 21 | firstName: given_name, 22 | lastName: family_name, 23 | username: email, 24 | password: sub, 25 | zipCode: 10001, 26 | }; 27 | 28 | try { 29 | // makes request to verify if user exists 30 | const res = await fetch(`${PROTOCOL}${DOMAIN}:${SERVER_PORT}/oauth`, { 31 | method: 'POST', 32 | headers: { 'Content-Type': 'application/json' }, 33 | body: JSON.stringify({ 34 | username: email, 35 | password: sub, 36 | }), 37 | }); 38 | const loginJSON = await res.json(); 39 | 40 | // if user already exists 41 | if (loginJSON) done(null, loginJSON); 42 | 43 | // if user does not exist 44 | else { 45 | // make request to create user 46 | const response = await fetch(`${PROTOCOL}${DOMAIN}:${SERVER_PORT}/signup`, { 47 | method: 'POST', 48 | headers: { 'Content-Type': 'application/json' }, 49 | body: JSON.stringify(userBody), 50 | }); 51 | const signupJSON = await response.json(); 52 | 53 | done(null, signupJSON); 54 | } 55 | } catch (err) { 56 | console.log('err: ', err); 57 | } 58 | }, 59 | )); 60 | 61 | passport.serializeUser((user, done) => { 62 | done(null, user); 63 | }); 64 | 65 | passport.deserializeUser((user, done) => { 66 | User.findById(user._id, (err, profile) => done(err, profile)); 67 | }); 68 | } 69 | 70 | module.exports = googleAuth; 71 | -------------------------------------------------------------------------------- /server/controllers/cookie/deleteCookie.js: -------------------------------------------------------------------------------- 1 | const deleteCookie = (req, res, next) => { 2 | if (req.cookies.ssid) { 3 | res.locals.cookieId = req.cookies.ssid; 4 | res.clearCookie('ssid'); 5 | return next(); 6 | } 7 | return next({ 8 | log: 'ERROR: deleteCookie', 9 | status: 500, 10 | message: { err: 'an error occurred while attempting to delete cookie' }, 11 | }); 12 | }; 13 | module.exports = deleteCookie; 14 | -------------------------------------------------------------------------------- /server/controllers/cookie/deleteCookie.test.js: -------------------------------------------------------------------------------- 1 | const deleteCookie = require('./deleteCookie'); 2 | 3 | const next = jest.fn(); 4 | const clearCookie = jest.fn() 5 | 6 | describe('deleteCookie middleware', () => { 7 | 8 | beforeEach(() => { 9 | next.mockClear() 10 | }) 11 | 12 | it('deletes the ssid cookie', () => { 13 | const req = { 14 | cookies: { 15 | ssid: 'test-cookie-id' 16 | } 17 | }; 18 | const res = { 19 | locals: {}, 20 | clearCookie: clearCookie 21 | }; 22 | 23 | deleteCookie(req, res, next); 24 | expect(res.clearCookie).toHaveBeenCalledWith('ssid'); 25 | expect(res.locals.cookieId).toEqual('test-cookie-id'); 26 | expect(next).toHaveBeenCalled(); 27 | }); 28 | 29 | it('handles missing ssid cookie', () => { 30 | const req = { 31 | cookies: {} 32 | }; 33 | const res = {}; 34 | 35 | deleteCookie(req, res, next); 36 | const [error] = next.mock.calls[0]; 37 | expect(error).toEqual({ 38 | log: 'ERROR: deleteCookie', 39 | status: 500, 40 | message: { err: 'an error occurred while attempting to delete cookie' } 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /server/controllers/cookie/getCookie.js: -------------------------------------------------------------------------------- 1 | // Verify existing logged in user 2 | const getCookie = async (req, res, next) => { 3 | const userId = req.cookies.ssid; 4 | if (!userId) return res.status(200).send(false); 5 | 6 | res.locals.userId = userId; 7 | return next(); 8 | }; 9 | 10 | module.exports = getCookie; 11 | -------------------------------------------------------------------------------- /server/controllers/cookie/getCookie.test.js: -------------------------------------------------------------------------------- 1 | const getCookie = require('./getCookie'); 2 | 3 | const next = jest.fn(); 4 | 5 | describe('getCookie middleware', () => { 6 | 7 | beforeEach(() => { 8 | next.mockClear() 9 | }) 10 | 11 | it('Finds ssid cookie', async () => { 12 | const req = { 13 | cookies: { 14 | ssid: 'test-cookie-id' 15 | } 16 | }; 17 | const res = { 18 | locals: {}, 19 | }; 20 | 21 | await getCookie(req, res, next); 22 | 23 | expect(res.locals.userId).toEqual('test-cookie-id'); 24 | expect(next).toHaveBeenCalled(); 25 | }); 26 | 27 | it('Unable to find ssid cookie', async() => { 28 | const req = { 29 | cookies: {} 30 | }; 31 | const res = { 32 | locals: {}, 33 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 34 | }; 35 | 36 | await getCookie(req, res, next); 37 | 38 | expect(res.status).toHaveBeenCalledWith(200); 39 | expect(res.status().send).toHaveBeenCalledWith(false); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /server/controllers/cookie/index.js: -------------------------------------------------------------------------------- 1 | exports.deleteCookie = require('./deleteCookie'); 2 | exports.getCookie = require('./getCookie'); 3 | exports.setSSIDCookie = require('./setSSIDCookie'); 4 | -------------------------------------------------------------------------------- /server/controllers/cookie/setSSIDCookie.js: -------------------------------------------------------------------------------- 1 | const setSSIDCookie = (req, res, next) => { 2 | if (res.locals.user !== null) { 3 | res.cookie('ssid ', res.locals.user._id, { 4 | httpOnly: true, 5 | secure: true, 6 | // maxAge: 30000 7 | }); 8 | } 9 | 10 | return next(); 11 | }; 12 | 13 | module.exports = setSSIDCookie; 14 | -------------------------------------------------------------------------------- /server/controllers/cookie/setSSIDCookie.test.js: -------------------------------------------------------------------------------- 1 | const setSSIDCookie = require('./setSSIDCookie'); 2 | 3 | const cookie = jest.fn() 4 | const next = jest.fn(); 5 | 6 | describe('setSSIDCookie middleware', () => { 7 | 8 | beforeEach(() => { 9 | next.mockClear() 10 | }) 11 | 12 | it('Finds ssid cookie', () => { 13 | const req = {} 14 | const res = { 15 | cookie: cookie, 16 | locals: { user: { _id: 'test_id' } }, 17 | }; 18 | 19 | setSSIDCookie(req, res, next); 20 | 21 | expect(cookie).toHaveBeenCalled(); 22 | expect(next).toHaveBeenCalled(); 23 | }); 24 | 25 | it('no user passed', () => { 26 | const req = {} 27 | const res = { 28 | locals: { user: null }, 29 | }; 30 | 31 | setSSIDCookie(req, res, next); 32 | 33 | expect(next).toHaveBeenCalled(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /server/controllers/metaData/createPost.js: -------------------------------------------------------------------------------- 1 | // metadata is for post creation data 2 | 3 | /* 4 | username: {type: String, required: true}, - email 5 | firstName: {type: String, required: true}, 6 | lastName: {type: String, required: true}, 7 | createdAt: {type: Date, required: true, default: Date.now}, 8 | profilePicture: {type: Buffer}, 9 | id: {type: String, required: true} 10 | */ 11 | 12 | const metadata = require('../../db/metadata'); 13 | 14 | const createPost = async (req, res, next) => { 15 | try { 16 | // deconstruct 17 | const { username, firstName, lastName, createdAt, id } = req.body; 18 | 19 | // place in db 20 | const newMeta = await metadata.create({ 21 | username: username, 22 | firstName: firstName, 23 | lastName: lastName, 24 | createdAt: createdAt, 25 | id: id 26 | }) 27 | 28 | res.locals.newMeta = newMeta; 29 | return next(); 30 | 31 | } catch (err) { 32 | return next({ 33 | log: `ERROR: createPost, ${err}`, 34 | message: { err: 'an error occurred while attempting to create a post in Meta data controller...' } 35 | }) 36 | } 37 | }; 38 | 39 | module.exports = createPost; -------------------------------------------------------------------------------- /server/controllers/metaData/index.js: -------------------------------------------------------------------------------- 1 | exports.createPost = require('./createPost'); -------------------------------------------------------------------------------- /server/controllers/post/createPost.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../db/postModel'); 2 | 3 | const createPost = async (req, res, next) => { 4 | const { 5 | address, roommate, description, 6 | moveInDate, utilities, rent, 7 | bio, userData, geoData, images, 8 | } = req.body; 9 | 10 | if (!address || !roommate || !description 11 | || !moveInDate || !utilities || !rent 12 | || !bio || !userData || !geoData || !images) { 13 | return res.sendStatus(400); 14 | } 15 | 16 | try { 17 | const newPost = await Post.create({ 18 | address: { 19 | street1: address.street1, 20 | street2: address.street2, 21 | city: address.city, 22 | state: address.state, 23 | zipCode: address.zipCode, 24 | }, 25 | roommate: { 26 | gender: roommate.gender, 27 | }, 28 | description: { 29 | BR: description.BR, 30 | BA: description.BA, 31 | sqFt: description.sqFt, 32 | pets: description.pets, 33 | smoking: description.smoking, 34 | parking: description.parking, 35 | condition: description.condition, 36 | }, 37 | moveInDate, 38 | utilities, 39 | rent, 40 | bio, 41 | userData: { 42 | username: userData.username, 43 | firstName: userData.firstName, 44 | lastName: userData.lastName, 45 | }, 46 | applicantData: [], 47 | geoData: { 48 | type: 'Point', 49 | coordinates: [geoData.lng, geoData.lat], 50 | lat: geoData.lat, 51 | lng: geoData.lng, 52 | }, 53 | images, 54 | }); 55 | return res.status(200).json(newPost); 56 | } catch (err) { 57 | return next({ 58 | log: `ERROR: createPost, ${err}`, 59 | status: 500, 60 | message: { err: 'an error occurred while attempting to create a post' }, 61 | }); 62 | } 63 | }; 64 | 65 | module.exports = createPost; 66 | -------------------------------------------------------------------------------- /server/controllers/post/createPost.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const createPost = require('./createPost'); 6 | 7 | jest.mock('../../db/postModel', () => ({ 8 | create: jest.fn().mockResolvedValue({}) 9 | })); 10 | 11 | const sendStatus = jest.fn() 12 | const next = jest.fn(); 13 | 14 | const goodReqBody = { 15 | address: { 16 | street1: 'Street 1', 17 | street2: 'Street 2', 18 | city: 'City', 19 | state: 'State', 20 | zipCode: '12345' 21 | }, 22 | roommate: {gender: 'male'}, 23 | description: { 24 | BR: 1, 25 | BA: 1, 26 | sqFt: 100, 27 | parking: true, 28 | smoking: true, 29 | pets: true 30 | }, 31 | moveInDate: Date.now(), 32 | utilities: 100, 33 | rent: 100, 34 | bio: 'Description', 35 | images: { 36 | imgUrl: '', 37 | imgPath: '' 38 | }, 39 | geoData: { 40 | lat: 10, 41 | lng: 10 42 | }, 43 | userData: { 44 | firstName: 'John', 45 | lastName: 'Smith', 46 | username: 'test@gmail.com' 47 | } 48 | } 49 | 50 | const badReqBody = {} 51 | 52 | describe('createPost', () => { 53 | 54 | beforeEach(() => { 55 | next.mockClear() 56 | }) 57 | 58 | it('Returns newly created post object', async () => { 59 | const req = { 60 | body: goodReqBody 61 | }; 62 | const res = { 63 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 64 | }; 65 | 66 | await createPost(req, res, next); 67 | expect(res.status).toHaveBeenCalledWith(200); 68 | expect(res.status().json).toHaveBeenCalledWith({}); 69 | }); 70 | 71 | it('Returns 400 status with bad request body', async () => { 72 | const req = { 73 | body: badReqBody 74 | }; 75 | const res = { 76 | sendStatus: sendStatus 77 | }; 78 | 79 | await createPost(req, res, next); 80 | const [status] = sendStatus.mock.calls[0]; 81 | expect(status).toEqual(400); 82 | }); 83 | 84 | xit('Returns 500 status with bad database request', async () => { 85 | jest.mock('../../db/postModel', () => ({ 86 | create: null 87 | })); 88 | 89 | const req = { 90 | body: badReqBody 91 | }; 92 | const res = { 93 | sendStatus: sendStatus 94 | }; 95 | 96 | await createPost(req, res, next); 97 | const [error] = next.mock.calls[0]; 98 | expect(error).toEqual({ 99 | log: `ERROR: createPost, undefined`, 100 | status: 500, 101 | message: {err: 'an error occurred while attempting to create a post'} 102 | }); 103 | }); 104 | }); -------------------------------------------------------------------------------- /server/controllers/post/deletePost.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../util/logger'); 2 | const Post = require('../../db/postModel'); 3 | 4 | const deletePost = async (req, res, next) => { 5 | const id = req.params._id; 6 | if (!id) { 7 | logger.error('ERROR: Delete Post, could not resolve post id'); 8 | return next({ 9 | log: 'ERROR: deletePost', 10 | status: 400, 11 | message: { err: 'Cannot resolve id from params' }, 12 | }); 13 | } 14 | 15 | try { 16 | const queryResult = await Post.deleteOne({ _id: id }); 17 | if (!queryResult.acknowledged) { 18 | logger.error(`ERROR: Delete Post, conflict in database. Unable to delete post ${id}`); 19 | return res.status(409).json(queryResult); 20 | } 21 | 22 | logger.info(`SUCCESS: Deleted post ${id}`); 23 | return res.status(200).json(queryResult); 24 | } catch (err) { 25 | logger.error('ERROR: Delete Post, an error occurred while attempting to delete a post in profile'); 26 | return next({ 27 | log: `ERROR: deletePost, ${err}`, 28 | status: 500, 29 | message: { err: 'an error occurred while attempting to delete a post in profile' }, 30 | }); 31 | } 32 | }; 33 | 34 | module.exports = deletePost; 35 | -------------------------------------------------------------------------------- /server/controllers/post/deletePost.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const deletePost = require('./deletePost'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('deletePost', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns acknowledgement for deleted object', async () => { 19 | const req = { 20 | params: { _id: 123 } 21 | }; 22 | const res = { 23 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 24 | }; 25 | 26 | Post.deleteOne.mockResolvedValue({ acknowledged: true }); 27 | 28 | await deletePost(req, res, next); 29 | expect(res.status).toHaveBeenCalledWith(200); 30 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 31 | }); 32 | 33 | it('Returns 409 status with no acknowledgement from database', async () => { 34 | const req = { 35 | params: { _id: 123 } 36 | }; 37 | const res = { 38 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 39 | }; 40 | 41 | Post.deleteOne.mockResolvedValue({ acknowledged: false }); 42 | 43 | await deletePost(req, res, next); 44 | expect(res.status).toHaveBeenCalledWith(409); 45 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: false }); 46 | }); 47 | 48 | it('Returns 400 status with no params id', async () => { 49 | const req = { 50 | params: {} 51 | }; 52 | const res = {}; 53 | 54 | await deletePost(req, res, next); 55 | const [error] = next.mock.calls[0]; 56 | expect(error).toEqual({ 57 | log: `ERROR: deletePost`, 58 | status: 400, 59 | message: {err: 'Cannot resolve id from params'} 60 | }); 61 | }); 62 | 63 | it('Returns 500 status with error in database request', async () => { 64 | const req = { 65 | params: { _id: 123 } 66 | }; 67 | const res = {}; 68 | 69 | Post.deleteOne.mockRejectedValue(new Error('error')); 70 | 71 | await deletePost(req, res, next); 72 | const [error] = next.mock.calls[0]; 73 | expect(error).toEqual({ 74 | log: "ERROR: deletePost, Error: error", 75 | status: 500, 76 | message: {err: 'an error occurred while attempting to delete a post in profile'} 77 | }); 78 | }); 79 | }); -------------------------------------------------------------------------------- /server/controllers/post/filterPostsByDistance.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../db/postModel'); 2 | 3 | const filterPostsByDistance = async (req, res, next) => { 4 | const { 5 | lng, lat, minDistance, maxDistance, 6 | } = req.body; 7 | const { username } = req.params; 8 | if (username === undefined || lng === undefined || lat === undefined 9 | || minDistance === undefined || maxDistance === undefined) { 10 | return res.sendStatus(400); 11 | } 12 | 13 | try { 14 | const queryResult = await Post.find({ 15 | geoData: { 16 | $near: { 17 | $geometry: { type: 'Point', coordinates: [lng, lat] }, 18 | $minDistance: minDistance, 19 | $maxDistance: maxDistance, 20 | }, 21 | }, 22 | 'userData.username': { $ne: username }, 23 | }); 24 | return res.status(200).json(queryResult); 25 | } catch (err) { 26 | return next({ 27 | log: `ERROR: filterPostsByDistance, ${err}`, 28 | status: 500, 29 | message: { err: 'an error occurred while attempting to filter for posts' }, 30 | }); 31 | } 32 | }; 33 | 34 | module.exports = filterPostsByDistance; 35 | -------------------------------------------------------------------------------- /server/controllers/post/filterPostsByDistance.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const filterPostsByDistance = require('./filterPostsByDistance'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const sendStatus = jest.fn() 11 | const next = jest.fn(); 12 | 13 | describe('filterPostsByDistance', () => { 14 | 15 | beforeEach(() => { 16 | next.mockClear() 17 | }) 18 | 19 | it('Returns found posts', async () => { 20 | const req = { 21 | params: { username: 'test@gmail.com'}, 22 | body: { 23 | lng: 10, 24 | lat: 10, 25 | minDistance: 1, 26 | maxDistance: 2 27 | } 28 | }; 29 | const res = { 30 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 31 | }; 32 | 33 | Post.find.mockResolvedValue({ acknowledged: true }); 34 | 35 | await filterPostsByDistance(req, res, next); 36 | expect(res.status).toHaveBeenCalledWith(200); 37 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 38 | }); 39 | 40 | it('Returns 400 status with no params username', async () => { 41 | const req = { 42 | params: {}, 43 | body: { 44 | lng: 10, 45 | lat: 10, 46 | minDistance: 1, 47 | maxDistance: 2 48 | } 49 | }; 50 | const res = { 51 | sendStatus: sendStatus 52 | }; 53 | 54 | await filterPostsByDistance(req, res, next); 55 | expect(sendStatus).toHaveBeenCalledWith(400) 56 | }); 57 | 58 | it('Returns 400 status with bad request body', async () => { 59 | const req = { 60 | params: { username: 'test@gmail.com'}, 61 | body: {} 62 | }; 63 | const res = { 64 | sendStatus: sendStatus 65 | }; 66 | 67 | await filterPostsByDistance(req, res, next); 68 | expect(sendStatus).toHaveBeenCalledWith(400) 69 | }); 70 | 71 | it('Returns 500 status with error in database request', async () => { 72 | const req = { 73 | params: { username: 'test@gmail.com'}, 74 | body: { 75 | lng: 10, 76 | lat: 10, 77 | minDistance: 1, 78 | maxDistance: 2 79 | } 80 | }; 81 | const res = {}; 82 | 83 | Post.find.mockRejectedValue(new Error('error')); 84 | 85 | await filterPostsByDistance(req, res, next); 86 | const [error] = next.mock.calls[0]; 87 | expect(error).toEqual({ 88 | log: "ERROR: filterPostsByDistance, Error: error", 89 | status: 500, 90 | message: {err: 'an error occurred while attempting to filter for posts'} 91 | }); 92 | }); 93 | }); -------------------------------------------------------------------------------- /server/controllers/post/getAllPosts.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../util/logger'); 2 | const Post = require('../../db/postModel'); 3 | 4 | const getAllPosts = async (req, res, next) => { 5 | const { username } = req.params; 6 | try { 7 | // updated query results - find everything where username is not username 8 | const queryResult = await Post.find({ 'userData.username': { $ne: username } }); 9 | 10 | if (queryResult) { 11 | logger.info(`SUCCESS: Retrieved all posts, user: ${username}`); 12 | return res.status(200).json(queryResult); 13 | } 14 | 15 | logger.error(`ERROR: Could not get all posts, user: ${username}`); 16 | return next({ 17 | log: 'ERROR: getAllPosts', 18 | status: 404, 19 | message: { err: 'Cannot find posts for user' }, 20 | }); 21 | } catch (err) { 22 | logger.error(`ERROR: getAllPosts, user: ${username}, ${err}`); 23 | return next({ 24 | log: `ERROR: getAllPosts, ${err}`, 25 | status: 500, 26 | message: { err: 'an error occurred while attempting to get a post' }, 27 | }); 28 | } 29 | }; 30 | 31 | module.exports = getAllPosts; 32 | -------------------------------------------------------------------------------- /server/controllers/post/getAllPosts.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const getAllPosts = require('./getAllPosts'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('getAllPosts', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns found posts', async () => { 19 | const req = { 20 | params: { username: 'test@gmail.com'}, 21 | }; 22 | const res = { 23 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 24 | }; 25 | 26 | Post.find.mockResolvedValue({ acknowledged: true }); 27 | 28 | await getAllPosts(req, res, next); 29 | expect(res.status).toHaveBeenCalledWith(200); 30 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 31 | }); 32 | 33 | it('Returns 404 status unable to find user in database', async () => { 34 | const req = { 35 | params: { username: 'test@gmail.com'}, 36 | }; 37 | const res = {}; 38 | 39 | Post.find.mockResolvedValue(false); 40 | 41 | await getAllPosts(req, res, next); 42 | const [error] = next.mock.calls[0]; 43 | expect(error).toEqual({ 44 | log: `ERROR: getAllPosts`, 45 | status: 404, 46 | message: {err: 'Cannot find posts for user'} 47 | }); 48 | }); 49 | 50 | it('Returns 500 status with error in database request', async () => { 51 | const req = { 52 | params: { username: 'test@gmail.com'}, 53 | }; 54 | const res = {}; 55 | 56 | Post.find.mockRejectedValue(new Error('error')); 57 | 58 | await getAllPosts(req, res, next); 59 | const [error] = next.mock.calls[0]; 60 | expect(error).toEqual({ 61 | log: "ERROR: getAllPosts, Error: error", 62 | status: 500, 63 | message: {err: 'an error occurred while attempting to get a post'} 64 | }); 65 | }); 66 | }); -------------------------------------------------------------------------------- /server/controllers/post/getProfilePosts.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../db/postModel'); 2 | 3 | const getProfilePosts = async (req, res, next) => { 4 | const { username } = req.params; 5 | if (!username) { 6 | return next({ 7 | log: 'ERROR: getProfilePosts', 8 | status: 400, 9 | message: { err: 'Username not supplied' }, 10 | }); 11 | } 12 | try { 13 | const queryResult = await Post.find({ 'userData.username': username }); 14 | return res.status(200).json(queryResult); 15 | } catch (err) { 16 | return next({ 17 | log: `ERROR: getProfilePosts, ${err}`, 18 | status: 500, 19 | message: { err: 'an error occurred while attempting to get user posts' }, 20 | }); 21 | } 22 | }; 23 | 24 | module.exports = getProfilePosts; 25 | -------------------------------------------------------------------------------- /server/controllers/post/getProfilePosts.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const getProfilePosts = require('./getProfilePosts'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('getProfilePosts', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns found posts', async () => { 19 | const req = { 20 | params: { username: 'test@gmail.com'}, 21 | }; 22 | const res = { 23 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 24 | }; 25 | 26 | Post.find.mockResolvedValue({ acknowledged: true }); 27 | 28 | await getProfilePosts(req, res, next); 29 | expect(res.status).toHaveBeenCalledWith(200); 30 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 31 | }); 32 | 33 | it('Returns 404 status unable to find user in database', async () => { 34 | const req = { 35 | params: {}, 36 | }; 37 | const res = {}; 38 | 39 | await getProfilePosts(req, res, next); 40 | const [error] = next.mock.calls[0]; 41 | expect(error).toEqual({ 42 | log: `ERROR: getProfilePosts`, 43 | status: 400, 44 | message: {err: 'Username not supplied'} 45 | }); 46 | }); 47 | 48 | it('Returns 500 status with error in database request', async () => { 49 | const req = { 50 | params: { username: 'test@gmail.com'}, 51 | }; 52 | const res = {}; 53 | 54 | Post.find.mockRejectedValue(new Error('error')); 55 | 56 | await getProfilePosts(req, res, next); 57 | const [error] = next.mock.calls[0]; 58 | expect(error).toEqual({ 59 | log: "ERROR: getProfilePosts, Error: error", 60 | status: 500, 61 | message: {err: 'an error occurred while attempting to get user posts'} 62 | }); 63 | }); 64 | }); -------------------------------------------------------------------------------- /server/controllers/post/index.js: -------------------------------------------------------------------------------- 1 | exports.createPost = require('./createPost'); 2 | exports.deletePost = require('./deletePost'); 3 | exports.filterPostsByDistance = require('./filterPostsByDistance'); 4 | exports.getAllPosts = require('./getAllPosts'); 5 | exports.getProfilePosts = require('./getProfilePosts'); 6 | exports.removeImage = require('./removeImage'); 7 | exports.updateApplicationPost = require('./updateApplicationPost'); 8 | exports.updatePost = require('./updatePost'); 9 | -------------------------------------------------------------------------------- /server/controllers/post/removeImage.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../util/logger'); 2 | const Post = require('../../db/postModel'); 3 | 4 | const removeImage = async (req, res, next) => { 5 | const { imgUrl, imgPath } = req.body; 6 | const id = req.params._id; 7 | 8 | if (!imgUrl || !imgPath || !id) { 9 | logger.error(`ERROR: Remove Image, could not resolve input in update application post: ${id}`); 10 | return next({ 11 | log: 'ERROR: removeImage', 12 | status: 400, 13 | message: { err: 'Could not resolve input in update application post' }, 14 | }); 15 | } 16 | 17 | /** 18 | * NEED LOGIC TO REMOVE IMAGES FROM FIREBASE 19 | */ 20 | 21 | try { 22 | const queryResult = await Post.updateOne( 23 | { _id: id }, 24 | { $pull: { images: { imgUrl } } }, 25 | { new: true }, 26 | ); 27 | 28 | if (queryResult.modifiedCount === 0) { 29 | logger.error(`ERROR: Remove image, conflict in database. Unable to modify image array ${id}`); 30 | return res.status(409).send(queryResult); 31 | } 32 | 33 | logger.info(`SUCCESS: Removed image from ${id}`); 34 | return res.status(200).send(queryResult); 35 | } catch (err) { 36 | logger.error('ERROR: Remove image, an error occurred while attempting to remove image from post'); 37 | return next({ 38 | log: `ERROR: removeImage, ${err}`, 39 | status: 500, 40 | message: { err: 'an error occurred while attempting to remove image from post' }, 41 | }); 42 | } 43 | }; 44 | 45 | module.exports = removeImage; 46 | -------------------------------------------------------------------------------- /server/controllers/post/removeImage.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const removeImage = require('./removeImage'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('removeImage', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns modified count and acknowledgement from database', async () => { 19 | const req = { 20 | params: { _id: 'test-id' }, 21 | body: { 22 | imgUrl: 'testUrl', 23 | imgPath: 'testPath' 24 | } 25 | }; 26 | const res = { 27 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 28 | }; 29 | 30 | Post.updateOne.mockResolvedValue({ modifiedCount: 1 }); 31 | 32 | await removeImage(req, res, next); 33 | expect(res.status).toHaveBeenCalledWith(200); 34 | expect(res.status().send).toHaveBeenCalledWith({ modifiedCount: 1 }); 35 | }); 36 | 37 | it('Returns acknowledged update from database', async () => { 38 | const req = { 39 | params: { _id: 'test-id' }, 40 | body: { 41 | imgUrl: 'testUrl', 42 | imgPath: 'testPath' 43 | } 44 | }; 45 | const res = { 46 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 47 | }; 48 | 49 | Post.updateOne.mockResolvedValue({ modifiedCount: 0 }); 50 | 51 | await removeImage(req, res, next); 52 | expect(res.status).toHaveBeenCalledWith(409); 53 | expect(res.status().send).toHaveBeenCalledWith({ modifiedCount: 0 }); 54 | }); 55 | 56 | it('Returns 400 status with no params _id', async () => { 57 | const req = { 58 | params: {}, 59 | body: { 60 | imgUrl: 'testUrl', 61 | imgPath: 'testPath' 62 | } 63 | }; 64 | const res = {} 65 | 66 | await removeImage(req, res, next); 67 | const [error] = next.mock.calls[0]; 68 | expect(error).toEqual({ 69 | log: `ERROR: removeImage`, 70 | status: 400, 71 | message: {err: 'Could not resolve input in update application post'} 72 | }); 73 | }); 74 | 75 | it('Returns 400 status with bad request body', async () => { 76 | const req = { 77 | params: { _id: 'test-id' }, 78 | body: {} 79 | }; 80 | const res = {} 81 | 82 | await removeImage(req, res, next); 83 | const [error] = next.mock.calls[0]; 84 | expect(error).toEqual({ 85 | log: `ERROR: removeImage`, 86 | status: 400, 87 | message: {err: 'Could not resolve input in update application post'} 88 | }); 89 | }); 90 | 91 | it('Returns 500 status with error in database request', async () => { 92 | const req = { 93 | params: { _id: 'test-id' }, 94 | body: { 95 | imgUrl: 'testUrl', 96 | imgPath: 'testPath' 97 | } 98 | }; 99 | const res = {}; 100 | 101 | Post.updateOne.mockRejectedValue(new Error('error')); 102 | 103 | await removeImage(req, res, next); 104 | const [error] = next.mock.calls[0]; 105 | expect(error).toEqual({ 106 | log: "ERROR: removeImage, Error: error", 107 | status: 500, 108 | message: {err: 'an error occurred while attempting to remove image from post'} 109 | }); 110 | }); 111 | }); -------------------------------------------------------------------------------- /server/controllers/post/updateApplicationPost.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../db/postModel'); 2 | 3 | const updateApplicationPost = async (req, res, next) => { 4 | const { firstName, lastName, username } = req.body; 5 | const id = req.params._id; 6 | 7 | if (!firstName || !lastName || !username || !id) { 8 | return next({ 9 | log: 'ERROR: updateApplicationPost', 10 | status: 400, 11 | message: { err: 'Could not resolve input in update application post' }, 12 | }); 13 | } 14 | 15 | try { 16 | // Step1: Find the post matching the id 17 | const foundPost = await Post.findOne({ _id: id }); 18 | 19 | // Step2: Linear search the post's applicantData array 20 | // and find a match with applicantData.username 21 | foundPost.applicantData.map((applicant) => { 22 | if (applicant.username === username) return res.status(409).send(false); 23 | }); 24 | 25 | const newApplicant = { 26 | firstName, 27 | lastName, 28 | username, 29 | }; 30 | 31 | // continue if user is not applicant 32 | const queryResult = await Post.updateOne( 33 | { _id: id }, 34 | { $push: { applicantData: newApplicant } }, 35 | ); 36 | return res.status(201).send(queryResult.acknowledged); 37 | } catch (err) { 38 | return next({ 39 | log: `ERROR: updateApplicationPost, ${err}`, 40 | status: 500, 41 | message: { err: 'an error occurred while attempting to update the applications # int he posts' }, 42 | }); 43 | } 44 | }; 45 | 46 | module.exports = updateApplicationPost; 47 | -------------------------------------------------------------------------------- /server/controllers/post/updateApplicationPost.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const updateApplicationPost = require('./updateApplicationPost'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('updateApplicationPost', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns acknowledgement from database', async () => { 19 | const req = { 20 | params: { _id: 123 }, 21 | body: { 22 | firstName: 'John', 23 | lastName: 'Smith', 24 | username: 'test@gmail.com' 25 | } 26 | }; 27 | const res = { 28 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 29 | }; 30 | 31 | Post.findOne.mockResolvedValue({ 32 | applicantData: [] 33 | }); 34 | 35 | Post.updateOne.mockResolvedValue({ acknowledged: true }) 36 | 37 | await updateApplicationPost(req, res, next); 38 | expect(res.status).toHaveBeenCalledWith(201); 39 | expect(res.status().send).toHaveBeenCalledWith(true); 40 | }); 41 | 42 | it('Returns 409 if applicant already applied', async () => { 43 | const req = { 44 | params: { _id: 123 }, 45 | body: { 46 | firstName: 'John', 47 | lastName: 'Smith', 48 | username: 'test@gmail.com' 49 | } 50 | }; 51 | const res = { 52 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 53 | }; 54 | 55 | Post.findOne.mockResolvedValue({ 56 | applicantData: [{ username: 'test@gmail.com' }] 57 | }); 58 | 59 | await updateApplicationPost(req, res, next); 60 | expect(res.status).toHaveBeenCalledWith(409); 61 | expect(res.status().send).toHaveBeenCalledWith(false); 62 | }); 63 | 64 | it('Returns 400 status with no params _id', async () => { 65 | const req = { 66 | params: {}, 67 | body: { 68 | firstName: 'John', 69 | lastName: 'Smith', 70 | username: 'test@gmail.com' 71 | } 72 | }; 73 | const res = {} 74 | 75 | await updateApplicationPost(req, res, next); 76 | const [error] = next.mock.calls[0]; 77 | expect(error).toEqual({ 78 | log: `ERROR: updateApplicationPost`, 79 | status: 400, 80 | message: {err: 'Could not resolve input in update application post'} 81 | }); 82 | }); 83 | 84 | it('Returns 400 status with bad request body', async () => { 85 | const req = { 86 | params: { _id: 123 }, 87 | body: {} 88 | }; 89 | const res = {} 90 | 91 | await updateApplicationPost(req, res, next); 92 | const [error] = next.mock.calls[0]; 93 | expect(error).toEqual({ 94 | log: `ERROR: updateApplicationPost`, 95 | status: 400, 96 | message: {err: 'Could not resolve input in update application post'} 97 | }); 98 | }); 99 | 100 | it('Returns 500 status with error in database request', async () => { 101 | const req = { 102 | params: { _id: 123 }, 103 | body: { 104 | firstName: 'John', 105 | lastName: 'Smith', 106 | username: 'test@gmail.com' 107 | } 108 | }; 109 | const res = {}; 110 | 111 | Post.findOne.mockRejectedValue(new Error('error')); 112 | 113 | await updateApplicationPost(req, res, next); 114 | const [error] = next.mock.calls[0]; 115 | expect(error).toEqual({ 116 | log: "ERROR: updateApplicationPost, Error: error", 117 | status: 500, 118 | message: {err: 'an error occurred while attempting to update the applications # int he posts'} 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /server/controllers/post/updatePost.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../db/postModel'); 2 | 3 | const updatePost = async (req, res, next) => { 4 | const { 5 | address, rent, roommate, description, moveInDate, bio, geoData, 6 | } = req.body; 7 | const id = req.params._id; 8 | 9 | if (!address || !rent || !roommate || !description || !moveInDate || !bio || !geoData || !id) { 10 | return next({ 11 | log: 'ERROR: updatePost', 12 | status: 400, 13 | message: { err: 'Could not resolve input in update application post' }, 14 | }); 15 | } 16 | 17 | try { 18 | const queryResult = await Post.updateOne( 19 | { _id: id }, 20 | { 21 | $set: { 22 | address, 23 | rent, 24 | roommate, 25 | description, 26 | moveInDate, 27 | bio, 28 | geoData: { 29 | type: 'Point', 30 | coordinates: [geoData.lng, geoData.lat], 31 | lat: geoData.lat, 32 | lng: geoData.lng, 33 | }, 34 | }, 35 | }, 36 | ); 37 | 38 | return res.status(200).send(queryResult.acknowledged); 39 | } catch (err) { 40 | return next({ 41 | log: `ERROR: updatePost, ${err}`, 42 | status: 500, 43 | message: { err: 'an error occurred while attempting to update the posts' }, 44 | }); 45 | } 46 | }; 47 | 48 | module.exports = updatePost; 49 | -------------------------------------------------------------------------------- /server/controllers/post/updatePost.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const updatePost = require('./updatePost'); 6 | const Post = require('../../db/postModel'); 7 | 8 | jest.mock('../../db/postModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('updatePost', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns acknowledgement from database', async () => { 19 | const req = { 20 | params: { _id: 123 }, 21 | body: { 22 | address: 'address', 23 | rent: 100, 24 | roommate: 'roommate', 25 | description: 'description', 26 | moveInDate: Date.now(), 27 | bio: 'bio', 28 | geoData: 'geoData' 29 | } 30 | }; 31 | const res = { 32 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 33 | }; 34 | 35 | Post.updateOne.mockResolvedValue({ acknowledged: true }); 36 | 37 | await updatePost(req, res, next); 38 | expect(res.status).toHaveBeenCalledWith(200); 39 | expect(res.status().send).toHaveBeenCalledWith(true); 40 | }); 41 | 42 | it('Returns 400 status with no params _id', async () => { 43 | const req = { 44 | params: {}, 45 | body: { 46 | address: 'address', 47 | rent: 100, 48 | roommate: 'roommate', 49 | description: 'description', 50 | moveInDate: Date.now(), 51 | bio: 'bio', 52 | geoData: 'geoData' 53 | } 54 | }; 55 | const res = {} 56 | 57 | await updatePost(req, res, next); 58 | const [error] = next.mock.calls[0]; 59 | expect(error).toEqual({ 60 | log: `ERROR: updatePost`, 61 | status: 400, 62 | message: {err: 'Could not resolve input in update application post'} 63 | }); 64 | }); 65 | 66 | it('Returns 400 status with bad request body', async () => { 67 | const req = { 68 | params: { _id: 123 }, 69 | body: {} 70 | }; 71 | const res = {} 72 | 73 | await updatePost(req, res, next); 74 | const [error] = next.mock.calls[0]; 75 | expect(error).toEqual({ 76 | log: `ERROR: updatePost`, 77 | status: 400, 78 | message: {err: 'Could not resolve input in update application post'} 79 | }); 80 | }); 81 | 82 | it('Returns 500 status with error in database request', async () => { 83 | const req = { 84 | params: { _id: 123 }, 85 | body: { 86 | address: 'address', 87 | rent: 100, 88 | roommate: 'roommate', 89 | description: 'description', 90 | moveInDate: Date.now(), 91 | bio: 'bio', 92 | geoData: 'geoData' 93 | } 94 | }; 95 | const res = {}; 96 | 97 | Post.updateOne.mockRejectedValue(new Error('error')); 98 | 99 | await updatePost(req, res, next); 100 | const [error] = next.mock.calls[0]; 101 | expect(error).toEqual({ 102 | log: "ERROR: updatePost, Error: error", 103 | status: 500, 104 | message: {err: 'an error occurred while attempting to update the posts'} 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /server/controllers/session/deleteSession.js: -------------------------------------------------------------------------------- 1 | const Session = require('../../db/sessionModel'); 2 | 3 | const deleteSession = async (req, res, next) => { 4 | const { cookieId } = res.locals; 5 | try { 6 | const response = await Session.deleteOne({ cookieId }); 7 | return res.status(204).send(true); 8 | } catch (err) { 9 | return next({ 10 | log: `ERROR: deleteSession, ${err}`, 11 | status: 500, 12 | message: { err: 'An error occurred while attempting to delete session in signout' }, 13 | }); 14 | } 15 | }; 16 | 17 | module.exports = deleteSession; 18 | -------------------------------------------------------------------------------- /server/controllers/session/deleteSession.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const deleteSession = require('./deleteSession'); 6 | const Session = require('../../db/sessionModel'); 7 | 8 | jest.mock('../../db/sessionModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('deleteSession', () => { 13 | 14 | it('Returns 204 status and true when session is deleted', async () => { 15 | const req = {}; 16 | const res = { 17 | locals: { cookieId: 'cookie-id'}, 18 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 19 | }; 20 | 21 | Session.deleteOne.mockResolvedValue({}); 22 | 23 | await deleteSession(req, res, next); 24 | expect(res.status).toHaveBeenCalledWith(204); 25 | expect(res.status().send).toHaveBeenCalledWith(true); 26 | }); 27 | 28 | it('Returns 500 status with error in database request', async () => { 29 | const req = {}; 30 | const res = { 31 | locals: { cookieId: 'cookie-id'}, 32 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 33 | }; 34 | 35 | Session.deleteOne.mockRejectedValue(new Error('error')); 36 | 37 | await deleteSession(req, res, next); 38 | const [error] = next.mock.calls[0]; 39 | expect(error).toEqual({ 40 | log: "ERROR: deleteSession, Error: error", 41 | status: 500, 42 | message: {err: "An error occurred while attempting to delete session in signout"}, 43 | }); 44 | }); 45 | }); -------------------------------------------------------------------------------- /server/controllers/session/findSession.js: -------------------------------------------------------------------------------- 1 | const Session = require('../../db/sessionModel'); 2 | 3 | const findSession = async (req, res, next) => { 4 | try { 5 | const { userId } = res.locals; 6 | 7 | // create a new session in the session model 8 | // the cookie ID value will be the ssid cookie value 9 | const currentSession = await Session.findOne({ cookieId: userId }); 10 | 11 | // check to see if session is found 12 | // redirect to the signup page if session was not found 13 | // otherwise we can return to the next piece of middle ware 14 | if (!currentSession) return res.status(200).send(false); 15 | return next(); 16 | } catch (err) { 17 | return next({ 18 | log: `ERROR: findSession, ${err}`, 19 | status: 500, 20 | message: { err: 'An error occurred while attempting to find a user session' }, 21 | }); 22 | } 23 | }; 24 | 25 | module.exports = findSession; 26 | -------------------------------------------------------------------------------- /server/controllers/session/findSession.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const findSession = require('./findSession'); 6 | const Session = require('../../db/sessionModel'); 7 | 8 | jest.mock('../../db/sessionModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('findSession', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Returns next middleware if session exists', async () => { 19 | const req = {}; 20 | const res = { 21 | locals: { userId: 'user-id'}, 22 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 23 | }; 24 | 25 | Session.findOne.mockResolvedValue({}); 26 | 27 | await findSession(req, res, next); 28 | expect(next).toHaveBeenCalled(); 29 | }); 30 | 31 | it('Returns a 200 status and false if no session exists', async () => { 32 | const req = {}; 33 | const res = { 34 | locals: { userId: 'user-id'}, 35 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 36 | }; 37 | 38 | Session.findOne.mockResolvedValue(null); 39 | 40 | await findSession(req, res, next); 41 | expect(res.status).toHaveBeenCalledWith(200); 42 | expect(res.status().send).toHaveBeenCalledWith(false); 43 | }); 44 | 45 | it('Returns 500 status with error in database request', async () => { 46 | const req = {}; 47 | const res = { 48 | locals: { userId: 'user-id'}, 49 | status: jest.fn().mockReturnValue({ send: jest.fn() }) 50 | }; 51 | 52 | Session.findOne.mockRejectedValue(new Error('error')); 53 | 54 | await findSession(req, res, next); 55 | const [error] = next.mock.calls[0]; 56 | expect(error).toEqual({ 57 | log: "ERROR: findSession, Error: error", 58 | status: 500, 59 | message: {err: `An error occurred while attempting to find a user session`} 60 | }); 61 | }); 62 | }); -------------------------------------------------------------------------------- /server/controllers/session/index.js: -------------------------------------------------------------------------------- 1 | exports.deleteSession = require('./deleteSession'); 2 | exports.findSession = require('./findSession'); 3 | exports.startSession = require('./startSession'); 4 | -------------------------------------------------------------------------------- /server/controllers/session/startSession.js: -------------------------------------------------------------------------------- 1 | const Session = require('../../db/sessionModel'); 2 | 3 | const startSession = async (req, res, next) => { 4 | if (res.locals.user) { 5 | try { 6 | const id = res.locals.user._id.toString(); 7 | await Session.create({ cookieId: id }); 8 | return next(); 9 | } catch (err) { 10 | return next({ 11 | log: `ERROR: startSession, ${err}`, 12 | status: 500, 13 | message: { err: 'an error occured while attempting to start a session' }, 14 | }); 15 | } 16 | } else return next(); 17 | }; 18 | 19 | module.exports = startSession; 20 | -------------------------------------------------------------------------------- /server/controllers/session/startSession.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const startSession = require('./startSession'); 6 | const Session = require('../../db/sessionModel'); 7 | 8 | jest.mock('../../db/sessionModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('startSession', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Creates sessions if passed user object', async () => { 19 | const req = {}; 20 | const res = { 21 | locals: { user: { _id: 'user-id'} } , 22 | }; 23 | 24 | Session.create.mockResolvedValue({}); 25 | 26 | await startSession(req, res, next); 27 | expect(next).toHaveBeenCalled(); 28 | }); 29 | 30 | it('Returns next middleware if no user object', async () => { 31 | const req = {}; 32 | const res = { 33 | locals: {}, 34 | }; 35 | 36 | Session.create.mockResolvedValue({}); 37 | 38 | await startSession(req, res, next); 39 | expect(next).toHaveBeenCalled(); 40 | }); 41 | 42 | it('Returns 500 status with error in database request', async () => { 43 | const req = {}; 44 | const res = { 45 | locals: { user: { _id: 'user-id'} } , 46 | }; 47 | 48 | Session.create.mockRejectedValue(new Error('error')); 49 | 50 | await startSession(req, res, next); 51 | const [error] = next.mock.calls[0]; 52 | expect(error).toEqual({ 53 | log: "ERROR: startSession, Error: error", 54 | status: 500, 55 | message: { err: 'an error occured while attempting to start a session'} 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /server/controllers/user/createUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../db/userModel'); 2 | 3 | const createUser = async (req, res, next) => { 4 | try { 5 | const { 6 | firstName, lastName, username, password, zipCode, 7 | } = req.body; 8 | 9 | // checking if username or password is empty 10 | if (!username || !password) { 11 | return next({ 12 | log: 'ERROR: createUser', 13 | status: 422, 14 | message: { err: 'Username or password missing in userContoller.createUser' }, 15 | }); 16 | } 17 | 18 | const results = await User.findOne({ username }); 19 | 20 | if (results) return res.status(409).json(null); 21 | 22 | // if username/password is not empty, we will create our user 23 | const queryResult = await User.create({ 24 | firstName, lastName, username, password, zipCode, 25 | }); 26 | 27 | return res.status(201).json(queryResult); 28 | } catch (err) { 29 | return next({ 30 | log: `ERROR: createUser, ${err}`, 31 | status: 500, 32 | message: { err: 'an error occurred when attempting to create a user' }, 33 | }); 34 | } 35 | }; 36 | 37 | module.exports = createUser; 38 | -------------------------------------------------------------------------------- /server/controllers/user/createUser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const createUser = require('./createUser'); 6 | const User = require('../../db/userModel'); 7 | 8 | jest.mock('../../db/userModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('createUser', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Creates Users if passed user object', async () => { 19 | const req = { 20 | body: { 21 | firstName: 'John', 22 | lastName: 'Smith', 23 | username: 'test@gmail.com', 24 | password: '123', 25 | zipCode: '12345' 26 | } 27 | }; 28 | const res = { 29 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 30 | }; 31 | 32 | User.findOne.mockResolvedValue(false); 33 | User.create.mockResolvedValue({ acknowledged: true }); 34 | 35 | await createUser(req, res, next); 36 | expect(res.status).toHaveBeenCalledWith(201); 37 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 38 | }); 39 | 40 | it('Returns 409 if user exists', async () => { 41 | const req = { 42 | body: { 43 | firstName: 'John', 44 | lastName: 'Smith', 45 | username: 'test@gmail.com', 46 | password: '123', 47 | zipCode: '12345' 48 | } 49 | }; 50 | const res = { 51 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 52 | }; 53 | 54 | User.findOne.mockResolvedValue(true); 55 | 56 | await createUser(req, res, next); 57 | expect(res.status).toHaveBeenCalledWith(409); 58 | expect(res.status().json).toHaveBeenCalledWith(null); 59 | }); 60 | 61 | it('Returns 422 with incorrect request body', async () => { 62 | const req = { 63 | body: {} 64 | }; 65 | const res = {}; 66 | 67 | await createUser(req, res, next); 68 | const [error] = next.mock.calls[0]; 69 | expect(error).toEqual({ 70 | log: "ERROR: createUser", 71 | status: 422, 72 | message: {err: 'Username or password missing in userContoller.createUser'} 73 | }); 74 | }); 75 | 76 | it('Returns 500 status with error in database request', async () => { 77 | const req = { 78 | body: { 79 | firstName: 'John', 80 | lastName: 'Smith', 81 | username: 'test@gmail.com', 82 | password: '123', 83 | zipCode: '12345' 84 | } 85 | }; 86 | const res = {}; 87 | 88 | User.findOne.mockRejectedValue(new Error('error')); 89 | 90 | await createUser(req, res, next); 91 | const [error] = next.mock.calls[0]; 92 | expect(error).toEqual({ 93 | log: "ERROR: createUser, Error: error", 94 | status: 500, 95 | message: {err: 'an error occurred when attempting to create a user'} 96 | }); 97 | }); 98 | }); -------------------------------------------------------------------------------- /server/controllers/user/findUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../db/userModel'); 2 | 3 | const findUser = async (req, res, next) => { 4 | const { userId } = res.locals; 5 | try { 6 | const data = await User.findOne({ _id: userId }); 7 | if (!data) { 8 | return next({ 9 | log: 'ERROR: findUser', 10 | status: 404, 11 | message: { err: 'Cannot find user based on userId' }, 12 | }); 13 | } 14 | return res.status(200).json(data); 15 | } catch (err) { 16 | return next({ 17 | log: `ERROR: findUser, ${err}`, 18 | status: 500, 19 | message: { err: 'an error occurred while attempting to find a user' }, 20 | }); 21 | } 22 | }; 23 | 24 | module.exports = findUser; 25 | -------------------------------------------------------------------------------- /server/controllers/user/findUser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const findUser = require('./findUser'); 6 | const User = require('../../db/userModel'); 7 | 8 | jest.mock('../../db/userModel'); 9 | 10 | const next = jest.fn(); 11 | 12 | describe('findUser', () => { 13 | 14 | beforeEach(() => { 15 | next.mockClear() 16 | }) 17 | 18 | it('Finds Users if passed userId', async () => { 19 | const req = {}; 20 | const res = { 21 | locals: { userId: 'user-id' }, 22 | status: jest.fn().mockReturnValue({ json: jest.fn() }) 23 | }; 24 | 25 | User.findOne.mockResolvedValue({ acknowledged: true }); 26 | 27 | await findUser(req, res, next); 28 | expect(res.status).toHaveBeenCalledWith(200); 29 | expect(res.status().json).toHaveBeenCalledWith({ acknowledged: true }); 30 | }); 31 | 32 | it('Returns 404 if user does not exist', async () => { 33 | const req = {}; 34 | const res = { 35 | locals: { userId: 'user-id' } 36 | }; 37 | 38 | User.findOne.mockResolvedValue(false); 39 | 40 | await findUser(req, res, next); 41 | const [error] = next.mock.calls[0]; 42 | expect(error).toEqual({ 43 | log: "ERROR: findUser", 44 | status: 404, 45 | message: {err: 'Cannot find user based on userId'} 46 | }); 47 | }); 48 | 49 | it('Returns 500 status with error in database request', async () => { 50 | const req = {}; 51 | const res = { 52 | locals: { userId: 'user-id' } 53 | }; 54 | 55 | User.findOne.mockRejectedValue(new Error('error')); 56 | 57 | await findUser(req, res, next); 58 | const [error] = next.mock.calls[0]; 59 | expect(error).toEqual({ 60 | log: "ERROR: findUser, Error: error", 61 | status: 500, 62 | message: {err: 'an error occurred while attempting to find a user'} 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /server/controllers/user/index.js: -------------------------------------------------------------------------------- 1 | exports.createUser = require('./createUser'); 2 | exports.findUser = require('./findUser'); 3 | exports.verifyUser = require('./verifyUser'); 4 | -------------------------------------------------------------------------------- /server/controllers/user/verifyUser.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | 3 | const User = require('../../db/userModel'); 4 | 5 | const verifyUser = async (req, res, next) => { 6 | const { username, password } = req.body; 7 | if (!username || !password) { 8 | return next({ 9 | log: 'ERROR: verifyUser', 10 | status: 400, 11 | message: { err: 'Could not resolve username or password' }, 12 | }); 13 | } 14 | 15 | try { 16 | const queryResult = await User.findOne({ username }); 17 | 18 | if (!queryResult) { 19 | res.locals.user = null; 20 | return next(); 21 | } 22 | 23 | const comparePass = await bcrypt.compare(password, queryResult.password); 24 | res.locals.user = (comparePass) ? queryResult : null; 25 | return next(); 26 | } catch (err) { 27 | console.log('ERROR: verifyUser', err); 28 | return next({ 29 | log: `ERROR: verifyUser, ${err}`, 30 | status: 500, 31 | message: { err: 'an error occurred while attempting to verify a user' }, 32 | }); 33 | } 34 | }; 35 | 36 | module.exports = verifyUser; 37 | -------------------------------------------------------------------------------- /server/controllers/user/verifyUser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const verifyUser = require('./verifyUser'); 6 | const User = require('../../db/userModel'); 7 | const bcrypt = require('bcryptjs') 8 | 9 | jest.mock('../../db/userModel'); 10 | jest.mock('bcryptjs') 11 | 12 | const next = jest.fn(); 13 | 14 | describe('verifyUser', () => { 15 | 16 | beforeEach(() => { 17 | next.mockClear() 18 | }) 19 | 20 | it('Verfies user credentials', async () => { 21 | const req = { 22 | body: { 23 | username: 'test@gmail.com', 24 | password: '123' 25 | } 26 | }; 27 | const res = { 28 | locals: {}, 29 | }; 30 | 31 | User.findOne.mockResolvedValue(true); 32 | bcrypt.compare.mockResolvedValue(true) 33 | 34 | await verifyUser(req, res, next); 35 | expect(res.locals.user).toEqual(true) 36 | expect(next).toHaveBeenCalledWith(); 37 | }); 38 | 39 | it('Passes no user object to next fn if user not found', async () => { 40 | const req = { 41 | body: { 42 | username: 'badUser', 43 | password: 'badPassword' 44 | } 45 | }; 46 | const res = { 47 | locals: {}, 48 | }; 49 | 50 | User.findOne.mockResolvedValue(false); 51 | 52 | await verifyUser(req, res, next); 53 | expect(res.locals.user).toEqual(null) 54 | expect(next).toHaveBeenCalledWith(); 55 | }); 56 | 57 | it('Passes null value to next fn if password does not match', async () => { 58 | const req = { 59 | body: { 60 | username: 'test@gmail.com', 61 | password: 'badPassword' 62 | } 63 | }; 64 | const res = { 65 | locals: {}, 66 | }; 67 | 68 | User.findOne.mockResolvedValue(true); 69 | bcrypt.compare.mockResolvedValue(false) 70 | 71 | await verifyUser(req, res, next); 72 | expect(res.locals.user).toEqual(null) 73 | expect(next).toHaveBeenCalledWith(); 74 | }); 75 | 76 | it('Returns 400 if bad request body', async () => { 77 | const req = { 78 | body: {} 79 | }; 80 | const res = {}; 81 | 82 | await verifyUser(req, res, next); 83 | const [error] = next.mock.calls[0]; 84 | expect(error).toEqual({ 85 | log: `ERROR: verifyUser`, 86 | status: 400, 87 | message: {err: 'Could not resolve username or password'} 88 | }); 89 | }); 90 | 91 | it('Returns 500 status with error in database request', async () => { 92 | const req = { 93 | body: { 94 | username: 'test@gmail.com', 95 | password: 'badPassword' 96 | } 97 | }; 98 | const res = { 99 | locals: {}, 100 | }; 101 | 102 | User.findOne.mockRejectedValue(new Error('error')); 103 | 104 | await verifyUser(req, res, next); 105 | const [error] = next.mock.calls[0]; 106 | expect(error).toEqual({ 107 | log: "ERROR: verifyUser, Error: error", 108 | status: 500, 109 | message: {err: 'an error occurred while attempting to verify a user'} 110 | }); 111 | }); 112 | }); -------------------------------------------------------------------------------- /server/db/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const logger = require('../util/logger'); 3 | 4 | mongoose 5 | .connect(process.env.ATLAS_URI, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | dbName: 'findARoommate', 9 | }) 10 | .then(() => { 11 | console.log('SUCCESS: Connected to MongoDB!'); 12 | logger.info('SUCCESS: Connected to MongoDB!'); 13 | }) 14 | .catch((err) => { 15 | console.log('ERROR: Unable to connect to MongoDB!', err); 16 | logger.error('ERROR: Unable to connect to MongoDB!', err); 17 | }); 18 | 19 | module.exports = mongoose.connection; 20 | -------------------------------------------------------------------------------- /server/db/metadata.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const metadata = Schema({ 6 | username: { type: String, required: true }, 7 | firstName: { type: String, required: true }, 8 | lastName: { type: String, required: true }, 9 | createdAt: { type: Date, required: true, default: Date.now }, 10 | 11 | // from navBar 12 | id: { type: String, required: true }, 13 | // profilePicture: {type: Buffer}, 14 | }); 15 | 16 | const MetaData = mongoose.model('Metadata', metadata); 17 | 18 | module.exports = MetaData; 19 | -------------------------------------------------------------------------------- /server/db/postModel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const mongoose = require('mongoose'); 3 | 4 | const { Schema } = mongoose; 5 | 6 | const post = Schema({ 7 | picture: { type: Array }, 8 | address: { 9 | street1: { type: String, required: true }, 10 | street2: { type: String }, 11 | city: { type: String, required: true }, 12 | state: { type: String, required: true }, 13 | zipCode: { type: String, required: true }, 14 | }, 15 | roommate: { 16 | gender: { type: String, required: true, default: 'No Preference' }, 17 | }, 18 | description: { 19 | BR: { type: Number }, 20 | BA: { type: Number }, 21 | sqFt: { type: Number }, 22 | pets: { type: Boolean }, 23 | smoking: { type: Boolean }, 24 | parking: { type: Boolean }, 25 | condition: { type: String }, 26 | }, 27 | moveInDate: { type: Date, default: Date.now }, 28 | utilities: { type: Number, required: true }, 29 | rent: { type: Number, required: true }, 30 | bio: { type: String }, 31 | userData: { 32 | firstName: { type: String }, 33 | lastName: { type: String }, 34 | username: { type: String }, 35 | }, 36 | applicantData: { type: Array }, 37 | geoData: { 38 | type: { type: String }, // Type: 'Point' (GeoJSON Object) 39 | coordinates: { type: Array }, // [long, lat] 40 | lat: { type: Number }, 41 | lng: { type: Number }, 42 | }, 43 | images: { type: Array }, 44 | }); 45 | 46 | const Post = mongoose.model('post', post); 47 | 48 | module.exports = Post; 49 | -------------------------------------------------------------------------------- /server/db/sessionModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const sessionSchema = new Schema({ 6 | cookieId: { type: String, required: true, unique: true }, 7 | createdAt: { type: Date, expires: 600, default: Date.now }, 8 | }); 9 | 10 | module.exports = mongoose.model('Session', sessionSchema); 11 | -------------------------------------------------------------------------------- /server/db/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | // Bcrypt library to help encrypt our password 6 | const bcrypt = require('bcryptjs'); 7 | 8 | const SALT_FACTOR = 10; 9 | 10 | const user = Schema({ 11 | firstName: { type: String, required: true }, 12 | lastName: { type: String, required: true }, 13 | username: { type: String, required: true, unique: true }, 14 | password: { type: String, required: true }, 15 | zipCode: { type: String }, 16 | }); 17 | 18 | // Prefunction that uses bcrypt to hash our password and resave 19 | // into our database 20 | user.pre('save', function (next) { 21 | const user = this; 22 | 23 | // bcrypt hash function here 24 | bcrypt.hash(user.password, SALT_FACTOR, (err, hash) => { 25 | if (err) return next(err); 26 | user.password = hash; 27 | return next(); 28 | }); 29 | }); 30 | 31 | const User = mongoose.model('User', user); 32 | 33 | module.exports = User; 34 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | exports.user = require('./user'); 2 | exports.oauth = require('./oauth'); 3 | -------------------------------------------------------------------------------- /server/routes/oauth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | const passport = require('passport'); 5 | 6 | const { startSession } = require('../controllers/session'); 7 | 8 | router.get( 9 | '/', 10 | passport.authenticate('google', { scope: ['profile', 'email'] }), 11 | ); 12 | 13 | router.get( 14 | '/callback', 15 | passport.authenticate('google', { failureDirect: '/' }), 16 | (req, res, next) => { 17 | res.cookie('ssid', req.session.passport.user._id); 18 | const user = { _id: req.session.passport.user._id }; 19 | res.locals.user = req.session.passport.user; 20 | next(); 21 | }, 22 | startSession, 23 | (req, res) => res.redirect('/'), 24 | ); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | 4 | const router = express.Router(); 5 | 6 | // Require all controllers 7 | const user = require('../controllers/user'); 8 | const post = require('../controllers/post'); 9 | const cookie = require('../controllers/cookie'); 10 | const session = require('../controllers/session'); 11 | 12 | // Serve index.html to client 13 | router.get('/health', (req, res) => res.sendStatus(200)); 14 | router.get('/home/:username', post.getAllPosts); 15 | router.get( 16 | '/currentSession', 17 | cookie.getCookie, 18 | session.findSession, 19 | user.findUser, 20 | ); 21 | router.get('/profile/:username', post.getProfilePosts); 22 | router.get('/signout', cookie.deleteCookie, session.deleteSession); 23 | // router.get('/findUser', 24 | // user.findUser, 25 | // (req, res) => { 26 | // // post.createPost 27 | // return res.status(200).json({}); 28 | // }); 29 | router.get('/', (req, res) => res.status(201).sendFile(path.resolve(__dirname, '../../index.html'))); 30 | 31 | router.post('/signup', user.createUser); 32 | router.post('/home/:username', post.filterPostsByDistance); 33 | router.post('/createPost', post.createPost); 34 | router.post( 35 | '/oauth', 36 | user.verifyUser, 37 | (req, res) => res.status(200).json(res.locals.user), 38 | ); 39 | router.post( 40 | '/', 41 | user.verifyUser, 42 | cookie.setSSIDCookie, 43 | session.startSession, 44 | (req, res) => res.status(200).json(res.locals.user), 45 | ); 46 | 47 | router.patch('/home/:_id', post.updateApplicationPost); 48 | router.patch('/posts/update/:_id', post.updatePost); 49 | router.patch('/posts/image/remove/:_id', post.removeImage); 50 | 51 | router.delete('/posts/:_id', post.deletePost); 52 | 53 | module.exports = router; 54 | -------------------------------------------------------------------------------- /server/routes/user.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const request = require('supertest'); 6 | const express = require('express') 7 | 8 | const app = express() 9 | app.use('/', require('./index').user); 10 | 11 | describe('API Test', () => { 12 | 13 | test('GET /', (done) => { 14 | request(app) 15 | .get('/') 16 | .expect(201) 17 | .expect("Content-Type", /html/) 18 | .end(function(err, res) { 19 | if (err) throw err; 20 | return done() 21 | }) 22 | }); 23 | 24 | test('GET Bad Route Request', (done) => { 25 | request(app) 26 | .get('/abc') 27 | .expect(404) 28 | .end(function(err, res) { 29 | if (err) throw err; 30 | return done() 31 | }) 32 | }); 33 | 34 | 35 | 36 | }); -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const express = require('express'); 3 | const cookieparser = require('cookie-parser'); 4 | const cors = require('cors'); 5 | const expressSession = require('express-session'); 6 | const path = require('path'); 7 | const passport = require('passport'); 8 | 9 | require('dotenv').config(); 10 | 11 | const { SERVER_PORT } = require('config'); 12 | const logger = require('./util/logger'); 13 | const { startSession } = require('./controllers/session'); 14 | const db = require('./db/db'); 15 | 16 | const app = express(); 17 | const port = SERVER_PORT || 3000; 18 | 19 | app.use(cookieparser()); 20 | app.use(cors()); 21 | app.use(express.json()); 22 | app.use(express.urlencoded({ extended: true })); 23 | 24 | app.use(express.static(path.join(__dirname, '../client/assets/'))); 25 | app.use(express.static(path.join(__dirname, '../dist/'))); 26 | 27 | // --------------------------------------------------------------------------------------------// 28 | // Route all requests to MainRouter 29 | app.use('/', require('./routes/user')); 30 | 31 | // --------------------------------------------------------------------------------------------// 32 | // oAuth connection 33 | app.use( 34 | expressSession({ 35 | secret: 'google auth', 36 | resave: false, 37 | saveUninitialized: false, 38 | }), 39 | ); 40 | 41 | require('./config/passport')(passport); 42 | 43 | app.use(passport.initialize()); 44 | app.use(passport.session()); 45 | 46 | app.use('/login/auth/google', require('./routes/oauth')); 47 | 48 | // --------------------------------------------------------------------------------------------// 49 | // catch-all route handler for any requests to an unknown route 50 | app.use((req, res) => { 51 | logger.info('ERROR: Unknown route/path'); 52 | res.sendStatus(404); 53 | }); 54 | 55 | // global error handler 56 | app.use((err, req, res, next) => { 57 | const defaultErr = { 58 | log: 'Express error handler caught unknown middleware error', 59 | status: 500, 60 | message: { err: 'An error occurred' }, 61 | }; 62 | const errorObj = { ...defaultErr, ...err }; 63 | logger.error(`${errorObj.log}, STATUS: ${errorObj.status}, MESSAGE: ${errorObj.message.err}`); 64 | return res.status(errorObj.status).json(errorObj.message); 65 | }); 66 | 67 | app.listen(port, () => { 68 | console.log(`SUCCESS: Server is running on port: ${port}`); 69 | logger.info(`SUCCESS: Server is running on port: ${port}`); 70 | }); 71 | 72 | module.exports = app; 73 | -------------------------------------------------------------------------------- /server/server.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const app = require('./server'); 6 | const request = require('supertest'); 7 | 8 | describe('Server', () => { 9 | it('Respond with 200 status code on health', async () => { 10 | const response = await request(app).get('/health'); 11 | expect(response.statusCode).toBe(200); 12 | }); 13 | 14 | xit('Respond with 302 status code', async () => { 15 | const response = await request(app).get('/login/auth/google'); 16 | expect(response.statusCode).toBe(302); 17 | }); 18 | 19 | it('Respond with 404 status code for unknown routes', async () => { 20 | const response = await request(app).get('/unknown'); 21 | expect(response.statusCode).toBe(404); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /server/util/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const { format } = require('winston'); 3 | 4 | const { combine } = format; 5 | 6 | const logger = winston.createLogger({ 7 | level: 'info', 8 | format: combine( 9 | format.timestamp({ 10 | format: 'MM-DD-YYYY HH:mm:ss', 11 | }), 12 | format.json(), 13 | ), 14 | transports: [ 15 | new winston.transports.File({ filename: 'error.log', level: 'error', dirname: 'logs' }), 16 | new winston.transports.File({ filename: 'allLogs.log', dirname: 'logs' }), 17 | ], 18 | }); 19 | 20 | module.exports = logger; 21 | -------------------------------------------------------------------------------- /webpack/common.webpack.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const Dotenv = require('dotenv-webpack'); 5 | 6 | const { 7 | PROTOCOL, DOMAIN, PORT, SERVER_PORT, 8 | } = require('config'); 9 | 10 | module.exports = { 11 | entry: { 12 | app: './client/index.js', 13 | }, 14 | 15 | devtool: 'inline-source-map', 16 | 17 | devServer: { 18 | host: DOMAIN, 19 | port: PORT, 20 | static: { 21 | directory: path.join(__dirname, '..', 'dist'), 22 | publicPath: '/', 23 | }, 24 | client: { 25 | progress: true, 26 | }, 27 | hot: true, 28 | proxy: { 29 | '/': `${PROTOCOL}${DOMAIN}:${SERVER_PORT}`, 30 | compress: true, 31 | port: PORT, 32 | }, 33 | }, 34 | 35 | output: { 36 | publicPath: '', 37 | path: path.join(__dirname, '..', 'dist'), 38 | filename: '[name].[contenthash:5]].js', 39 | clean: true, 40 | }, 41 | 42 | plugins: [ 43 | new HtmlWebpackPlugin({ 44 | template: path.resolve(__dirname, '/index.html'), 45 | filename: 'index.html', 46 | favicon: path.join(__dirname, '..', 'client/assets/roomier.svg'), 47 | }), 48 | new Dotenv(), 49 | ], 50 | 51 | module: { 52 | rules: [ 53 | { 54 | test: /\.jsx?/, 55 | exclude: path.resolve(__dirname, './node_modules'), 56 | use: { 57 | loader: 'babel-loader', 58 | options: { 59 | presets: ['@babel/preset-env', '@babel/preset-react'], 60 | }, 61 | }, 62 | }, 63 | { 64 | test: /\.s?css$/, 65 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader' }], 66 | }, 67 | ], 68 | }, 69 | 70 | }; 71 | -------------------------------------------------------------------------------- /webpack/development.webpack.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const commonConfig = require('./common.webpack'); 5 | 6 | const development = { 7 | mode: 'development', 8 | plugins: [ 9 | new ReactRefreshPlugin(), 10 | new HtmlWebpackPlugin({ 11 | template: path.resolve(__dirname, '/index.html'), 12 | filename: 'index.html', 13 | favicon: path.join(__dirname, '..', 'client/assets/roomier.svg'), 14 | }), 15 | ].filter(Boolean), 16 | resolve: { 17 | extensions: ['.js', '.jsx'], 18 | }, 19 | }; 20 | 21 | module.exports = { ...commonConfig, ...development }; 22 | -------------------------------------------------------------------------------- /webpack/production.webpack.js: -------------------------------------------------------------------------------- 1 | const commonConfig = require('./common.webpack'); 2 | 3 | const production = { 4 | mode: 'production', 5 | devtool: 'source-map', 6 | optimization: { 7 | moduleIds: 'deterministic', 8 | runtimeChunk: 'single', 9 | minimize: true, 10 | splitChunks: { 11 | cacheGroups: { 12 | vendor: { 13 | name: 'node_vendors', 14 | test: /[\\/]node_modules[\\/]/, 15 | chunks: 'all', 16 | }, 17 | }, 18 | }, 19 | }, 20 | resolve: { 21 | extensions: ['.js', '.jsx'], 22 | }, 23 | }; 24 | 25 | module.exports = { ...commonConfig, ...production }; 26 | --------------------------------------------------------------------------------