├── .gitignore
├── README.md
├── backend
├── .gitignore
├── .sequelizerc
├── README.md
├── app.js
├── bin
│ └── www
├── config
│ ├── database.js
│ └── index.js
├── db
│ ├── migrations
│ │ ├── 20220825220833-create-user.js
│ │ ├── 20220829224626-create-spot.js
│ │ ├── 20220829232518-create-booking.js
│ │ ├── 20220829234315-create-review.js
│ │ ├── 20220829234400-create-review-image.js
│ │ └── 20220829234454-create-spot-image.js
│ ├── models
│ │ ├── booking.js
│ │ ├── index.js
│ │ ├── review.js
│ │ ├── reviewimage.js
│ │ ├── spot.js
│ │ ├── spotimage.js
│ │ └── user.js
│ └── seeders
│ │ ├── 20220825223406-demo-user.js
│ │ ├── 20220830002952-demo-spot.js
│ │ ├── 20220923044211-demo-spot-image.js
│ │ └── 20220926101531-Reviews.js
├── package-lock.json
├── package.json
├── routes
│ ├── api
│ │ ├── bookings.js
│ │ ├── index.js
│ │ ├── maps.js
│ │ ├── review-images.js
│ │ ├── reviews.js
│ │ ├── session.js
│ │ ├── spot-images.js
│ │ ├── spots.js
│ │ ├── testingAuthMware.js
│ │ └── users.js
│ └── index.js
└── utils
│ ├── auth.js
│ └── validation.js
├── frontend
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favIcon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ └── site.webmanifest
│ ├── index.html
│ └── manifest.json
└── src
│ ├── App.js
│ ├── assets
│ ├── airnbn.png
│ ├── globe.svg
│ ├── logo.png
│ ├── readMeLogo.svg
│ └── up-arrow-svgrepo-com (1).svg
│ ├── components
│ ├── AllReviews
│ │ ├── AllReviews.css
│ │ └── index.js
│ ├── AllSpots
│ │ ├── allSpots.css
│ │ └── index.js
│ ├── CreateReviewForm
│ │ ├── CreateReviewForm.css
│ │ └── index.js
│ ├── CreateSpotForm
│ │ ├── CreateSpotForm.css
│ │ ├── countries.js
│ │ └── index.js
│ ├── Footer
│ │ ├── Footer.js
│ │ └── footer.css
│ ├── GetCurrentReviews
│ │ ├── GetCurrentReviews.css
│ │ └── index.js
│ ├── GetCurrentSpots
│ │ ├── GetCurrentSpots.css
│ │ └── index.js
│ ├── GetCurrentUser
│ │ ├── GetCurrentUser.css
│ │ └── index.js
│ ├── LoginFormModal
│ │ ├── LoginForm.css
│ │ ├── LoginForm.js
│ │ └── index.js
│ ├── Maps
│ │ ├── BigMap.js
│ │ ├── SpotMap.js
│ │ ├── bigmap.css
│ │ └── memoizationMaps_pass.js
│ ├── Navigation
│ │ ├── Navigation.css
│ │ ├── ProfileButton.js
│ │ └── index.js
│ ├── SignupFormPage
│ │ ├── SignupForm.css
│ │ └── index.js
│ ├── SingleSpot
│ │ ├── SingleSpot.css
│ │ └── index.js
│ ├── SpotDetails
│ │ ├── SpotDetails.css
│ │ └── index.js
│ ├── UpdateReviewForm
│ │ ├── UpdateReviewForm.css
│ │ └── index.js
│ └── UpdateSpot
│ │ ├── UpdateSpot.css
│ │ └── index.js
│ ├── context
│ ├── Modal.css
│ └── Modal.js
│ ├── index.css
│ ├── index.js
│ └── store
│ ├── bookings.js
│ ├── csrf.js
│ ├── index.js
│ ├── maps.js
│ ├── reviews.js
│ ├── session.js
│ └── spots.js
├── package-lock.json
├── package.json
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | build
4 | .DS_Store
5 | *.db
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AirNbN
2 | 
3 |
4 |
5 |
6 | If you haven't guessed it already, AirNbN is a web app inspired by [AirBnB](https://www.airbnb.com/).
7 | AirNbN can be used as online marketplace for lodging, primarily homestays for vacation rentals, and tourism activities.
8 | AirNbN's greatest attraction is the ability to leverage nights at the houses you own and trade them for nights in others' homes, the only requirement being that both owners are in agreement.
9 |
10 | **Check it out [HERE](https://airnbn-api-project.herokuapp.com/)!**
11 | ## Wiki Links
12 |
13 | * [Backend Routes (API Documentation)](https://github.com/Samsuhhh/API-Project/wiki/Backend-Routes---API-Documentation)
14 | * [Database Schema](https://github.com/Samsuhhh/API-Project/wiki/Database-Schema)
15 | * [Feature List](https://github.com/Samsuhhh/API-Project/wiki/Feature-List)
16 | * [Redux Store Shape](https://github.com/Samsuhhh/API-Project/wiki/Redux-State)
17 |
18 |
19 | ## Tech Stack
20 |
21 | ### Frameworks, Platforms, and Libararies:
22 |
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
29 | 
30 |
31 |
32 | ### Database and Host:
33 | 
34 | 
35 |
36 | ## Get Started
37 |
38 | * Clone the Repo
39 | * run npm install to install dependencies
40 | * cd into frontend and backend/src folders and run npm start in both.
41 | * This will open up our site. Happy Exploring!
42 |
43 |
44 | ### Site Functionality
45 | Here is some functionality to airnbn-api-project.herokuapp.com
46 |
47 |
48 | * This is how a visitor can explore the site without creating account, using a demo login.
49 | (Landing Page)
50 | 
51 |
52 | * This is how you create a review for a spot.
53 | 
54 |
55 | * This is how you become a host at airnbn!
56 | 
57 |
58 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | build
4 | .DS_Store
5 | *.db
6 |
--------------------------------------------------------------------------------
/backend/.sequelizerc:
--------------------------------------------------------------------------------
1 | // backend/.sequelizerc
2 | const path = require('path');
3 |
4 | module.exports = {
5 | config: path.resolve('config', 'database.js'),
6 | 'models-path': path.resolve('db', 'models'),
7 | 'seeders-path': path.resolve('db', 'seeders'),
8 | 'migrations-path': path.resolve('db', 'migrations')
9 | };
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | require('express-async-errors');
3 | const morgan = require('morgan');
4 | const cors = require('cors');
5 | const csurf = require('csurf');
6 | const helmet = require('helmet');
7 | const cookieParser = require('cookie-parser');
8 |
9 | const { environment } = require('./config');
10 | const isProduction = environment === 'production';
11 | // backend/app.js
12 |
13 | const routes = require('./routes')
14 | const app = express();
15 | app.use(morgan('dev'));
16 |
17 | app.use(cookieParser());
18 | app.use(express.json());
19 |
20 | // Security Middleware
21 | if (!isProduction) {
22 | // enable cors only in development
23 | app.use(cors());
24 | }
25 |
26 | // helmet helps set a variety of headers to better secure your app
27 | app.use(
28 | helmet.crossOriginResourcePolicy({
29 | policy: "cross-origin"
30 | })
31 | );
32 |
33 |
34 | // Set the _csrf token and create req.csrfToken method
35 | app.use(
36 | csurf({
37 | cookie: {
38 | secure: isProduction,
39 | sameSite: isProduction && "Lax",
40 | httpOnly: true
41 | }
42 | })
43 | );
44 |
45 | // THIS MUST BE USED BEFORE ERROR HANDLERS
46 | app.use(routes); // Connect all the routes
47 |
48 |
49 | // Catch unhandled requests and forward to error handler.
50 | app.use((_req, _res, next) => {
51 | const err = new Error("The requested resource couldn't be found.");
52 | err.title = "Resource Not Found";
53 | err.errors = ["The requested resource couldn't be found."];
54 | err.status = 404;
55 | next(err);
56 | });
57 |
58 | const { ValidationError } = require('sequelize');
59 |
60 |
61 | // Process sequelize errors
62 | app.use((err, _req, _res, next) => {
63 | // check if error is a Sequelize error:
64 | if (err instanceof ValidationError) {
65 | err.errors = err.errors.map((e) => e.message);
66 | err.title = 'Validation error';
67 | }
68 | next(err);
69 | });
70 |
71 | // Error formatter
72 | app.use((err, _req, res, _next) => {
73 | res.status(err.status || 500);
74 | console.error(err);
75 | res.json({
76 | title: err.title || 'Server Error',
77 | message: err.message,
78 | errors: err.errors,
79 | stack: isProduction ? null : err.stack
80 | });
81 | });
82 |
83 |
84 |
85 |
86 |
87 | module.exports = app;
--------------------------------------------------------------------------------
/backend/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // backend/bin/www
3 |
4 | // Import environment variables
5 | require('dotenv').config();
6 |
7 | const { port } = require('../config');
8 |
9 | const app = require('../app');
10 | const db = require('../db/models');
11 |
12 | // Check the database connection before starting the app
13 | db.sequelize
14 | .authenticate()
15 | .then(() => {
16 | console.log('Database connection success! :) Sequelize is ready to use...');
17 |
18 | // Start listening for connections
19 | app.listen(port, () => console.log(`Listening on port ${port}...`));
20 | })
21 | .catch((err) => {
22 | console.log('Database connection failure :(');
23 | console.error(err);
24 | });
--------------------------------------------------------------------------------
/backend/config/database.js:
--------------------------------------------------------------------------------
1 | // backend/config/database.js
2 | const config = require('./index');
3 |
4 | module.exports = {
5 | development: {
6 | storage: config.dbFile,
7 | dialect: "sqlite",
8 | seederStorage: "sequelize",
9 | logQueryParameters: true,
10 | typeValidation: true
11 | },
12 | production: {
13 | use_env_variable: 'DATABASE_URL',
14 | dialect: 'postgres',
15 | seederStorage: 'sequelize',
16 | dialectOptions: {
17 | ssl: {
18 | require: true,
19 | rejectUnauthorized: false
20 | }
21 | }
22 | }
23 | };
--------------------------------------------------------------------------------
/backend/config/index.js:
--------------------------------------------------------------------------------
1 |
2 | // backend/config/index.js
3 | module.exports = {
4 | environment: process.env.NODE_ENV || 'development',
5 | port: process.env.PORT || 8000,
6 | dbFile: process.env.DB_FILE,
7 | jwtConfig: {
8 | secret: process.env.JWT_SECRET,
9 | expiresIn: process.env.JWT_EXPIRES_IN
10 | },
11 | googleMapsAPIKey: process.env.MAPS_API_KEY
12 | };
13 |
--------------------------------------------------------------------------------
/backend/db/migrations/20220825220833-create-user.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | module.exports = {
3 | up: async (queryInterface, Sequelize) => {
4 | return queryInterface.createTable("Users", {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | firstName: {
12 | type: Sequelize.STRING(25),
13 | allowNull: false
14 | },
15 | lastName: {
16 | type: Sequelize.STRING(25),
17 | allowNull: false
18 | },
19 | username: {
20 | type: Sequelize.STRING(30),
21 | allowNull: false,
22 | unique: true
23 | },
24 | email: {
25 | type: Sequelize.STRING(256),
26 | allowNull: false,
27 | unique: true
28 | },
29 | hashedPassword: {
30 | type: Sequelize.STRING.BINARY,
31 | allowNull: false
32 | },
33 | createdAt: {
34 | allowNull: false,
35 | type: Sequelize.DATE,
36 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
37 | },
38 | updatedAt: {
39 | allowNull: false,
40 | type: Sequelize.DATE,
41 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
42 | }
43 | });
44 | },
45 | async down(queryInterface, Sequelize) {
46 | await queryInterface.dropTable('Users');
47 | }
48 | };
--------------------------------------------------------------------------------
/backend/db/migrations/20220829224626-create-spot.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | async up(queryInterface, Sequelize) {
4 | await queryInterface.createTable('Spots', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER,
10 | onDelete: 'CASCADE'
11 | },
12 | ownerId: {
13 | type: Sequelize.INTEGER,
14 | onDelete: 'CASCADE',
15 | references: {model: 'Users'}
16 | },
17 | address: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | },
21 | city: {
22 | type: Sequelize.STRING,
23 | allowNull: false
24 | },
25 | state: {
26 | type: Sequelize.STRING,
27 | allowNull: false
28 | },
29 | country: {
30 | type: Sequelize.STRING,
31 | allowNull: false
32 | },
33 | lat: {
34 | type: Sequelize.DECIMAL
35 | },
36 | lng: {
37 | type: Sequelize.DECIMAL
38 | },
39 | name: {
40 | type: Sequelize.STRING,
41 | allowNull: false,
42 | // unique: true
43 | },
44 | description: {
45 | type: Sequelize.STRING,
46 | allowNull: false
47 | },
48 | price: {
49 | type: Sequelize.DECIMAL,
50 | allowNull: false
51 | },
52 | createdAt: {
53 | allowNull: false,
54 | type: Sequelize.DATE,
55 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
56 | },
57 | updatedAt: {
58 | allowNull: false,
59 | type: Sequelize.DATE,
60 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
61 | }
62 | });
63 | },
64 | async down(queryInterface, Sequelize) {
65 | await queryInterface.dropTable('Spots');
66 | }
67 | };
--------------------------------------------------------------------------------
/backend/db/migrations/20220829232518-create-booking.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | async up(queryInterface, Sequelize) {
4 | await queryInterface.createTable('Bookings', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | spotId: {
12 | type: Sequelize.INTEGER,
13 | references: { model: 'Spots'},
14 | onDelete: 'CASCADE'
15 | },
16 | userId: {
17 | type: Sequelize.INTEGER,
18 | references: { model: 'Users'},
19 | onDelete: 'CASCADE'
20 | },
21 | startDate: {
22 | type: Sequelize.DATEONLY
23 | },
24 | endDate: {
25 | type: Sequelize.DATEONLY
26 | },
27 | createdAt: {
28 | allowNull: false,
29 | type: Sequelize.DATE,
30 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
31 | },
32 | updatedAt: {
33 | allowNull: false,
34 | type: Sequelize.DATE,
35 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
36 | }
37 | });
38 | },
39 | async down(queryInterface, Sequelize) {
40 | await queryInterface.dropTable('Bookings');
41 | }
42 | };
--------------------------------------------------------------------------------
/backend/db/migrations/20220829234315-create-review.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | async up(queryInterface, Sequelize) {
4 | await queryInterface.createTable('Reviews', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | spotId: {
12 | type: Sequelize.INTEGER,
13 | references: { model: 'Spots'},
14 | onDelete: 'CASCADE'
15 | },
16 | userId: {
17 | type: Sequelize.INTEGER,
18 | references: { model: 'Users'},
19 | onDelete: 'CASCADE'
20 | },
21 | review: {
22 | type: Sequelize.STRING
23 | },
24 | stars: {
25 | type: Sequelize.INTEGER
26 | },
27 | createdAt: {
28 | allowNull: false,
29 | type: Sequelize.DATE
30 | },
31 | updatedAt: {
32 | allowNull: false,
33 | type: Sequelize.DATE
34 | }
35 | });
36 | },
37 | async down(queryInterface, Sequelize) {
38 | await queryInterface.dropTable('Reviews');
39 | }
40 | };
--------------------------------------------------------------------------------
/backend/db/migrations/20220829234400-create-review-image.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | async up(queryInterface, Sequelize) {
4 | await queryInterface.createTable('ReviewImages', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | reviewId: {
12 | type: Sequelize.INTEGER,
13 | references: { model: 'Reviews'},
14 | onDelete: 'CASCADE'
15 | },
16 | url: {
17 | type: Sequelize.STRING
18 | },
19 | createdAt: {
20 | allowNull: false,
21 | type: Sequelize.DATE
22 | },
23 | updatedAt: {
24 | allowNull: false,
25 | type: Sequelize.DATE
26 | }
27 | });
28 | },
29 | async down(queryInterface, Sequelize) {
30 | await queryInterface.dropTable('ReviewImages');
31 | }
32 | };
--------------------------------------------------------------------------------
/backend/db/migrations/20220829234454-create-spot-image.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = {
3 | async up(queryInterface, Sequelize) {
4 | await queryInterface.createTable('SpotImages', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: Sequelize.INTEGER
10 | },
11 | spotId: {
12 | type: Sequelize.INTEGER,
13 | references: { model: 'Spots'},
14 | onDelete: 'CASCADE'
15 | },
16 | url: {
17 | type: Sequelize.STRING
18 | },
19 | preview: {
20 | type: Sequelize.BOOLEAN,
21 | defaultValue: false
22 | },
23 | createdAt: {
24 | allowNull: false,
25 | type: Sequelize.DATE
26 | },
27 | updatedAt: {
28 | allowNull: false,
29 | type: Sequelize.DATE
30 | }
31 | });
32 | },
33 | async down(queryInterface, Sequelize) {
34 | await queryInterface.dropTable('SpotImages');
35 | }
36 | };
--------------------------------------------------------------------------------
/backend/db/models/booking.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | Model
4 | } = require('sequelize');
5 | module.exports = (sequelize, DataTypes) => {
6 | class Booking extends Model {
7 | /**
8 | * Helper method for defining associations.
9 | * This method is not a part of Sequelize lifecycle.
10 | * The `models/index` file will call this method automatically.
11 | */
12 | static associate(models) {
13 | // define association here
14 | Booking.belongsTo(models.User, {foreignKey: 'userId'});
15 | Booking.belongsTo(models.Spot, {foreignKey: 'spotId'});
16 | }
17 | }
18 | Booking.init({
19 | spotId: DataTypes.INTEGER,
20 | userId: DataTypes.INTEGER,
21 | startDate: DataTypes.DATEONLY,
22 | endDate: DataTypes.DATEONLY
23 | }, {
24 | sequelize,
25 | modelName: 'Booking',
26 | });
27 | return Booking;
28 | };
--------------------------------------------------------------------------------
/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 = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
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 |
--------------------------------------------------------------------------------
/backend/db/models/review.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | Model
4 | } = require('sequelize');
5 | module.exports = (sequelize, DataTypes) => {
6 | class Review extends Model {
7 | /**
8 | * Helper method for defining associations.
9 | * This method is not a part of Sequelize lifecycle.
10 | * The `models/index` file will call this method automatically.
11 | */
12 | static associate(models) {
13 | // define association here
14 | Review.hasMany(models.ReviewImage, {foreignKey: 'reviewId'});
15 | Review.belongsTo(models.User, {foreignKey: 'userId'});
16 | Review.belongsTo(models.Spot, {foreignKey: 'spotId'});
17 | }
18 | }
19 | Review.init({
20 | spotId: DataTypes.INTEGER,
21 | userId: DataTypes.INTEGER,
22 | review: DataTypes.STRING,
23 | stars: DataTypes.INTEGER
24 | }, {
25 | sequelize,
26 | modelName: 'Review'
27 | });
28 | return Review;
29 | };
--------------------------------------------------------------------------------
/backend/db/models/reviewimage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | Model
4 | } = require('sequelize');
5 | module.exports = (sequelize, DataTypes) => {
6 | class ReviewImage extends Model {
7 | /**
8 | * Helper method for defining associations.
9 | * This method is not a part of Sequelize lifecycle.
10 | * The `models/index` file will call this method automatically.
11 | */
12 | static associate(models) {
13 | // define association here
14 | ReviewImage.belongsTo(models.Review, {foreignKey: 'reviewId'});
15 | }
16 | }
17 | ReviewImage.init({
18 | reviewId: DataTypes.INTEGER,
19 | url: DataTypes.STRING
20 | }, {
21 | sequelize,
22 | modelName: 'ReviewImage',
23 | });
24 | return ReviewImage;
25 | };
--------------------------------------------------------------------------------
/backend/db/models/spot.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | Model
4 | } = require('sequelize');
5 | module.exports = (sequelize, DataTypes) => {
6 | class Spot extends Model {
7 | /**
8 | * Helper method for defining associations.
9 | * This method is not a part of Sequelize lifecycle.
10 | * The `models/index` file will call this method automatically.
11 | */
12 | static associate(models) {
13 | // define association here
14 | Spot.belongsTo(models.User, {as: 'Owner', foreignKey: 'ownerId'});
15 | Spot.hasMany(models.Review, {foreignKey: 'spotId'});
16 | Spot.hasMany(models.Booking, {foreignKey: 'spotId'});
17 | Spot.hasMany(models.SpotImage, {foreignKey: 'spotId'});
18 |
19 | }
20 | }
21 | Spot.init({
22 | ownerId: DataTypes.INTEGER,
23 | address: {
24 | type: DataTypes.STRING,
25 | allowNull: false,
26 | validate: {
27 | len: [2, 250]
28 | }
29 | },
30 | city: {
31 | type: DataTypes.STRING,
32 | allowNull: false,
33 | validate: {len: [2, 250]}
34 | },
35 | state: {
36 | type: DataTypes.STRING,
37 | allowNull: false,
38 | validate: { len: [2, 250] }
39 | },
40 | country: {
41 | type: DataTypes.STRING,
42 | allowNull: false,
43 | validate: { len: [2, 250] }
44 | },
45 | lat: {
46 | type: DataTypes.DECIMAL,
47 | validate: {
48 | isDecimal: true
49 | }
50 | },
51 | lng: {
52 | type: DataTypes.DECIMAL,
53 | validate: {
54 | isDecimal: true
55 | }
56 | },
57 | name: {
58 | type: DataTypes.STRING,
59 | allowNull: false,
60 | validate: { len: [2, 250] }
61 | },
62 | description: {
63 | type: DataTypes.STRING,
64 | allowNull: false,
65 | validate: { len: [2, 250] }
66 | },
67 | price: {
68 | type: DataTypes.DECIMAL,
69 | allowNull: false,
70 | validate: { min: 1 }
71 | },
72 | }, {
73 | sequelize,
74 | modelName: 'Spot',
75 | });
76 | return Spot;
77 | };
--------------------------------------------------------------------------------
/backend/db/models/spotimage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | Model
4 | } = require('sequelize');
5 | module.exports = (sequelize, DataTypes) => {
6 | class SpotImage extends Model {
7 | /**
8 | * Helper method for defining associations.
9 | * This method is not a part of Sequelize lifecycle.
10 | * The `models/index` file will call this method automatically.
11 | */
12 | static associate(models) {
13 | // define association here
14 | SpotImage.belongsTo(models.Spot, {foreignKey: 'spotId'});
15 | }
16 | }
17 | SpotImage.init({
18 | spotId: {
19 | type: DataTypes.INTEGER,
20 | onDelete: 'CASCADE'
21 | },
22 | url: DataTypes.STRING,
23 | preview: {
24 | type: DataTypes.BOOLEAN,
25 | defaultValue: false
26 | }
27 | }, {
28 | sequelize,
29 | modelName: 'SpotImage',
30 | });
31 | return SpotImage;
32 | };
--------------------------------------------------------------------------------
/backend/db/models/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { Model, Validator } = require('sequelize');
3 | const bcrypt = require('bcryptjs');
4 |
5 | module.exports = (sequelize, DataTypes) => {
6 | class User extends Model {
7 | toSafeObject() {
8 | const { id, username, email, firstName, lastName } = this; // context will be the User instance
9 | return { id, username, email, firstName, lastName };
10 | }
11 | // checking if ??
12 | validatePassword(password) {
13 | return bcrypt.compareSync(password, this.hashedPassword.toString());
14 | }
15 |
16 | static getCurrentUserById(id){
17 | return User.scope("currentUser").findByPk(id);
18 | }
19 |
20 | // defining static method for login;
21 | static async login({ credential, password }) {
22 | const { Op } = require('sequelize');
23 | const user = await User.scope('loginUser').findOne({
24 | where: {
25 | [Op.or]: {
26 | username: credential,
27 | email: credential
28 | }
29 | }
30 | });
31 | if (user && user.validatePassword(password)) {
32 | return await User.scope('currentUser').findByPk(user.id);
33 | }
34 | }
35 | // defining static method for signup; accepts obj with username, email, pw
36 | // then hash the pw using bcrypt
37 | static async signup({ username, email, password, firstName, lastName }) {
38 | const hashedPassword = bcrypt.hashSync(password);
39 | const user = await User.create({
40 | firstName,
41 | lastName,
42 | username,
43 | email,
44 | hashedPassword
45 | });
46 | return await User.scope('currentUser').findByPk(user.id);
47 | }
48 |
49 | static associate(models) {
50 | // define association here
51 | User.hasMany(models.Spot, {foreignKey: 'ownerId'});
52 | User.hasMany(models.Booking, {foreignKey: 'userId'});
53 | User.hasMany(models.Review, {foreignKey: 'userId'})
54 | }
55 | };
56 |
57 | User.init(
58 | {
59 | firstName:{
60 | type: DataTypes.STRING,
61 | allowNull: false,
62 | validate: {
63 | len: [1, 20]
64 | }
65 | },
66 | lastName: {
67 | type: DataTypes.STRING,
68 | allowNull: false,
69 | validate: {
70 | len: [1, 20]
71 | }
72 | },
73 | username: {
74 | type: DataTypes.STRING,
75 | allowNull: false,
76 | validate: {
77 | len: [4, 30],
78 | isNotEmail(value) {
79 | if (Validator.isEmail(value)) {
80 | throw new Error("Cannot be an email.");
81 | }
82 | },
83 | // isUnique(value){
84 | // if(!isUnique) throw new Error('User already exists')
85 | // }
86 | }
87 | },
88 | email: {
89 | type: DataTypes.STRING,
90 | allowNull: false,
91 | validate: {
92 | len: [3, 256]
93 | }
94 | },
95 | hashedPassword: {
96 | type: DataTypes.STRING.BINARY,
97 | allowNull: false,
98 | validate: {
99 | len: [60, 60]
100 | }
101 | }
102 | },
103 | {
104 | sequelize,
105 | modelName: "User",
106 | indexes: [{
107 | unique: true,
108 | fields: ['username', 'email'] }
109 | ],
110 | defaultScope: {
111 | attributes: {
112 | exclude: ["hashedPassword", "email", "createdAt", "updatedAt"]
113 | }
114 | },
115 | scopes: {
116 | currentUser: {
117 | attributes: { exclude: ["hashedPassword", "createdAt", "updatedAt"] }
118 | },
119 | loginUser: {
120 | attributes: {
121 | exclude: ['createdAt', 'updatedAt']
122 | }
123 | }
124 | }
125 | }
126 | );
127 | return User;
128 | };
--------------------------------------------------------------------------------
/backend/db/seeders/20220825223406-demo-user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const bcrypt = require("bcryptjs");
3 |
4 | module.exports = {
5 | up: async (queryInterface, Sequelize) => {
6 | return queryInterface.bulkInsert('Users', [
7 | {
8 | firstName: 'John',
9 | lastName: 'Carrera',
10 | email: 'John@Carrera.com',
11 | username: 'AyCarrera',
12 | hashedPassword: bcrypt.hashSync('password')
13 | },
14 | {
15 | firstName: 'Jake',
16 | lastName: 'Matillano',
17 | email: 'Jake@Mat.com',
18 | username: 'JakeyJake123',
19 | hashedPassword: bcrypt.hashSync('soundcloud')
20 | },
21 | {
22 | firstName: 'Gary',
23 | lastName: 'myBoo',
24 | email: 'user2@user.io',
25 | username: 'GaryAndSam4Ever',
26 | hashedPassword: bcrypt.hashSync('iloveSam')
27 | },
28 | {
29 | firstName: "Karen",
30 | lastName: 'Smith',
31 | email: 'Karen@Smith.com',
32 | username: 'MamaKaren',
33 | hashedPassword: bcrypt.hashSync('password')
34 | },
35 | {
36 | firstName: 'David',
37 | lastName: 'Rogers',
38 | email: 'ziggy99@demo.com',
39 | username: '9ziggy9',
40 | hashedPassword: bcrypt.hashSync('June2022')
41 | }
42 | ], {});
43 | },
44 |
45 | down: async (queryInterface, Sequelize) => {
46 | const Op = Sequelize.Op;
47 | return queryInterface.bulkDelete('Users', {
48 | username: { [Op.in]: ['AyCarrera', 'JakeyJake123', 'GaryAndSam4Ever', '9ziggy9'] }
49 | }, {});
50 | }
51 | };
--------------------------------------------------------------------------------
/backend/db/seeders/20220830002952-demo-spot.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const demoSpots = [
4 | {
5 | ownerId: 1,
6 | address: "7000 Joe Morgan Wy",
7 | city: "Oakland",
8 | state: "California",
9 | country: "United States",
10 | lat: 37.75,
11 | lng: -122.20,
12 | name: "BIG Tree house",
13 | description: "300 sq ft treehouse with 3 bedrooms. Close to 7 different hiking trails with a gorgeous open wall bed/bath to breathe in the fresh air.",
14 | price: 100.00
15 | },
16 | {
17 | ownerId: 1,
18 | address: "495 Bonanza Dr",
19 | city: "Tahoe City",
20 | state: "California",
21 | country: "United States",
22 | lat: 39.16,
23 | lng: -120.15,
24 | name: "Cabin Getaway",
25 | description: "Our custom built guesthouse was completed in 2016. Clean and cozy, this space is perfect for your Tahoe getaway. Recently remodeled with brand new furnishings, kitchen, and bathroom, you will have everything you need for a long or short-term stay!",
26 | price: 530.00
27 | },
28 | {
29 | ownerId: 1,
30 | address: "45 Brightwood Circle",
31 | city: "Danville",
32 | state: "California",
33 | country: "United States",
34 | lat: 37.82,
35 | lng: -121.94,
36 | name: "City of Stars Entire mansion",
37 | description: "Our location is a great destination for sight-seeing and experiencing boujee Hollywood. With a gorgeous view over all of Los Angeles, our location can fit up to 20 guests.",
38 | price: 3230.00
39 | },
40 | {
41 | ownerId: 2,
42 | address: "Jalan Danau Tamblingan 89",
43 | city: "Bali",
44 | state: "Sanur",
45 | country: "Indonesia",
46 | lat: -8.26,
47 | lng: 115.09,
48 | name: "Kawasan Pariwisata Nusa Dua",
49 | description:
50 | "Escape in Bali is a distinguished unique eco stay hidden in the jungle of Tampaksiring a village with spiritual ambiance & ancient Balinese mythology. A perfect hideaway for all adventurous travelers, nature lovers, spiritual-minded people, backpackers, artists, long-time travelers & eco-enthusiast to have their like-no-others Bali experience. It is truly live up the concept of back-to-nature.",
51 | price: 490.00
52 | },
53 | {
54 | ownerId: 2,
55 | address: "23-2 Youido-Dong",
56 | city: "Seoul",
57 | state: "Gyeonggi",
58 | country: "South Korea",
59 | lat: 37.12,
60 | lng: 121.12,
61 | name: "Entire traditional home",
62 | description: "Experience the traditions of Korea with a stay at our beautiful countryside location. Close enough to experience the city life, but far enough to be at peace.",
63 | price: 990.00
64 | },
65 | {
66 | ownerId: 2,
67 | address: "662 Las Vegas Blvd S",
68 | city: "Las Vegas",
69 | state: "Nevada",
70 | country: 'United States',
71 | lat: 36.16,
72 | lng: -115.15,
73 | name: "Viva Vegas penthouse",
74 | description: "This probably won't be the best vacation spot, but if you're looking for a fun time in Sin City, this is the spot for you and yours.",
75 | price: 770.00
76 | },
77 |
78 | {
79 | ownerId: 3,
80 | address: "511 Ekekela Pl.",
81 | city: "Kahuku",
82 | state: "Hawaii",
83 | country: "United States",
84 | lat: 21.33,
85 | lng: -157.85,
86 | name: "Island Stay",
87 | description: "The Island Stay beachhouse is a one-of-a-kind beatiful location with direct access to the North Shore of Oahu. With endless activities in the city and tons more in nature, come experience the beauty of Hawaii.",
88 | price: 800.00
89 | },
90 |
91 | {
92 | ownerId: 3,
93 | address: "1501 Fillmore St.",
94 | city: "San Francisco",
95 | state: "California",
96 | country: "United States",
97 | lat: 37.78,
98 | lng: -122.43,
99 | name: "Entire apartment",
100 | description: "Our location is a great destination for sight-seeing and experiencing, The City. There is no judgement here, just enjoy our city and leave with good vibes.",
101 | price: 420.00
102 | },
103 | {
104 | ownerId: 3,
105 | address: "6551 Del Playa Drive",
106 | city: "Isla Vista",
107 | state: "California",
108 | country: "United States",
109 | lat: 34.40,
110 | lng: -119.86,
111 | name: "Home Sweet Home",
112 | description: "Tau Palace is a magical place for which words cannot even begin to describe. It's an experience you have to FEEL.. There is also a fun beach and amazing sunsets. Come make memories you will never forget.",
113 | price: 6510.00
114 | },
115 | {
116 | ownerId: 4,
117 | address: "1 Zion Park Blvd",
118 | city: "Springdale",
119 | state: "Utah",
120 | country: "United States",
121 | lat: 37.20,
122 | lng: -112.98,
123 | name: "Nature's Getaway",
124 | description: "Stay at our beautiful location in Zion National Park. It is a unique experience that you will never forget. No smoking. No pets. No Wifi. Bring your own toiletries.",
125 | price: 888.00
126 | },
127 |
128 |
129 | {
130 | ownerId: 5,
131 | address: "100 Aker Wood West",
132 | city: "London",
133 | state: "England",
134 | country: "United Kingdom",
135 | lat: 51.61,
136 | lng: 0.135,
137 | name: "Pooh's House",
138 | description: "Come experience what it feels like to live like Winnie-the-Pooh. A great and extremely spacious location for animals of all shapes and sizes. Food and Honey included.",
139 | price: 100.00
140 | },
141 |
142 | {
143 | ownerId: 1,
144 | address: "1230 Ocean Drive ",
145 | city: "Miami Beach",
146 | state: "Florida",
147 | country: "United States",
148 | lat: 25.78,
149 | lng: -80.13,
150 | name: "Miami Vice",
151 | description: "Our location offers a place for people to let go and just dance the night away. It is a vacation from all the stress and weight life puts on us, so just come to have some fun, dance, and laugh.",
152 | price: 300.00
153 | },
154 | {
155 | ownerId: 2,
156 | address: "12 Fifth Avenue ",
157 | city: "Manhattan",
158 | state: "New York",
159 | country: "United States",
160 | lat: 40.73,
161 | lng: -73.99,
162 | name: "New York New York",
163 | description: "The pictures speak for themselves, but do not do the location justice. Although cold, the best time to visit is in the winter so you can see the snow blanket our wonderful city.",
164 | price: 910.00
165 | },
166 | {
167 | ownerId: 3,
168 | address: "4 Dufftown Pl",
169 | city: "Perth",
170 | state: "Inveralmond",
171 | country: "United Kingdom",
172 | lat: 56.42,
173 | lng: -3.47,
174 | name: "!Azkaban",
175 | description: "You know what it is. You know you want to come. Book your stay ASAP as reservations are almost booked for the next year! Experience the magic. ",
176 | price: 910.00
177 | },
178 |
179 | {
180 | ownerId: 4,
181 | address: "Reineveien 46",
182 | city: "8390 Reine",
183 | state: "Reine",
184 | country: "Norway",
185 | lat: 37.56,
186 | lng: 121.23,
187 | name: "The Halls",
188 | description: "One cannot simply purchase their way into our location. You must be vetted and cleared to enter the sacred halls of Valhalla. Only the worthy shall enter.",
189 | price: 9999999.00
190 | }
191 | ]
192 | const { Spot } = require('../models')
193 |
194 | module.exports = {
195 | async up(queryInterface, Sequelize) {
196 | /**
197 | * Add seed commands here.
198 | *
199 | * Example:
200 | * await queryInterface.bulkInsert('People', [{
201 | * name: 'John Doe',
202 | * isBetaMember: false
203 | * }], {});
204 | */
205 | // return Spot.bulkCreate(demoSpots, { validate: true })
206 | await queryInterface.bulkInsert('Spots', demoSpots)
207 | },
208 |
209 | async down(queryInterface, Sequelize) {
210 | for (let spotInfo of validSpots) {
211 | await Spot.destroy({
212 | where: spotInfo
213 | });
214 | }
215 | /**
216 | * Add commands to revert seed here.
217 | *
218 | * Example:
219 | * await queryInterface.bulkDelete('People', null, {});
220 | */
221 | }
222 | };
223 |
--------------------------------------------------------------------------------
/backend/db/seeders/20220926101531-Reviews.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const demoReviews = [
4 | {
5 | userId: 1,
6 | spotId: 6,
7 | review: "Wow you guys weren't kidding, this place was 'fun' to say the least. I don't remember half of it, but it was awesome.",
8 | stars: 5,
9 | createdAt: 'Now',
10 | updatedAt: 'Now'
11 | },
12 | {
13 | userId: 1,
14 | spotId: 8,
15 | review: "The City was an amazing place. It was so cute and I just wanted to stay here forever. I loved the vibes and how accepting everyone was.",
16 | stars: 5,
17 | createdAt: 'Now',
18 | updatedAt: 'Now'
19 | },
20 | {
21 | userId: 1,
22 | spotId: 4,
23 | review: "What an experience. It felt like I was in another world entirely. It was so relaxing and I just felt so at peace. 10/10",
24 | stars: 5,
25 | createdAt: 'Now',
26 | updatedAt: 'Now'
27 | },
28 | {
29 | userId: 1,
30 | spotId: 5,
31 | review: "I got to travel across the globe to a foreign land and experience the traditional Korean culture. It felt so awesome to be here and experience this.",
32 | stars: 5,
33 | createdAt: 'Now',
34 | updatedAt: 'Now'
35 | },
36 | {
37 | userId: 1,
38 | spotId: 10,
39 | review: "Zion was a breathtaking trip. The place was so secluded and being in nature was so beautiful. The location was prepped to perfection and after a long day of hiking and nature-ing, it was a great place to relax and rest up.",
40 | stars: 5,
41 | createdAt: 'Now',
42 | updatedAt: 'Now'
43 | },
44 | {
45 | userId: 2,
46 | spotId: 2,
47 | review: "I am originally from California, so seeing the snow is foreign to me, yet alone living in it. However, the place was beautiful and I have a greater appreciation for the other seasons I didn't know existed. Great Cabin!",
48 | stars: 5,
49 | createdAt: 'Now',
50 | updatedAt: 'Now'
51 | },
52 | {
53 | userId: 2,
54 | spotId: 3,
55 | review: "The weather was nice, but I was not a fan of Los Angeles. I heard so many stories of it being so great and HOLLYWOOD! but I was dissapointed by the city. Location was 10/10 but the city was underwhelming.",
56 | stars: 3,
57 | createdAt: 'Now',
58 | updatedAt: 'Now'
59 | },
60 | {
61 | userId: 2,
62 | spotId: 7,
63 | review: "The SUN and the BEACHES were all we wanted, but we got so so so much more! Such a great location, literally on the beach with lots of surfboards and canoes to take out free of charge. Awesome location 11/10!!",
64 | stars: 5,
65 | createdAt: 'Now',
66 | updatedAt: 'Now'
67 | },
68 | {
69 | userId: 3,
70 | spotId: 2,
71 | review: "Wow. This place looked like it was ripped straight out of a fairytale. The snow was mesmerizing and made me feel all warm and cuddly inside. The fireplace was a huge bonus. 10/10 recommend.",
72 | stars: 5,
73 | createdAt: 'Now',
74 | updatedAt: 'Now'
75 | },
76 | {
77 | userId: 3,
78 | spotId: 3,
79 | review: "Wow the pictures do not do this location justice. I have never seen a more beautiful house and it was an honor to stay here for our trip. My friends and I had a blast in the house and an even better time going out into the city at night. AWESOME trip!",
80 | stars: 5,
81 | createdAt: 'Now',
82 | updatedAt: 'Now'
83 | },
84 | {
85 | userId: 3,
86 | spotId: 10,
87 | review: "What can I say to make you read this review and book this location ASAP. Such a great location for the price and TONS of things to do. Conquer your fears and go on the Angels Landing hike and as many others as you can!",
88 | stars: 5,
89 | createdAt: 'Now',
90 | updatedAt: 'Now'
91 | },
92 | {
93 | userId: 3,
94 | spotId: 4,
95 | review: "Wow. Bali. An experience like no other. Cannot wait to go back. Owners did a great job preparing the place for us and we felt bad having so much fun! Great place for you and yours to vacay!",
96 | stars: 5,
97 | createdAt: 'Now',
98 | updatedAt: 'Now'
99 | },
100 | {
101 | userId: 4,
102 | spotId: 4,
103 | review: "I booked this place because it had so many great reviews, but I was not impressed. House was NOT clean when we got there and it was not up to my expectations. If you're going to do your job, do it right!!",
104 | stars: 1,
105 | createdAt: 'Now',
106 | updatedAt: 'Now'
107 | },
108 | {
109 | userId: 4,
110 | spotId: 1,
111 | review: "I don't even know why I chose to book this location in the first place. It's a TREE. Who in their right mind would book this place. If I wanted to live in a tree, I would have been a monkey. If you're stupid, go ahead and book this tree.",
112 | stars: 1,
113 | createdAt: 'Now',
114 | updatedAt: 'Now'
115 | },
116 | {
117 | userId: 4,
118 | spotId: 7,
119 | review: "I don't get why there are so many good reviews for this location. It was so loud at night, the waves would NOT stop. The owner did not mention it would be so loud I wouldn't even be able to sleep. I want a refund!!!",
120 | stars: 1,
121 | createdAt: 'Now',
122 | updatedAt: 'Now'
123 | },
124 | {
125 | userId: 4,
126 | spotId: 11,
127 | review: "I only booked this location because it said free food and honey provided, BUT NONE OF IT WAS VEGAN AND GLUTEN FREE. I WANT A REFUND!! Pooh bear wasn't even there. This is craziness.",
128 | stars: 1,
129 | createdAt: 'Now',
130 | updatedAt: 'Now'
131 | },
132 | {
133 | userId: 3,
134 | spotId: 6,
135 | review: "I'm never coming back. It wasn't a bad stay at all in terms of the Penthouse, but that was a wild ride. Whatever happened here will hopefully remain here. I cannot in good conscious give this place of Sin 5 stars..",
136 | stars: 4,
137 | createdAt: 'Now',
138 | updatedAt: 'Now'
139 | }
140 | ]
141 |
142 |
143 |
144 |
145 | module.exports = {
146 | async up(queryInterface, Sequelize) {
147 | /**
148 | * Add seed commands here.
149 | *
150 | * Example:
151 | * await queryInterface.bulkInsert('People', [{
152 | * name: 'John Doe',
153 | * isBetaMember: false
154 | * }], {});
155 | */
156 | await queryInterface.bulkInsert('Reviews', demoReviews);
157 | },
158 |
159 | async down(queryInterface, Sequelize) {
160 | /**
161 | * Add commands to revert seed here.
162 | *
163 | * Example:
164 | * await queryInterface.bulkDelete('People', null, {});
165 | */
166 | await queryInterface.bulkDelete('Reviews')
167 | }
168 | };
169 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "bcryptjs": "^2.4.3",
4 | "cookie-parser": "^1.4.6",
5 | "cors": "^2.8.5",
6 | "csurf": "^1.11.0",
7 | "dotenv": "^16.0.1",
8 | "express": "^4.18.1",
9 | "express-async-errors": "^3.1.1",
10 | "express-validator": "^6.14.2",
11 | "helmet": "^5.1.1",
12 | "jsonwebtoken": "^8.5.1",
13 | "morgan": "^1.10.0",
14 | "per-env": "^1.0.2",
15 | "pg": "^8.8.0",
16 | "sequelize": "^6.21.4",
17 | "sequelize-cli": "^6.4.1"
18 | },
19 | "devDependencies": {
20 | "dotenv-cli": "^6.0.0",
21 | "nodemon": "^2.0.19",
22 | "sqlite3": "^5.0.11"
23 | },
24 | "scripts": {
25 | "sequelize": "sequelize",
26 | "sequelize-cli": "sequelize-cli",
27 | "start": "per-env",
28 | "start:development": "nodemon ./bin/www",
29 | "start:production": "node ./bin/www"
30 | },
31 | "name": "backend",
32 | "version": "1.0.0",
33 | "main": "app.js",
34 | "keywords": [],
35 | "author": "",
36 | "license": "ISC",
37 | "description": ""
38 | }
39 |
--------------------------------------------------------------------------------
/backend/routes/api/bookings.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { Booking, User, Spot, SpotImage } = require('../../db/models');
3 | const { requireAuth } = require('../../utils/auth');
4 | const router = express.Router();
5 | const sequelize = require('sequelize');
6 | const booking = require('../../db/models/booking');
7 |
8 |
9 | // GET All current User's BOOKINGS
10 | router.get('/current', requireAuth, async (req, res) => {
11 | const userId = req.user.id
12 | // const findUser = await User.findByPk(req.user.id)
13 | const myBookings = await Booking.findAll({
14 | where: {
15 | userId: userId
16 | },
17 | include: [
18 | {
19 | model: Spot,
20 | attributes: {
21 | exclude: ['createdAt', 'updatedAt']
22 | },
23 | },
24 |
25 | ],
26 | });
27 |
28 |
29 | // LAZY LOADED
30 | for (let i = 0; i < myBookings.length; i++) {
31 | let bookingsPreview = myBookings[i].toJSON();
32 |
33 | let bookingImg = bookingsPreview.Spot.id;
34 | const spotImg = await SpotImage.findOne({
35 | where: {
36 | spotId: bookingImg,
37 | preview: true
38 | }
39 | });
40 |
41 | if (spotImg) {
42 | bookingsPreview.Spot.previewImage = spotImg.url
43 | } else {
44 | bookingsPreview.Spot.previewImage = 'No image available :('
45 | }
46 | myBookings[i] = bookingsPreview;
47 | };
48 |
49 |
50 |
51 |
52 | return res.json({ Bookings: myBookings })
53 | });
54 |
55 |
56 |
57 |
58 | // EDIT a Booking with all conflicting date checks
59 | router.put('/:bookingId', requireAuth, async (req, res) => {
60 | const { bookingId } = req.params;
61 | const { startDate, endDate } = req.body;
62 | const findBooking = await Booking.findByPk(bookingId);
63 |
64 | if (!findBooking) {
65 | return res
66 | .status(404)
67 | .json({
68 | message: "Booking couldn't be found",
69 | statusCode: res.statusCode
70 | })
71 | };
72 |
73 | let currentDate = new Date();
74 |
75 | if (findBooking.endDate < currentDate) {
76 | return res
77 | .status(403)
78 | .json({
79 | message: "Past bookings can't be modified",
80 | statusCode: res.statusCode
81 | });
82 | };
83 |
84 | const currentBookings = await Booking.findAll({
85 | raw: true,
86 | where: { spotId: findBooking.spotId },
87 |
88 | });
89 |
90 | for (let i = 0; i < currentBookings.length; i++) {
91 | if (
92 | // Date.parse(startDate) >= Date.parse(currentBookings[i].startDate) &&
93 | // Date.parse(endDate) <= Date.parse(currentBookings[i].endDate) ||
94 |
95 | // Date.parse(startDate) <= Date.parse(currentBookings[i].startDate) &&
96 | // Date.parse(endDate) >= Date.parse(currentBookings[i].endDate) ||
97 |
98 | // Date.parse(startDate) >= Date.parse(currentBookings[i].startDate) &&
99 | // Date.parse(startDate) <= Date.parse(currentBookings[i].endDate) ||
100 |
101 | // Date.parse(endDate) >= Date.parse(currentBookings[i].startDate) &&
102 | // Date.parse(endDate) <= Date.parse(currentBookings[i].endDate)
103 |
104 | startDate >= currentBookings[i].startDate &&
105 | endDate <= currentBookings[i].endDate ||
106 |
107 | startDate <= currentBookings[i].startDate &&
108 | endDate >= currentBookings[i].endDate ||
109 |
110 | startDate >= currentBookings[i].startDate &&
111 | startDate <= currentBookings[i].endDate ||
112 |
113 | endDate >= currentBookings[i].startDate &&
114 | endDate <= currentBookings[i].endDate
115 |
116 |
117 |
118 | ) {
119 | return res
120 | .status(403)
121 | .json({
122 | message: "Sorry this spot is already booked for the specified dates",
123 | statusCode: res.statusCode,
124 | errors: {
125 | startDate: "Start date conflicts with an existing booking",
126 | endDate: "End date conflicts with an existing booking"
127 | }
128 | });
129 | };
130 | };
131 |
132 |
133 | try {
134 | await findBooking.update({
135 | startDate: startDate,
136 | endDate: endDate
137 | });
138 |
139 | return res.json(findBooking);
140 |
141 | } catch (error) {
142 | return res
143 | .status(400)
144 | .json({
145 | message: "Validation Error",
146 | statusCode: 400,
147 | errors: {
148 | endDate: "endDate cannot come before startDate"
149 | }
150 | });
151 | }
152 | });
153 |
154 |
155 |
156 |
157 |
158 |
159 | // DELETE a Booking
160 | router.delete('/:bookingId', requireAuth, async (req, res) => {
161 | const { bookingId } = req.params;
162 | const findBooking = await Booking.findByPk(bookingId)
163 |
164 | if (!findBooking) {
165 | return res
166 | .status(404)
167 | .json({
168 | message: "Booking couldn't be found",
169 | statusCode: res.statusCode
170 | })
171 | };
172 |
173 |
174 | const newDate = new Date();
175 | let day = newDate.getDate();
176 | let month = newDate.getMonth();
177 | let year = newDate.getFullYear();
178 | let currentDate = `${year}-${month}-${day}`;
179 |
180 | // cannot delete a booking that has already started
181 | if (findBooking.startDate <= currentDate) {
182 | return res
183 | .status(404)
184 | .json({
185 | message: "Bookings that have been started can't be deleted",
186 | statusCode: res.statusCode
187 | })
188 | };
189 |
190 | await findBooking.destroy();
191 |
192 | return res.json({
193 | message: 'Successfully deleted',
194 | statusCode: 200
195 | })
196 | });
197 |
198 |
199 |
200 |
201 |
202 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/index.js:
--------------------------------------------------------------------------------
1 | // backend/routes/api/index.js
2 | const router = require("express").Router();
3 | const bookingsRouter = require('./bookings.js');
4 | const sessionRouter = require('./session.js');
5 | const usersRouter = require('./users.js');
6 | const reviewImagesRouter = require('./review-images.js');
7 | const spotsRouter = require('./spots.js');
8 | const spotImagesRouter = require('./spot-images.js');
9 | const reviewsRouter = require('./reviews.js');
10 | const { restoreUser } = require("../../utils/auth.js");
11 | const mapsRouter = require('./maps');
12 |
13 | // Connect restoreUser middleware to the API router
14 | // If current user session is valid, set req.user to the user in the database
15 | // If current user session is not valid, set req.user to null
16 | router.use(restoreUser);
17 |
18 | router.use('/maps', mapsRouter);
19 |
20 | router.use('/bookings', bookingsRouter);
21 |
22 | router.use('/session', sessionRouter);
23 |
24 | router.use('/users', usersRouter);
25 |
26 | router.use('/review-images', reviewImagesRouter);
27 |
28 | router.use('/spots', spotsRouter);
29 |
30 | router.use('/spot-images', spotImagesRouter);
31 |
32 | router.use('/reviews', reviewsRouter);
33 |
34 | router.post('/test', (req, res) => {
35 | res.json({ requestBody: req.body});
36 | });
37 |
38 |
39 |
40 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/maps.js:
--------------------------------------------------------------------------------
1 | // backend/routes/api/maps.js
2 | const router = require('express').Router();
3 | const { googleMapsAPIKey } = require('../../config');
4 |
5 | router.post('/key', (req, res) => {
6 | res.json({ googleMapsAPIKey });
7 | });
8 |
9 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/review-images.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { ReviewImage, Review, Spot } = require('../../db/models');
3 | const { requireAuth } = require('../../utils/auth');
4 | const router = express.Router();
5 | const sequelize = require('sequelize');
6 |
7 |
8 |
9 | router.delete('/:imageId', requireAuth, async (req, res) => {
10 | const { imageId } = req.params;
11 | const deleteImage = await ReviewImage.findByPk(imageId);
12 |
13 | if (!deleteImage) {
14 | return res
15 | .status(404)
16 | .json({
17 | message: "Review Image couldn't be found",
18 | statusCode: res.statusCode
19 | });
20 | };
21 |
22 | deleteImage.destroy();
23 | res.statusCode = 200
24 | return res.json({
25 | message: 'Successfully deleted',
26 | statusCode: res.statusCode
27 | });
28 |
29 | });
30 |
31 |
32 |
33 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/reviews.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { Spot, SpotImage, Review, ReviewImage, User } = require('../../db/models');
3 | const { requireAuth } = require('../../utils/auth');
4 | const router = express.Router();
5 |
6 |
7 |
8 | //GET ALL REVIEWS OF CURRENT USER
9 | router.get('/current', requireAuth, async (req, res) => {
10 | const userId = req.user.id
11 |
12 | const reviews = await Review.findAll({
13 | include: [
14 | {
15 | model: User, as: 'User',
16 | attributes:
17 | { exclude: ['email', 'hashedPassword', 'username', 'createdAt', 'updatedAt'] }
18 | },
19 | {
20 | model: ReviewImage, as: "ReviewImages",
21 | attributes:
22 | { exclude: ['reviewId', 'createdAt', 'updatedAt'] }
23 | },
24 | {
25 | model: Spot, as: "Spot",
26 | attributes: {
27 | exclude: ['createdAt', 'updatedAt', 'description']
28 | },
29 | include: {
30 | model: SpotImage,
31 | where: { preview: true },
32 | attributes: ['url']
33 | }
34 | },
35 |
36 | ],
37 | where: {
38 | userId: userId
39 | },
40 | });
41 | for (let i = 0; i < reviews.length; i++) {
42 | let resPreview = reviews[i].toJSON();
43 | // console.log(resPreview)
44 | let spotImageUrl = resPreview.Spot.SpotImages[0]
45 | // if (!spotImageUrl) return null;
46 |
47 | if (spotImageUrl) {
48 | resPreview.Spot.previewImage = spotImageUrl.url
49 | } else {
50 | resPreview.Spot.previewImage = 'No Image Available'
51 | }
52 | delete resPreview.Spot.SpotImages;
53 | reviews[i] = resPreview;
54 | };
55 | // console.log('HELLLOO FROM THE BACKEND',reviews)
56 |
57 | return res.json({ Reviews: reviews })
58 | });
59 |
60 |
61 |
62 | //EDIT a REVIEW
63 |
64 | router.put('/:reviewId', requireAuth, async (req, res) => {
65 | const { reviewId } = req.params;
66 | const findReview = await Review.findByPk(reviewId);
67 | const { review, stars } = req.body;
68 |
69 | if (!findReview) {
70 | return res
71 | .status(404)
72 | .json({
73 | message: "Review couldn't be found",
74 | statusCode: res.statusCode
75 | })
76 | };
77 |
78 |
79 | try {
80 | findReview.update({
81 | review: review,
82 | stars, stars
83 | });
84 |
85 | res.json( findReview )
86 |
87 | } catch (error) {
88 | return res
89 | .status(400)
90 | .json({
91 | "message": "Validation error",
92 | statusCode: res.statusCode,
93 | errors: {
94 | review: "Review text is required",
95 | stars: "Stars must be an integer from 1 to 5",
96 | }
97 | });
98 | };
99 | });
100 |
101 |
102 |
103 |
104 | // create an image for a review
105 | router.post('/:reviewId/images', requireAuth, async (req, res) => {
106 | const { reviewId } = req.params;
107 | const { url } = req.body;
108 | const findReview = await Review.findByPk(reviewId);
109 | const userId = req.user.id;
110 |
111 | if (!findReview) {
112 | return res
113 | .status(404)
114 | .json({
115 | message: "Review couldn't be found",
116 | statusCode: res.statusCode
117 | })
118 | };
119 |
120 | if (findReview.userId !== userId) {
121 | return res
122 | .status(403)
123 | .json({
124 | message: "Slow down there buckaroooo, this doesn't belong to you!",
125 | statusCode: 403
126 | })
127 | };
128 |
129 | const hasImages = await ReviewImage.findAll({
130 | where: { reviewId: reviewId }
131 | });
132 |
133 | if (hasImages.length >= 10) {
134 | return res
135 | .status(403)
136 | .json({
137 | message: "Maximum number of images for this resource was reached",
138 | statusCode: res.statusCode
139 | })
140 | };
141 |
142 | const addImage = await ReviewImage.create({
143 | url,
144 | reviewId: reviewId
145 | });
146 |
147 | return res.json({
148 | id: addImage.id,
149 | url: url
150 | });
151 | });
152 |
153 |
154 |
155 |
156 | // DELETE a REVIEW
157 |
158 | router.delete('/:reviewId', requireAuth, async (req, res) => {
159 | const { reviewId } = req.params;
160 | const deleteReview = await Review.findByPk(reviewId);
161 | const userId = req.user.id;
162 |
163 | if (!deleteReview) {
164 | return res
165 | .status(404)
166 | .json({
167 | message: "Review couldn't be found",
168 | statusCode: res.statusCode
169 | });
170 | };
171 |
172 | if (deleteReview.userId === userId) {
173 | deleteReview.destroy();
174 | return res.json({
175 | message: "Successfully deleted",
176 | statusCode: 200
177 | });
178 | } else {
179 | return res.json({
180 | message: "You are not the owner of this Review",
181 | })
182 | }
183 | });
184 |
185 |
186 |
187 |
188 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/session.js:
--------------------------------------------------------------------------------
1 | // backend/routes/api/session.js
2 | // create and export an express router
3 | const express = require('express');
4 |
5 | //phase 4 import
6 | const { setTokenCookie, restoreUser } = require('../../utils/auth');
7 | const { User } = require('../../db/models');
8 | const router = express.Router();
9 |
10 | //phase 5 import
11 | const { check } = require('express-validator'); //used with handleValidationErrors to VALIDATE body of a req
12 | const { handleValidationErrors } = require('../../utils/validation');
13 |
14 | const { requireAuth } = require('../../utils/auth');
15 |
16 |
17 | //phase 5; is composed of the check and handleValidationErrors middleware
18 | // checks to see if req.body.credentials and req.body.password are EMPTY
19 | const validateLogin = [
20 | check('credential')
21 | .exists({ checkFalsy: true })
22 | .notEmpty()
23 | .withMessage('Please provide a valid email or username.'),
24 | check('password')
25 | .exists({ checkFalsy: true })
26 | .withMessage('Please provide a password.'),
27 | handleValidationErrors
28 | ];
29 |
30 |
31 | // Log in route handler
32 | // "Add User login backend endpoint"
33 | router.post('/', validateLogin, async (req, res, next) => {
34 | const { credential, password } = req.body;
35 |
36 | const user = await User.login({ credential, password });
37 |
38 | if (!user) {
39 | // const err = new Error('Login failed');
40 | // err.status = 401;
41 | // err.title = 'Login failed';
42 | // err.errors = ['The provided credentials were invalid.'];
43 | res
44 | .status(401)
45 | .json({
46 | message: "Invalid Credentials",
47 | statusCode: res.statusCode
48 | });
49 |
50 | return next(err);
51 | }
52 |
53 | const token = await setTokenCookie(res, user);
54 |
55 | const userObj = user.toJSON();
56 | userObj.token = token;
57 |
58 | return res.json(userObj);
59 | }
60 | );
61 |
62 | // Log out route handler
63 | // "Add User logout backend endpoint"
64 | router.delete(
65 | '/',
66 | (_req, res) => {
67 | res.clearCookie('token');
68 | return res.json({ message: 'success' });
69 | }
70 | );
71 |
72 | // Restore session user route handlers
73 | // "Add a backend endpoint to get the current user session"
74 | router.get(
75 | '/',
76 | restoreUser,
77 | (req, res) => {
78 | const { user } = req;
79 | if (user) {
80 | return res.json(user);
81 | } else return res.json(null);
82 | // this was wrong :(
83 | // return res
84 | // .status(401)
85 | // .json({
86 | // message: "Authentication required",
87 | // statusCode: 401
88 | // });
89 | }
90 | );
91 |
92 |
93 |
94 |
95 |
96 | module.exports = router;
97 |
--------------------------------------------------------------------------------
/backend/routes/api/spot-images.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { SpotImage } = require('../../db/models');
3 | const { requireAuth } = require('../../utils/auth');
4 | const router = express.Router();
5 |
6 |
7 | // Router is live test
8 | // router.get('/', async (req, res) => {
9 | // res.send('HELLO')
10 | // })
11 |
12 | router.delete('/:imageId', requireAuth, async (req, res) => {
13 | const {imageId} = req.params;
14 | const deleteImage = await SpotImage.findByPk(imageId);
15 |
16 | if(!deleteImage){
17 | return res
18 | .status(404)
19 | .json({
20 | message: "Spot Image couldn't be found",
21 | statusCode: res.statusCode
22 | });
23 | };
24 |
25 | await deleteImage.destroy();
26 | res.statusCode = 200
27 | return res.json({
28 | message: 'Successfully deleted',
29 | statusCode: res.statusCode
30 | });
31 |
32 | });
33 |
34 |
35 |
36 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/api/testingAuthMware.js:
--------------------------------------------------------------------------------
1 |
2 | // // TESTING USER AUTH MIDDLEWARES (DELETE LATER)
3 | // // router.post('/test', function (req, res) {
4 | // // res.json({ requestBody: req.body });
5 | // // });
6 |
7 | // // // TEST ROUTER (delete me later?)
8 | // // GET /api/set-token-cookie
9 | // const { setTokenCookie } = require('../../utils/auth.js');
10 | // const { User } = require('../../db/models');
11 | // router.get('/set-token-cookie', async (_req, res) => {
12 | // const user = await User.findOne({
13 | // where: {
14 | // username: 'Demo-lition'
15 | // }
16 | // });
17 | // setTokenCookie(res, user);
18 | // return res.json({ user });
19 | // });
20 |
21 |
--------------------------------------------------------------------------------
/backend/routes/api/users.js:
--------------------------------------------------------------------------------
1 | // backend/routes/api/users.js
2 | // Create and export an Express router
3 | const express = require('express');
4 |
5 | const { setTokenCookie, requireAuth } = require('../../utils/auth');
6 | const { User } = require('../../db/models');
7 |
8 | const router = express.Router();
9 |
10 | //phase 5 import
11 | const { check } = require('express-validator');
12 | const { handleValidationErrors } = require('../../utils/validation');
13 |
14 |
15 | // POST /api/users singup route expects req to have username, email, pw w/
16 | //password of the user being created. ValidateSignup checks and validates these keys
17 | // DO NOT REMOVE YET (as per end of Phase 5)
18 |
19 | const validateSignup = [
20 | check('firstName')
21 | .exists({checkFalsy: true})
22 | .withMessage('Please provide your First Name'),
23 | check('lastName')
24 | .exists({ checkFalsy: true })
25 | .withMessage('Please provide your Last Name'),
26 | check('email')
27 | // checks if req.body.email exists and is an email
28 | .exists({ checkFalsy: true })
29 | .isEmail()
30 | .withMessage('Please provide a valid email.'),
31 | check('username')
32 | // checks if req.body.username is a min length of 4, and is not an email
33 | .exists({ checkFalsy: true })
34 | .isLength({ min: 4 })
35 | .withMessage('Please provide a username with at least 4 characters.'),
36 | check('username')
37 | .not()
38 | .isEmail()
39 | .withMessage('Username cannot be an email.'),
40 | check('password')
41 | // checks if req.body.password is not empty and has a min length of 6
42 | .exists({ checkFalsy: true })
43 | .isLength({ min: 6 })
44 | .withMessage('Password must be 6 characters or more.'),
45 | handleValidationErrors
46 | ];
47 |
48 |
49 | // Sign up route handler
50 | //"Add User signup backend endpoint"
51 | router.post('/', validateSignup, async (req, res) => {
52 | const { email, password, username, firstName, lastName} = req.body;
53 |
54 | const emailExists = await User.findOne({where: {email}});
55 | const usernameExists = await User.findOne({where: {username}});
56 |
57 | if ( emailExists ) {
58 | return res
59 | .status(403)
60 | .json({
61 | message: "User already exists",
62 | statusCode: res.statusCode,
63 | errors: [ "User with that email already exists."]
64 |
65 | })
66 | };
67 |
68 | if ( usernameExists ) {
69 | return res
70 | .status(403)
71 | .json({
72 | message: "User already exists",
73 | statusCode: res.statusCode,
74 | errors: [ "User with that username already exists"]
75 |
76 | })
77 | }
78 |
79 |
80 | const user = await User.signup({ firstName, lastName, email, username, password });
81 |
82 | await setTokenCookie(res, user);
83 |
84 | return res.json(user);
85 | }
86 |
87 | // router.post('/', validateSignup, async (req, res) => {
88 | // const { email, password, username, firstName, lastName } = req.body;
89 |
90 | // try {
91 | // const user = await User.signup({
92 | // firstName, lastName, email, username, password
93 | // });
94 | // }
95 | // catch {
96 | // res.statusCode = 403;
97 | // res.json({
98 | // message: 'User already exists',
99 | // statusCode: res.statusCode,
100 | // errors: {
101 | // 'email': 'User with that email already exists',
102 | // 'username': 'User with that username already exists'
103 | // }
104 | // })
105 | // }
106 |
107 |
108 |
109 |
110 | );
111 |
112 |
113 |
114 |
115 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/index.js:
--------------------------------------------------------------------------------
1 | // backend/routes/index.js
2 | const express = require('express');
3 | const router = express.Router();
4 | const apiRouter = require('./api');
5 |
6 | router.use('/api', apiRouter);
7 |
8 | // Static routes
9 | // Serve React build files in production
10 | if (process.env.NODE_ENV === 'production') {
11 | const path = require('path');
12 | // Serve the frontend's index.html file at the root route
13 | router.get('/', (req, res) => {
14 | res.cookie('XSRF-TOKEN', req.csrfToken());
15 | res.sendFile(
16 | path.resolve(__dirname, '../../frontend', 'build', 'index.html')
17 | );
18 | });
19 |
20 | // Serve the static assets in the frontend's build folder
21 | router.use(express.static(path.resolve("../frontend/build")));
22 |
23 | // Serve the frontend's index.html file at all other routes NOT starting with /api
24 | router.get(/^(?!\/?api).*/, (req, res) => {
25 | res.cookie('XSRF-TOKEN', req.csrfToken());
26 | res.sendFile(
27 | path.resolve(__dirname, '../../frontend', 'build', 'index.html')
28 | );
29 | });
30 | }
31 |
32 | // Add a XSRF-TOKEN cookie in development
33 | if (process.env.NODE_ENV !== 'production') {
34 | router.get('/api/csrf/restore', (req, res) => {
35 | res.cookie('XSRF-TOKEN', req.csrfToken());
36 | res.status(201).json({});
37 | });
38 | }
39 |
40 | module.exports = router;
--------------------------------------------------------------------------------
/backend/utils/auth.js:
--------------------------------------------------------------------------------
1 | // backend/utils/auth.js
2 | const jwt = require('jsonwebtoken');
3 | const { jwtConfig } = require('../config');
4 | const { User } = require('../db/models');
5 |
6 | const { secret, expiresIn } = jwtConfig;
7 |
8 | // SENDS a JWT Cookie
9 | const setTokenCookie = (res, user) => {
10 | // Create the token.
11 | const token = jwt.sign(
12 | { data: user.toSafeObject() },
13 | secret,
14 | { expiresIn: parseInt(expiresIn) } // 604,800 seconds = 1 week
15 | );
16 |
17 | const isProduction = process.env.NODE_ENV === "production";
18 |
19 | // Set the token cookie
20 | res.cookie('token', token, {
21 | maxAge: expiresIn * 1000, // maxAge in milliseconds
22 | httpOnly: true,
23 | secure: isProduction,
24 | sameSite: isProduction && "Lax"
25 | });
26 |
27 | return token;
28 | };
29 |
30 |
31 | // Create (auth) middleware function that will restore the session user based on JWT cookie
32 | // Create a middleware function that will verify and parse the JWT's payload and search
33 | // database for a USER with the id matching the payload.id
34 |
35 | // restoreUser middleware will be connected to the API router so that all API
36 | // route handlers will check if there is a current user logged in or not
37 |
38 | const restoreUser = (req, res, next) => {
39 | // token parsed from cookies
40 | const { token } = req.cookies;
41 | req.user = null;
42 |
43 | return jwt.verify(token, secret, null, async (err, jwtPayload) => {
44 | if (err) {
45 | return next();
46 | }
47 |
48 | try {
49 | const { id } = jwtPayload.data;
50 | req.user = await User.scope('currentUser').findByPk(id);
51 | } catch (e) {
52 | res.clearCookie('token');
53 | return next();
54 | }
55 |
56 | if (!req.user) res.clearCookie('token');
57 |
58 | return next();
59 | });
60 | };
61 |
62 | // If there is no current user, return an error
63 | // prevents users who are not logged in from accessing stage changing routes
64 | const requireAuth = [
65 | restoreUser,
66 | function (req, _res, next) {
67 | if (req.user) return next();
68 |
69 | const err = new Error('Unauthorized');
70 | err.title = 'Unauthorized';
71 | err.errors = ['Unauthorized'];
72 | err.status = 401;
73 | return next(err);
74 | }
75 | ];
76 |
77 |
78 |
79 |
80 | module.exports = { setTokenCookie, restoreUser, requireAuth }
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/backend/utils/validation.js:
--------------------------------------------------------------------------------
1 | // backend/utils/validation.js
2 | const { validationResult } = require('express-validator');
3 |
4 | // middleware for formatting errors from express-validator middleware
5 | // (to customize, see express-validator's documentation)
6 | const handleValidationErrors = (req, _res, next) => {
7 | const validationErrors = validationResult(req);
8 |
9 | if (!validationErrors.isEmpty()) {
10 | const errors = validationErrors
11 | .array()
12 | .map((error) => `${error.msg}`);
13 |
14 | const err = Error('Bad request.');
15 | err.errors = errors;
16 | err.status = 400;
17 | err.title = 'Bad request.';
18 | next(err);
19 | }
20 | next();
21 | };
22 |
23 | module.exports = {
24 | handleValidationErrors
25 | };
--------------------------------------------------------------------------------
/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 | .eslintcache
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Create React App Template
3 |
4 | A no-frills template from which to create React + Redux 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-redux-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/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@react-google-maps/api": "^2.17.1",
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^11.2.7",
9 | "@testing-library/user-event": "^12.8.3",
10 | "date-fns": "^2.29.3",
11 | "js-cookie": "^3.0.1",
12 | "react": "^18.2.0",
13 | "react-date-range": "^1.4.0",
14 | "react-dom": "^18.2.0",
15 | "react-loading-skeleton": "^3.1.0",
16 | "react-redux": "^7.2.8",
17 | "react-router-dom": "^5.3.3",
18 | "react-scripts": "5.0.1",
19 | "redux": "^4.2.0",
20 | "redux-thunk": "^2.4.1"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "redux-logger": "^3.0.6"
45 | },
46 | "proxy": "http://localhost:8000"
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/public/favIcon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/public/favIcon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/frontend/public/favIcon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/apple-touch-icon.png
--------------------------------------------------------------------------------
/frontend/public/favIcon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/public/favIcon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon-32x32.png
--------------------------------------------------------------------------------
/frontend/public/favIcon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/favIcon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | AirNbN
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | // frontend/src/App.js
2 | import React, { useState, useEffect } from "react";
3 | import { useDispatch } from "react-redux";
4 | import { Route, Switch, useParams } from "react-router-dom";
5 | import SignupFormPage from "./components/SignupFormPage";
6 | import * as sessionActions from "./store/session";
7 | import Navigation from "./components/Navigation";
8 | import SpotsBrowser from './components/AllSpots/index'
9 | import SpotDetail from "./components/SpotDetails";
10 | // import { getSpotDetails } from "./store/spots";
11 | import CreateSpotForm from "./components/CreateSpotForm";
12 | import UpdateSpotFormPage from "./components/UpdateSpot";
13 | import CreateReviewForm from "./components/CreateReviewForm";
14 | import GetCurrentUser from "./components/GetCurrentUser";
15 | import Footer from "./components/Footer/Footer";
16 |
17 | function App() {
18 | const dispatch = useDispatch();
19 | const [isLoaded, setIsLoaded] = useState(false);
20 |
21 | // const spotDetails = useSelector(state => state.spotDetails)
22 |
23 | useEffect(() => {
24 | dispatch(sessionActions.restoreUser()).then(() => setIsLoaded(true));
25 | }, [dispatch]);
26 |
27 |
28 |
29 | return (
30 | <>
31 |
32 | {isLoaded && (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | )}
59 |
60 | >
61 | );
62 | }
63 |
64 | export default App;
--------------------------------------------------------------------------------
/frontend/src/assets/airnbn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/assets/airnbn.png
--------------------------------------------------------------------------------
/frontend/src/assets/globe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/readMeLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/frontend/src/assets/up-arrow-svgrepo-com (1).svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/frontend/src/components/AllReviews/AllReviews.css:
--------------------------------------------------------------------------------
1 |
2 | #review-container-style {
3 | width: 100vw;
4 | display: flex;
5 | justify-content: center;
6 | flex-direction: column;
7 | align-items: center;
8 | box-sizing: border-box;
9 | /* border: 5px solid blue; */
10 | }
11 |
12 | #reviews-container {
13 | display: block;
14 | text-align: center;
15 | width: 100%;
16 | padding-top: 15px;
17 | padding-bottom: 15px;
18 |
19 | }
20 |
21 |
22 | #review-firstName {
23 | font-size: 16px;
24 | font-family: system-ui;
25 | font-weight: 600;
26 | line-height: 20px;
27 | color: rgb(34, 34, 34);
28 | box-sizing: border-box;
29 | letter-spacing: .8px;
30 | }
31 |
32 | #existing-reviews {
33 | width: 1120px;
34 | height: fit-content;
35 | display: grid;
36 | column-gap: 25px;
37 | row-gap: 25px;
38 | grid-template-columns: repeat(2, 1fr);
39 | /* grid-template-areas:
40 | "a a"
41 | "a a"
42 | "a a"; */
43 | /* border: 2px solid pink; */
44 | font-family: system-ui;
45 | margin-bottom: 25px;
46 | padding-bottom: 25px;
47 | border-bottom: 1px solid lightgrey;
48 | }
49 |
50 |
51 | #reviews-header {
52 | width: 1120px;
53 | display: flex;
54 | justify-content: space-between;
55 | align-items: center;
56 | font-size: 24px;
57 | font-weight: 500;
58 | color: rgb(34, 34, 34);
59 | margin: 25px 0;
60 | }
61 |
62 | #delete-review-btn {
63 | width: 50%;
64 | /* margin-top: 15px; */
65 | height: 40px;
66 | background-color: #E31C5F;
67 | color: white;
68 | border-radius: 5px;
69 | border: 1px solid lightgrey;
70 | font-size: 16px;
71 | font-weight: 500;
72 | font-family: system-ui;
73 | box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 16px 0px;
74 | margin-left: 100px;
75 | cursor: pointer;
76 | }
77 |
78 | #review-card {
79 | display: flex;
80 | flex-direction: column;
81 | /* border: 1px solid red; */
82 | text-align: start;
83 | padding: 10px, 10px, 10px, 10px;
84 | width: 100%;
85 | height: 100%;
86 | /* grid-area: a; */
87 | /* overflow-wrap: break-word; */
88 | }
89 |
90 |
91 |
92 | #review-profile-details{
93 | display:flex;
94 | flex-direction: row;
95 | justify-content: space-between;
96 | width: 100%;
97 | /* border: 1px solid purple; */
98 | box-sizing: border-box;
99 | }
100 |
101 | #review-header-left{
102 | display: flex;
103 | }
104 |
105 | #review-name-date{
106 | /* padding-left: 8px;
107 | padding-bottom: 10px; */
108 | /* width: fit-content; */
109 | display: flex;
110 | flex-direction: column;
111 | justify-content: space-around;
112 | width: 100%;
113 | margin-left: 5px
114 | }
115 |
116 | #review-time {
117 | font-size: 14px;
118 | width: 100%;
119 | box-sizing: border-box;
120 | color: rgb(113, 113, 113);
121 | font-weight: 400;
122 | }
123 |
124 | #user-review {
125 | overflow-wrap: break-word;
126 | /* font-style: italic; */
127 | font-weight: 400;
128 | padding-top: 10px;
129 | width: 90%;
130 | line-height: 24px;
131 | color: rgb(34, 34, 34);
132 | box-sizing: border-box;
133 | letter-spacing: .25px;
134 | margin-top: 10px;
135 | }
--------------------------------------------------------------------------------
/frontend/src/components/AllReviews/index.js:
--------------------------------------------------------------------------------
1 | import { useHistory, useParams } from "react-router-dom"
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useEffect } from "react";
4 | import { deleteReview, getSpotReviews } from "../../store/reviews";
5 | import './AllReviews.css'
6 | import { getSpotDetails } from "../../store/spots";
7 |
8 |
9 |
10 | const SpotReviews = () => {
11 |
12 | const params = useParams();
13 | const { spotId } = params;
14 | // const history = useHistory();
15 | const dispatch = useDispatch();
16 | const history = useHistory();
17 |
18 | // const spot = useSelector(state => state.spots.singleSpot);
19 | const spotReviews = useSelector(state => state.reviews.spot);
20 | const currentUser = useSelector(state => state.session.user);
21 | const singleSpot = useSelector(state => state.spots.singleSpot)
22 |
23 | const months = {
24 | '01': 'January',
25 | '02': 'February',
26 | '03': 'March',
27 | '04': 'April',
28 | '05': 'May',
29 | '06': 'June',
30 | '07': 'July',
31 | '08': 'August',
32 | '09': 'September',
33 | '10': 'October',
34 | '11': 'November',
35 | '12': 'December'
36 | };
37 |
38 | function parseDate(date) {
39 | const year = date.split('-')[0];
40 | const month = date.split('-')[1];
41 | // const day = date.split('-')[2];
42 |
43 | return `${months[month]} ${year}`
44 | };
45 |
46 | useEffect(() => {
47 | dispatch(getSpotReviews(spotId))
48 | }, [dispatch, spotId]);
49 |
50 | const deleteHandler = async (reviewId) => {
51 |
52 | await dispatch(deleteReview(reviewId))
53 | dispatch(getSpotDetails(spotId))
54 | history.push(`/spots/${spotId}`)
55 | }
56 |
57 |
58 | return (
59 |
60 |
61 |
66 |
67 | {Object.values(spotReviews).map(review => {
68 | return (
69 |
70 |
71 |
72 |
73 |
93 |
94 | {currentUser && currentUser.id === review.userId && (
95 |
96 | )}
97 |
98 |
99 |
100 | {review.review}
101 |
102 |
103 |
104 | )
105 | })}
106 |
107 |
108 |
109 | )
110 | }
111 |
112 | export default SpotReviews;
--------------------------------------------------------------------------------
/frontend/src/components/AllSpots/allSpots.css:
--------------------------------------------------------------------------------
1 | .SpotsContainer {
2 | display: grid;
3 | grid-template-columns: repeat(5, 1fr);
4 | /* border: 5px blue solid; */
5 | height: 100%;
6 | /* width: 100%; */
7 | column-gap: 24px;
8 | row-gap: 45px;
9 | margin-top: 100px;
10 | margin-bottom: 50px;
11 | /* padding-left: 80px;
12 | padding-right: 80px; */
13 | }
14 |
15 |
16 | .wrapper {
17 | height: 100%;
18 | /* border: solid red 5px; */
19 | display: flex;
20 | justify-content: center;
21 | /* width: 100vw; */
22 | /* padding-top: 180px; */
23 | margin-bottom: 25px;
24 | }
25 |
26 | #single-card {
27 | /* display: flex;
28 | justify-content: center; */
29 | /* height: 325x; */
30 | height: 300px;
31 | /* width: 300px; */
32 | width: 100%;
33 | max-width: 320px;
34 | /* border: 5px red dashed; */
35 | font-size: 15px;
36 | line-height: 19px;
37 | position: absolute;
38 | object-fit: cover;
39 | }
40 |
41 |
42 |
43 | @media screen and (max-width: 1536px) {
44 | .SpotsContainer {
45 | display: grid;
46 | grid-template-columns: repeat(4, 1fr);
47 | height: 100%;
48 | column-gap: 24px;
49 | row-gap: 45px;
50 | margin-top: 100px;
51 | }
52 | }
53 |
54 | @media screen and (max-width: 1280px) {
55 | .SpotsContainer {
56 | display: grid;
57 | grid-template-columns: repeat(3, 1fr);
58 | height: 100%;
59 | column-gap: 50px;
60 | row-gap: 45px;
61 | margin-top: 100px;
62 | }
63 | }
64 |
65 | @media screen and (max-width: 1024px) {
66 | .SpotsContainer {
67 | display: grid;
68 | grid-template-columns: repeat(2, 1fr);
69 | column-gap: 75px;
70 | row-gap: 45px;
71 | margin-top: 100px;
72 | }
73 | }
74 |
75 | @media screen and (max-width: 480px) {
76 | .SpotsContainer {
77 | display: grid;
78 | grid-template-columns: repeat(1, 1fr);
79 | margin-top: 100px;
80 | }
81 | }
--------------------------------------------------------------------------------
/frontend/src/components/AllSpots/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { NavLink, Route, useParams } from 'react-router-dom';
4 | import { getAllSpots } from '../../store/spots';
5 | import SingleSpot from '../SingleSpot/index';
6 | import SpotDetail from '../SpotDetails';
7 | import './allSpots.css'
8 |
9 | const SpotsBrowser = () => {
10 | const dispatch = useDispatch();
11 |
12 | const spots = useSelector(state => Object.values(state.spots.allSpots));
13 | // console.log('ALL SPOTS', spots)
14 | const [count, setCount] = useState(0);
15 |
16 |
17 | useEffect(() => {
18 | let counter = count;
19 | let countInterval = setInterval(() => {
20 | if (counter >= spots.length) {
21 | clearInterval(countInterval)
22 | } else {
23 | setCount(count => count + 1);
24 | }
25 | }, 30);
26 |
27 | return () => (clearInterval(countInterval))
28 | }, [spots])
29 |
30 | useEffect(() => {
31 | const getAllspotsDispatch = dispatch(getAllSpots());
32 | // console.log('get all spots dispatch', getAllspotsDispatch);
33 | }, [dispatch])
34 |
35 | const allSpotsArray = spots.slice(0, count);
36 | // console.log('allSpotsArray', allSpotsArray)
37 | // return null;
38 | if (!spots.length) return null;
39 |
40 |
41 |
42 | return (
43 |
44 |
45 |
46 |
47 | {allSpotsArray.map(spot => (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 |
61 |
62 | )
63 |
64 |
65 |
66 | }
67 |
68 | export default SpotsBrowser;
--------------------------------------------------------------------------------
/frontend/src/components/CreateReviewForm/CreateReviewForm.css:
--------------------------------------------------------------------------------
1 | #create-review-form {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | padding-top: 150px;
7 |
8 | }
9 |
10 | #form-container {
11 | margin-top: 100px;
12 | }
13 |
14 | #review-form-styling {
15 | display: block;
16 | width: 500px;
17 | height: 550px;
18 | text-align: center;
19 | border-color: rgb(221, 221, 221);
20 | box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 16px 0px;
21 |
22 | }
23 |
24 |
25 | #review-h1 {
26 | border-bottom: 1px lightgrey solid;
27 | }
28 |
29 | #createReview-buttonDiv {
30 | display: flex;
31 | flex-direction: column;
32 | align-items: center;
33 | margin-top: 15px;
34 | }
35 |
36 | #createReview-submitBtn:hover,
37 | #cancelReviewbtn:hover {
38 | cursor: pointer;
39 | }
40 |
41 | #createReview-submitBtn,
42 | #cancelReviewbtn {
43 | width: 75%;
44 | height: 40px;
45 | background-color: #E31C5F;
46 | color: white;
47 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
48 | margin-top: 15px;
49 | border-radius: 5px;
50 | border: 1px solid lightgrey;
51 | font-size: 16px;
52 | font-weight: 500;
53 | }
54 |
55 | #review-description {
56 | border-radius: 15px;
57 | font-size: 16px;
58 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
59 | padding-left: 10px;
60 | padding-top: 10px;
61 | }
62 |
63 |
64 | #fieldset-stars {
65 | border: 1px solid lightgrey;
66 | border-radius: 9999px;
67 | box-shadow: rgba(0, 0, 0, 0.07) 0px 6px 16px 0px;
68 | }
69 |
70 | #starsContainer {
71 | margin-top: -25px;
72 | margin-bottom: -15px;
73 | }
74 |
75 | #starHeading {
76 | font-size: 18px;
77 | margin-top: 28px;
78 | margin-bottom: 10px;
79 | font: system-ui;
80 | font-weight: 400;
81 | }
82 |
83 | .rate {
84 | display: inline-block;
85 | border: 0;
86 | }
87 |
88 | .rate>input {
89 | display: none;
90 | }
91 |
92 | .rate>label {
93 | float: right;
94 | }
95 |
96 | .rate>label:before {
97 | display: inline-block;
98 | font-size: 2rem;
99 | padding: .3rem .2rem;
100 | margin: 0;
101 | cursor: pointer;
102 | font-family: system-ui;
103 | content: "★";
104 | /* position: relative; */
105 | /* color: lightgrey; */
106 | }
107 |
108 | .starInput:checked ~ label,
109 | label:hover,
110 | label:hover~label {
111 | color: #FFD700
112 | }
113 |
114 | .starInput:checked + label:hover,
115 | .starInput:checked~label:hover,
116 | input:checked~label:hover~label,
117 | label:hover~.starInput:checked~label {
118 | color: #FFD700;
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateReviewForm/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { useHistory, useParams } from "react-router-dom";
4 | import { createReview } from "../../store/reviews";
5 | import './CreateReviewForm.css'
6 |
7 |
8 | const CreateReviewForm = () => {
9 |
10 | const dispatch = useDispatch();
11 | const history = useHistory();
12 | const params = useParams();
13 | const { spotId } = params;
14 |
15 | const [review, setReview] = useState('');
16 | const [stars, setStars] = useState(0);
17 |
18 | const submitHandler = async (e) => {
19 | e.preventDefault();
20 |
21 | const payload = {
22 | review,
23 | stars
24 | }
25 | let newReview = await dispatch(createReview(payload, spotId))
26 |
27 | if ( newReview ) {
28 | history.push(`/spots/${spotId}`)
29 | }
30 |
31 | // if (!newReview) {
32 | // window.alert('You have already created a review for this Spot.')
33 | // }
34 | }
35 |
36 | const handleCancel = (e) => {
37 | e.preventDefault();
38 | history.push(`/spots/${spotId}`)
39 | };
40 |
41 | return (
42 |
88 | )
89 |
90 |
91 | }
92 |
93 | export default CreateReviewForm;
--------------------------------------------------------------------------------
/frontend/src/components/CreateSpotForm/CreateSpotForm.css:
--------------------------------------------------------------------------------
1 | .form-container {
2 | border: lightgrey solid 1px;
3 | border-radius: 20px;
4 | }
5 |
6 | #createSpot-form {
7 | display: flex;
8 | align-items: center;
9 | flex-direction: column;
10 | margin-right: 150px;
11 | margin-left: 150px;
12 | /* margin-top: 100px; */
13 | width: 550px;
14 | height: 750px;
15 | border-color: rgb(221, 221, 221);
16 | box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 16px 0px;
17 | border-radius: 20px;
18 |
19 |
20 | }
21 |
22 | #create-h1-header {
23 | width: 550px;
24 | padding-left: 48px;
25 | /* border-color: rgb(221, 221, 221);
26 | box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 16px 0px; */
27 | }
28 |
29 | #create-form-styling {
30 | display: flex;
31 | justify-content: center;
32 | flex-direction: row;
33 | align-items: center;
34 | padding-top: 100px;
35 | height: 100%;
36 | border: none;
37 | }
38 |
39 |
40 | #form-styling {
41 | width: 450px;
42 | height: 100%;
43 | display: flex;
44 | flex-direction: column;
45 | /* line-height: 20px; */
46 | /* border: 1px solid lightgrey; */
47 |
48 | }
49 |
50 |
51 | .createForm-inputs-div {
52 | width: 100%;
53 | display: flex;
54 | justify-content: center;
55 | align-items: center;
56 | flex-direction: column;
57 |
58 | }
59 |
60 | .createSpot-inputs::placeholder {
61 | font-size: 16px;
62 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
63 | }
64 |
65 |
66 | select {
67 | padding-left: 6px;
68 | width: 103%;
69 | border: 1px solid lightgrey;
70 | height: 45px;
71 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
72 |
73 | }
74 |
75 | option {
76 | background-color: rgb(221, 221, 221);
77 | border: 1px solid lightgrey;
78 | }
79 |
80 | /*
81 | input {
82 | width: 496px;
83 | height: 40px;
84 | border-bottom: lightgrey .1px solid !important;
85 | border: none;
86 | border-right: none;
87 | border-left: none;
88 | border-top: none;
89 | border-bottom: .5 solid lightgrey;
90 |
91 | } */
92 |
93 | .createSpot-input {
94 | padding-left: 10px;
95 | border: 1px solid lightgrey;
96 | height: 40px;
97 | width: 100%;
98 | font-size: 16px;
99 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
100 | }
101 |
102 | .createSpot-input:focus {
103 | outline: none
104 | }
105 |
106 | select:focus {
107 | outline: none
108 | }
109 |
110 | textarea:focus {
111 | outline: none
112 | }
113 |
114 | #createName {
115 | border-top-left-radius: 15px;
116 | border-top-right-radius: 15px;
117 | }
118 |
119 | #create-description {
120 | padding-left: 10px;
121 | border: 1px solid lightgrey;
122 | border-bottom-right-radius: 15px;
123 | border-bottom-left-radius: 15px;
124 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
125 | font-size: 16px;
126 | }
127 |
128 | #number-input[type='number']::-webkit-inner-spin-button,
129 | #number-input[type='number']::-webkit-outer-spin-button {
130 | -webkit-appearance: none;
131 | }
132 |
133 | /* #createSpot-form>#create-h1-header>#form-styling {
134 | width: 500px;
135 | height: 500px;
136 | display: grid;
137 | grid-template-columns: repeat(2, 1fr);
138 | line-height: 20px;
139 | border: 1px solid red;
140 | } */
141 |
142 | #create-buttons {
143 | display: flex;
144 | justify-content: space-between;
145 | flex-direction: column;
146 | align-items: center;
147 | width: 100%;
148 | height: 90px;
149 | padding-top: 30px;
150 |
151 | }
152 |
153 | .createSpot-buttons {
154 | width: 103%;
155 | height: 40px;
156 | border-radius: 5px;
157 | border: lightgrey 1px solid;
158 | background-color: #E31C5F;
159 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
160 | font-size: 16px;
161 | font-weight: 500;
162 | box-shadow: rgba(0, 0, 0, 0.08) 0px 6px 16px 0px;
163 | color: white;
164 | }
165 |
166 | .createSpot-buttons:hover {
167 | cursor: pointer;
168 | }
169 |
170 |
171 | #error-handling {
172 | display: flex;
173 | flex-direction: column;
174 | align-items: center;
175 | height: 400px;
176 | width: 300px;
177 | border: 1px solid lightgrey;
178 | border-radius: 20px;
179 | box-shadow: rgba(0, 0, 0, 0.08) 0px 6px 16px 0px;
180 | font-weight: 300;
181 |
182 | }
183 |
184 | #form-requirements-heading {
185 | border-bottom: 1px solid lightgrey;
186 | padding-bottom: 10px;
187 | padding-left: 20px;
188 | font-weight: 400;
189 | width: 90%;
190 | }
191 |
192 | #allclear-message {
193 | font-size: 20px;
194 | font-weight: 200px;
195 | color: #00A699;
196 | }
197 |
198 | #error-list {
199 | font-size: 15px;
200 | padding-right: 25px;
201 | margin-top: -5px;
202 | }
203 |
204 | #list-error {
205 | margin-bottom: 7px;
206 | }
207 |
208 | #select-country:hover {
209 | cursor: pointer;
210 | }
--------------------------------------------------------------------------------
/frontend/src/components/CreateSpotForm/countries.js:
--------------------------------------------------------------------------------
1 | export const countries = [
2 |
3 | "United Kingdom"
4 | , "China"
5 | , "Japan"
6 | , "United States"
7 | , "Afghanistan"
8 | , "Albania"
9 | , "Algeria"
10 | , "Andorra"
11 | , "Angola"
12 | , "Argentina"
13 | , "Armenia"
14 | , "Australia"
15 | , "Austria"
16 | , "Azerbaijan"
17 | , "Bahamas"
18 | , "Bahrain"
19 | , "Bangladesh"
20 | , "Barbados"
21 | , "Belarus"
22 | , "Belgium"
23 | , "Belize"
24 | , "Benin"
25 | , "Bhutan"
26 | , "Bolivia"
27 | , "Bosnia Herzegovina"
28 | , "Botswana"
29 | , "Brazil"
30 | , "Brunei"
31 | , "Bulgaria"
32 | , "Burkina"
33 | , "Burundi"
34 | , "Cambodia"
35 | , "Cameroon"
36 | , "Canada"
37 | , "Cape Verde"
38 | , "Chile"
39 | , "China"
40 | , "Colombia"
41 | , "Congo"
42 | , "Congo {Democratic Rep}"
43 | , "Costa Rica"
44 | , "Croatia"
45 | , "Cuba"
46 | , "Cyprus"
47 | , "Czech Republic"
48 | , "Denmark"
49 | , "Djibouti"
50 | , "Dominica"
51 | , "Dominican Republic"
52 | , "East Timor"
53 | , "Ecuador"
54 | , "Egypt"
55 | , "El Salvador"
56 | , "Equatorial Guinea"
57 | , "Eritrea"
58 | , "Estonia"
59 | , "Ethiopia"
60 | , "Fiji"
61 | , "Finland"
62 | , "France"
63 | , "Gabon"
64 | , "Gambia"
65 | , "Georgia"
66 | , "Germany"
67 | , "Ghana"
68 | , "Greece"
69 | , "Grenada"
70 | , "Guatemala"
71 | , "Guinea"
72 | , "Guinea - Bissau"
73 | , "Guyana"
74 | , "Haiti"
75 | , "Honduras"
76 | , "Hungary"
77 | , "Iceland"
78 | , "India"
79 | , "Indonesia"
80 | , "Iran"
81 | , "Iraq"
82 | , "Ireland { Republic }"
83 | , "Israel"
84 | , "Italy"
85 | , "Ivory Coast"
86 | , "Jamaica"
87 | , "Japan"
88 | , "Jordan"
89 | , "Kazakhstan"
90 | , "Kenya"
91 | , "Kiribati"
92 | , "Korea North"
93 | , "Korea South"
94 | , "Kosovo"
95 | , "Kuwait"
96 | , "Kyrgyzstan"
97 | , "Laos"
98 | , "Latvia"
99 | , "Lebanon"
100 | , "Lesotho"
101 | , "Liberia"
102 | , "Libya"
103 | , "Liechtenstein"
104 | , "Lithuania"
105 | , "Luxembourg"
106 | , "Macedonia"
107 | , "Madagascar"
108 | , "Malawi"
109 | , "Malaysia"
110 | , "Maldives"
111 | , "Mali"
112 | , "Malta"
113 | , "Marshall Islands"
114 | , "Mauritania"
115 | , "Mauritius"
116 | , "Mexico"
117 | , "Micronesia"
118 | , "Moldova"
119 | , "Monaco"
120 | , "Mongolia"
121 | , "Montenegro"
122 | , "Morocco"
123 | , "Mozambique"
124 | , "Myanmar, { Burma }"
125 | , "Namibia"
126 | , "Nauru"
127 | , "Nepal"
128 | , "Netherlands"
129 | , "New Zealand"
130 | , "Nicaragua"
131 | , "Niger"
132 | , "Nigeria"
133 | , "Norway"
134 | , "Oman"
135 | , "Pakistan"
136 | , "Palau"
137 | , "Panama"
138 | , "Papua New Guinea"
139 | , "Paraguay"
140 | , "Peru"
141 | , "Philippines"
142 | , "Poland"
143 | , "Portugal"
144 | , "Qatar"
145 | , "Romania"
146 | , "Russian Federation"
147 | , "Rwanda"
148 | , "St Kitts & Nevis"
149 | , "St Lucia"
150 | , "Saint Vincent & the Grenadines"
151 | , "Samoa"
152 | , "San Marino"
153 | , "Sao Tome & Principe"
154 | , "Saudi Arabia"
155 | , "Senegal"
156 | , "Serbia"
157 | , "Seychelles"
158 | , "Sierra Leone"
159 | , "Singapore"
160 | , "Slovakia"
161 | , "Slovenia"
162 | , "Solomon Islands"
163 | , "Somalia"
164 | , "South Africa"
165 | , "South Sudan"
166 | , "Spain"
167 | , "Sri Lanka"
168 | , "Sudan"
169 | , "Suriname"
170 | , "Swaziland"
171 | , "Sweden"
172 | , "Switzerland"
173 | , "Syria"
174 | , "Taiwan"
175 | , "Tajikistan"
176 | , "Tanzania"
177 | , "Thailand"
178 | , "Togo"
179 | , "Tonga"
180 | , "Trinidad & Tobago"
181 | , "Tunisia"
182 | , "Turkey"
183 | , "Turkmenistan"
184 | , "Tuvalu"
185 | , "Uganda"
186 | , "Ukraine"
187 | , "United Arab Emirates"
188 | , "United Kingdom"
189 | , "United States"
190 | , "Uruguay"
191 | , "Uzbekistan"
192 | , "Vanuatu"
193 | , "Vatican City"
194 | , "Venezuela"
195 | , "Vietnam"
196 | , "Yemen"
197 | , "Zambia"
198 | , "Zimbabwe"
199 | ]
--------------------------------------------------------------------------------
/frontend/src/components/CreateSpotForm/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useHistory } from 'react-router-dom';
4 | import { createNewSpot } from '../../store/spots';
5 | import { addSpotImage } from '../../store/spots';
6 | import './CreateSpotForm.css'
7 | import {countries} from './countries.js'
8 |
9 |
10 | const CreateSpotForm = () => {
11 |
12 | const dispatch = useDispatch();
13 | const history = useHistory();
14 |
15 | const [address, setAddress] = useState('');
16 | const [city, setCity] = useState('');
17 | const [state, setState] = useState('');
18 | const [country, setCountry] = useState('');
19 | // const [lat, setLat] = useState('');
20 | // const [lng, setLng] = useState('');
21 | const [name, setName] = useState('');
22 | const [description, setDescription] = useState('');
23 | const [price, setPrice] = useState('');
24 | const [imgUrl, setImgUrl] = useState('');
25 | const [createSpotErrors, setCreateSpotErrors] = useState([]);
26 |
27 | useEffect(() => {
28 | const errors = [];
29 |
30 | if (!address || address.length > 25 || address.length < 5) {
31 | errors.push('Address must be greater than 5, and less than 25 characters.')
32 | };
33 | if (!city || city.length > 25 || city.length < 3) {
34 | errors.push('City must be greater than 3, and less than 25 characters.')
35 | };
36 | if (!name || name.length > 40 || name.length < 3) {
37 | errors.push('Name must be greater than 3, and less than 25 characters.')
38 | };
39 | if (!description || description.length > 250 || description.length < 5) {
40 | errors.push('Description must be greater than 5, and less than 250 characters.')
41 | };
42 | if (!price || !Number(price)) {
43 | errors.push('Price must be a number gretaer than 0.')
44 | };
45 | if (!state || state.length > 25 || state.length < 4) {
46 | errors.push('State must be greater than 4, and less than 25 characters.')
47 | }
48 | if (!country) {
49 | errors.push('Please select a country.')
50 | }
51 | if (!imgUrl.match(/\.(jpg|jpeg|png|gif)$/)) errors.push('Image url must end in .jpg .jpeg .png or .gif')
52 |
53 | setCreateSpotErrors(errors);
54 | }, [address, city, name, description, price, state, country, imgUrl]);
55 |
56 |
57 |
58 | const handleSubmit = async (e) => {
59 | e.preventDefault();
60 |
61 | const payload = {
62 | address,
63 | city,
64 | state,
65 | country,
66 | // lat,
67 | // lng,
68 | name,
69 | description,
70 | price
71 | };
72 |
73 | let newSpot = await dispatch(createNewSpot(payload));
74 | // console.log('NEw spot', newSpot)
75 |
76 | // include an image for the new spot
77 | if (newSpot) {
78 |
79 | const imgReq = ({
80 | url: imgUrl,
81 | preview: true
82 | });
83 |
84 | await dispatch(addSpotImage(imgReq, newSpot.id))
85 | history.push(`/spots/${newSpot.id}`)
86 | }
87 | };
88 |
89 | const cancelHandler = (e) => {
90 | e.preventDefault();
91 | history.push('/');
92 | };
93 |
94 | return (
95 |
96 |
246 |
247 |
248 |
249 |
250 | )
251 |
252 |
253 | }
254 |
255 |
256 | export default CreateSpotForm;
--------------------------------------------------------------------------------
/frontend/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import './footer.css';
4 |
5 | const Footer = () => {
6 |
7 | // const location = useLocation();
8 |
9 | // useEffect(() => {
10 | // if (location.pathname === '/spots/3') {
11 | // let wrap = document.getElementById('footer-wrapper')
12 | // wrap.style.setProperty("position", "absolute")
13 | // }
14 | // })
15 |
16 | return (
17 |
55 | )
56 | }
57 |
58 | export default Footer;
--------------------------------------------------------------------------------
/frontend/src/components/Footer/footer.css:
--------------------------------------------------------------------------------
1 | #footer-wrapper{
2 | width: 100vw;
3 | position: fixed;
4 | display: flex;
5 | justify-content: space-between;
6 | background-color: white;
7 | z-index: 3;
8 | color: black;
9 | bottom: 0;
10 | border-top: 1px solid lightgrey;
11 | }
12 |
13 | #right-footer-nav-container{
14 | display: flex;
15 | }
16 |
17 |
18 | .footer-containers{
19 | padding: 15px 6%;
20 | }
21 |
22 | .footer-dot{
23 | padding: 0 10px;
24 | text-align: center;
25 | }
26 |
27 | .footer-span{
28 | font-size: 14px;
29 | text-align: center;
30 | color: rgb(34, 34, 34);
31 | font-weight: 300;
32 | }
33 |
34 | .footer-span-right{
35 | padding-left: 20px;
36 | color: rgb(34, 34, 34);
37 | font-size: 14px;
38 | font-weight: 500;
39 | display: flex;
40 | align-items: center;
41 | cursor: not-allowed;
42 | }
43 |
44 | #span-with-svg{
45 | display: flex;
46 | align-items: center;
47 | column-gap: 5px;
48 | }
49 |
50 | .footer-nav{
51 | text-decoration: none;
52 | color: rgb(34, 34, 34);
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentReviews/GetCurrentReviews.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/components/GetCurrentReviews/GetCurrentReviews.css
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentReviews/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { getCurrentUserReviews } from "../../store/reviews";
4 |
5 |
6 | const CurrentUserReviews = () => {
7 | const dispatch = useDispatch();
8 |
9 | const reviews = useSelector(state => state.reviews.user);
10 | // console.log('heyyyoo ', reviews)
11 |
12 | useEffect(() => {
13 | dispatch(getCurrentUserReviews())
14 | }, [dispatch]);
15 |
16 |
17 | return (
18 |
19 | {Object.values(reviews).map(review => (
20 |
21 |
Review's name: {review.review}
22 |
Review's stars: {review.stars}
23 |
Review's id: {review.id}
24 |
25 | ))}
26 |
27 | )
28 | }
29 |
30 |
31 |
32 | export default CurrentUserReviews;
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentSpots/GetCurrentSpots.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/components/GetCurrentSpots/GetCurrentSpots.css
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentSpots/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { getCurrentUserSpots } from "../../store/spots";
4 |
5 |
6 | const CurrentUserSpots = () => {
7 | const dispatch = useDispatch();
8 |
9 | const spots = useSelector(state => state.spots.allSpots);
10 |
11 | useEffect(() => {
12 | dispatch(getCurrentUserSpots())
13 | }, [dispatch])
14 |
15 | return (
16 |
17 | {Object.values(spots).map(spot => (
18 |
19 |
Spot's name: {spot.name}
20 |
Spot Id: {spot.id}
21 |
Spot's price: {spot.price}
22 |
23 | ))}
24 |
25 | )
26 | }
27 |
28 | export default CurrentUserSpots;
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentUser/GetCurrentUser.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/components/GetCurrentUser/index.js:
--------------------------------------------------------------------------------
1 | import CurrentUserReviews from "../GetCurrentReviews";
2 | import CurrentUserSpots from "../GetCurrentSpots";
3 |
4 |
5 | const GetCurrentUser = () => {
6 |
7 |
8 | return (
9 |
10 |
YOUR REVIEWS
11 |
12 |
13 |
14 |
15 | YOUR SPOTS
16 |
17 |
18 |
19 | )
20 |
21 |
22 | }
23 |
24 | export default GetCurrentUser;
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/LoginForm.css:
--------------------------------------------------------------------------------
1 | #login-form-container {
2 | width: 400px;
3 | height: 500px;
4 | display: flex;
5 | align-items: center;
6 | flex-direction: column;
7 |
8 |
9 | }
10 |
11 | #loginForm-form {
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | }
16 |
17 | #username-div,
18 | #password-div {
19 | height: 40px;
20 | }
21 |
22 |
23 | .login-input-div {
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | width: 350px;
28 | height: 40px;
29 | box-shadow: rgba(0, 0, 0, 0.08) 0px 6px 16px 0px;
30 | }
31 |
32 | #credential-input,
33 | #credential-input:focus {
34 | padding-left: 10px;
35 | width: 100%;
36 | height: 40px;
37 | border-top-right-radius: 10px;
38 | border-top-left-radius: 10px;
39 | border: 1px solid lightgrey;
40 | outline: none;
41 | }
42 |
43 | #auth-error{
44 | padding-top: 10px;
45 | font-family: system-ui;
46 | color: #FF5A5F;
47 | font-size: 15px;
48 | }
49 |
50 |
51 | #password-input,
52 | #password-input:focus {
53 | padding-left: 10px;
54 | width: 100%;
55 | height: 40px;
56 | border-bottom-left-radius: 10px;
57 | border-bottom-right-radius: 10px;
58 | border: 1px solid lightgrey;
59 | outline: none;
60 | }
61 |
62 | #loginBtn {
63 | margin-top: 15px;
64 | width: 100%;
65 | height: 35px;
66 | box-shadow: rgba(0, 0, 0, 0.08) 0px 6px 16px 0px;
67 | background-color: #E31C5F;
68 | border-radius: 5px;
69 | border: lightgrey 1px solid;
70 | color: white;
71 | }
72 | #loginBtn:hover,
73 | #demoBtn:hover{
74 | cursor: pointer;
75 | }
76 |
77 | #demoBtn {
78 | margin-top: 7px;
79 | width: 100%;
80 | height: 35px;
81 | box-shadow: rgba(0, 0, 0, 0.08) 0px 6px 16px 0px;
82 | background-color: #E31C5F;
83 | border-radius: 5px;
84 | border: lightgrey 1px solid;
85 | color: white;
86 | }
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/LoginForm.js:
--------------------------------------------------------------------------------
1 | // frontend/src/components/LoginFormModal/LoginForm.js
2 | import React, { useState } from "react";
3 | import * as sessionActions from "../../store/session";
4 | import { useDispatch } from "react-redux";
5 | import './LoginForm.css'
6 |
7 | function LoginForm() {
8 | const dispatch = useDispatch();
9 | const [credential, setCredential] = useState("");
10 | const [password, setPassword] = useState("");
11 | const [errors, setErrors] = useState([]);
12 |
13 | const handleSubmit = (e) => {
14 | e.preventDefault();
15 | setErrors([]);
16 | return dispatch(sessionActions.login({ credential, password })).catch(
17 | async (res) => {
18 | const data = await res.json();
19 | if (data && data.message) setErrors(['Invalid username or password!']);
20 | }
21 | );
22 | };
23 |
24 | const handleDemo = (e) => {
25 | e.preventDefault();
26 |
27 | return dispatch(sessionActions.login({ credential: '9ziggy9', password:'June2022' })).catch(
28 | async (res) => {
29 | const data = await res.json();
30 | if (data && data.errors) setErrors(data.errors);
31 | }
32 | );
33 | }
34 |
35 |
36 | return (
37 |
88 | );
89 | }
90 |
91 | export default LoginForm;
--------------------------------------------------------------------------------
/frontend/src/components/LoginFormModal/index.js:
--------------------------------------------------------------------------------
1 | // frontend/src/components/LoginFormModal/index.js
2 | import React, { useState } from 'react';
3 | import { Modal } from '../../context/Modal';
4 | import LoginForm from './LoginForm';
5 |
6 | function LoginFormModal({classProp}) {
7 | const [showModal, setShowModal] = useState(false);
8 |
9 | return (
10 | <>
11 | setShowModal(true)}>Log In
12 | {showModal && (
13 | setShowModal(false)}>
14 |
15 |
16 | )}
17 | >
18 | );
19 | }
20 |
21 | export default LoginFormModal;
--------------------------------------------------------------------------------
/frontend/src/components/Maps/BigMap.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import './bigmap.css';
3 | import { GoogleMap, useJsApiLoader, OverlayView } from '@react-google-maps/api';
4 |
5 | const SpotBigMap = ({onClose, spot, map}) => {
6 | // const {description, city, state, country} = spot
7 | // const [currPosition, setCurrPosition] = useState({ lat: parseFloat(lat), lng: parseFloat(lng) })
8 |
9 | // // Not good practice, should run it in the backend but this is faster for personal project
10 | // const { isLoaded } = useJsApiLoader({
11 | // id: 'google-map-script',
12 | // googleMapsApiKey: process.env.REACT_APP_MAPS_API_KEY
13 | // });
14 |
15 | // console.log('$$$$$$$', lat, lng)
16 |
17 | // if (!lat || !lng) return null;
18 |
19 | return (
20 |
21 |
26 |
27 |
{spot.description}
28 |
{map}
29 |
30 |
31 | )
32 | }
33 |
34 | export default SpotBigMap;
--------------------------------------------------------------------------------
/frontend/src/components/Maps/SpotMap.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { GoogleMap, useJsApiLoader, OverlayView } from '@react-google-maps/api';
3 |
4 | const containerStyle = {
5 | width: '100%',
6 | height: '100%',
7 | };
8 |
9 | const center = {
10 | lat: 37.787664,
11 | lng: -122.405942,
12 | };
13 |
14 | const SpotMap = ({ spot }) => {
15 | console.log(spot, '#######')
16 | const lat = spot.lat;
17 | const lng = spot.lng;
18 |
19 | const [currPosition, setCurrPosition] = useState({ lat: lat, lng: lng })
20 |
21 | // Not good practice, should run it in the backend but this is faster for personal project
22 | const { isLoaded } = useJsApiLoader({
23 | id: 'google-map-script',
24 | googleMapsApiKey: process.env.REACT_APP_MAPS_API_KEY
25 | });
26 |
27 | console.log('$$$$$$$', spot.lat, spot.lng)
28 |
29 | if (!spot.lat || !spot.lng) return null;
30 |
31 | return (
32 | <>
33 | {isLoaded && (
34 | <>
35 |
41 |
45 |
53 |
54 |
55 | >
56 | )}
57 | >
58 | );
59 | };
60 |
61 | export default SpotMap;
--------------------------------------------------------------------------------
/frontend/src/components/Maps/bigmap.css:
--------------------------------------------------------------------------------
1 | /* #BigMap-wrapper{
2 | margin-top: 10px;
3 | background-color: white;
4 | position: fixed;
5 | padding: 30px;
6 | width: 100%;
7 | box-sizing: border-box;
8 | }
9 |
10 | #fsModal-header{
11 | width: 100%;
12 | height: 80px;
13 | border: 1px solid red;
14 | box-sizing: border-box;
15 | }
16 |
17 | #fsModal-close-div{
18 | height: 35px;
19 | width: 35px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | border: 1px solid blue;
24 | border-radius: 50%;
25 | }
26 |
27 | #fsModal-close-div:hover{
28 | background-color: rgba(113, 113, 113, .2);
29 | cursor: pointer;
30 | }
31 |
32 | #fsModal-body{
33 | display: flex;
34 | width: 100%;
35 | height: 100%;
36 | border: 1px solid purple;
37 | box-sizing: border-box;
38 | justify-content: space-between;
39 | }
40 |
41 | #fsBody-left{
42 | width: 30%;
43 | border: 3px solid gold;
44 | display: flex;
45 | flex-direction: column;
46 | }
47 |
48 | #fsBody-right{
49 | width: 70%;
50 | height: 100%;
51 | box-sizing: border-box;
52 | } */
--------------------------------------------------------------------------------
/frontend/src/components/Maps/memoizationMaps_pass.js:
--------------------------------------------------------------------------------
1 | // import { useEffect } from 'react';
2 | // import { useDispatch, useSelector } from 'react-redux';
3 |
4 | // import { getKey } from '../../store/maps';
5 | // // import Maps from './Maps';
6 |
7 | // const oldMap = ({spot}) => {
8 | // const key = useSelector((state) => state.maps.key);
9 | // const dispatch = useDispatch();
10 |
11 | // useEffect(() => {
12 | // if (!key) {
13 | // dispatch(getKey());
14 | // }
15 | // }, [dispatch, key]);
16 |
17 | // if (!key) {
18 | // return null;
19 | // }
20 |
21 | // return (
22 | // //
23 | //
24 | // );
25 | // };
26 |
27 | // export default oldMap;
--------------------------------------------------------------------------------
/frontend/src/components/Navigation/Navigation.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | align-items: center;
6 | /* border: 5px solid red; */
7 | width: 100vw;
8 | box-sizing: border-box;
9 | background-color: white;
10 | border-bottom: lightgray 1px solid;
11 | /* border: black 1px dashed; */
12 | position: fixed;
13 | /* display: none; */
14 | /* border: solid black 5px; */
15 | height: 80px;
16 | /* padding-left: 150px;
17 | padding-right: 150px; */
18 | padding-bottom: 10px;
19 | z-index: 3;
20 |
21 | }
22 |
23 | .nav-bar {
24 | height: 80px;
25 | /* width: 100%; */
26 | width: 83.5%;
27 | font-size: 16px;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: center;
31 | /* padding-left: 80px;
32 | padding-right: 80px; */
33 | position: fixed;
34 | background-color: white;
35 | grid-template-columns: repeat(5, 1fr);
36 | box-sizing: border-box;
37 | /* border: dashed blue 2px */
38 | /* border: solid red 1px; */
39 |
40 | }
41 |
42 | #avatar,
43 | #menu {
44 | width: 15px;
45 | height: 15px;
46 | }
47 |
48 | #navBar-host {
49 | top: 37px;
50 | right: 250px;
51 | position: fixed;
52 | color: #484848
53 | }
54 |
55 |
56 |
57 |
58 | .home-button-dropdown {
59 | display: flex;
60 | justify-content: space-around;
61 | align-items: center;
62 | background-color: #fff;
63 | border: 1.5px solid lightgrey;
64 | position: fixed;
65 | right: 150px;
66 | top: 20px;
67 | margin-bottom: 10px;
68 | cursor: pointer;
69 | height: 40px;
70 | width: 80px;
71 | border-radius: 9999px;
72 | }
73 |
74 | .home-dropdownMenu-visible {
75 | /* visibility: visible; */
76 | position: absolute;
77 | background-color: white;
78 | border: 1px solid lightgrey;
79 | border-radius: 15px;
80 | /* z-index: 5; */
81 | right: 0;
82 | top: 75px;
83 | width: 200px;
84 | height: 150px;
85 | display: flex;
86 | flex-direction: column;
87 | align-items: center;
88 | row-gap: 20px;
89 | padding-top: 10px;
90 |
91 | }
92 |
93 | .home-dropdownMenu-hidden {
94 | visibility: hidden !important;
95 | color: white;
96 | text-decoration: none;
97 | font-size: 100px;
98 | }
99 |
100 | .hide-login {
101 | visibility: hidden;
102 | }
103 |
104 | .modal-link {
105 | color: #767676;
106 | }
107 |
108 | .signup-navlink {
109 | color: #767676;
110 | text-decoration: none;
111 | text-decoration-color: currentColor;
112 | }
113 |
114 |
115 | .home-dropdownMenu-hidden:link {
116 | color: white;
117 | text-decoration: none;
118 | }
119 |
120 | .home-dropdownMenu-hidden:visited {
121 | color: white;
122 | text-decoration: none;
123 | }
124 |
125 | .home-dropdownMenu-visible a {
126 | visibility: visible;
127 |
128 |
129 | }
130 |
131 | .home-dropdownMenu-hidden a {
132 | visibility: hidden;
133 |
134 | }
135 |
136 | #logout:hover {
137 | background-color: #f8f8f8;
138 | cursor: pointer;
139 | }
140 |
141 |
142 | .sessionLinks-styling {
143 | border-bottom: 1px solid lightgrey;
144 | width: 90%;
145 | height: 30px;
146 | position: relative;
147 | color: #767676;
148 | padding-left: 10px;
149 | border-radius: 15px;
150 | padding-top: 7px;
151 | margin-bottom: 5px;
152 | }
153 |
154 | .dropdown-links-fix {
155 | width: 90%;
156 | height: 25px;
157 | padding-top: 10px;
158 | position: relative;
159 | border: 1px solid #F8f8f8;
160 | padding-left: 10px;
161 | border-radius: 15px;
162 | margin-top: 10px
163 | }
164 |
165 | .dropdown-links-fix:hover {
166 | background-color: #f8f8f8;
167 | cursor: pointer;
168 | }
169 |
170 | #logo {
171 | height: 55px;
172 | margin-left: -15px;
173 | }
--------------------------------------------------------------------------------
/frontend/src/components/Navigation/ProfileButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useDispatch } from 'react-redux';
3 | import * as sessionActions from '../../store/session';
4 | import './Navigation.css'
5 |
6 | function ProfileButton({ user }) {
7 | const dispatch = useDispatch();
8 | const [showMenu, setShowMenu] = useState(false);
9 |
10 | const dropdownClass = () => {
11 | if (showMenu) {
12 | return 'home-dropdownMenu-visible';
13 | } else {
14 | return 'home-dropDownMenu-hidden';
15 | }
16 | }
17 |
18 | const openMenu = () => {
19 | if (showMenu) return;
20 | setShowMenu(true);
21 | };
22 |
23 | useEffect(() => {
24 | if (!showMenu) return;
25 |
26 | const closeMenu = () => setShowMenu(false);
27 | document.addEventListener('click', closeMenu);
28 | return () => document.removeEventListener("click", closeMenu);
29 |
30 | }, [showMenu]);
31 |
32 | const logout = (e) => {
33 | e.preventDefault();
34 | dispatch(sessionActions.logout());
35 | };
36 |
37 | return (
38 |
39 |
40 |
47 | {showMenu && (
48 |
49 |
{user.username}
50 |
{user.email}
51 |
Log out
52 |
53 | )}
54 |
55 |
56 |
57 |
58 | // <>
59 | //
62 | // {showMenu && (
63 | //
64 | //
{user.username}
65 | //
{user.email}
66 | //
67 | //
68 | //
69 | //
70 | // )}
71 | // >
72 | );
73 | }
74 |
75 | export default ProfileButton;
--------------------------------------------------------------------------------
/frontend/src/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | // frontend/src/components/Navigation/index.js
2 | import React, { useEffect, useState } from 'react';
3 | import { Link, NavLink, useHistory } from 'react-router-dom';
4 | import { useSelector } from 'react-redux';
5 | import ProfileButton from './ProfileButton';
6 | import LoginFormModal from '../LoginFormModal';
7 | import './Navigation.css';
8 |
9 |
10 | function Navigation({ isLoaded }) {
11 | const sessionUser = useSelector(state => state.session.user);
12 | // const spotDetails = useSelector(state => state.spots.singleSpot);
13 | let sessionLinks;
14 | const history = useHistory();
15 | const [showMenu, setShowMenu] = useState(false);
16 |
17 | // const dropdownClass = () => {
18 | // if (showMenu) {
19 | // return 'home-dropdownMenu-visible';
20 | // } else {
21 | // return 'home-dropDownMenu-hidden';
22 | // }
23 | // }
24 | // const dropdownClass2 = showMenu ? 'home-dropdownMenu-visible' : 'home-dropDownMenu-hidden';
25 |
26 | const OCRedirect = () => {
27 | setShowMenu(false);
28 | history.push('/signup');
29 | }
30 |
31 | const openMenu = () => {
32 | if (showMenu) return;
33 | setShowMenu(true);
34 | };
35 |
36 | useEffect(() => {
37 | if (!showMenu) return;
38 | const closeMenu = () => setShowMenu(false);
39 | document.addEventListener('click', closeMenu);
40 | return () => document.removeEventListener('click', closeMenu);
41 |
42 | }, [showMenu]);
43 |
44 |
45 | if (sessionUser) {
46 | sessionLinks = (
47 | //
48 | <>
49 |
50 |
Become a host
51 | >
52 | );
53 | } else {
54 | sessionLinks = (
55 |
56 |
78 |
79 | );
80 | }
81 |
82 |
83 | return (
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {/* {isLoaded && sessionLinks} */}
93 | {isLoaded && sessionLinks}
94 |
95 |
96 |
97 | {/*
98 | Home
99 |
100 |
101 | {sessionUser && (
102 | Host Your Spot
103 | )}
104 |
*/}
105 |
106 |
107 |
108 |
109 |
110 | {/*
space
*/}
111 | {/* footer goes here */}
112 |
113 |
114 | );
115 |
116 | }
117 |
118 | export default Navigation;
--------------------------------------------------------------------------------
/frontend/src/components/SignupFormPage/SignupForm.css:
--------------------------------------------------------------------------------
1 | #signup-container {
2 | height: 100%;
3 | width: 100%;
4 | position: fixed;
5 | margin-top: 120px;
6 | display: flex;
7 | justify-content: center;
8 | /* border: green solid 2px; */
9 | }
10 | /*
11 | #signup-form {
12 | display: block;
13 | justify-content: center;
14 | margin-top: 60px;
15 | margin-left: 10px;
16 | margin-right: 10px;
17 | text-align: center;
18 | border: 2px lightgray solid;
19 | border-radius: 25px;
20 | width: 400px;
21 | height: 500px;
22 | position: relative;
23 |
24 | } */
25 |
26 | #confirm-password {
27 | border-bottom-left-radius: 10px;
28 | border-bottom-right-radius: 10px;
29 | }
30 |
31 |
32 | #signup-form {
33 | display: flex;
34 | align-items: center;
35 | flex-direction: column;
36 | margin-right: 150px;
37 | margin-left: 150px;
38 | width: 550px;
39 | height: 600px;
40 | border-color: rgb(221, 221, 221);
41 | box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 16px 0px;
42 | border-radius: 20px;
43 | }
--------------------------------------------------------------------------------
/frontend/src/components/SignupFormPage/index.js:
--------------------------------------------------------------------------------
1 | // frontend/src/components/SignupFormPage/index.js
2 | import React, { useEffect, useState } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { Redirect, useHistory } from "react-router-dom";
5 | import * as sessionActions from "../../store/session";
6 | import './SignupForm.css';
7 | import '../CreateSpotForm/CreateSpotForm.css'
8 |
9 |
10 | function SignupFormPage() {
11 | const dispatch = useDispatch();
12 | const sessionUser = useSelector((state) => state.session.user);
13 | const [firstName, setFirstName] = useState("");
14 | const [lastName, setLastName] = useState("");
15 | const [email, setEmail] = useState("");
16 | const [username, setUsername] = useState("");
17 | const [password, setPassword] = useState("");
18 | const [confirmPassword, setConfirmPassword] = useState("");
19 | const [errors, setErrors] = useState([]);
20 | const history = useHistory();
21 |
22 | useEffect(() => {
23 | const vErrors = [];
24 | if (!firstName || firstName.length < 2) {
25 | vErrors.push('First Name must be greater than 1 characters.')
26 | };
27 | if (!lastName || lastName.length < 2) {
28 | vErrors.push('Last name must be greater than 1 characters.')
29 | };
30 | if (!email || !email.includes('@') || !email.includes('.')) {
31 | vErrors.push('Email must be a valid email.')
32 | };
33 | if (!username || username.length < 5) {
34 | vErrors.push('Username must be greater than 5 characters.')
35 | };
36 | if (!password || password.length < 8) {
37 | vErrors.push('Password must be at least 8 characters.')
38 | };
39 | if (password.length !== confirmPassword.length) {
40 | vErrors.push('Password fields must match!')
41 | }
42 |
43 | setErrors(vErrors)
44 | }, [firstName, lastName, email, username, password, confirmPassword])
45 |
46 | if (sessionUser) return
;
47 |
48 | const handleSubmit = (e) => {
49 | e.preventDefault();
50 | if (password === confirmPassword) {
51 | setErrors([]);
52 | return dispatch(sessionActions.signup({ firstName, lastName, email, username, password }))
53 | .catch(async (res) => {
54 | const data = await res.json();
55 | console.log('dataata', data)
56 | if (data && data.errors) setErrors(data.errors);
57 | });
58 | }
59 | return setErrors(['Password fields must match!']);
60 | };
61 |
62 |
63 | const cancelHandler = (e) => {
64 | e.preventDefault();
65 | history.push('/');
66 | };
67 |
68 |
69 | return (
70 |
71 |
179 |
180 |
181 |
182 | // old signup
183 |
184 | //
253 | );
254 | }
255 |
256 | export default SignupFormPage;
--------------------------------------------------------------------------------
/frontend/src/components/SingleSpot/SingleSpot.css:
--------------------------------------------------------------------------------
1 | .spot-card-container {
2 | display: grid;
3 | /* grid-template-rows: repeat(5, .8fr); */
4 | height: 370px;
5 | width: 300px;
6 | /* border: 5px red dashed; */
7 | font-size: 15px;
8 | line-height: 16px;
9 | text-decoration: none;
10 | }
11 |
12 | .image-section {
13 | /* margin-top: 100px; */
14 | width: 100%;
15 | height: 287px;
16 |
17 | }
18 |
19 | #spot-img {
20 | object-fit: cover;
21 | height: 285.7px;
22 | width: 100%;
23 | /* border: solid purple 5px; */
24 | display: flex;
25 | flex-direction: row;
26 | border-radius: 5%;
27 | justify-content: center;
28 | }
29 |
30 |
31 | #singleSpot-details-container {
32 | display: flex;
33 | flex-direction: column;
34 | justify-content: space-between;
35 | margin-top: 10px;
36 | height: 80px;
37 | }
38 |
39 | .description-stars {
40 | display: flex;
41 | justify-content: space-between;
42 | }
43 |
44 | .spot-values {
45 | color: black;
46 | }
47 |
48 | #grey-spot-values{
49 | color: rgb(113, 113, 113);
50 | font-weight: 300;
51 |
52 | }
53 |
54 | #nav-link {
55 | text-decoration: none;
56 | }
57 |
58 | #location-avgRating {
59 | display: flex;
60 | justify-content: space-between;
61 | text-decoration: none;
62 | }
63 |
64 | #location {
65 | font-weight: 620;
66 | }
--------------------------------------------------------------------------------
/frontend/src/components/SingleSpot/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import './SingleSpot.css';
4 | import Skeleton from 'react-loading-skeleton';
5 | import 'react-loading-skeleton/dist/skeleton.css';
6 |
7 | const SingleSpot = ({spot}) => {
8 |
9 | // console.log('SINGLE SPOT', spot)
10 |
11 | const [isLoaded, setIsLoaded] = useState(false);
12 |
13 | setTimeout(() => { setIsLoaded(true) }, 300);
14 |
15 | if(!spot) return null;
16 |
17 | if (!isLoaded) {
18 | return (
19 | <>
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 | )
30 | } return isLoaded && (
31 | <>
32 |
33 |
34 |
35 |

38 |
39 |
40 |
41 |
42 | {`${spot.city}, ${spot.state}`}
43 |
44 |
45 | {'★ ' + spot.avgRating}
46 |
47 |
48 |
49 | {spot.name}
50 |
51 |
52 | {spot.address}
53 |
54 |
55 |
56 | ${spot.price}
57 |
58 | night
59 |
60 |
61 |
62 |
63 |
64 | >
65 |
66 | )
67 |
68 | }
69 |
70 |
71 |
72 | export default SingleSpot;
--------------------------------------------------------------------------------
/frontend/src/components/UpdateReviewForm/UpdateReviewForm.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/components/UpdateReviewForm/UpdateReviewForm.css
--------------------------------------------------------------------------------
/frontend/src/components/UpdateReviewForm/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { useDispatch } from "react-redux"
3 | import { useHistory, useParams } from "react-router-dom"
4 | import { editReview } from "../../store/reviews"
5 |
6 |
7 | const UpdateReviewForm = () => {
8 |
9 | const dispatch = useDispatch();
10 | const history = useHistory();
11 | const params = useParams();
12 | const { reviewId } = useParams();
13 |
14 | const [review, setReview] = useState('');
15 | const [stars, setStars] = useState(0);
16 |
17 | const updateReview = e => setReview(e.target.value);
18 | const updateStars = e => setStars(e.target.value);
19 |
20 |
21 | const submitHandler = async (e) => {
22 | e.preventDefault();
23 |
24 | const payload = {
25 | review,
26 | stars
27 | }
28 | let newReview = await dispatch(editReview(payload, reviewId))
29 |
30 | if (newReview) {
31 | history.push(`/spots/${reviewId}`)
32 | }
33 | }
34 |
35 |
36 | return (
37 |
66 | )
67 |
68 | }
--------------------------------------------------------------------------------
/frontend/src/components/UpdateSpot/UpdateSpot.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/src/components/UpdateSpot/UpdateSpot.css
--------------------------------------------------------------------------------
/frontend/src/components/UpdateSpot/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { useHistory, useParams } from "react-router-dom";
4 | import { editSpot, getSpotDetails } from "../../store/spots";
5 | import { countries } from "../CreateSpotForm/countries";
6 |
7 | // maybe a modal
8 | const UpdateSpotFormPage = () => {
9 | const { spotId } = useParams();
10 | const dispatch = useDispatch();
11 | const history = useHistory();
12 |
13 | const spot = useSelector(state => state.spots.singleSpot);
14 | // const spotImg = useSelector(state => state.spots.singleSpot);
15 | // console.log(spotImg, spot)
16 |
17 | const [address, setAddress] = useState(spot.address);
18 | const [city, setCity] = useState(spot.city);
19 | const [state, setState] = useState(spot.state);
20 | const [country, setCountry] = useState(spot.country);
21 | // const [lat, setLat] = useState(spot.lat);
22 | // const [lng, setLng] = useState(spot.lng);
23 | const [name, setName] = useState(spot.name);
24 | const [description, setDescription] = useState(spot.description);
25 | const [price, setPrice] = useState(spot.price);
26 | // const [imgUrl, setImgUrl] = useState(spot?.SpotImages[0]?.url);
27 | const [UpdateErrors, setUpdateErrors] = useState([]);
28 |
29 | const updateAddress = e => setAddress(e.target.value);
30 | const updateCity = e => setCity(e.target.value);
31 | const updateState = e => setState(e.target.value);
32 | const updateCountry = e => setCountry(e.target.value);
33 | // const updateLat = e => setLat(e.target.value);
34 | // const updateLng = e => setLng(e.target.value);
35 | const updateName = e => setName(e.target.value);
36 | const updateDescription = e => setDescription(e.target.value);
37 | const updatePrice = e => setPrice(e.target.value);
38 | // const updateImgUrl = e => setImgUrl(e.target.value)
39 |
40 |
41 | useEffect(() => {
42 | const errors = [];
43 |
44 | if (!address || address.length > 25 || address.length < 5) {
45 | errors.push('Address must be greater than 5, and less than 25 characters.')
46 | };
47 | if (!city || city.length > 25 || city.length < 3) {
48 | errors.push('City must be greater than 3, and less than 25 characters.')
49 | };
50 | if (!name || name.length > 40 || name.length < 3) {
51 | errors.push('Name must be greater than 3, and less than 25 characters.')
52 | };
53 | if (!description || description.length > 250 || description.length < 5) {
54 | errors.push('Description must be greater than 5, and less than 250 characters.')
55 | };
56 | if (!price || !Number(price) || price < 1) {
57 | errors.push('Price must be a number greater than 0.')
58 | };
59 | if (!state || state.length > 25 || state.length < 4) {
60 | errors.push('State must be greater than 4, and less than 25 characters.')
61 | }
62 | if (!country) {
63 | errors.push('Please select a country.')
64 | }
65 | // if (!imgUrl.match(/\.(jpg|jpeg|png|gif)$/)) errors.push('Image url must end in .jpg .jpeg .png or .gif')
66 |
67 | setUpdateErrors(errors);
68 | }, [address, city, name, description, price, state, country ]);
69 |
70 |
71 | useEffect(() => {
72 | dispatch(getSpotDetails(spotId))
73 | }, [dispatch, spotId])
74 |
75 | if (!Object.keys(spot).length) return null;
76 |
77 | const handleSubmit = async (e) => {
78 | e.preventDefault();
79 | // e.stopPropagation();
80 |
81 | const payload = {
82 | address,
83 | city,
84 | state,
85 | country,
86 | // lat,
87 | // lng,
88 | name,
89 | description,
90 | price
91 | };
92 |
93 | let updatedSpot = await dispatch(editSpot(payload, spotId));
94 | console.log('edited spot', updatedSpot)
95 |
96 | if (!Object.keys(spot).length) return null;
97 |
98 | if (updatedSpot) {
99 | // const imgReq = ({
100 | // url: imgUrl,
101 | // preview: true
102 | // });
103 | // if (!Object.keys(spot).length) {
104 | // console.log('FUCK ME')
105 | // return null;
106 | // }
107 | // await dispatch(addSpotImage(imgReq, updatedSpot.id));
108 | history.push(`/spots/${updatedSpot.id}`);
109 | }
110 |
111 | };
112 |
113 | const cancelHandler = (e) => {
114 | e.preventDefault();
115 | history.push(`/spots/${spotId}`);
116 | };
117 |
118 | return (
119 |
120 |
272 |
273 |
274 |
275 |
276 |
277 | )
278 |
279 | }
280 |
281 |
282 | export default UpdateSpotFormPage;
--------------------------------------------------------------------------------
/frontend/src/context/Modal.css:
--------------------------------------------------------------------------------
1 | /* frontend/src/context/Modal.css */
2 | #modal {
3 | margin-top: -80px;
4 | position: fixed;
5 | top: 0;
6 | right: 0;
7 | left: 0;
8 | bottom: 0;
9 | padding: 40px 40px 40px 40px;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | /* animation-name: bounce; */
14 | /* animation-duration: .1s; */
15 | /* border: red 5px; */
16 | width: 100%;
17 | height: 100%;
18 | }
19 |
20 | /* @keyframes bounce {
21 | 0% {
22 | top: 300px
23 | }
24 | } */
25 |
26 | #top-modal {
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | flex-direction: row;
31 | width: 350px;
32 | height: 40px;
33 | box-sizing: border-box;
34 | /* position: 'fixed'; */
35 | /* overflow: 'hidden';
36 | inset: '-2066.25px 0px 0px';
37 | inset-inline-end: '19px';
38 | --scrollbar-gutter: '19px' */
39 | }
40 |
41 | /* #modal-container {
42 | width: 100%;
43 | height: 100%;
44 | border: red 10px;
45 | } */
46 |
47 | #modal-background {
48 | position: fixed;
49 | top: 0;
50 | right: 0;
51 | left: 0;
52 | bottom: 0;
53 | background-color: rgba(0, 0, 0, 0.7);
54 |
55 | }
56 |
57 |
58 | #loginForm-styling{
59 | display: flex;
60 | justify-content: center;
61 | flex-direction: row;
62 | width: 400px;
63 | height: 400px;
64 | /* border: 1px solid red; */
65 | }
66 |
67 |
68 | #loginSignup {
69 | font-size: 18px;
70 | width: 350px;
71 | height: 30px;
72 | display: block;
73 | text-align: center;
74 | border-bottom: 1px solid lightgrey;
75 | /* justify-content: space-around; */
76 | /* align-items: center; */
77 | /* margin-left: 75px;
78 | margin-right: 75px; */
79 | }
80 |
81 | #modal-content {
82 | position: fixed;
83 | background-color: white;
84 | width: 400px;
85 | height: 500px;
86 | border-radius: 20px;
87 | padding-bottom: -200px;
88 | /* padding: 20px; */
89 | display: flex;
90 | flex-direction: column;
91 | justify-content: center;
92 | align-items: center;
93 | }
94 |
95 | #close-modal {
96 | height: 16px;
97 | width: 16px;
98 | }
99 |
100 | #close-modal:hover {
101 | cursor: pointer
102 | }
103 |
104 | #fullscreen-modal-wrapper{
105 | position: fixed;
106 | /* top: 0;
107 | right: 0;
108 | bottom: 0;
109 | left: 0; */
110 | inset: 0;
111 | z-index: 5;
112 | width: 100vw;
113 | height: 100vh;
114 | box-sizing: border-box;
115 | background-color: white;
116 | /* border: 5px solid green; */
117 | overflow-y: hidden;
118 | /* z-index: 10;
119 | position: fixed; */
120 | /* display: flex;
121 | justify-content: center;
122 | align-items: center; */
123 |
124 | }
125 |
126 | .slide-up-now {
127 | opacity: 0;
128 | animation: slide-up 1s cubic-bezier(0.4, 0, 0.2, 1) 500ms forwards;
129 | }
130 |
131 | @keyframes slide-up {
132 | 0% {
133 | -webkit-transform: translateY(100%);
134 | opacity: 1
135 | }
136 |
137 | 100% {
138 | -webkit-transform: translateY(0);
139 | opacity: 1
140 | }
141 | }
--------------------------------------------------------------------------------
/frontend/src/context/Modal.js:
--------------------------------------------------------------------------------
1 | // frontend/src/context/Modal.js
2 | import React, { useContext, useRef, useState, useEffect } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import './Modal.css';
5 |
6 | const ModalContext = React.createContext();
7 |
8 | export function ModalProvider({ children }) {
9 | const modalRef = useRef();
10 | const [value, setValue] = useState();
11 |
12 | useEffect(() => {
13 | setValue(modalRef.current);
14 | }, [])
15 |
16 | return (
17 | <>
18 |
19 | {children}
20 |
21 |
22 | >
23 | );
24 | }
25 |
26 | export function Modal({ onClose, children, size }) {
27 | const modalNode = useContext(ModalContext);
28 | if (!modalNode) return null;
29 |
30 | if(size) {
31 | return ReactDOM.createPortal(
32 |
33 | {children}
34 |
,
35 | modalNode
36 | )
37 | } else {
38 | return ReactDOM.createPortal(
39 |
40 |
41 |
42 |
43 |
44 |
45 |

47 |
48 |
Login or Sign Up
49 |
50 |
51 |
52 |
53 | {children}
54 |
55 |
56 |
57 |
,
58 | modalNode
59 | );
60 | }
61 |
62 | }
63 |
64 | // export function FullscreenModal({onClose, children}) {
65 | // let bigModalNode = useContext(ModalContext);
66 | // if (!bigModalNode) return null;
67 |
68 |
69 | // )
70 | // }
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | /* TODO Add site wide styles */
2 |
3 | html,
4 | body {
5 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
6 | margin-left: 0px;
7 | margin-right: 0px;
8 | margin-top: 0px;
9 | max-width: 100vw;
10 | box-sizing: border-box;
11 | }
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | // frontend/src/index.js
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 | import "./index.css";
5 | import { Provider } from "react-redux";
6 | import { BrowserRouter } from "react-router-dom";
7 | import App from "./App";
8 | import { ModalProvider } from "./context/Modal";
9 |
10 | import configureStore from "./store";
11 | import { restoreCSRF, csrfFetch } from "./store/csrf";
12 | import * as sessionActions from "./store/session";
13 |
14 | const store = configureStore();
15 |
16 | if (process.env.NODE_ENV !== "production") {
17 | restoreCSRF();
18 |
19 | window.csrfFetch = csrfFetch;
20 | window.store = store;
21 | window.sessionActions = sessionActions;
22 | };
23 |
24 |
25 | function Root() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | ReactDOM.render(
38 |
39 |
40 | ,
41 | document.getElementById('root')
42 | );
--------------------------------------------------------------------------------
/frontend/src/store/bookings.js:
--------------------------------------------------------------------------------
1 | import { csrfFetch } from "./csrf";
2 |
3 | const NEW_BOOKING = "bookings/NEW_BOOKING";
4 | const SPOT_BOOKINGS = "bookings/SPOT_BOOKINGS";
5 |
6 | const createBooking = (booking) => ({
7 | type: NEW_BOOKING,
8 | booking
9 | })
10 |
11 | const getSpotBookings = (bookings) => ({
12 | type: SPOT_BOOKINGS,
13 | bookings
14 | })
15 |
16 | // CREATE A BOOKING
17 | export const createBookingThunk = (booking) => async dispatch => {
18 | // console.log('new booking thunk hit', booking)
19 | const res = await csrfFetch(`/api/spots/${booking.spotId}/bookings`, {
20 | method: 'POST',
21 | headers: { 'Content-Type': 'application/json' },
22 | body: JSON.stringify(booking)
23 | });
24 | if (res.ok) {
25 | const newBooking = await res.json();
26 | dispatch(createBooking(newBooking));
27 | // console.log('NEW BOOKING', newBooking);
28 | return newBooking;
29 | }
30 | return res;
31 | }
32 |
33 | export const getAllBookingsThunk = (spotId) => async dispatch => {
34 | const res = await fetch(`/api/spots/${spotId}/bookings`);
35 | if (res.ok) {
36 | const bookings = await res.json();
37 | // console.log('bookings from the thunk', bookings);
38 | dispatch(getSpotBookings(bookings));
39 |
40 | return bookings;
41 | }
42 | return res.errors
43 | }
44 |
45 |
46 | const initialState = {
47 | allBookings: {},
48 | spotBookings: {}
49 | };
50 |
51 | const bookingsReducer = (state = initialState, action) => {
52 | let newState;
53 | switch (action.type) {
54 | case NEW_BOOKING:
55 | newState = {...state}
56 | newState.spotBookings = {...state.spotBookings,
57 | [action.booking.spotId]: action.booking
58 | }
59 | return newState
60 | case SPOT_BOOKINGS:
61 | newState = {...state}
62 | // console.log('spot bookings reducer', action)
63 | newState.spotBookings = action.bookings.Bookings
64 | return newState
65 | default:
66 | return state
67 | }
68 | }
69 |
70 | export default bookingsReducer;
--------------------------------------------------------------------------------
/frontend/src/store/csrf.js:
--------------------------------------------------------------------------------
1 | // frontend/src/store/csrf.js
2 | import Cookies from 'js-cookie';
3 |
4 | export async function csrfFetch(url, options = {}) {
5 | // set options.method to 'GET' if there is no method
6 | options.method = options.method || 'GET';
7 | // set options.headers to an empty object if there is no headers
8 | options.headers = options.headers || {};
9 |
10 | // if the options.method is not 'GET', then set the "Content-Type" header to
11 | // "application/json", and set the "XSRF-TOKEN" header to the value of the
12 | // "XSRF-TOKEN" cookie
13 | if (options.method.toUpperCase() !== 'GET') {
14 | options.headers['Content-Type'] =
15 | options.headers['Content-Type'] || 'application/json';
16 | options.headers['XSRF-Token'] = Cookies.get('XSRF-TOKEN');
17 | }
18 | // call the default window's fetch with the url and the options passed in
19 | const res = await window.fetch(url, options);
20 |
21 | // if the response status code is 400 or above, then throw an error with the
22 | // error being the response
23 | if (res.status >= 400) throw res;
24 |
25 | // if the response status code is under 400, then return the response to the
26 | // next promise chain
27 | return res;
28 | };
29 |
30 | // call this to get the "XSRF-TOKEN" cookie, should only be used in development
31 | export function restoreCSRF() {
32 | return csrfFetch('/api/csrf/restore');
33 | }
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from "redux";
2 | import thunk from "redux-thunk";
3 | // import {sessionReducer} from './session'
4 | import sessionReducer from "./session";
5 | import spotsReducer from "./spots";
6 | import reviewsReducer from "./reviews";
7 | import bookingsReducer from "./bookings"
8 | import mapsReducer from "./maps";
9 |
10 |
11 | const rootReducer = combineReducers({
12 | session: sessionReducer,
13 | spots: spotsReducer,
14 | reviews: reviewsReducer,
15 | bookings: bookingsReducer,
16 | maps: mapsReducer
17 | });
18 |
19 | let enhancer;
20 |
21 | if (process.env.NODE_ENV === "production") {
22 | enhancer = applyMiddleware(thunk);
23 | } else {
24 | const logger = require("redux-logger").default;
25 | const composeEnhancers =
26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
27 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
28 | }
29 |
30 | const configureStore = (preloadedState) => {
31 | return createStore(rootReducer, preloadedState, enhancer);
32 | };
33 |
34 | export default configureStore;
35 |
--------------------------------------------------------------------------------
/frontend/src/store/maps.js:
--------------------------------------------------------------------------------
1 | // frontend/src/store/maps.js
2 | import { csrfFetch } from './csrf';
3 |
4 | const LOAD_API_KEY = 'maps/LOAD_API_KEY';
5 |
6 | const loadApiKey = (key) => ({
7 | type: LOAD_API_KEY,
8 | payload: key,
9 | });
10 |
11 | export const getKey = () => async (dispatch) => {
12 | const res = await csrfFetch('/api/maps/key', {
13 | method: 'POST',
14 | });
15 | const data = await res.json();
16 | dispatch(loadApiKey(data.googleMapsAPIKey));
17 | };
18 |
19 | const initialState = { key: null };
20 |
21 | const mapsReducer = (state = initialState, action) => {
22 | switch (action.type) {
23 | case LOAD_API_KEY:
24 | return { key: action.payload };
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | export default mapsReducer;
--------------------------------------------------------------------------------
/frontend/src/store/reviews.js:
--------------------------------------------------------------------------------
1 | import { csrfFetch } from "./csrf";
2 |
3 | export const LOAD_REVIEWS = 'reviews/LOAD_REVIEWS';
4 | export const CREATE_REVIEW = 'reviews/CREATE_REVIEW';
5 | export const EDIT_REVIEW = 'reviews/EDIT_REVIEW';
6 | export const DELETE_REVIEW = 'reviews/DELETE_REVIEW';
7 | export const LOAD_CURRENT = 'reviews/LOAD_CURRENT';
8 |
9 |
10 | const load = (reviews) => ({
11 | type: LOAD_REVIEWS,
12 | reviews,
13 | });
14 |
15 | const add = (review, spotId) => ({
16 | type: CREATE_REVIEW,
17 | review,
18 | spotId
19 | });
20 |
21 | const update = (review, reviewId) => ({
22 | type: EDIT_REVIEW,
23 | review,
24 | reviewId
25 |
26 | });
27 |
28 | const remove = (reviewId) => ({
29 | type: DELETE_REVIEW,
30 | reviewId
31 | });
32 |
33 | const current = (reviews) => ({
34 | type: LOAD_CURRENT,
35 | reviews
36 | })
37 |
38 |
39 |
40 | //GET CURRENT USER'S INFORMATION (ALL SPOTS and REVIEWS)
41 | export const getCurrentUserReviews = () => async dispatch => {
42 | const res = await csrfFetch('/api/reviews/current');
43 | // console.log('RESSERSERSR', res)
44 |
45 | if (res.ok) {
46 | const data = await res.json();
47 | // console.log('current user thunk hitting: ', data)
48 | dispatch(current(data));
49 | return data;
50 | }
51 | }
52 |
53 | // DELETE REVIEW
54 | export const deleteReview = (reviewId) => async dispatch => {
55 | const res = await csrfFetch(`/api/reviews/${reviewId}`, {
56 | method: 'DELETE'
57 | })
58 | // console.log('DELETE REVIEW THUNK: ', reviewId)
59 |
60 | if (res.ok) {
61 | dispatch(remove(reviewId));
62 | return null;
63 | }
64 | }
65 |
66 | // EDIT A REVIEW
67 | export const editReview = (review, reviewId) => async dispatch => {
68 | const res = await csrfFetch(`/api/reviews/${reviewId}`, {
69 | method: 'PUT',
70 | headers: { 'Content-Type': 'application/json' },
71 | body: JSON.stringify(review)
72 | });
73 |
74 | if (res.ok) {
75 | const updatedReview = await res.json();
76 | dispatch(update(updatedReview, reviewId));
77 | return updatedReview;
78 | }
79 | };
80 |
81 |
82 | // CREATE A REVIEW FOR A SPOT BY SPOT ID
83 | export const createReview = (review, spotId) => async dispatch => {
84 | const res = await csrfFetch(`/api/spots/${spotId}/reviews`, {
85 | method: 'POST',
86 | headers: { 'Content-Type': 'application/json' },
87 | body: JSON.stringify(review)
88 | });
89 |
90 | if (res.ok) {
91 | const newReview = await res.json();
92 | // console.log('NEW REVIEW THUNKER HITING: ', newReview);
93 | dispatch(add(newReview));
94 | return newReview;
95 | }
96 | }
97 |
98 |
99 | // LOAD REVIEWS BY SPOT ID
100 | export const getSpotReviews = (spotId) => async dispatch => {
101 | const res = await fetch(`/api/spots/${spotId}/reviews`)
102 |
103 | if (res.ok) {
104 | const reviews = await res.json();
105 | // console.log('REVIEWS THUNK', reviews);
106 | dispatch(load(reviews));
107 | return reviews;
108 | }
109 | }
110 |
111 |
112 | const initialState = {
113 | spot: {},
114 | user: {}
115 | }
116 |
117 | const reviewsReducer = (state = initialState, action) => {
118 |
119 | let newState;
120 | switch (action.type) {
121 | case LOAD_REVIEWS:
122 | const spotReviews = {};
123 | action.reviews.Reviews.forEach(review => {
124 | spotReviews[review.id] = review;
125 | });
126 | return {
127 | ...state,
128 | spot: spotReviews
129 | }
130 | case LOAD_CURRENT:
131 | const currentReviews = { ...state };
132 | action.reviews.Reviews.forEach(review => {
133 | currentReviews[review.id] = review;
134 | })
135 | return {user: currentReviews}
136 | case CREATE_REVIEW:
137 | newState = {
138 | ...state,
139 | spot: {
140 | [action.review.id]: action.review
141 | }
142 | }
143 | return {
144 | ...state, newState
145 | }
146 | // case EDIT_REVIEW:
147 | // newState = {...state};
148 | // newState.spot = action.review;
149 | // return {newState}
150 | case DELETE_REVIEW:
151 | newState = { ...state, spot:{...state.spot} };
152 | delete newState.spot[action.reviewId];
153 | return newState;
154 | default:
155 | return state
156 |
157 | }
158 | }
159 |
160 | export default reviewsReducer;
--------------------------------------------------------------------------------
/frontend/src/store/session.js:
--------------------------------------------------------------------------------
1 | // this file will contain all the actions specific to the session user's
2 | // information and the session user's Redux Reducer
3 |
4 | // frontend/src/store/session.js
5 | import { csrfFetch } from './csrf';
6 |
7 | const SET_USER = 'session/setUser';
8 | const REMOVE_USER = 'session/removeUser';
9 |
10 | const setUser = (user) => {
11 | return {
12 | type: SET_USER,
13 | payload: user,
14 | };
15 | };
16 |
17 | const removeUser = () => {
18 | return {
19 | type: REMOVE_USER,
20 | };
21 | };
22 |
23 |
24 | export const login = (user) => async (dispatch) => {
25 | const { credential, password } = user;
26 | const response = await csrfFetch('/api/session', {
27 | method: 'POST',
28 | body: JSON.stringify({
29 | credential,
30 | password,
31 | }),
32 | });
33 | const data = await response.json();
34 | dispatch(setUser(data));
35 | return response;
36 | };
37 |
38 | export const logout = (user) => async (dispatch) => {
39 | const response = await csrfFetch('/api/session', {
40 | method: 'DELETE',
41 | });
42 | dispatch(removeUser());
43 | return response;
44 | };
45 |
46 |
47 | export const restoreUser = () => async dispatch => {
48 | const response = await csrfFetch('/api/session');
49 | const data = await response.json();
50 | dispatch(setUser(data));
51 | return response;
52 | };
53 |
54 | export const signup = (user) => async (dispatch) => {
55 | const { username, email, password, firstName, lastName } = user;
56 | const response = await csrfFetch("/api/users", {
57 | method: "POST",
58 | body: JSON.stringify({
59 | firstName,
60 | lastName,
61 | username,
62 | email,
63 | password,
64 | }),
65 | });
66 | const data = await response.json();
67 | dispatch(setUser(data));
68 | return data;
69 | };
70 |
71 |
72 | const initialState = { user: null };
73 |
74 | const sessionReducer = (state = initialState, action) => {
75 | let newState;
76 | switch (action.type) {
77 | case SET_USER:
78 | newState = Object.assign({}, state);
79 | newState.user = action.payload;
80 | return newState;
81 | case REMOVE_USER:
82 | newState = Object.assign({}, state);
83 | newState.user = null;
84 | return newState;
85 | default:
86 | return state;
87 | }
88 | };
89 |
90 | export default sessionReducer;
--------------------------------------------------------------------------------
/frontend/src/store/spots.js:
--------------------------------------------------------------------------------
1 | import { csrfFetch } from "./csrf";
2 | const ADD_ONE = 'spots/ADD_ONE';
3 | const LOAD_SPOTS = 'spots/LOAD_SPOTS';
4 | const UPDATE_ONE = 'spots/UPDATE_ONE';
5 | const DELETE_ONE = 'spots/DELETE_ONE';
6 | const LOAD_ONE = 'spots/LOAD_ONE';
7 | const LOAD_CURRENT = 'spots/LOAD_CURRENT';
8 | const ADD_IMAGE = 'spots/ADD_IMAGE';
9 | const CLEAR_SPOT = 'spots/CLEAR_SPOT';
10 |
11 |
12 | export const clearSpot = () => ({
13 | type: CLEAR_SPOT
14 | })
15 |
16 | const load = (spots) => ({
17 | type: LOAD_SPOTS,
18 | spots
19 |
20 | });
21 |
22 | const loadOne = (spotDetails, spotId) => ({
23 | type: LOAD_ONE,
24 | spotDetails,
25 | spotId
26 | });
27 |
28 | const createSpot = (spot) => ({
29 | type: ADD_ONE,
30 | spot
31 | });
32 |
33 | const updateSpot = (spot) => ({
34 | type: UPDATE_ONE,
35 | spot
36 | });
37 |
38 | const deleteOne = (spotId) => ({
39 | type: DELETE_ONE,
40 | spotId
41 | });
42 |
43 | const current = (spots) => ({
44 | type: LOAD_CURRENT,
45 | spots
46 | });
47 |
48 | const addImage = (spotId) => ({
49 | type: ADD_IMAGE,
50 | spotId
51 | });
52 |
53 |
54 |
55 | // add an image to a spot
56 | export const addSpotImage = (imgUrl, spotId) => async dispatch => {
57 | const res = await csrfFetch(`/api/spots/${spotId}/images`, {
58 | method: 'POST',
59 | headers: { 'Content-Type': 'application/json' },
60 | body: JSON.stringify(imgUrl)
61 | });
62 |
63 | if (res.ok) {
64 | const image = await res.json();
65 | // console.log('add Image thunk hitting:', image);
66 | dispatch(addImage(image));
67 | return image
68 | };
69 |
70 | };
71 |
72 |
73 | //GET CURRENT USER'S INFORMATION (ALL SPOTS)
74 | export const getCurrentUserSpots = () => async dispatch => {
75 | const res = await csrfFetch('/api/spots/current')
76 |
77 | if (res.ok) {
78 | const data = await res.json();
79 | // console.log('current user SPOTS thunk hitting: ', data)
80 | dispatch(current(data));
81 | return data;
82 | }
83 | };
84 |
85 |
86 | // READ ALL SPOTS
87 | export const getAllSpots = () => async dispatch => {
88 | const res = await fetch('/api/spots');
89 |
90 | if (res.ok) {
91 | const spots = await res.json();
92 | // console.log('all spots THUNK', spots)
93 | const getAllSpotsThunk = dispatch(load(spots));
94 | // console.log('get all spots thunk dispatch', getAllSpotsThunk)
95 | return spots;
96 | }
97 | };
98 |
99 | // READ ONE SPOT
100 | export const getSpotDetails = (spotId) => async dispatch => {
101 | const res = await fetch(`/api/spots/${spotId}`)
102 |
103 | if (res.ok) {
104 | const spotDetails = await res.json();
105 | // console.log('spots THUNK: ', spotDetails)
106 | dispatch(loadOne(spotDetails))
107 | return spotDetails
108 | }
109 | };
110 |
111 | // CREATE NEW SPOT
112 | export const createNewSpot = (spot) => async dispatch => {
113 | const res = await csrfFetch('/api/spots', {
114 | method: 'POST',
115 | headers: { 'Content-Type': 'application/json' },
116 | body: JSON.stringify(spot)
117 | });
118 |
119 | if (res.ok) {
120 | const createdSpot = await res.json();
121 | // console.log('CREATED SPOT THUNK:', createdSpot)
122 | dispatch(createSpot(createdSpot));
123 | return createdSpot;
124 | }
125 | };
126 |
127 | // UPDATE SPOT
128 | export const editSpot = (spot, id) => async dispatch => {
129 | const res = await csrfFetch(`/api/spots/${id}`, {
130 | method: 'PUT',
131 | headers: { 'Content-Type': 'application/json' },
132 | body: JSON.stringify(spot)
133 | });
134 |
135 | if (res.ok) {
136 | const updatedSpot = await res.json();
137 | // console.log('UPDATED SPOTaroooo', updatedSpot);
138 | dispatch(updateSpot(updatedSpot));
139 | return updatedSpot;
140 | }
141 | };
142 |
143 | // DELETE SPOT
144 | export const deleteSpot = (spotId) => async dispatch => {
145 | const res = await csrfFetch(`/api/spots/${spotId}`, {
146 | method: 'DELETE'
147 | })
148 | if (res.ok) {
149 | dispatch(deleteOne(spotId));
150 | // console.log('Delete thunk is hitting', spotId)
151 | return null;
152 | }
153 | }
154 |
155 |
156 | const initialState = {
157 | allSpots: {},
158 | singleSpot: {}
159 | };
160 |
161 | const spotsReducer = (state = initialState, action) => {
162 | let newState;
163 | switch (action.type) {
164 | case LOAD_CURRENT:
165 | case LOAD_SPOTS:
166 | newState = {...state}
167 | const allSpots = {};
168 | // console.log('spots ReDUCER ACTION', action)
169 | action.spots.Spots.forEach(spot => {
170 | allSpots[spot.id] = spot;
171 | });
172 | // console.log('ALL SPOTS REDUCED', allSpots)
173 | newState.allSpots = allSpots
174 | return newState
175 | case LOAD_ONE:
176 | let singleSpot = { ...state };
177 | singleSpot = { ...action.spotDetails }
178 | // console.log('LOAD ONE', singleSpot)
179 | return {
180 | ...state,
181 | singleSpot: { ...singleSpot }
182 | }
183 |
184 | // spotDetails[spotId] = action.spotDetails
185 | case ADD_ONE:
186 | newState = { ...state }
187 | const newSpot = action.spot;
188 | newState = {
189 | singleSpot: {
190 | [action.spot.id]: newSpot
191 | }
192 | }
193 | // console.log('NEW SPOT THUNKAROO', newState)
194 | return newState;
195 | case ADD_IMAGE:
196 | newState = { ...state }
197 | newState.singleSpot.SpotImages = [action.spotId.url]
198 | return {
199 | ...newState
200 | }
201 | case UPDATE_ONE:
202 | newState = { ...state }
203 | newState.singleSpot = action.spot;
204 | newState.singleSpot.SpotImages = [...state.singleSpot.SpotImages]
205 | return newState;
206 | case DELETE_ONE:
207 | newState = { ...state };
208 | delete newState[action.spotId];
209 | return {
210 | newState,
211 | singleSpot: {}
212 | }
213 | case CLEAR_SPOT:
214 | newState = {...state};
215 | newState.singleSpot = {}
216 | return newState
217 | default:
218 | return state
219 | }
220 |
221 | };
222 |
223 | export default spotsReducer;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "date-fns": "^2.29.3",
4 | "pg": "^8.8.0",
5 | "react": "^18.2.0",
6 | "react-date-range": "^1.4.0"
7 | },
8 | "name": "api-project",
9 | "description": "api project starting on w12d3 # ``",
10 | "version": "1.0.0",
11 | "main": "index.js",
12 | "scripts": {
13 | "heroku-postbuild": "npm run build --prefix frontend",
14 | "install": "npm --prefix backend install backend && npm --prefix frontend install frontend",
15 | "dev:backend": "npm install --prefix backend start",
16 | "sequelize": "npm run --prefix backend sequelize",
17 | "sequelize-cli": "npm run --prefix backend sequelize-cli",
18 | "start": "npm start --prefix backend"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/Samsuhhh/API-Project.git"
23 | },
24 | "keywords": [],
25 | "author": "",
26 | "license": "ISC",
27 | "bugs": {
28 | "url": "https://github.com/Samsuhhh/API-Project/issues"
29 | },
30 | "homepage": "https://github.com/Samsuhhh/API-Project#readme"
31 | }
32 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | function encrypt(message, shift) {
2 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
3 | const numbers = "1234567890"
4 |
5 | let ans = ""
6 |
7 | for (let char of message) {
8 | if (char == "-") {
9 |
10 | } else if (numbers.includes(char)) {
11 | shift += parseInt(char)
12 | ans += char
13 | } else if (alphabet.includes(char)) {
14 | ans += alphabet[(alphabet.indexOf(char) + shift) % 26]
15 | } else {
16 | ans += char
17 | }
18 | }
19 |
20 | return ans;
21 | }
22 |
23 |
24 | function encrypt2(message, shift) {
25 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
26 | const numbers = "1234567890"
27 |
28 | let ans = ""
29 |
30 | for (let char of message) {
31 | if (numbers.includes(char)) {
32 | shift += parseInt(char)
33 | ans += char
34 | } else if (alphabet.includes(char)) {
35 | ans += alphabet[(alphabet.indexOf(char) + shift) % 26]
36 | } else {
37 | ans += char
38 | }
39 | }
40 | }
41 |
42 | function encrypt3(message, shift) {
43 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
44 | const numbers = "0123456789-"
45 |
46 | let ans = ""
47 |
48 | for (let char of message) {
49 | if (numbers.includes(char)) {
50 | shift += parseInt(char)
51 | ans += char
52 | } else if (alphabet.includes(char)) {
53 | ans += alphabet[(alphabet.indexOf(char) + shift + 26) % 26]
54 | } else {
55 | ans += char
56 | }
57 | }
58 |
59 | return ans
60 | }
61 |
62 | function encrypt4(message, shift) {
63 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
64 | const numbers = "0123456789-"
65 |
66 | let ans = ""
67 | let number = ""
68 |
69 | for (let char of message) {
70 | if (numbers.includes(char)) {
71 | number += char
72 | } else if (alphabet.includes(char)) {
73 | shift += parseInt(number) || 0
74 | number = ""
75 | ans += alphabet[(alphabet.indexOf(char) + shift + 26) % 26]
76 | } else {
77 | number = ""
78 | ans += char
79 | }
80 | }
81 |
82 | return ans
83 | }
84 |
85 | function encrypt5(message, shift) {
86 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
87 | const numbers = "0123456789-"
88 |
89 | let ans = ""
90 | let number = ""
91 |
92 | for (let char of message) {
93 | if (numbers.includes(char)) {
94 | number += char
95 | } else if (alphabet.includes(char)) {
96 | shift += parseInt(number) || 0
97 | number = ""
98 | ans += alphabet[(alphabet.indexOf(char) + shift + 26) % 26]
99 | } else {
100 | number = ""
101 | ans += char
102 | }
103 | }
104 |
105 | return ans
106 | }
107 |
108 | function encrypt6(message, shift) {
109 | const alphabet = "abcdefghijklmnopqrstuvwxyz"
110 | const numbers = "0123456789-"
111 | let ans = ""
112 | let number = ""
113 |
114 | for (let char of message) {
115 | if (numbers.includes(char)) {
116 | number += char
117 | ans += char
118 | } else if (alphabet.includes(char)) {
119 | shift += parseInt(number) || 0
120 | number = ""
121 | ans += alphabet[(alphabet.indexOf(char) + shift + 26) % 26]
122 | } else {
123 | number = ""
124 | ans += char
125 | }
126 | }
127 |
128 | return ans
129 | }
130 | console.log(encrypt6("he12l9lo wo-1rld", 7))
131 |
--------------------------------------------------------------------------------