├── .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 |
6 |
7 |
8 |
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 |
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 |

47 |
a LabRitz thing
48 |
looking for a Zillow corporate sponsorship
49 |
50 |
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 |
114 |
115 |
116 |
124 |
125 |
126 |
150 |
151 |
152 | navigate('/')} alt={userInfo.firstName} sx={{ width: 112, bgcolor: 'transparent' }} variant="rounded">
153 |
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 |
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 |

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 |
--------------------------------------------------------------------------------