├── .gitignore
├── frontend
├── public
│ ├── manifest.json
│ └── index.html
├── src
│ ├── components
│ │ ├── SearchBar
│ │ │ ├── SearchBar.css
│ │ │ └── index.js
│ │ ├── Landing
│ │ │ ├── index.js
│ │ │ └── Landing.css
│ │ ├── context
│ │ │ ├── SearchContext.js
│ │ │ ├── modal.css
│ │ │ └── Modal.js
│ │ ├── SearchResults
│ │ │ ├── SearchResults.css
│ │ │ └── index.js
│ │ ├── LoginFormModal
│ │ │ ├── DemoUser.js
│ │ │ ├── index.js
│ │ │ ├── LoginForm.css
│ │ │ └── LoginForm.js
│ │ ├── SignupFormModal
│ │ │ ├── index.js
│ │ │ ├── SignupForm.css
│ │ │ └── SignUpForm.js
│ │ ├── Footer
│ │ │ └── index.js
│ │ ├── UserHaunting
│ │ │ ├── CreateUserHauntingModal.js
│ │ │ ├── UserHaunting.css
│ │ │ └── index.js
│ │ ├── CreateUserHaunting
│ │ │ ├── index.js
│ │ │ ├── CreateUserHauntingModal.js
│ │ │ └── CreateUserHaunting.css
│ │ ├── HauntingCard
│ │ │ ├── HauntingCard.css
│ │ │ └── index.js
│ │ ├── Navigation
│ │ │ ├── ProfileButton.js
│ │ │ └── index.js
│ │ ├── HauntingProfile
│ │ │ ├── index.js
│ │ │ └── HauntingProfile.css
│ │ └── BookingDates
│ │ │ └── index.js
│ ├── store
│ │ ├── index.js
│ │ ├── hauntings.js
│ │ ├── csrf.js
│ │ ├── userHauntings.js
│ │ └── session.js
│ ├── index.js
│ ├── App.js
│ └── index.css
├── .gitignore
├── package.json
├── README.md
└── .eslintcache
├── backend
├── .sequelizerc
├── config
│ ├── index.js
│ └── database.js
├── bin
│ └── www
├── db
│ ├── models
│ │ ├── userhaunting.js
│ │ ├── index.js
│ │ ├── haunting.js
│ │ └── user.js
│ ├── migrations
│ │ ├── 20210126173505-price.js
│ │ ├── 20210122042207-create-user.js
│ │ ├── 20210126043124-create-user-haunting.js
│ │ └── 20210126042127-create-haunting.js
│ └── seeders
│ │ ├── 20210221042321-seedUser.js
│ │ └── 20210221042342-seedHauntings.js
├── utils
│ ├── validation.js
│ └── auth.js
├── routes
│ ├── api
│ │ ├── hauntings.js
│ │ ├── userHauntings.js
│ │ ├── users.js
│ │ ├── index.js
│ │ └── session.js
│ └── index.js
├── package.json
├── app.js
└── awsS3.js
├── README.md
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | build
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React Template",
3 | "name": "Create React App Template",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/backend/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | config: path.resolve('config', 'database.js'),
5 | 'models-path': path.resolve('db', 'models'),
6 | 'seeders-path': path.resolve('db', 'seeders'),
7 | 'migrations-path': path.resolve('db', 'migrations'),
8 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scarebnb
2 |
3 |
4 | ## What is it?
5 | * A spooky airbnb clone full of haunts to visit...if you dare.
6 |
7 | ## View the live site!
8 | * https://scarebnb-react.herokuapp.com/
9 |
10 |
11 | ## View this page's wiki!
12 | * https://github.com/CJNewcomer/scarebnb/wiki
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBar/SearchBar.css:
--------------------------------------------------------------------------------
1 | .search__container {
2 | display: flex;
3 | align-content: center;
4 | border: 0.5px solid #333333;
5 | border-radius: 5px;
6 | background-color: whitesmoke;
7 | }
8 |
9 | .search__text {
10 | font-family: 'Esteban', serif;
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/src/components/Landing/index.js:
--------------------------------------------------------------------------------
1 | import HauntingCard from '../HauntingCard';
2 | import './Landing.css';
3 |
4 | function Landing() {
5 | return (
6 | <>
7 |
12 | >
13 |
14 | )
15 |
16 | }
17 |
18 | export default Landing;
--------------------------------------------------------------------------------
/backend/config/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | environment: process.env.NODE_ENV || 'development',
3 | port: process.env.PORT || 5000,
4 | db: {
5 | username: process.env.DB_USERNAME,
6 | password: process.env.DB_PASSWORD,
7 | database: process.env.DB_DATABASE,
8 | host: process.env.DB_HOST,
9 | },
10 | jwtConfig: {
11 | secret: process.env.JWT_SECRET,
12 | expiresIn: process.env.JWT_EXPIRES_IN,
13 | },
14 | };
--------------------------------------------------------------------------------
/backend/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { port } = require('../config');
3 |
4 | const app = require('../app');
5 | const db = require('../db/models');
6 |
7 | db.sequelize
8 | .authenticate()
9 | .then(() => {
10 | console.log('Database connection success! Sequelize is ready to use...');
11 |
12 | app.listen(port, () => console.log(`Listening on port ${port}...`));
13 | })
14 | .catch((err) => {
15 | console.log('Database connection failure.');
16 | console.error(err);
17 | });
--------------------------------------------------------------------------------
/frontend/src/components/context/SearchContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useContext } from 'react';
2 |
3 | const SearchContext = createContext();
4 |
5 | export const useSearchContext = () => useContext(SearchContext);
6 |
7 | export default function SearchProvider({children}) {
8 | const [input, setInput] = useState('');
9 |
10 | return (
11 |
12 | { children }
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/frontend/src/components/SearchResults/SearchResults.css:
--------------------------------------------------------------------------------
1 | .search__wrapper {
2 | /* display: table; */
3 | width: 80%;
4 | margin: auto;
5 | height: 100%;
6 | /* margin-left: 30px; */
7 | }
8 |
9 | p.search__results {
10 | font-size: 25px;
11 | font-weight: 300;
12 | letter-spacing: 1px;
13 | text-decoration: underline;
14 | text-decoration-color: #333333;
15 | }
16 |
17 | div.search {
18 | display: flex;
19 | flex-wrap: wrap;
20 | justify-content: space-between;
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/frontend/src/components/context/modal.css:
--------------------------------------------------------------------------------
1 | #modal {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | left: 0;
6 | bottom: 0;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | }
11 |
12 | #modal-background {
13 | position: fixed;
14 | top: 0;
15 | right: 0;
16 | left: 0;
17 | bottom: 0;
18 | text-align: center;
19 | background-color: rgba(0, 0, 0, 0.7);
20 | }
21 |
22 | #modal-content {
23 | position: absolute;
24 | background-color:white;
25 | text-align: center;
26 | padding: 35px;
27 | }
--------------------------------------------------------------------------------
/backend/db/models/userhaunting.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = (sequelize, DataTypes) => {
3 | const UserHaunting = sequelize.define('UserHaunting', {
4 | userId: DataTypes.INTEGER,
5 | hauntingId: DataTypes.INTEGER,
6 | statusId: DataTypes.INTEGER,
7 | bookingStartDate: DataTypes.DATE,
8 | bookingEndDate: DataTypes.DATE,
9 | rating: DataTypes.INTEGER,
10 | comment: DataTypes.TEXT
11 | }, {});
12 | UserHaunting.associate = function(models) {
13 | // associations can be defined here
14 | };
15 | return UserHaunting;
16 | };
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/DemoUser.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 |
3 | import { demoLogin } from '../../store/session';
4 | import './LoginForm.css';
5 |
6 | const DemoUser = () => {
7 | const dispatch = useDispatch();
8 |
9 | const loginDemoUser = async () => {
10 | dispatch(demoLogin());
11 | };
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default DemoUser;
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal } from '../../components/context/Modal';
3 | import LoginForm from './LoginForm';
4 |
5 | function LoginFormModal() {
6 | const [showModal, setShowModal] = useState(false);
7 |
8 | return (
9 | <>
10 |
11 | {showModal && (
12 | setShowModal(false)}>
13 |
14 |
15 | )}
16 | >
17 | );
18 | }
19 |
20 | export default LoginFormModal;
--------------------------------------------------------------------------------
/backend/config/database.js:
--------------------------------------------------------------------------------
1 | const config = require('./index');
2 | const db = config.db;
3 | const username = db.username;
4 | const password = db.password;
5 | const database = db.database;
6 | const host = db.host;
7 | module.exports = {
8 | development: {
9 | username,
10 | password,
11 | database,
12 | host,
13 | dialect: 'postgres',
14 | seederStorage: 'sequelize'
15 | },
16 | production: {
17 | use_env_variable: 'DATABASE_URL',
18 | dialect: 'postgres',
19 | dialectOptions: {
20 | ssl: {
21 | require: true,
22 | rejectUnauthorized: false
23 | }
24 | },
25 | seederStorage: 'sequelize'
26 | }
27 | };
--------------------------------------------------------------------------------
/frontend/src/components/SignupFormModal/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import {Modal} from '../context/Modal';
4 | import SignupForm from './SignUpForm';
5 |
6 | const SignupFormModal = () => {
7 | const [showModal, setShowModal] = useState(false);
8 |
9 | return (
10 | <>
11 |
12 | {showModal && (
13 | setShowModal(false)}>
14 |
15 |
16 | )}
17 | >
18 | );
19 | };
20 |
21 | export default SignupFormModal;
--------------------------------------------------------------------------------
/backend/db/migrations/20210126173505-price.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | /*
6 | Add altering commands here.
7 | Return a promise to correctly handle asynchronicity.
8 |
9 | Example:
10 | */
11 | return queryInterface.addColumn('Hauntings', 'price', {
12 | type: Sequelize.INTEGER,
13 | allowNull: false });
14 | },
15 |
16 | down: (queryInterface, Sequelize) => {
17 | /*
18 | Add reverting commands here.
19 | Return a promise to correctly handle asynchronicity.
20 |
21 | Example:
22 | */
23 | return queryInterface.removeColumn('Hauntings', 'price');
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/backend/utils/validation.js:
--------------------------------------------------------------------------------
1 | const { validationResult } = require('express-validator');
2 |
3 | // middleware for formatting errors from express-validator middleware
4 | // (to customize, see express-validator's documentation)
5 | const handleValidationErrors = (req, _res, next) => {
6 | const validationErrors = validationResult(req);
7 |
8 | if (!validationErrors.isEmpty()) {
9 | const errors = validationErrors
10 | .array()
11 | .map((error) => `${error.msg}`);
12 |
13 | const err = Error('Bad request.');
14 | err.errors = errors;
15 | err.status = 400;
16 | err.title = 'Bad request.';
17 | next(err);
18 | }
19 | next();
20 | };
21 |
22 | module.exports = {
23 | handleValidationErrors,
24 | };
--------------------------------------------------------------------------------
/backend/routes/api/hauntings.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { Haunting } = require('../../db/models');
5 | const asyncHandler = require('express-async-handler');
6 |
7 |
8 | router.get('/:id(\\d+)', asyncHandler(async(req, res) => {
9 | const id = parseInt(req.params.id, 10);
10 | // console.log(id)
11 | const haunting = await Haunting.findOne({
12 | where: {
13 | id,
14 | }
15 | });
16 | // console.log(haunting)
17 | res.json({haunting});
18 | }));
19 |
20 | router.get('/', asyncHandler(async(req, res) => {
21 | const hauntings = await Haunting.findAll();
22 | // console.log(hauntings);
23 | res.json({hauntings});
24 | })),
25 |
26 |
27 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/userHauntings.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { UserHaunting } = require('../../db/models');
5 | const asyncHandler = require('express-async-handler');
6 |
7 | router.post('/', asyncHandler(async (req, res) => {
8 | const {userId, bookingStartDate, bookingEndDate, active} = req.body;
9 | const userHaunting = await UserHaunting.create({ userId, bookingStartDate, bookingEndDate, active});
10 |
11 | return res.json({userHaunting});
12 | }))
13 |
14 | router.get('/:id', asyncHandler(async (req, res) => {
15 | const userHauntings = await PromiseRejectionEvent.findAll({
16 | where: {
17 | userId: req.params.id
18 | }
19 | })
20 | return res.json({userHauntings});
21 | }));
22 |
23 | module.exports = router;
--------------------------------------------------------------------------------
/frontend/src/components/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | import { useHistory } from 'react-router-dom';
2 |
3 | import { useSearchContext } from '../context/SearchContext';
4 |
5 | import './SearchBar.css';
6 |
7 | const SearchBar = () => {
8 | const history = useHistory();
9 |
10 | const {input, setInput} = useSearchContext();
11 |
12 | const onSearch = (e) => {
13 | e.preventDefault();
14 | history.push(`/search?result1=${input}`)
15 | };
16 |
17 | return (
18 |
19 |
22 |
23 | )
24 | }
25 |
26 | export default SearchBar;
--------------------------------------------------------------------------------
/frontend/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 |
2 | function Footer() {
3 | return (
4 |
5 |
6 |
Created by Courtney J. Newcomer {new Date().getFullYear()}
7 |
15 |
16 |
17 | )
18 | }
19 | export default Footer;
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/src/components/UserHaunting/CreateUserHauntingModal.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Modal } from '../context/Modal';
3 |
4 | import UserHaunting from './index'
5 |
6 |
7 | const CreateUserHauntingModal = () => {
8 | const [ showModal, setShowModal] = useState(false);
9 |
10 | const handleClose = () => setShowModal(false);
11 |
12 | return (
13 | <>
14 |
18 | {showModal && (
19 | setShowModal(false)}>
20 |
21 |
22 | )}
23 | >
24 | )
25 | }
26 |
27 |
28 | export default CreateUserHauntingModal;
--------------------------------------------------------------------------------
/frontend/src/components/Landing/Landing.css:
--------------------------------------------------------------------------------
1 | div.main {
2 | width: 100%;
3 | height: 100%;
4 | display: block;
5 | position: relative;
6 | }
7 |
8 | div.main ::after {
9 | content: "";
10 | background: url("https://www.ft.com/__origami/service/image/v2/images/raw/http%3A%2F%2Fcom.ft.imagepublish.upp-prod-us.s3.amazonaws.com%2F86be13f0-e59f-11e9-9743-db5a370481bc?fit=scale-down&source=next&width=700");
11 | background-repeat: no-repeat;
12 | background-attachment: fixed;
13 | background-position: center;
14 | background-size: cover;
15 | width: 100%;
16 | opacity: 0.2;
17 | top: 0;
18 | left: 0;
19 | bottom: 0;
20 | right: 0;
21 | position: absolute;
22 | z-index: -1;
23 | }
24 |
25 | .main__layout {
26 | display: flex;
27 | justify-content: space-between;
28 | flex-wrap: wrap;
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateUserHaunting/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal } from '../context/Modal'
3 |
4 | import CreateUserHaunting from './CreateUserHauntingModal';
5 |
6 | const CreateUserHauntingModal = () =>{
7 | const [showModal, setShowModal] = useState(false);
8 |
9 | const handleClose = () => setShowModal(false);
10 |
11 | return (
12 | <>
13 |
18 | {showModal && (
19 | setShowModal(false)}>
20 |
21 |
22 | )}
23 | >
24 | )
25 | }
26 |
27 | export default CreateUserHauntingModal;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scarebnb",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "heroku-postbuild": "npm run build --prefix frontend",
8 | "install": "npm --prefix backend install backend && npm --prefix frontend install frontend",
9 | "dev:backend": "npm install --prefix backend start",
10 | "dev:frontend": "npm install --prefix frontend start",
11 | "sequelize": "npm run --prefix backend sequelize",
12 | "sequelize-cli": "npm run --prefix backend sequelize-cli",
13 | "start": "npm start --prefix backend"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/CJNewcomer/scarebnb.git"
18 | },
19 | "author": "",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/CJNewcomer/scarebnb/issues"
23 | },
24 | "homepage": "https://github.com/CJNewcomer/scarebnb#readme",
25 | "dependencies": {
26 | "lodash": "^4.17.21"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/components/UserHaunting/UserHaunting.css:
--------------------------------------------------------------------------------
1 | .form__container {
2 | margin: auto;
3 | display: flex;
4 | flex-direction: column;
5 | padding: 4rem;
6 | background-color: #333;
7 | }
8 |
9 | .form__content {
10 | display: flex;
11 | justify-content: space-between;
12 | flex-direction: column;
13 | }
14 |
15 | .form__title {
16 | color: #333;
17 | font-family: 'Playfair Display SC', serif;
18 | font-size: 25px;
19 | align-self: center;
20 | margin-top: -.5rem;
21 | letter-spacing: 1.2px;
22 | }
23 |
24 | .error-list {
25 | margin: auto;
26 | list-style: none;
27 | color: rgb(143, 18, 18);
28 | text-align: center;
29 | padding: .5rem rem;
30 | }
31 |
32 | .form__input {
33 | margin: .2rem;
34 | }
35 |
36 | .form__button-container {
37 | align-self: center;
38 | margin: 2rem 1rem 1rem 1rem;
39 | }
40 |
41 | .form__button {
42 | padding: .7rem;
43 | background-color: #333;
44 | border-radius: 5px;
45 | border: none;
46 | }
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React Template
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/LoginForm.css:
--------------------------------------------------------------------------------
1 | .form__login{
2 | margin:auto;
3 | display: flex;
4 | flex-direction: column;
5 | padding: 4rem;
6 | background-color: whitesmoke;
7 | }
8 |
9 | .form__content{
10 | display: flex;
11 | justify-content: space-between;
12 | flex-direction: column;
13 | }
14 |
15 | .error-list {
16 | margin: auto;
17 | list-style: none;
18 | color: rgb(143, 18, 18);
19 | text-align: center;
20 | padding: .5rem rem;
21 | }
22 |
23 | label {
24 | padding: 5px;
25 | font-family: 'Playfair Display SC', serif;
26 | letter-spacing: 1px;
27 | }
28 |
29 | .form__input {
30 | margin: .2rem;
31 | }
32 |
33 | .form__input-text {
34 | color: #333333;
35 | border-radius: 5px;
36 | border: .15px solid rgb(11,12,15);
37 | padding: .3rem;
38 | }
39 |
40 | .form__button {
41 | align-self: center;
42 | }
43 |
44 | .form__button-submit {
45 | padding: 5px 20px;
46 | background-color: rgb(197,198,200);
47 | border-radius: 5px;
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/components/context/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef, useState, useEffect } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './modal.css';
4 |
5 | const ModalContext = React.createContext();
6 |
7 | export function ModalProvider({ children }) {
8 | const modalRef = useRef();
9 | const [value, setValue] = useState();
10 |
11 | useEffect(() => {
12 | setValue(modalRef.current);
13 | }, [])
14 |
15 | return (
16 | <>
17 |
18 | {children}
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | export function Modal({ onClose, children }) {
26 | const modalNode = useContext(ModalContext);
27 | if (!modalNode) return null;
28 |
29 | return ReactDOM.createPortal(
30 |
31 |
32 |
33 | {children}
34 |
35 |
,
36 | modalNode
37 | );
38 | }
39 |
40 |
41 |
--------------------------------------------------------------------------------
/backend/db/seeders/20210221042321-seedUser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const faker = require('faker');
3 | const bcrypt = require('bcryptjs');
4 |
5 | module.exports = {
6 | up: async (queryInterface, Sequelize) => {
7 | const users = []
8 | const demoUser = {
9 | username: "demoUser",
10 | email: "demouser@demoUser.com",
11 | hashedPassword: await bcrypt.hash("demoUser123", 10),
12 | createdAt: new Date(),
13 | updatedAt: new Date(),
14 | };
15 | users.push(demoUser)
16 | for (let i = 0; i < 50; i++) {
17 | const newUser = {
18 | username: faker.internet.userName(),
19 | email: faker.internet.email(),
20 | hashedPassword: await bcrypt.hash(`Password${i}`, 10),
21 | createdAt: new Date(),
22 | updatedAt: new Date()
23 | }
24 | users.push(newUser)
25 |
26 | }
27 | return queryInterface.bulkInsert('Users', users, {});
28 | },
29 |
30 | down: (queryInterface, Sequelize) => {
31 | return queryInterface.bulkDelete('Users', null, {});
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/frontend/src/components/SignupFormModal/SignupForm.css:
--------------------------------------------------------------------------------
1 | .form__signup{
2 | margin:auto;
3 | display: flex;
4 | flex-direction: column;
5 | padding: 4rem;
6 | background-color: whitesmoke;
7 | }
8 |
9 | .form__container{
10 | display: flex;
11 | justify-content: space-between;
12 | flex-direction: column;
13 | }
14 |
15 | .error-list {
16 | margin: auto;
17 | list-style: none;
18 | color: rgb(143, 18, 18);;
19 | text-align: center;
20 | padding: .5rem rem;
21 |
22 | }
23 |
24 | label {
25 | padding: 5px;
26 | font-family: 'Playfair Display SC', serif;
27 | letter-spacing: 1px;
28 | }
29 |
30 | .form__input{
31 | margin: .2rem;
32 | }
33 |
34 | .form__input-text{
35 | color: #333333;
36 | border-radius: 5px;
37 | border: .15px solid rgb(11,12,15);
38 | padding: .3rem;
39 | }
40 |
41 | .form__button{
42 | align-self: center;
43 |
44 | }
45 |
46 | .form__button-submit{
47 | padding: 5px 20px;
48 | background-color: rgb(197,198,200);
49 | border-radius: 5px;
50 | }
--------------------------------------------------------------------------------
/frontend/src/components/HauntingCard/HauntingCard.css:
--------------------------------------------------------------------------------
1 | .card__container {
2 | display: flex;
3 | width: 350px;
4 | background-color: whitesmoke;
5 | margin: 30px;
6 | border-radius: 10px;
7 | height: min-content;
8 | }
9 |
10 | .card {
11 | border-radius: 10px;
12 | overflow: hidden;
13 | width: 350px;
14 | box-shadow: 0px 6px 18px -9px rgba(0, 0, 0, 0.75);
15 | cursor: pointer;
16 | }
17 |
18 | a:visted {
19 | text-decoration: none;
20 | color: black;
21 | }
22 |
23 | a:active, a:link {
24 | text-decoration: none;
25 | color: black;
26 | }
27 |
28 | .card > img {
29 | object-fit: cover;
30 | min-width: 300px;
31 | height: 200px;
32 | width: 350px;
33 | }
34 |
35 | .card__info {
36 | margin-top: -9px;
37 | border-radius: 10px;
38 | padding: 20px;
39 | padding-top: 20px;
40 | border: 1;
41 | text-decoration: none;
42 | color: black;
43 | }
44 |
45 | .card__info > h2 {
46 | font-size: 18px;
47 | font-weight: 600;
48 | }
49 |
50 | .card__info > h3 {
51 | font-size: 14px;
52 | font-weight: 300;
53 | margin: auto;
54 | }
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | //import reducer from session into file w/root reducer
4 | import sessionReducer from './session';
5 | import hauntingsReducer from './hauntings';
6 | import userHauntingsReducer from './userHauntings';
7 |
8 | const rootReducer = combineReducers({
9 | // Set key of session to session reducer
10 | session: sessionReducer,
11 | hauntings: hauntingsReducer,
12 | userHauntings: userHauntingsReducer,
13 | });
14 |
15 | let enhancer;
16 |
17 | if (process.env.NODE_ENV === 'production') {
18 | enhancer = applyMiddleware(thunk);
19 | } else {
20 | const logger = require('redux-logger').default;
21 | const composeEnhancers =
22 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
23 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
24 | }
25 |
26 | const configureStore = (preloadedState) => {
27 | return createStore(rootReducer, preloadedState, enhancer);
28 | }
29 |
30 |
31 |
32 | export default configureStore;
--------------------------------------------------------------------------------
/backend/db/migrations/20210122042207-create-user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => {
4 | return queryInterface.createTable('Users', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | username: {
12 | allowNull: false,
13 | type: Sequelize.STRING(30),
14 | unique: true
15 | },
16 | email: {
17 | type: Sequelize.STRING(256),
18 | allowNull: false,
19 | unique: true
20 | },
21 | hashedPassword: {
22 | type: Sequelize.STRING.BINARY,
23 | allowNull: false
24 | },
25 | createdAt: {
26 | allowNull: false,
27 | type: Sequelize.DATE,
28 | defaultValue: Sequelize.fn('now')
29 | },
30 | updatedAt: {
31 | allowNull: false,
32 | type: Sequelize.DATE,
33 | defaultValue: Sequelize.fn('now')
34 | }
35 | });
36 | },
37 | down: (queryInterface, Sequelize) => {
38 | return queryInterface.dropTable('Users');
39 | }
40 | };
--------------------------------------------------------------------------------
/backend/db/models/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const Sequelize = require('sequelize');
6 | const basename = path.basename(__filename);
7 | const env = process.env.NODE_ENV || 'development';
8 | const config = require(__dirname + '/../../config/database.js')[env];
9 | const db = {};
10 |
11 | let sequelize;
12 | if (config.use_env_variable) {
13 | sequelize = new Sequelize(process.env[config.use_env_variable], config);
14 | } else {
15 | sequelize = new Sequelize(config.database, config.username, config.password, config);
16 | }
17 |
18 | fs
19 | .readdirSync(__dirname)
20 | .filter(file => {
21 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
22 | })
23 | .forEach(file => {
24 | const model = sequelize['import'](path.join(__dirname, file));
25 | db[model.name] = model;
26 | });
27 |
28 | Object.keys(db).forEach(modelName => {
29 | if (db[modelName].associate) {
30 | db[modelName].associate(db);
31 | }
32 | });
33 |
34 | db.sequelize = sequelize;
35 | db.Sequelize = Sequelize;
36 |
37 | module.exports = db;
38 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './index.css';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { BrowserRouter } from 'react-router-dom';
6 | import { restoreCSRF, fetch } from './store/csrf';
7 | import * as sessionActions from './store/session';
8 | import App from './App';
9 | import { ModalProvider } from "./components/context/Modal";
10 | import SearchProvider from './components/context/SearchContext'
11 |
12 | import configureStore from './store';
13 |
14 | const store = configureStore();
15 |
16 | if (process.env.NODE_ENV !== 'production') {
17 | restoreCSRF();
18 |
19 | window.csrfFetch = fetch;
20 | window.store = store;
21 | window.sessionActions = sessionActions;
22 | }
23 | function Root() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | ReactDOM.render(
38 |
39 |
40 | ,
41 | document.getElementById('root')
42 | );
43 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "sequelize": "sequelize",
8 | "sequelize-cli": "sequelize-cli",
9 | "start": "per-env",
10 | "start:development": "nodemon -r dotenv/config ./bin/www",
11 | "start:production": "node ./bin/www",
12 | "reseed": "npx dotenv sequelize db:drop && npx dotenv sequelize db:create && npx dotenv sequelize db:migrate && npx dotenv sequelize db:seed:all"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "aws-sdk": "^2.831.0",
19 | "bcryptjs": "^2.4.3",
20 | "body-parser": "^1.19.0",
21 | "cookie-parser": "^1.4.5",
22 | "cors": "^2.8.5",
23 | "csurf": "^1.11.0",
24 | "dotenv": "^8.2.0",
25 | "express": "^4.17.1",
26 | "express-async-handler": "^1.1.4",
27 | "express-validator": "^6.9.2",
28 | "faker": "^5.1.0",
29 | "helmet": "^4.4.1",
30 | "jsonwebtoken": "^8.5.1",
31 | "morgan": "^1.10.0",
32 | "multer": "^1.4.2",
33 | "per-env": "^1.0.2",
34 | "pg": "^8.5.1",
35 | "sequelize": "^5.22.3",
36 | "sequelize-cli": "^5.5.1"
37 | },
38 | "devDependencies": {
39 | "dotenv-cli": "^4.0.0",
40 | "nodemon": "^2.0.7"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:5000",
6 | "dependencies": {
7 | "@material-ui/core": "^4.11.3",
8 | "@material-ui/icons": "^4.11.2",
9 | "@testing-library/jest-dom": "^5.11.9",
10 | "@testing-library/react": "^11.2.3",
11 | "@testing-library/user-event": "^12.6.2",
12 | "date-fns": "^2.16.1",
13 | "js-cookie": "^2.2.1",
14 | "react": "^17.0.1",
15 | "react-date-range": "^1.1.3",
16 | "react-dom": "^17.0.1",
17 | "react-redux": "^7.2.2",
18 | "react-router-dom": "^5.2.0",
19 | "react-scripts": "4.0.1",
20 | "redux": "^4.0.5",
21 | "redux-thunk": "^2.3.0"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "devDependencies": {
45 | "redux-logger": "^3.0.6"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/backend/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const apiRouter = require('./api');
4 |
5 | router.use('/api', apiRouter);
6 |
7 | // Static routes
8 | // Serve React build files in production
9 | if (process.env.NODE_ENV === 'production') {
10 | const path = require('path');
11 | // Serve the frontend's index.html file at the root route
12 | router.get('/', (req, res) => {
13 | res.cookie('XSRF-TOKEN', req.csrfToken());
14 | res.sendFile(
15 | path.resolve(__dirname, '../../frontend', 'build', 'index.html')
16 | );
17 | });
18 |
19 | // Serve the static assets in the frontend's build folder
20 | router.use(express.static(path.resolve("../frontend/build")));
21 |
22 | // Serve the frontend's index.html file at all other routes NOT starting with /api
23 | router.get(/^(?!\/?api).*/, (req, res) => {
24 | res.cookie('XSRF-TOKEN', req.csrfToken());
25 | res.sendFile(
26 | path.resolve(__dirname, '../../frontend', 'build', 'index.html')
27 | );
28 | });
29 | }
30 |
31 | // Add a XSRF-TOKEN cookie in development
32 | if (process.env.NODE_ENV !== 'production') {
33 | router.get('/api/csrf/restore', (req, res) => {
34 | res.cookie('XSRF-TOKEN', req.csrfToken());
35 | return res.json({});
36 | });
37 | }
38 |
39 | module.exports = router;
--------------------------------------------------------------------------------
/backend/db/migrations/20210126043124-create-user-haunting.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => {
4 | return queryInterface.createTable('UserHauntings', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | userId: {
12 | type: Sequelize.INTEGER,
13 | references: { model: "Users" }
14 | },
15 | hauntingId: {
16 | type: Sequelize.INTEGER,
17 | references: { model: "Hauntings" }
18 | },
19 | statusId: {
20 | type: Sequelize.INTEGER,
21 | references: { model: "Hauntings" }
22 | },
23 | bookingStartDate: {
24 | type: Sequelize.DATE
25 | },
26 | bookingEndDate: {
27 | type: Sequelize.DATE
28 | },
29 | rating: {
30 | type: Sequelize.INTEGER
31 | },
32 | comment: {
33 | type: Sequelize.TEXT(500),
34 | allowNull: true,
35 | },
36 | createdAt: {
37 | allowNull: false,
38 | type: Sequelize.DATE
39 | },
40 | updatedAt: {
41 | allowNull: false,
42 | type: Sequelize.DATE
43 | }
44 | });
45 | },
46 | down: (queryInterface, Sequelize) => {
47 | return queryInterface.dropTable('UserHauntings');
48 | }
49 | };
--------------------------------------------------------------------------------
/backend/db/models/haunting.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = (sequelize, DataTypes) => {
3 | const Haunting = sequelize.define('Haunting', {
4 | statusId: {
5 | type: DataTypes.INTEGER,
6 | allowNull: false,
7 | },
8 | locationName: {
9 | type: DataTypes.STRING,
10 | allowNull: false,
11 | unique: true
12 | },
13 | address: {
14 | type: DataTypes.STRING,
15 | allowNull: true
16 | },
17 | city: {
18 | type: DataTypes.STRING,
19 | allowNull: false
20 | },
21 | state: {
22 | type: DataTypes.STRING,
23 | allowNull: false
24 | },
25 | country: {
26 | type: DataTypes.STRING,
27 | allowNull: false
28 | },
29 | description: {
30 | type: DataTypes.TEXT,
31 | allowNull: false,
32 | validate: {
33 | len: [30, 750]
34 | },
35 | },
36 | imgPath: {
37 | type: DataTypes.STRING,
38 | allowNull: false,
39 | },
40 | price: {
41 | type: DataTypes.INTEGER,
42 | allowNull: false,
43 | }
44 | }, {});
45 | Haunting.associate = function(models) {
46 | Haunting.hasMany(models.UserHaunting, {foreignKey: 'hauntingId'});
47 | const columnMapping = {
48 | through: 'UserHaunting',
49 | otherKey: 'userId',
50 | foreignKey: 'hauntingId'
51 | }
52 | Haunting.belongsToMany(models.User, columnMapping);
53 | };
54 | return Haunting;
55 | };
--------------------------------------------------------------------------------
/backend/routes/api/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const asyncHandler = require('express-async-handler');
3 | const { check } = require('express-validator');
4 | const { handleValidationErrors } = require('../../utils/validation');
5 |
6 | const { setTokenCookie, requireAuth } = require('../../utils/auth');
7 | const { User } = require('../../db/models');
8 | const router = express.Router();
9 |
10 | const validateSignup = [
11 | check('email')
12 | .exists({ checkFalsy: true })
13 | .isEmail()
14 | .withMessage('Please provide a valid email.'),
15 | check('username')
16 | .exists({ checkFalsy: true })
17 | .isLength({ min: 4 })
18 | .withMessage('Please provide a username with at least 4 characters.'),
19 | check('username')
20 | .not()
21 | .isEmail()
22 | .withMessage('Username cannot be an email.'),
23 | check('password')
24 | .exists({ checkFalsy: true })
25 | .isLength({ min: 6 })
26 | .withMessage('Password must be 6 characters or more.'),
27 | handleValidationErrors,
28 | ];
29 |
30 | // Sign up
31 | router.post(
32 | '',
33 | validateSignup,
34 | asyncHandler(async (req, res) => {
35 | const { email, password, username } = req.body;
36 | const user = await User.signup({ email, username, password });
37 |
38 | await setTokenCookie(res, user);
39 |
40 | return res.json({
41 | user,
42 | });
43 | }),
44 | );
45 |
46 | module.exports = router;
--------------------------------------------------------------------------------
/frontend/src/components/Navigation/ProfileButton.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import * as sessionActions from '../../store/session';
4 |
5 | const ProfileButton = ({user}) => {
6 | const dispatch = useDispatch();
7 | const [showMenu, setShowMenu] = useState(false);
8 |
9 | const openMenu = () => {
10 | if (showMenu) return;
11 | setShowMenu(true);
12 | };
13 |
14 | useEffect(() => {
15 | if (!showMenu) return;
16 |
17 | const closeMenu = () => {
18 | setShowMenu(false);
19 | };
20 |
21 | document.addEventListener('click', closeMenu);
22 |
23 | return () => document.removeEventListener('click', closeMenu);
24 | }, [showMenu]);
25 |
26 | const logout = (e) => {
27 | e.preventDefault();
28 | dispatch(sessionActions.logout());
29 | };
30 |
31 | return (
32 | <>
33 |
36 | {showMenu && (
37 |
38 | - {user.username}
39 | - {user.email}
40 |
41 |
42 | )}
43 | >
44 | );
45 | }
46 |
47 | export default ProfileButton;
--------------------------------------------------------------------------------
/frontend/src/components/HauntingProfile/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import {useDispatch, useSelector} from 'react-redux';
3 | import {useParams} from 'react-router-dom';
4 | import { getHauntingsProfile } from '../../store/hauntings';
5 | import CreateUserHauntingModal from '../CreateUserHaunting/index';
6 | import './HauntingProfile.css';
7 |
8 | function HauntingProfile() {
9 | const dispatch = useDispatch();
10 | const {id} = useParams();
11 |
12 | const haunting = useSelector(state => state.hauntings[id])
13 |
14 | useEffect(() => {
15 | dispatch(getHauntingsProfile(id))
16 | }, [dispatch, id])
17 |
18 | if (!haunting) return null;
19 | const {imgPath, locationName, address, city, state, country, price, description} = haunting;
20 |
21 | return (
22 | <>
23 |
24 |

25 |
26 |
{locationName}
27 |
{address}
28 |
{city}, {state} {country}
29 |
{price} / Night
30 |
{description}
31 |
32 |
33 |
34 |
35 |
36 | >
37 | )
38 | }
39 |
40 | export default HauntingProfile;
--------------------------------------------------------------------------------
/backend/routes/api/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const sessionRouter = require('./session.js');
3 | const usersRouter = require('./users.js');
4 | const hauntingsRouter = require('./hauntings.js');
5 | // const asyncHandler = require('express-async-handler');
6 | // const { setTokenCookie } = require('../../utils/auth.js');
7 | // const { User } = require('../../db/models');
8 |
9 | router.use('/session', sessionRouter);
10 |
11 | router.use('/users', usersRouter);
12 |
13 | router.use('/hauntings', hauntingsRouter);
14 |
15 | // router.post("/test", function (req, res) {
16 | // res.json({requestBody: req.body });
17 | // });
18 |
19 | // // GET /api/set-token-cookie
20 | // router.get('/set-token-cookie', asyncHandler(async (req, res) => {
21 | // const user = await User.findOne({
22 | // where: {
23 | // username: 'Demo-lition'
24 | // },
25 | // })
26 | // setTokenCookie(res, user);
27 | // return res.json({ user });
28 | // }));
29 |
30 | // // GET /api/restore-user
31 | // const { restoreUser } = require('../../utils/auth.js');
32 | // router.get(
33 | // '/restore-user',
34 | // restoreUser,
35 | // (req, res) => {
36 | // return res.json(req.user);
37 | // }
38 | // );
39 |
40 | // // GET /api/require-auth
41 | // const { requireAuth } = require('../../utils/auth.js');
42 | // router.get(
43 | // '/require-auth',
44 | // requireAuth,
45 | // (req, res) => {
46 | // return res.json(req.user);
47 | // }
48 | // );
49 |
50 |
51 |
52 | module.exports = router;
53 |
--------------------------------------------------------------------------------
/backend/db/migrations/20210126042127-create-haunting.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => {
4 | return queryInterface.createTable('Hauntings', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | statusId: {
12 | type: Sequelize.INTEGER,
13 | allowNull: false,
14 | },
15 | locationName: {
16 | type: Sequelize.STRING,
17 | allowNull: false,
18 | },
19 | address: {
20 | type: Sequelize.STRING,
21 | allowNull: true,
22 | },
23 | city: {
24 | type: Sequelize.STRING,
25 | allowNull: false,
26 | },
27 | state: {
28 | type: Sequelize.STRING,
29 | allowNull: false,
30 | },
31 | country: {
32 | type: Sequelize.STRING,
33 | allowNull: false,
34 | },
35 | description: {
36 | type: Sequelize.TEXT,
37 | allowNull: false,
38 | },
39 | imgPath: {
40 | type: Sequelize.STRING,
41 | allowNull: false,
42 | },
43 | createdAt: {
44 | allowNull: false,
45 | type: Sequelize.DATE,
46 | defaultValue: Sequelize.fn('now')
47 | },
48 | updatedAt: {
49 | allowNull: false,
50 | type: Sequelize.DATE,
51 | defaultValue: Sequelize.fn('now')
52 | }
53 | });
54 | },
55 | down: (queryInterface, Sequelize) => {
56 | return queryInterface.dropTable('Hauntings');
57 | }
58 | };
--------------------------------------------------------------------------------
/frontend/src/components/HauntingProfile/HauntingProfile.css:
--------------------------------------------------------------------------------
1 | .spot {
2 | display: flex;
3 | width: 60%;
4 | margin: auto;
5 | padding: 25px 25px;
6 | margin-top: 100px;
7 | border-radius: 10px;
8 | overflow: hidden;
9 | box-shadow: 0px 6px 18px -9px rgba(0, 0, 0, 0.75);
10 | transition: transform 100ms ease-in;
11 | text-align: left;
12 | }
13 |
14 | .spot > img {
15 | flex-direction: column;
16 | justify-content: left;
17 | object-fit: cover;
18 | min-width: 300px;
19 | min-height: 250px;
20 | text-align: center;
21 | width: 100%;
22 | margin: 5.5px 20px 10px 0;
23 | border-radius: 5px;
24 | }
25 |
26 | .spot__info {
27 | margin-top: -5.5px;
28 | border-radius: 10px;
29 | padding: 20px;
30 | padding-top: 15px;
31 | border: 1;
32 | }
33 |
34 | .spot__info > h2, h3 {
35 | font-size: 18px;
36 | font-weight: 600;
37 | }
38 |
39 | h4.price {
40 | font-family: 'Esteban', serif;
41 | font-weight: 600;
42 | font-size: 16px;
43 | padding-bottom: 3px;
44 | }
45 |
46 | h4.description {
47 | font-size: 14px;
48 | font-family: 'Esteban', serif;
49 | font-weight: 500;
50 | margin-top: 8px;
51 | margin-bottom: 8px;
52 | }
53 |
54 | h2, h3 {
55 | font-family: 'Playfair Display SC', serif;
56 | }
57 |
58 | .book__now {
59 | display: flex;
60 | justify-content: center;
61 | }
62 |
63 | button {
64 | padding-left: 20px;
65 | padding-right: 20px;
66 | font-weight: 600;
67 | font-family: 'Playfair Display SC', serif;
68 | cursor: pointer;
69 | }
70 |
71 | i.fas {
72 | padding-left: 15px;
73 | padding-right: 15px;
74 | }
--------------------------------------------------------------------------------
/frontend/src/store/hauntings.js:
--------------------------------------------------------------------------------
1 | import { fetch } from './csrf';
2 |
3 | const SET_HAUNTINGS_PROFILE = 'hauntings/SET_HAUNTINGS_PROFILE';
4 | const DISPLAY_MULTIPLE_HAUNTINGS = 'hauntings/DISPLAY_MULTIPLE_HAUNTINGS';
5 |
6 | export const setHauntingsProfile = (payload) => ({
7 | type: SET_HAUNTINGS_PROFILE,
8 | payload,
9 | });
10 |
11 | export const displayMultipleHauntings = (payload) => ({
12 | type: DISPLAY_MULTIPLE_HAUNTINGS,
13 | payload,
14 | });
15 |
16 | export const getHauntingsProfile = (id) => async (dispatch) => {
17 | const res = await fetch(`/api/hauntings/${id}`);
18 | if (res.ok) {
19 | dispatch(setHauntingsProfile(res.data.haunting))
20 | return res;
21 | }
22 | }
23 |
24 | export const getMultipleHauntings = () => async (dispatch) => {
25 | const res = await fetch(`/api/hauntings`);
26 | if (res.ok) {
27 | dispatch(displayMultipleHauntings(res.data.hauntings));
28 | return res;
29 | }
30 | }
31 |
32 | const initialState = {};
33 |
34 | const hauntingsReducer = (state = initialState, action) => {
35 | const newState = Object.assign({}, state);
36 | switch (action.type) {
37 | case SET_HAUNTINGS_PROFILE:
38 | newState[action.payload.id] = action.payload;
39 | return newState;
40 | case DISPLAY_MULTIPLE_HAUNTINGS:
41 | for (let haunting of action.payload) {
42 | newState[haunting.id] = haunting;
43 | }
44 | return newState;
45 | default:
46 | return state;
47 | }
48 | }
49 |
50 | export default hauntingsReducer;
--------------------------------------------------------------------------------
/frontend/src/store/csrf.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 |
3 | export async function fetch(url, options = {}) {
4 | // set options.method to 'GET' if there is no method
5 | options.method = options.method || 'GET';
6 | // set options.headers to an empty object if there is no headers
7 | options.headers = options.headers || {};
8 |
9 | // if the options.method is not 'GET', then set the "Content-Type" header to
10 | // "application/json", and set the "XSRF-TOKEN" header to the value of the
11 | // "XSRF-TOKEN" cookie
12 | if (options.method.toUpperCase() !== 'GET') {
13 | options.headers['Content-Type'] =
14 | options.headers['Content-Type'] || 'application/json';
15 | options.headers['XSRF-Token'] = Cookies.get('XSRF-TOKEN');
16 | }
17 | // call the default window's fetch with the url and the options passed in
18 | const res = await window.fetch(url, options);
19 |
20 | // if the response's body is JSON, then parse the JSON body and set it to a
21 | // key of `data` on the response
22 | const contentType = res.headers.get('content-type');
23 | if (contentType && contentType.includes('application/json')) {
24 | const data = await res.json();
25 | res.data = data;
26 | }
27 |
28 | // if the response status code is 400 or above, then throw an error with the
29 | // error being the response
30 | if (res.status >= 400) throw res;
31 |
32 | // if the response status code is under 400, then return the response to the
33 | // next promise chain
34 | return res;
35 | }
36 | // call this to get the "XSRF-TOKEN" cookie, should only be used in development
37 | export function restoreCSRF() {
38 | return fetch('/api/csrf/restore');
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Route, Switch } from 'react-router-dom';
4 | import SignupForm from './components/SignupFormModal';
5 | import * as sessionActions from './store/session';
6 | import Navigation from './components/Navigation';
7 | import Footer from './components/Footer';
8 | import Landing from './components/Landing';
9 | import HauntingProfile from './components/HauntingProfile';
10 | import SearchResults from './components/SearchResults';
11 | import UserHaunting from './components/UserHaunting';
12 | import './index.css';
13 |
14 |
15 | function App() {
16 | const dispatch = useDispatch();
17 | const [isLoaded, setIsLoaded] = useState(false);
18 | useEffect(() => {
19 | dispatch(sessionActions.restore()).then(() => setIsLoaded(true))
20 | }, [dispatch]);
21 |
22 | if (!isLoaded) {
23 | return Loading...
24 | }
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Page Not Found
47 |
48 |
49 |
50 | >
51 | );
52 | }
53 |
54 | export default App;
55 |
56 |
57 |
--------------------------------------------------------------------------------
/frontend/src/components/HauntingCard/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | // import { useParams } from 'react-router-dom';
5 | import { getMultipleHauntings } from '../../store/hauntings';
6 | import './HauntingCard.css';
7 |
8 | function HauntingCard() {
9 | const dispatch = useDispatch();
10 |
11 | const landingPageCards = useSelector((state) => Object.values(state.hauntings));
12 | const randomNumber = Math.floor(Math.random()*32)
13 | const hauntingCards = landingPageCards.slice(randomNumber, (randomNumber + 8));
14 |
15 |
16 | useEffect(() => {
17 | dispatch(getMultipleHauntings())
18 | }, [dispatch])
19 |
20 | if (!hauntingCards) return null;
21 | return (
22 | <>
23 | { hauntingCards.map(hauntingCard => {
24 | const { id, imgPath, locationName, city, state, price } = hauntingCard;
25 | return (
26 |
27 |
28 |
29 |

30 |
31 |
{locationName}
32 | {city}, {state}
33 | {price} / Night
34 |
35 |
36 |
37 |
38 | )
39 | })}
40 | >
41 | )
42 | }
43 |
44 | export default HauntingCard;
--------------------------------------------------------------------------------
/backend/utils/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { jwtConfig } = require('../config');
3 | const { User } = require('../db/models');
4 |
5 | const { secret, expiresIn } = jwtConfig;
6 |
7 | // Sends a JWT Cookie
8 | const setTokenCookie = (res, user) => {
9 | // Create the token.
10 | const token = jwt.sign(
11 | { data: user.toSafeObject() },
12 | secret,
13 | { expiresIn: parseInt(expiresIn) }, // 604,800 seconds = 1 week
14 | );
15 |
16 | const isProduction = process.env.NODE_ENV === "production";
17 |
18 | // Set the token cookie
19 | res.cookie('token', token, {
20 | maxAge: expiresIn * 1000, // maxAge in milliseconds
21 | httpOnly: true,
22 | secure: isProduction,
23 | sameSite: isProduction && "Lax",
24 | });
25 |
26 | return token;
27 | };
28 |
29 | const restoreUser = (req, res, next) => {
30 | // token parsed from cookies
31 | const { token } = req.cookies;
32 |
33 | return jwt.verify(token, secret, null, async (err, jwtPayload) => {
34 | if (err) {
35 | return next();
36 | }
37 |
38 | try {
39 | const { id } = jwtPayload.data;
40 | req.user = await User.scope('currentUser').findByPk(id);
41 | } catch (e) {
42 | res.clearCookie('token');
43 | return next();
44 | }
45 |
46 | if (!req.user) res.clearCookie('token');
47 |
48 | return next();
49 | });
50 | };
51 |
52 | // If there is no current user, return an error
53 | const requireAuth = [
54 | restoreUser,
55 | function (req, res, next) {
56 | if (req.user) return next();
57 |
58 | const err = new Error('Unauthorized');
59 | err.title = 'Unauthorized';
60 | err.errors = ['Unauthorized'];
61 | err.status = 401;
62 | return next(err);
63 | },
64 | ];
65 |
66 |
67 | module.exports = { setTokenCookie, restoreUser, requireAuth };
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const cors = require('cors');
4 | const csurf = require('csurf');
5 | const helmet = require('helmet');
6 | const cookieParser = require('cookie-parser');
7 | const bodyParser = require("body-parser");
8 | const routes = require('./routes');
9 | const { environment } = require('./config');
10 | const isProduction = environment === 'production';
11 |
12 | const app = express();
13 |
14 | app.use(morgan('dev'));
15 | app.use(cookieParser());
16 | app.use(bodyParser.urlencoded({ extended: false }));
17 | app.use(bodyParser.json());
18 |
19 | if (!isProduction) {
20 | app.use(cors());
21 | }
22 | app.use(helmet({contentSecurityPolicy: false}));
23 | app.use(
24 | csurf({
25 | cookie: {
26 | secure: isProduction,
27 | sameSite: isProduction && "Lax",
28 | httpOnly: true,
29 | },
30 | })
31 | );
32 | app.use(routes);
33 |
34 | app.use((_req, _res, next) => {
35 | const err = new Error("The requested resource couldn't be found.");
36 | err.title = "Resource Not Found";
37 | err.errors = ["The requested resource couldn't be found."];
38 | err.status = 404;
39 | next(err);
40 | });
41 |
42 | const { ValidationError } = require('sequelize');
43 |
44 | app.use((err, _req, _res, next) => {
45 | // check if error is a Sequelize error:
46 | if (err instanceof ValidationError) {
47 | err.errors = err.errors.map((e) => e.message);
48 | err.title = 'Validation error';
49 | }
50 | next(err);
51 | });
52 |
53 | app.use((err, _req, res, _next) => {
54 | res.status(err.status || 500);
55 | console.error(err);
56 | res.json({
57 | title: err.title || 'Server Error',
58 | message: err.message,
59 | errors: err.errors,
60 | stack: isProduction ? null : err.stack,
61 | });
62 | });
63 |
64 | module.exports = app;
--------------------------------------------------------------------------------
/frontend/src/components/SearchResults/index.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import { useDispatch, useSelector} from 'react-redux';
3 | import { useLocation, Link } from 'react-router-dom';
4 | import { getMultipleHauntings } from '../../store/hauntings';
5 | import { useSearchContext } from '../context/SearchContext';
6 | import './SearchResults.css';
7 |
8 | const SearchResults = () => {
9 | const dispatch = useDispatch();
10 | const location = useLocation();
11 | const query = location.search.split('=')[1];
12 |
13 | const hauntings = useSelector((state) => state.hauntings);
14 | const similarMatch = Object.values(hauntings).filter(haunting => haunting.locationName.toLowerCase().includes(query.toLowerCase()));
15 |
16 | useEffect(() => {
17 | dispatch(getMultipleHauntings())
18 | }, [dispatch])
19 |
20 | const {setInput} = useSearchContext();
21 |
22 | useEffect(() => {
23 | setInput('');
24 | }, [setInput]);
25 |
26 |
27 | return (
28 |
29 |
Search Results
30 |
31 | {similarMatch.map(haunting => {
32 | const { id, imgPath, locationName, price } = haunting;
33 | return (
34 |
35 |
36 |

37 |
38 |
{locationName}
39 | {price} / Night
40 |
41 |
42 |
43 | )})}
44 |
45 |
46 | )
47 | }
48 |
49 | export default SearchResults;
--------------------------------------------------------------------------------
/frontend/src/store/userHauntings.js:
--------------------------------------------------------------------------------
1 | import { fetch } from './csrf';
2 |
3 | const CREATE_USERHAUNTINGS = 'userHauntings/createUserHauntings';
4 | const SET_USERHAUNTINGS = 'userHauntings/setUserHauntings';
5 |
6 | export const createUserHauntings = (userHaunting) => {
7 | return {
8 | type: CREATE_USERHAUNTINGS,
9 | payload: userHaunting,
10 | }
11 | }
12 |
13 | export const setUserHauntings = (hauntings) => ({
14 | type: SET_USERHAUNTINGS,
15 | hauntings,
16 | });
17 |
18 | export const getUserHauntings = () => async (dispatch) => {
19 | const res = await fetch(`/api/userhauntings`);
20 | if (res.ok) {
21 | dispatch(setUserHauntings(res.data.userHaunting))
22 | return res;
23 | }
24 | }
25 |
26 | export const userHauntingCreate = (userHaunting) => async (dispatch) => {
27 | const { sessionUser, bookingStartDate, bookingEndDate } = userHaunting;
28 | const today = new Date()
29 | today.setTime(0,0)
30 | const response = await fetch('/api/userHauntings', {
31 | method: 'POST',
32 | body: JSON.stringify({
33 | ownerId: sessionUser.id,
34 | bookingStartDate,
35 | bookingEndDate,
36 | active: (((bookingStartDate <= today) && (bookingEndDate >= today)) || (!bookingStartDate && !bookingEndDate))
37 | }),
38 | });
39 | dispatch(createUserHauntings(response.data.userHaunting))
40 | return response
41 | }
42 |
43 | const initialState = {};
44 |
45 | const userHauntingsReducer = (state = initialState, action) => {
46 | const newState = Object.assign({}, state);
47 | switch (action.type) {
48 | case CREATE_USERHAUNTINGS:
49 | newState[action.payload.id] = action.payload;
50 | return newState;
51 | case SET_USERHAUNTINGS:
52 | newState[action.payload] = action.payload;
53 | return newState;
54 | default:
55 | return state;
56 | }
57 | }
58 |
59 | export default userHauntingsReducer;
--------------------------------------------------------------------------------
/frontend/src/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import ProfileButton from './ProfileButton';
5 | import CreateUserHauntingModal from '../UserHaunting/CreateUserHauntingModal'
6 | import LoginFormModal from '../LoginFormModal';
7 | import SignUpFormModal from '../SignupFormModal';
8 | import SearchBar from '../SearchBar';
9 |
10 | function Navigation({ isLoaded }) {
11 | const sessionUser = useSelector(state => state.session.user);
12 |
13 | let sessionLinks;
14 |
15 | if (sessionUser) {
16 | sessionLinks = (
17 | <>
18 |
19 |
20 | >
21 | );
22 | } else {
23 | sessionLinks = (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {/* */}
35 |
36 |
37 | );
38 | }
39 |
40 | return (
41 | <>
42 |
53 | >
54 | );
55 | }
56 |
57 | export default Navigation;
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: whitesmoke;
3 | height: 100vh;
4 | position: relative;
5 | margin: 0;
6 | width: 100vw;
7 | overflow-x: hidden;
8 | }
9 |
10 | #root {
11 | height: 200%;
12 | }
13 |
14 | .nav__container {
15 | box-sizing: border-box;
16 | margin: auto;
17 | width: 100%;
18 | top: 0;
19 | height: 45px;
20 | background-color: whitesmoke;
21 | z-index: 100;
22 | display: block;
23 | position: fixed;
24 | }
25 |
26 | .navbar__left-container {
27 | display: flex;
28 | align-items: center;
29 | justify-content: space-between;
30 | box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.1);
31 | }
32 |
33 | .navbar__button {
34 | padding: 5px 20px;
35 | background-color: rgb(197,198,200);
36 | border-radius: 5px;
37 | }
38 |
39 | .navbar__right-container {
40 | display: flex;
41 | flex-direction: column;
42 | }
43 |
44 | .navbar__wrapper {
45 | display:flex;
46 | }
47 |
48 | .navbar__home {
49 | display: flex;
50 | align-items: center;
51 | }
52 |
53 | h1 {
54 | font-family: 'Playfair Display SC', serif;
55 | letter-spacing: 3px;
56 | font-size: 35px;
57 | color: #333333;
58 | cursor: pointer;
59 | text-decoration: none;
60 | }
61 |
62 | .navbar__links {
63 | display: flex;
64 | }
65 |
66 | .navbar__middle-container {
67 | display: flex;
68 | width: 100%;
69 | }
70 |
71 | .footer {
72 | z-index: 100;
73 | display: block;
74 | position: fixed;
75 | width: 100%;
76 | background-color: whitesmoke;
77 | bottom: 0;
78 | margin: auto;
79 | height: 80px;
80 | }
81 |
82 | .footer__developer {
83 | justify-content: center;
84 | }
85 |
86 | p {
87 | font-family: 'Playfair Display SC', serif;
88 | text-align: center;
89 | font-size: 16px;
90 | color: black;
91 | }
92 |
93 | .footer_developer-container{
94 | display: flex;
95 | justify-content: center;
96 | }
97 |
98 | i.fab {
99 | color: black;
100 | margin: 0 10px;
101 | }
102 |
--------------------------------------------------------------------------------
/backend/routes/api/session.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const asyncHandler = require('express-async-handler');
3 | const { check } = require('express-validator');
4 | const { handleValidationErrors } = require('../../utils/validation');
5 |
6 | const { setTokenCookie, restoreUser } = require('../../utils/auth');
7 | const { User } = require('../../db/models');
8 | const router = express.Router();
9 |
10 | const validateLogin = [
11 | check('credential')
12 | .exists({ checkFalsy: true })
13 | .notEmpty()
14 | .withMessage('Please provide a valid email or username.'),
15 | check('password')
16 | .exists({ checkFalsy: true })
17 | .withMessage('Please provide a password.'),
18 | handleValidationErrors,
19 | ];
20 |
21 | // Restore session user
22 | router.get(
23 | '/',
24 | restoreUser,
25 | (req, res) => {
26 | const { user } = req;
27 | if (user) {
28 | return res.json({
29 | user: user.toSafeObject()
30 | });
31 | } else return res.json({});
32 | }
33 | );
34 |
35 | // Log in
36 | router.post(
37 | '/',
38 | validateLogin,
39 | asyncHandler(async (req, res, next) => {
40 | const { credential, password } = req.body;
41 |
42 | const user = await User.login({ credential, password });
43 |
44 | if (!user) {
45 | const err = new Error('Login failed');
46 | err.status = 401;
47 | err.title = 'Login failed';
48 | err.errors = ['The provided credentials were invalid.'];
49 | return next(err);
50 | }
51 |
52 | await setTokenCookie(res, user);
53 |
54 | return res.json({
55 | user,
56 | });
57 | }),
58 | );
59 |
60 | // Demo User
61 | router.post('/demo', asyncHandler(async(req, res) => {
62 | const { credential, password } = req.body;
63 |
64 | const demoUser = await User.login({credential,password
65 | });
66 |
67 | await setTokenCookie(res, demoUser);
68 |
69 | return res.json({
70 | demoUser: demoUser.toSafeObject(),
71 | });
72 | }),
73 | );
74 |
75 | // Log out
76 | router.delete(
77 | '/',
78 | (_req, res) => {
79 | res.clearCookie('token');
80 | return res.json({ message: 'success' });
81 | }
82 | );
83 |
84 | module.exports = router;
--------------------------------------------------------------------------------
/frontend/src/components/BookingDates/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { DateRangePicker } from "react-date-range";
4 | import 'react-date-range/dist/styles.css';
5 | import 'react-date-range/dist/theme/default.css';
6 | import { userHauntingCreate } from '../../store/userHauntings';
7 | import PeopleIcon from "@material-ui/icons/People";
8 | import "./BookingDates.css"
9 |
10 | function BookingDates() {
11 | const [ bookingStartDate, setBookingStartDate ] = useState(new Date());
12 | const [ bookingEndDate, setBookingEndDate ] = useState(new Date());
13 | const [ errors, setErrors ] = useState([]);
14 |
15 | const sessionUser = useSelector(state => state.session.user);
16 | const dispatch = useDispatch();
17 |
18 | const selectionRange = {
19 | bookingStartDate: bookingStartDate,
20 | bookingEndDate: bookingEndDate,
21 | key: 'selection',
22 | };
23 |
24 | function handleSelect(ranges) {
25 | setBookingStartDate(ranges.selection.bookingStartDate);
26 | setBookingEndDate(ranges.selection.bookingEndDate);
27 | }
28 |
29 | const onSubmit = (e) => {
30 | e.preventDefault();
31 | setErrors([]);
32 | return dispatch(userHauntingCreate({ sessionUser, bookingStartDate, bookingEndDate }))
33 | }
34 |
35 | return (
36 |
55 | )
56 | }
57 | export default BookingDates;
58 |
--------------------------------------------------------------------------------
/frontend/src/components/UserHaunting/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { DateRangePicker } from 'react-date-range'
4 | import {userHauntingCreate} from '../../store/userHauntings'
5 | import 'react-date-range/dist/styles.css';
6 | import 'react-date-range/dist/theme/default.css';
7 |
8 | const UserHaunting = ({handleClose}) => {
9 | const [bookingStartDate, setBookingStartDate] = useState(new Date());
10 | const [bookingEndDate, setBookingEndDate] = useState(new Date());
11 | const [errors, setErrors] = useState([]);
12 |
13 | const sessionUser = useSelector(state => state.session.user);
14 | const dispatch = useDispatch();
15 |
16 | const selectionRange = {
17 | bookingStartDate,
18 | bookingEndDate,
19 | key: 'selection'
20 | }
21 |
22 | const onSelection = (dates) => {
23 | setBookingStartDate(dates.selection.bookingStartDate)
24 | setBookingEndDate(dates.selection.bookingEndDate)
25 | }
26 |
27 | const onSubmit = (e) => {
28 | e.preventDefault();
29 | setErrors([])
30 | return dispatch(userHauntingCreate({sessionUser, bookingStartDate, bookingEndDate})).then(handleClose()).catch(
31 | (res) => {
32 | if (res.data && res.data.errors) setErrors(res.data.errors);
33 | }
34 | );
35 | }
36 |
37 |
38 | return (
39 |
61 | )
62 | }
63 |
64 | export default UserHaunting;
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Redirect } from 'react-router-dom';
3 | import * as sessionActions from '../../store/session';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import DemoUser from './DemoUser';
6 | import './LoginForm.css';
7 |
8 | const LoginForm = () => {
9 | const dispatch = useDispatch();
10 | const sessionUser = useSelector(state => state.session.user);
11 | const [credential, setCredential] = useState('');
12 | const [password, setPassword] = useState('');
13 | const [errors, setErrors] = useState([]);
14 |
15 | if (sessionUser) return (
16 |
17 | );
18 |
19 | const handleSubmit = (e) => {
20 | e.preventDefault();
21 | setErrors([]);
22 | return dispatch(sessionActions.login({ credential, password }))
23 | .catch((res) => {
24 | if (res.data && res.data.errors) setErrors(res.data.errors);
25 | });
26 | }
27 |
28 | return (
29 |
64 | );
65 | }
66 |
67 | export default LoginForm;
--------------------------------------------------------------------------------
/frontend/src/components/CreateUserHaunting/CreateUserHauntingModal.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { DateRangePicker } from 'react-date-range'
3 | import { useDispatch, useSelector } from 'react-redux';
4 |
5 | import './CreateUserHaunting.css'
6 | import 'react-date-range/dist/styles.css';
7 | import 'react-date-range/dist/theme/default.css';
8 | import {userHauntingCreate} from '../../store/userHauntings';
9 |
10 |
11 | const CreateUserHaunting = ({handleClose}) => {
12 | const [bookingStartDate, setBookingStartDate] = useState(new Date());
13 | const [bookingEndDate, setBookingEndDate] = useState(new Date());
14 | const [errors, setErrors] = useState([]);
15 |
16 | const sessionUser = useSelector(state => state.session.user);
17 | const dispatch = useDispatch();
18 |
19 | const selectionRange = {
20 | bookingStartDate,
21 | bookingEndDate,
22 | key: 'selection'
23 | }
24 |
25 |
26 | const onSelection = (dates) => {
27 | setBookingStartDate(dates.selection.bookingStartDate)
28 | setBookingEndDate(dates.selection.bookingEndDate)
29 | }
30 | const onSubmit = (e) => {
31 | e.preventDefault();
32 | setErrors([])
33 | return dispatch(userHauntingCreate({ sessionUser, bookingStartDate, bookingEndDate })).then(handleClose()).catch(
34 | (res) => {
35 | if (res.data && res.data.errors) setErrors(res.data.errors);
36 | }
37 | );
38 | }
39 |
40 | return (
41 |
63 | )
64 | }
65 |
66 | export default CreateUserHaunting;
--------------------------------------------------------------------------------
/backend/db/models/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { Validator } = require('sequelize');
3 | const bcrypt = require('bcryptjs');
4 |
5 | module.exports = (sequelize, DataTypes) => {
6 | const User = sequelize.define('User', {
7 | username: {
8 | type: DataTypes.STRING(30),
9 | allowNull: false,
10 | unique: true,
11 | validate: {
12 | len: [4, 30],
13 | isNotEmail(value) {
14 | if (Validator.isEmail(value)) {
15 | throw new Error('Cannot be an email.');
16 | }
17 | },
18 | },
19 | },
20 | email: {
21 | type: DataTypes.STRING(256),
22 | unique: true,
23 | allowNull: false,
24 | validate: {
25 | len: [3, 256]
26 | },
27 | },
28 | hashedPassword: {
29 | type: DataTypes.STRING.BINARY,
30 | allowNull: false,
31 | validate: {
32 | len: [60, 60]
33 | },
34 | },
35 | },
36 | {
37 | defaultScope: {
38 | attributes: {
39 | exclude: ['hashedPassword', 'email', 'createdAt', 'updatedAt'],
40 | },
41 | },
42 | scopes: {
43 | currentUser: {
44 | attributes: { exclude: ['hashedPassword'] },
45 | },
46 | loginUser: {
47 | attributes: {},
48 | },
49 | },
50 | });
51 | User.prototype.toSafeObject = function() { // remember, this cannot be an arrow function
52 | const { id, username, email } = this; // context will be the User instance
53 | return { id, username, email };
54 | };
55 | User.prototype.validatePassword = function (password) {
56 | return bcrypt.compareSync(password, this.hashedPassword.toString());
57 | };
58 | User.getCurrentUserById = async function (id) {
59 | return await User.scope('currentUser').findByPk(id);
60 | };
61 | User.login = async function ({ credential, password }) {
62 | const { Op } = require('sequelize');
63 | const user = await User.scope('loginUser').findOne({
64 | where: {
65 | [Op.or]: {
66 | username: credential,
67 | email: credential,
68 | },
69 | },
70 | });
71 | if (user && user.validatePassword(password)) {
72 | return await User.scope('currentUser').findByPk(user.id);
73 | }
74 | };
75 | User.signup = async function ({ username, email, password }) {
76 | const hashedPassword = bcrypt.hashSync(password);
77 | const user = await User.create({
78 | username,
79 | email,
80 | hashedPassword,
81 | });
82 | return await User.scope('currentUser').findByPk(user.id);
83 | };
84 | User.associate = function(models) {
85 | User.hasMany(models.UserHaunting, {foreignKey: 'userId'});
86 | const columnMapping = {
87 | through: 'UserHaunting',
88 | otherKey: 'hauntingId',
89 | foreignKey: 'userId'
90 | }
91 | User.belongsToMany(models.Haunting,columnMapping);
92 | };
93 | return User;
94 | };
--------------------------------------------------------------------------------
/frontend/src/components/CreateUserHaunting/CreateUserHaunting.css:
--------------------------------------------------------------------------------
1 | .form__login{
2 | margin:auto;
3 | display: flex;
4 | flex-direction: column;
5 | padding: 2rem;
6 | background-color:#333;
7 | }
8 |
9 | .form__content-container{
10 | display: flex;
11 | justify-content: space-between;
12 | flex-direction: column;
13 | }
14 |
15 | .form__title{
16 | color: #333;
17 | margin-top: -.5rem;
18 | align-self: center;
19 | font-size: 30px;
20 | font-family: 'Playfair Display SC', serif;
21 | letter-spacing:1.2px;
22 | }
23 |
24 | .form__error-container{
25 | display: flex;
26 | justify-content: space-between;
27 | flex-direction: column;
28 | padding: 1.5rem;
29 | }
30 |
31 | .error-list {
32 | margin: auto;
33 | list-style: none;
34 | color: rgb(143, 18, 18);
35 | text-align: center;
36 | padding: .5rem rem;
37 |
38 | }
39 |
40 | .form__input-container{
41 | margin: .2rem;
42 | }
43 |
44 | .form__input-container--textarea{
45 | background-color: rgb(50,52,54);
46 | border: .15px solid #333;
47 | color:rgb(197,198,200);
48 | width: 100%;
49 | height: 3rem;
50 | }
51 | .form__input-container--textarea:focus{
52 | background-color: rgb(50,52,54);
53 | border: .15px solid #333;
54 | color:rgb(197,198,200);
55 | width: 100%;
56 | height: 3rem;
57 | }
58 |
59 | .form__input-container--text:focus{
60 | background-color: rgb(50,52,54);
61 | color:rgb(197,198,200);
62 | border-radius: 4px;
63 | border: .15px solid #333;
64 | padding: .3rem;
65 | }
66 |
67 | .form__button{
68 | align-self: center;
69 | margin: 2rem 1rem 1rem 1rem;
70 | }
71 |
72 | .form__button-button{
73 | padding: .7rem;
74 | background-color: #333;
75 | border-radius: 5px;
76 | border: none;
77 | }
78 | #cancel{
79 | background-color: rgb(143, 18, 18);
80 | margin-right: .3rem;
81 |
82 | }
83 | .rdrDefinedRangesWrapper{
84 | display:none;
85 | }
86 | .rdrMonth {
87 | background-color: rgb(118,120,124);
88 | }
89 |
90 | .rdrDateDisplay{
91 | background-color: rgb(50, 52, 54);
92 | }
93 |
94 | .rdrDay.rdrDayDisabled{
95 | background-color: rgb(118,120,124);
96 | }
97 |
98 | div.rdrDateDisplayWrapper, div.rdrMonthAndYearWrapper, div.rdrWeekDays{
99 | background-color: rgb(50, 52, 54);
100 | }
101 | span.rdrYearPicker > select{
102 | color:rgb(118,120,124);
103 | }
104 |
105 | span.rdrMonthPicker > select{
106 | color:rgb(118,120,124);
107 | }
108 | div.rdrMonthName{
109 | color:rgb(197,198,200);
110 | }
111 |
112 | span.rdrDateInput > input{
113 | background-color: rgb(51,63,81);
114 |
115 | }
116 | span.rdrDateInput{
117 | border: .15px solid rgb(11,12,15);
118 | border-right: .15px solid rgb(11,12,15);
119 | }
120 |
121 | button.rdrNextPrevButton{
122 | background-color: rgb(197,198,200);
123 | }
124 |
125 | span.rdrInRange, span.rdrEndEdge, span.rdrStartEdge{
126 | background-color:rgb(144,146,150)
127 | }
128 |
--------------------------------------------------------------------------------
/backend/awsS3.js:
--------------------------------------------------------------------------------
1 | const AWS = require("aws-sdk");
2 | // name of your bucket here
3 | const NAME_OF_BUCKET = "reactsolobucket";
4 |
5 | const multer = require("multer");
6 |
7 | // make sure to set environment variables in production for:
8 | // AWS_ACCESS_KEY_ID
9 | // AWS_SECRET_ACCESS_KEY
10 | // and aws will automatically use those environment variables
11 |
12 | const s3 = new AWS.S3({ apiVersion: "2006-03-01" });
13 |
14 | // --------------------------- Public UPLOAD ------------------------
15 |
16 | const singlePublicFileUpload = async (file) => {
17 | const { originalname, mimetype, buffer } = await file;
18 | const path = require("path");
19 | // name of the file in your S3 bucket will be the date in ms plus the extension name
20 | const Key = new Date().getTime().toString() + path.extname(originalname);
21 | const uploadParams = {
22 | Bucket: NAME_OF_BUCKET,
23 | Key,
24 | Body: buffer,
25 | ACL: "public-read",
26 | };
27 | const result = await s3.upload(uploadParams).promise();
28 |
29 | // save the name of the file in your bucket as the key in your database to retrieve for later
30 | return result.Location;
31 | };
32 |
33 | const multiplePublicFileUpload = async (files) => {
34 | return await Promise.all(
35 | files.map((file) => {
36 | return singlePublicFileUpload(file);
37 | })
38 | );
39 | };
40 |
41 | // --------------------------- Prviate UPLOAD ------------------------
42 |
43 | const singlePrivateFileUpload = async (file) => {
44 | const { originalname, mimetype, buffer } = await file;
45 | const path = require("path");
46 | // name of the file in your S3 bucket will be the date in ms plus the extension name
47 | const Key = new Date().getTime().toString() + path.extname(originalname);
48 | const uploadParams = {
49 | Bucket: NAME_OF_BUCKET,
50 | Key,
51 | Body: buffer,
52 | };
53 | const result = await s3.upload(uploadParams).promise();
54 |
55 | // save the name of the file in your bucket as the key in your database to retrieve for later
56 | return result.Key;
57 | };
58 |
59 | const multiplePrivateFileUpload = async (files) => {
60 | return await Promise.all(
61 | files.map((file) => {
62 | return singlePrivateFileUpload(file);
63 | })
64 | );
65 | };
66 |
67 | const retrievePrivateFile = (key) => {
68 | let fileUrl;
69 | if (key) {
70 | fileUrl = s3.getSignedUrl("getObject", {
71 | Bucket: NAME_OF_BUCKET,
72 | Key: key,
73 | });
74 | }
75 | return fileUrl || key;
76 | };
77 |
78 | // --------------------------- Storage ------------------------
79 |
80 | const storage = multer.memoryStorage({
81 | destination: function (req, file, callback) {
82 | callback(null, "");
83 | },
84 | });
85 |
86 | const singleMulterUpload = (nameOfKey) =>
87 | multer({ storage: storage }).single(nameOfKey);
88 | const multipleMulterUpload = (nameOfKey) =>
89 | multer({ storage: storage }).array(nameOfKey);
90 |
91 | module.exports = {
92 | s3,
93 | singlePublicFileUpload,
94 | multiplePublicFileUpload,
95 | singlePrivateFileUpload,
96 | multiplePrivateFileUpload,
97 | retrievePrivateFile,
98 | singleMulterUpload,
99 | multipleMulterUpload,
100 | };
--------------------------------------------------------------------------------
/frontend/src/store/session.js:
--------------------------------------------------------------------------------
1 | import { fetch } from './csrf';
2 |
3 | const SET_USER = 'session/setUser';
4 | const REMOVE_USER = 'session/removeUser'
5 |
6 | const setUser = (user) => {
7 | return {
8 | type: SET_USER,
9 | payload: user
10 | };
11 | };
12 |
13 | const removeUser = () => {
14 | return {
15 | type: REMOVE_USER,
16 | };
17 | };
18 |
19 |
20 | // Call API to login, then set session user from response
21 | // Thunk action for POST/api/session
22 | export const login = (user) => async (dispatch) => {
23 | const {credential, password } = user;
24 | // Use custom fetch function
25 | const response = await fetch('/api/session', {
26 | method: 'POST',
27 | // Route expects req.body to have key of credential-username/email & key of password
28 | body: JSON.stringify({
29 | credential,
30 | password,
31 | }),
32 | });
33 | // After response from AJAX call, dispatch action for setting session user to response's data
34 | dispatch(setUser(response.data.user));
35 | return response;
36 | };
37 |
38 |
39 | export const restore = () => async (dispatch) => {
40 | const response = await fetch('/api/session');
41 | dispatch(setUser(response.data.user));
42 | return response;
43 | };
44 |
45 | export const signup = (user) => async (dispatch) => {
46 | const { username, email, password } = user;
47 | const response = await fetch("/api/users", {
48 | method: "POST",
49 | body: JSON.stringify({
50 | username,
51 | email,
52 | password,
53 | }),
54 | });
55 | dispatch(setUser(response.data.user));
56 | return response;
57 | };
58 |
59 | export const logout = () => async (dispatch) => {
60 | const response = await fetch("/api/session", {
61 | method: "DELETE",
62 | });
63 | dispatch(removeUser());
64 | return response;
65 | };
66 |
67 | export const demoLogin = () => async (dispatch) => {
68 | const response = await fetch('/api/session/demo', {
69 | method: 'POST',
70 | body: JSON.stringify({
71 | credential: 'demoUser',
72 | password: 'demoUser123',
73 | }),
74 | });
75 | dispatch(setUser(response.data.demoUser));
76 | return response;
77 | };
78 | // By default, no session user in session slice of state
79 | const initialState = {user: null};
80 | // Reducer will hold current session user's information
81 | const sessionReducer = (state = initialState, action) => {
82 | let newState;
83 | switch (action.type) {
84 | // POJO Action Creator - set session user in session slice of state to AC's input parameter
85 | case SET_USER:
86 | newState = Object.assign({}, state);
87 | // if current session user, {user: {id, email, username, createdAt, updatedAt}}
88 | newState.user = action.payload;
89 | return newState;
90 | // POJO Action Creator - will remove session user
91 | case REMOVE_USER:
92 | newState = Object.assign({}, state);
93 | // if no session user, session slice of state {user:null}
94 | newState.user = null;
95 | return newState;
96 | default:
97 | return state;
98 | }
99 | };
100 |
101 | export default sessionReducer;
--------------------------------------------------------------------------------
/frontend/src/components/SignupFormModal/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from "react-router-dom";
4 | import './SignupForm.css';
5 | import {signup} from '../../store/session';
6 |
7 | function SignupForm() {
8 | const dispatch = useDispatch();
9 | const sessionUser = useSelector((state) => state.session.user);
10 | const [email, setEmail] = useState("");
11 | const [username, setUsername] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [confirmPassword, setConfirmPassword] = useState("");
14 | const [errors, setErrors] = useState([]);
15 |
16 | if (sessionUser) return ;
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 | setErrors([]);
21 | if (password === confirmPassword) {
22 | return dispatch(signup({ email, username, password }))
23 | .catch(res => {
24 | if (res.data && res.data.errors) setErrors(res.data.errors);
25 | });
26 | }
27 | return setErrors(['Confirm Password field must be the same as the Password field']);
28 | };
29 |
30 | return (
31 |
90 | );
91 | }
92 |
93 | export default SignupForm;
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Create React App Template
3 |
4 | A no-frills template from which to create React applications with
5 | [Create React App](https://github.com/facebook/create-react-app).
6 |
7 | ```sh
8 | npx create-react-app my-app --template @appacademy/react-v17 --use-npm
9 | ```
10 |
11 | ## Available Scripts
12 |
13 | In the project directory, you can run:
14 |
15 | ### `npm start`
16 |
17 | Runs the app in the development mode.\
18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
19 |
20 | The page will reload if you make edits.\
21 | You will also see any lint errors in the console.
22 |
23 | ### `npm test`
24 |
25 | Launches the test runner in the interactive watch mode.\
26 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
27 |
28 | ### `npm run build`
29 |
30 | Builds the app for production to the `build` folder.\
31 | It correctly bundles React in production mode and optimizes the build for the best performance.
32 |
33 | The build is minified and the filenames include the hashes.\
34 | Your app is ready to be deployed!
35 |
36 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
37 |
38 | ### `npm run eject`
39 |
40 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
41 |
42 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
43 |
44 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
45 |
46 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
47 |
48 | ## Learn More
49 |
50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
51 |
52 | To learn React, check out the [React documentation](https://reactjs.org/).
53 |
54 | ### Code Splitting
55 |
56 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
57 |
58 | ### Analyzing the Bundle Size
59 |
60 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
61 |
62 | ### Making a Progressive Web App
63 |
64 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
65 |
66 | ### Advanced Configuration
67 |
68 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
69 |
70 | ### Deployment
71 |
72 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
73 |
74 | ### `npm run build` fails to minify
75 |
76 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
77 |
--------------------------------------------------------------------------------
/frontend/.eslintcache:
--------------------------------------------------------------------------------
1 | <<<<<<< HEAD
2 | [{"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/index.js":"1","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/App.js":"2","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/session.js":"3","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/csrf.js":"4","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/index.js":"5","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/index.js":"6","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/ProfileButton.js":"7","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Footer/index.js":"8","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingProfile/index.js":"9","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/hauntings.js":"10","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Landing/index.js":"11","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingCard/index.js":"12","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/userHauntings.js":"13","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/BookingDates/index.js":"14","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchBar/index.js":"15","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/Modal.js":"16","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/index.js":"17","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/LoginForm.js":"18","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/index.js":"19","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/SignUpForm.js":"20","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/DemoUser.js":"21","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchResults/index.js":"22","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/SearchContext.js":"23","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/UserHaunting/index.js":"24"},{"size":1032,"mtime":1612062717170,"results":"25","hashOfConfig":"26"},{"size":1491,"mtime":1613609381456,"results":"27","hashOfConfig":"26"},{"size":2924,"mtime":1612036760418,"results":"28","hashOfConfig":"26"},{"size":1534,"mtime":1611701802193,"results":"29","hashOfConfig":"26"},{"size":940,"mtime":1611879413899,"results":"30","hashOfConfig":"26"},{"size":1584,"mtime":1612152019764,"results":"31","hashOfConfig":"26"},{"size":1257,"mtime":1612195197294,"results":"32","hashOfConfig":"26"},{"size":711,"mtime":1611859133591,"results":"33","hashOfConfig":"26"},{"size":1956,"mtime":1613621294974,"results":"34","hashOfConfig":"26"},{"size":1972,"mtime":1612064494570,"results":"35","hashOfConfig":"26"},{"size":320,"mtime":1612064241039,"results":"36","hashOfConfig":"26"},{"size":1635,"mtime":1612154692244,"results":"37","hashOfConfig":"26"},{"size":1727,"mtime":1613523550946,"results":"38","hashOfConfig":"26"},{"size":2068,"mtime":1613523550945,"results":"39","hashOfConfig":"26"},{"size":683,"mtime":1612062472563,"results":"40","hashOfConfig":"26"},{"size":839,"mtime":1611960766082,"results":"41","hashOfConfig":"26"},{"size":497,"mtime":1611979022342,"results":"42","hashOfConfig":"26"},{"size":2316,"mtime":1611983941512,"results":"43","hashOfConfig":"26"},{"size":545,"mtime":1611972281913,"results":"44","hashOfConfig":"26"},{"size":3157,"mtime":1611975848961,"results":"45","hashOfConfig":"26"},{"size":469,"mtime":1611983815465,"results":"46","hashOfConfig":"26"},{"size":1705,"mtime":1612154031999,"results":"47","hashOfConfig":"26"},{"size":407,"mtime":1612062843377,"results":"48","hashOfConfig":"26"},{"size":2322,"mtime":1613621180495,"results":"49","hashOfConfig":"26"},{"filePath":"50","messages":"51","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},"mwlq1j",{"filePath":"53","messages":"54","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"55","messages":"56","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"57","messages":"58","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"59","messages":"60","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"61","messages":"62","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"63","messages":"64","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"65","messages":"66","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"67","messages":"68","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"69","messages":"70","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"71","messages":"72","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"73","messages":"74","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"75","messages":"76","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"77","messages":"78","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"79","messages":"80","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"81","messages":"82","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"83","messages":"84","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"85","messages":"86","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"87","messages":"88","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"89","messages":"90","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"91","messages":"92","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"93"},{"filePath":"94","messages":"95","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"96","messages":"97","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"98","messages":"99","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/index.js",[],["100","101"],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/App.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/session.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/csrf.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/ProfileButton.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Footer/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingProfile/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/hauntings.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Landing/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingCard/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/userHauntings.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/BookingDates/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchBar/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/Modal.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/LoginForm.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/SignUpForm.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/DemoUser.js",[],["102","103"],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchResults/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/SearchContext.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/UserHaunting/index.js",[],{"ruleId":"104","replacedBy":"105"},{"ruleId":"106","replacedBy":"107"},{"ruleId":"104","replacedBy":"108"},{"ruleId":"106","replacedBy":"109"},"no-native-reassign",["110"],"no-negated-in-lhs",["111"],["110"],["111"],"no-global-assign","no-unsafe-negation"]
3 | =======
4 | [{"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/index.js":"1","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/App.js":"2","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/session.js":"3","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/csrf.js":"4","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/index.js":"5","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/index.js":"6","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/ProfileButton.js":"7","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Footer/index.js":"8","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingProfile/index.js":"9","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/hauntings.js":"10","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Landing/index.js":"11","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingCard/index.js":"12","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/userHauntings.js":"13","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchBar/index.js":"14","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/Modal.js":"15","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/index.js":"16","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/LoginForm.js":"17","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/index.js":"18","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/SignUpForm.js":"19","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/DemoUser.js":"20","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchResults/index.js":"21","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/SearchContext.js":"22","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/CreateUserHaunting/index.js":"23","/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/CreateUserHaunting/CreateUserHauntingModal.js":"24"},{"size":1032,"mtime":1612062717170,"results":"25","hashOfConfig":"26"},{"size":1346,"mtime":1614055179164,"results":"27","hashOfConfig":"26"},{"size":2924,"mtime":1612036760418,"results":"28","hashOfConfig":"26"},{"size":1534,"mtime":1611701802193,"results":"29","hashOfConfig":"26"},{"size":940,"mtime":1611879413899,"results":"30","hashOfConfig":"26"},{"size":1584,"mtime":1614055179173,"results":"31","hashOfConfig":"26"},{"size":1257,"mtime":1612195197294,"results":"32","hashOfConfig":"26"},{"size":711,"mtime":1611859133591,"results":"33","hashOfConfig":"26"},{"size":1318,"mtime":1614057639863,"results":"34","hashOfConfig":"26"},{"size":1385,"mtime":1614057992744,"results":"35","hashOfConfig":"26"},{"size":320,"mtime":1612064241039,"results":"36","hashOfConfig":"26"},{"size":1635,"mtime":1612154692244,"results":"37","hashOfConfig":"26"},{"size":1621,"mtime":1614058150945,"results":"38","hashOfConfig":"26"},{"size":683,"mtime":1612062472563,"results":"39","hashOfConfig":"26"},{"size":839,"mtime":1611960766082,"results":"40","hashOfConfig":"26"},{"size":497,"mtime":1611979022342,"results":"41","hashOfConfig":"26"},{"size":2316,"mtime":1611983941512,"results":"42","hashOfConfig":"26"},{"size":545,"mtime":1611972281913,"results":"43","hashOfConfig":"26"},{"size":3157,"mtime":1611975848961,"results":"44","hashOfConfig":"26"},{"size":469,"mtime":1611983815465,"results":"45","hashOfConfig":"26"},{"size":1705,"mtime":1612154031999,"results":"46","hashOfConfig":"26"},{"size":407,"mtime":1612062843377,"results":"47","hashOfConfig":"26"},{"size":759,"mtime":1614057582471,"results":"48","hashOfConfig":"26"},{"size":2424,"mtime":1614058195113,"results":"49","hashOfConfig":"26"},{"filePath":"50","messages":"51","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},"1a0vi5k",{"filePath":"53","messages":"54","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"55","messages":"56","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"57","messages":"58","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"59","messages":"60","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"61","messages":"62","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"63","messages":"64","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"65","messages":"66","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"67","messages":"68","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"69","messages":"70","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"71","messages":"72","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"73","messages":"74","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"75","messages":"76","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"77","messages":"78","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"79","messages":"80","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"81","messages":"82","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"83","messages":"84","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"85","messages":"86","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"87","messages":"88","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"89","messages":"90","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"91"},{"filePath":"92","messages":"93","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"94","messages":"95","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"96","messages":"97","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},{"filePath":"98","messages":"99","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"52"},"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/index.js",[],["100","101"],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/App.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/session.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/csrf.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Navigation/ProfileButton.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Footer/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingProfile/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/hauntings.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/Landing/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/HauntingCard/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/store/userHauntings.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchBar/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/Modal.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/LoginForm.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SignupFormModal/SignUpForm.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/LoginFormModal/DemoUser.js",[],["102","103"],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/SearchResults/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/context/SearchContext.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/CreateUserHaunting/index.js",[],"/Users/courtneynewcomer/Desktop/solo-react-redux-project/scarebnb/frontend/src/components/CreateUserHaunting/CreateUserHauntingModal.js",[],{"ruleId":"104","replacedBy":"105"},{"ruleId":"106","replacedBy":"107"},{"ruleId":"104","replacedBy":"108"},{"ruleId":"106","replacedBy":"109"},"no-native-reassign",["110"],"no-negated-in-lhs",["111"],["110"],["111"],"no-global-assign","no-unsafe-negation"]
5 | >>>>>>> main
6 |
--------------------------------------------------------------------------------
/backend/db/seeders/20210221042342-seedHauntings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 |
6 | return queryInterface.bulkInsert('Hauntings', [
7 | {
8 | statusId: '1',
9 | locationName: 'Beetlejuice House',
10 | city: 'East Corinth',
11 | state: 'VT',
12 | country: 'United States',
13 | description: 'Newly renovated by the Deets family, this modern architectural wonder will leave you saying "Beetleguise, Beetleguise, Beetleguise!" Featuring a dining room with enough space to host a conga line, a family room with exotic plants and a roaring fireplace, and a miniature replica of the entire quaint town in Connecticut.',
14 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Beetlejuice_1.png',
15 | price: 250
16 | },
17 | {
18 | statusId: '1',
19 | locationName: 'Buckner Mansion',
20 | city: 'New Orleans',
21 | state: 'LA',
22 | country: 'United States',
23 | description: 'Ready to practice and cast your spells? You surely can with these fellow witches and supremes, casting necromancy and herbology encantations in the great open meeting room.',
24 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Buckner_1.png',
25 | price: 500
26 | },
27 | {
28 | statusId: '1',
29 | locationName: 'Thornewood Castle',
30 | city: 'Seattle',
31 | state: 'WA',
32 | country: 'United States',
33 | description: 'Reports of a faceless figure roaming the halls at night, going in and out of the house.',
34 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Thornewood_1.png',
35 | price: 800
36 | },
37 | {
38 | statusId: '1',
39 | locationName: 'Bisham Manor',
40 | city: 'LaGrange',
41 | state: 'GA',
42 | country: 'United States',
43 | description: 'Broken-neck lady appears at the foot of your bed, some friendly spirits.',
44 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Bisham_1.png',
45 | price: 250
46 | },
47 | {
48 | statusId: '1',
49 | locationName: 'Palacio de Los Hornillos',
50 | city: 'Cantabria',
51 | state: '',
52 | country: 'Spain',
53 | description: 'Apparitions of a mother and children all around the house, shutting curtains and doors, trying to enshroud the mansion in darkness',
54 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Palacio_1.png',
55 | price: 822
56 | },
57 | {
58 | statusId: '1',
59 | locationName: 'Allerdale Hall',
60 | city: 'Guillermo',
61 | state: 'British Columbia',
62 | country: 'Canada',
63 | description: 'A woman in a white dress wanders the halls of this Gothic mansion crying out for her lost lover.',
64 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Allerdale_1.png',
65 | price: 900
66 | },
67 | {
68 | statusId: '1',
69 | locationName: 'Cotterstock Hall',
70 | city: 'Cotterstock',
71 | state: 'Peterborough PE8 5HD',
72 | country: 'UK',
73 | description: 'Lady in black Victorian clothing weeping walks throughout the grounds.',
74 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Cotterstock_1.png',
75 | price: 220
76 | },
77 | {
78 | statusId: '1',
79 | locationName: 'Amityville House',
80 | city: 'Amityville',
81 | state: 'NY',
82 | country: 'United States',
83 | description: 'Green slime coming out cracks in walls. Figure with red eyes.',
84 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Amityville_1.png',
85 | price: 250
86 | },
87 | {
88 | statusId: '1',
89 | locationName: 'Old Arnold Estate',
90 | city: 'Harrisville',
91 | state: 'RI',
92 | country: 'United States',
93 | description: 'The witch Bathsheeba Sherman haunts this house, poking guests that fall asleep with a large sewing needle.',
94 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Conjuring_1.png',
95 | price: 75
96 | },
97 | {
98 | statusId: '1',
99 | locationName: 'Shaker Mansion',
100 | city: 'Roanoke',
101 | state: 'NC',
102 | country: 'United States',
103 | description: 'A lone indigenous spirit haunts this mansion, having ceremonies in front of the large windows and outside.',
104 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/ShakerMansion_1.png',
105 | price: 200
106 | },
107 | {
108 | statusId: '1',
109 | locationName: 'Dunsmuir House',
110 | city: 'Oakland',
111 | state: 'CA',
112 | country: 'United States',
113 | description: 'Billowing, floating figure supposedly follows you around, waiting to steal your happiness.',
114 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Dunsmuir_1.png',
115 | price: 500
116 | },
117 | {
118 | statusId: '1',
119 | locationName: 'Hatley Castle',
120 | city: 'Hatley Castle',
121 | state: 'British Columbia',
122 | country: 'Canada',
123 | description: 'Elderly man roams the halls knocking on all of the doors looking for his wife.',
124 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Hatley_1.png',
125 | price: 900
126 | },
127 | {
128 | statusId: '1',
129 | locationName: 'Murder House',
130 | city: 'Los Angeles',
131 | state: 'CA',
132 | country: 'United States',
133 | description: 'A figure dressed in black with no mouth, a moody teenager, a mother who died during childbirth, and a father who was framed all haunt this location.',
134 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/MurderHouse_1.png',
135 | price: 500
136 | },
137 | {
138 | statusId: '1',
139 | locationName: 'Gilmoure House',
140 | city: 'Kalamazoo',
141 | state: 'MI',
142 | country: 'United States',
143 | description: 'The house is over a hundred years old and used to belong to the Gilmoure family. They were a wealthy family in the Kalamazoo area, it is said that the butler fell down the servants stair case all the way to the basement and died. There is a built in grand father clock on the second floor that always stops on the same time whenever the clock is started, supposedly the same time the butler died. Brothers that live there report the presence of "something" at night, lights have turned them selves on or off and things have been moved with no one being present.',
144 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Gilmoure_1.png',
145 | price: 250
146 | },
147 | {
148 | statusId: '1',
149 | locationName: 'Hathaway House',
150 | city: 'New Baltimore',
151 | state: 'MI',
152 | country: 'United States',
153 | description: 'This house used to be an insane asylum. Many people are said to have killed themselves while there. At night you can see these ghosts in the windows. When inside, you hear voices. Also, if you have lights they may flicker in the presence of the ghost.',
154 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Hathaway_1.png',
155 | price: 100
156 | },
157 | {
158 | statusId: '1',
159 | locationName: 'Old Lonsinger House',
160 | city: 'Carnegie',
161 | state: 'PA',
162 | country: 'United States',
163 | description: 'Reports of hearing an organ playing and seeing an apparition of a "Blue Lady." Many claim that she would knock on their door in the middle of the night saying that something was very wrong in the house though nothing was ever found out of the ordinary.',
164 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/OldLongsinger_1.png',
165 | price: 100
166 | },
167 | {
168 | statusId: '1',
169 | locationName: '"Red House"',
170 | city: 'Gettysburg',
171 | state: 'PA',
172 | country: 'United States',
173 | description: 'This large, civil war era house is always occupied by female students living off-campus, who attend Gettysburg College. There is a small grave in the backyard of the house and it belonged to a young woman who was living there during the civil war. She is said to move around the house at night, moving objects around, breaking plates, glasses, etc. The smell of her perfume, lilac, can be smelled at night when she is walking about the house.',
174 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/RedHouse_1.png',
175 | price: 250
176 | },
177 | {
178 | statusId: '1',
179 | locationName: 'Cary House',
180 | city: 'Chelsea',
181 | state: 'MA',
182 | country: 'United States',
183 | description: 'The Cary House was a mansion in the civil war. The daughter of the family had two lovers, who fought on opposing sides and ending up killing each other on the stairs inside the house. For quite a few years now, one couple would live in the house (a newer part of the house was added onto the old) and give tours.Once things started getting stolen, the tours stopped. It is said that one the night of the wedding anniversary of the Cary parents, you can hear laughter, mumbled conversation, and wine glasses clinking from the old kitchen. You constantly get the feeling of being followed or watched over. And this house has many secret passageways and hidden spots as well.',
184 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Cary_1.png',
185 | price: 100
186 | },
187 | {
188 | statusId: '1',
189 | locationName: 'Heceta Head Lighthouse',
190 | city: 'Florence',
191 | state: 'OR',
192 | country: 'United States',
193 | description: 'Many stories abound around the haunting here, the most widely heard is probably the story of an old woman who haunts the place, the wife of a lighthouse keeper. You can stay here and see for yourself, it is now a bed & breakfast.',
194 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/HecetaHead_1.png',
195 | price: 150
196 | },
197 | {
198 | statusId: '1',
199 | locationName: 'Oliver House',
200 | city: 'Bisbee',
201 | state: 'AZ',
202 | country: 'United States',
203 | description: 'This bed and breakfast is the site of a mass murder. When a husband found his wife with a man who resided here, he killed his wife and her lover. He then walked to the day room and killed whoever he came across. He left, drove to the edge of town, and then killed himself. There was also another murder, but the culprit was never found. The building is pleasant and very nostalgic. Guests that have stayed there have heard what they thought were firecrackers exploding in the hall. Later, they heard the opening and slamming of doors, with heavy footsteps going down the hall. They found out in the morning that no one else had heard any of the sounds they did, but that previous guests had reported similar events. The owner was quite helpful and knowledgeable as to who may be haunting his bed and breakfast.',
204 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Oliver_1.png',
205 | price: 250
206 | },
207 | {
208 | statusId: '1',
209 | locationName: 'Poe House',
210 | city: 'Baltimore',
211 | state: 'MD',
212 | country: 'United States',
213 | description: 'This house is haunted by a female spirit dressed in gray.',
214 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/craigdarroch_1.png',
215 | price: 150
216 | },
217 | {
218 | statusId: '1',
219 | locationName: 'The Dillinger House',
220 | city: 'Sandwich',
221 | state: 'ME',
222 | country: 'United States',
223 | description: 'Cold chills will run down your spine over and over.',
224 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Dillinger_1.png',
225 | price: 150
226 | },
227 | {
228 | statusId: '1',
229 | locationName: 'T\'Frere\'s House',
230 | city: 'Lafayette',
231 | state: 'LA',
232 | country: 'United States',
233 | description: 'There is a young woman said to haunt this Bed and Breakfast. It is one of the oldest homes in Lafayette and it is said she once lived there a hundred or so years ago. The story goes she drowned in the big barrell used to catch the rainwater that is still in place today. The story varies as to whether it was an accident or suicide. She is a friendly ghost who makes objects move, walks around upstairs at night, and moves furniture around. People say she has a comforting presence.',
234 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/TFrere_1.png',
235 | price: 150
236 | },
237 | {
238 | statusId: '1',
239 | locationName: 'Beauregard House',
240 | city: 'New Orleans',
241 | state: 'LA',
242 | country: 'United States',
243 | description: 'The Beauregard-Keyes house is known to be haunted more by Paul Munni, who was a world-class chess master, who went insane. As a matter of fact, Charles Dickenson said that Thomas Jefferson, and Paul Munni were the only two geniuses that America ever produced. In a fit of mania, Munni ran, in the nude, down Ursaline with an axe, looking to kill anyone unfortunate enough to cross his path. When not playing chess, he liked to play the piano. It is the piano, and his screaming that can be heard at night. The Beauregard-Keyes house is also the sight of a mafia massacre. It is said that in the garden, you can smell gunpowder, and sometimes you can hear shots in the garden.',
244 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Beauregard_1.png',
245 | price: 150
246 | },
247 | {
248 | statusId: '1',
249 | locationName: 'The Scott House',
250 | city: 'Napoleon',
251 | state: 'OH',
252 | country: 'United States',
253 | description: 'Built in the mid 1800\'s by Gen Robert Scott, the house has many incidents of paranormal. Gen Scott can be seen walking down the steps in his suit and top hat, levitation of people sleeping on the 3rd floor bedrooms, sounds of people walking through the hallways, cold spots, and infants waking in the middle of the night, then giggling at a misty figure above their crib. Supposedly haunted by the General and at least one female servant of his.',
254 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Scott_1.png',
255 | price: 100
256 | },
257 | {
258 | statusId: '1',
259 | locationName: 'Ivy House Inn ',
260 | city: 'Casper',
261 | state: 'WY',
262 | country: 'United States',
263 | description: 'There is a spirit of a man, who sets off the alarms in the parking lot where a house used to be. There is a lady, a former owner , who goes from room to room. Small catlike animals run down the halls and up the stairs from time to time. There are cold spots in the house. Items have eerily moved. The original 1916 house had a traveling cloud in the sitting room, but after it was torn down for parking, any car parked where the sitting room now sounds off when set, usually after 2:30 am and late in the morning. Many pictures in the house with anomalies or vortices have been taken.',
264 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Ivy_1.png',
265 | price: 150
266 | },
267 | {
268 | statusId: '1',
269 | locationName: 'Loudoun House',
270 | city: 'Lexington',
271 | state: 'KY',
272 | country: 'United States',
273 | description: 'Loudoun house is an 1852 Gothic Villa that was once home to the families of Francis Key Hunt and William Cassius Goodloe that is home to the Lexington Art League. A partial list of activity includes a Victorian woman who haunts the western half of the house, an apparition of a black cat, the apparition of another Victorian woman seen in the former dining room. Aroma of an antique floral perfume in one of the upstairs rooms now used as a studio, voices and distant strains of ballroom music are sometimes heard.',
274 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Loudoun_1.png',
275 | price: 150
276 | },
277 | {
278 | statusId: '1',
279 | locationName: 'The Dupree House (also known as Dunwich Manor)',
280 | city: 'Fort Covington',
281 | state: 'NY',
282 | country: 'United States',
283 | description: 'This old Victorian mansion, once owned by occult writer Gerina Dunwich, is known by local residents to be haunted. In the early 20th century, a mentally disturbed woman was believed to have been locked away in one of the small attic rooms. She died there, leaving bloodied hand prints on the walls and ceiling. Several coats of primer and paint were necessary to cover the stains. The room contains a cold spot and several séances have been held there in an attempt to contact the restless spirit. Strange thumps have also been heard in the attic at night. Glowing lights have been seen moving through the cellar, and a ghostly presence has been felt by numerous people in the upstairs bedrooms. The carriage house (which is older than the main house and reputed to have underground tunnels leading to Canada) is also believed to be haunted. Strange moaning sounds have been heard at night coming from its second floor.',
284 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Dupree_1.png',
285 | price: 150
286 | },
287 | {
288 | statusId: '1',
289 | locationName: 'Old Mary Buth House',
290 | city: 'Germantown',
291 | state: 'WI',
292 | country: 'United States',
293 | description: 'A woman dressed in gothic, translucent, white clothing has been seen standing in near the house. Crying sounds coming from the upstairs bedroom have been heard.',
294 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/OldMaryButh_1.png',
295 | price: 150
296 | },
297 | {
298 | statusId: '1',
299 | locationName: 'Chateau Ste Michelle Winery Manor House',
300 | city: 'Woodinville',
301 | state: 'WA',
302 | country: 'United States',
303 | description: 'This beautiful winery is set up on 150 acres previously owned by Seattle lumber and dairy baron Fredrick Stimson. Although the winery itself was constructed in the mid-1970s, Mr. Stimson\'s old (early 1900s) mansion still occupies the land (manor house). Kept in its original state, the house is still used constantly for meetings and dinners. Staff at the winery for years has had paranormal experiences here. There have been many reports of cold spots that follow you, strange shadows, and noises. One of the upstairs restrooms by opening the window, closing the door, and turning the light on, and reported hearing footsteps upstairs when no one is there. Other occurrences include lights turning on, toilets flushing by themselves, and security systems going on the fritz. There always seems to be something behind you and you get the chills right down to your bones when walking through the house alone at night. ',
304 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/ChateauSte_1.png',
305 | price: 250
306 | },
307 | {
308 | statusId: '1',
309 | locationName: 'The Hare House',
310 | city: 'Noblesville',
311 | state: 'IN',
312 | country: 'United States',
313 | description: 'Another historic building on South 8th Street in Noblesville. It was owned by one of the founding families in Noblesville. The house was used for some time as a funeral parlor. It was moved and restored in a new location within the past few years. Hauntings that have been reported include "a creepy feeling when being upstairs," "a white lady with a candle on the staircase," and "a little boy who runs around, pointing and giggling at people."',
314 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/HareHouse_1.png',
315 | price: 150
316 | },
317 | {
318 | statusId: '1',
319 | locationName: 'Vennum House',
320 | city: 'Watseka',
321 | state: 'IL',
322 | country: 'United States',
323 | description: 'Convinced that Vennum was a reincarnation of their daughter, the Roff family allowed the girl to live with them for several weeks. Stevens wrote that when Vennum later married, Roff\'s spirit supposedly inhabited Vennum, resulting in a painless childbirth for her.',
324 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/images/Vennum_1.png',
325 | price: 100
326 | },
327 | {
328 | statusId: '1',
329 | locationName: 'The Warren House',
330 | city: 'Jonesboro',
331 | state: 'GA',
332 | country: 'United States',
333 | description: 'The Warren House was used as a hospital during the Civil War. While at the house many soldiers carved their names in the wall. At night a figure of a soldier can be seen holding a candle and looking out the window. There have been several reports of this. There is supposedly a bloodstain still on the floor in the attic. The confederate cemetery across the street is said to be haunted as well.',
334 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Warren_1.png',
335 | price: 150
336 | },
337 | {
338 | statusId: '1',
339 | locationName: 'Kennesaw House',
340 | city: 'Marietta',
341 | state: 'GA',
342 | country: 'United States',
343 | description: 'Used as a hospital during the Civil War, now a museum, people have seen ghosts of soldiers here. An entire hospital "scene" completes with soldiers in their hospital beds and 2 people witnessed doctors working with them at the same time in the basement area or lower floor. Employees have also heard several unexplained noises. While on a field trip of the Marietta Museum located in the Kennesaw House, a girl reported seeing a woman dressed in a light blue antebellum style dress with pink flowers around the neckline and bodice. This woman smiled at her and then vanished. She claims it is a woman in a picture at the museum. The picture is of the original owners of the Kennesaw House.',
344 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Kennesaw_1.png',
345 | price: 100
346 | },
347 | {
348 | statusId: '1',
349 | locationName: 'Hampton Lillibridge House',
350 | city: 'Savannah',
351 | state: 'GA',
352 | country: 'United States',
353 | description: 'This is said to be the most haunted house in Savannah, and the only house known to have had an exorcism. When the house was being restored in the 1960s, some workmen refused to finish the job because of strange occurrences (tools disappearing, hearing footsteps when they knew they were alone, strange feelings). On one occasion, a man was seen wearing a black suit and bow tie in the third floor window when no one was living in the house. Neighbors have also heard a woman\'s scream coming from within the house, and a gray haired man has also been sighted.',
354 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Hampton_1.png',
355 | price: 150
356 | },
357 | {
358 | statusId: '1',
359 | locationName: 'The Excelsior House',
360 | city: 'Jefferson',
361 | state: 'TX',
362 | country: 'United States',
363 | description: 'The Excelsior House is haunted by a headless man on the second floor, and a woman in black who has a baby. The woman has appeared and frightened many guests, including the famous film director Stephan Spielberg. They are possibly the spirits of guests or former employees.',
364 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Excelsior_1.png',
365 | price: 250
366 | },
367 | {
368 | statusId: '1',
369 | locationName: 'The Winchester House',
370 | city: 'Cragfont',
371 | state: 'TN',
372 | country: 'United States',
373 | description: 'Built in 1802. General James Winchester was a hero of the revolution, a Tennessee pioneer, and one of the founders of Memphis, Tennessee. Cragfont has reopened for the season. I can attest to strong energy levels in the master bedroom and the nursery, as well as a few other areas of the house. Be sure to stop by on a Sunday and ask Joanne for the "ghost tour."',
374 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Winchester_1.png',
375 | price: 100
376 | },
377 | {
378 | statusId: '1',
379 | locationName: 'Woodruff-Fontaine House',
380 | city: 'Memphis',
381 | state: 'TN',
382 | country: 'United States',
383 | description: 'The "Rose Room" is haunted by Molly Woodruff whose bedroom it once was. Her picture is on the wall and she wanders the house, makes a depression in the bedclothes as if she slept there, and there are cold areas. Her most famous appearance was the day the house museum opened in the 1960\'s when a docent saw a woman in the bedroom who said, "My bed doesn\'t go there." That was the first of many such stories. Strange odors like cigar smoke have also been in evidence on the house\'s third floor from an unnamed visitor.',
384 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Woodruff_1.png',
385 | price: 250
386 | },
387 | {
388 | statusId: '1',
389 | locationName: 'The Hemingway House',
390 | city: 'Key West',
391 | state: 'FL',
392 | country: 'United States',
393 | description: 'Many people have witnessed Ernest Hemingway waving from his studio window. and lights on and sounds of a typewriter being used after the tours are over and the house is unoccupied.',
394 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/Poe_1.png',
395 | price: 650
396 | },
397 | {
398 | statusId: '1',
399 | locationName: 'Molly Brown House',
400 | city: 'Denver',
401 | state: 'CO',
402 | country: 'United States',
403 | description: 'The infamous house of Margaret Brown is haunted, although by who has not yet been determined. An investigator found cold spots throughout the house, the scent of pipe tobacco when no one was smoking, & doors that had "a mind of their own". Apparitions also seen. ',
404 | imgPath: 'https://reactsolobucket.s3.us-east-2.amazonaws.com/MollyBrown_1.png',
405 | price: 250
406 | },
407 | ], {});
408 | },
409 |
410 | down: (queryInterface, Sequelize) => {
411 | return queryInterface.bulkDelete('Hauntings', null, {});
412 | }
413 | };
414 |
415 |
--------------------------------------------------------------------------------