Initial commit 🚀

This commit is contained in:
Jonas Schmedtmann
2019-06-13 15:43:15 +01:00
commit 7f81af0ddf
1052 changed files with 2123177 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
{
"extends": ["airbnb", "prettier", "plugin:node/recommended"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"spaced-comment": "off",
"no-console": "warn",
"consistent-return": "off",
"func-names": "off",
"object-shorthand": "off",
"no-process-exit": "off",
"no-param-reassign": "off",
"no-return-await": "off",
"no-underscore-dangle": "off",
"class-methods-use-this": "off",
"prefer-destructuring": ["error", { "object": true, "array": false }],
"no-unused-vars": ["error", { "argsIgnorePattern": "req|res|next|val" }]
}
}

View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@@ -0,0 +1,116 @@
const path = require('path');
const express = require('express');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const compression = require('compression');
const cors = require('cors');
const AppError = require('./utils/appError');
const globalErrorHandler = require('./controllers/errorController');
const tourRouter = require('./routes/tourRoutes');
const userRouter = require('./routes/userRoutes');
const reviewRouter = require('./routes/reviewRoutes');
const bookingRouter = require('./routes/bookingRoutes');
const bookingController = require('./controllers/bookingController');
const viewRouter = require('./routes/viewRoutes');
// Start express app
const app = express();
app.enable('trust proxy');
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
// 1) GLOBAL MIDDLEWARES
// Implement CORS
app.use(cors());
// Access-Control-Allow-Origin *
// api.natours.com, front-end natours.com
// app.use(cors({
// origin: 'https://www.natours.com'
// }))
app.options('*', cors());
// app.options('/api/v1/tours/:id', cors());
// Serving static files
app.use(express.static(path.join(__dirname, 'public')));
// Set security HTTP headers
app.use(helmet());
// Development logging
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// Limit requests from same API
const limiter = rateLimit({
max: 100,
windowMs: 60 * 60 * 1000,
message: 'Too many requests from this IP, please try again in an hour!'
});
app.use('/api', limiter);
// Stripe webhook, BEFORE body-parser, because stripe needs the body as stream
app.post(
'/webhook-checkout',
bodyParser.raw({ type: 'application/json' }),
bookingController.webhookCheckout
);
// Body parser, reading data from body into req.body
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
app.use(cookieParser());
// Data sanitization against NoSQL query injection
app.use(mongoSanitize());
// Data sanitization against XSS
app.use(xss());
// Prevent parameter pollution
app.use(
hpp({
whitelist: [
'duration',
'ratingsQuantity',
'ratingsAverage',
'maxGroupSize',
'difficulty',
'price'
]
})
);
app.use(compression());
// Test middleware
app.use((req, res, next) => {
req.requestTime = new Date().toISOString();
// console.log(req.cookies);
next();
});
// 3) ROUTES
app.use('/', viewRouter);
app.use('/api/v1/tours', tourRouter);
app.use('/api/v1/users', userRouter);
app.use('/api/v1/reviews', reviewRouter);
app.use('/api/v1/bookings', bookingRouter);
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});
app.use(globalErrorHandler);
module.exports = app;

View File

@@ -0,0 +1,241 @@
const crypto = require('crypto');
const { promisify } = require('util');
const jwt = require('jsonwebtoken');
const User = require('./../models/userModel');
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const Email = require('./../utils/email');
const signToken = id => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
const createSendToken = (user, statusCode, req, res) => {
const token = signToken(user._id);
res.cookie('jwt', token, {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000
),
httpOnly: true,
secure: req.secure || req.headers('x-forwarded-proto') === 'https'
});
// Remove password from output
user.password = undefined;
res.status(statusCode).json({
status: 'success',
token,
data: {
user
}
});
};
exports.signup = catchAsync(async (req, res, next) => {
const newUser = await User.create(req.body);
const url = `${req.protocol}://${req.get('host')}/me`;
// console.log(url);
await new Email(newUser, url).sendWelcome();
createSendToken(newUser, 201, req, res);
});
exports.login = catchAsync(async (req, res, next) => {
const { email, password } = req.body;
// 1) Check if email and password exist
if (!email || !password) {
return next(new AppError('Please provide email and password!', 400));
}
// 2) Check if user exists && password is correct
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password, user.password))) {
return next(new AppError('Incorrect email or password', 401));
}
// 3) If everything ok, send token to client
createSendToken(user, 200, req, res);
});
exports.logout = (req, res) => {
res.cookie('jwt', 'loggedout', {
expires: new Date(Date.now() + 10 * 1000),
httpOnly: true
});
res.status(200).json({ status: 'success' });
};
exports.protect = catchAsync(async (req, res, next) => {
// 1) Getting token and check of it's there
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
token = req.headers.authorization.split(' ')[1];
} else if (req.cookies.jwt) {
token = req.cookies.jwt;
}
if (!token) {
return next(
new AppError('You are not logged in! Please log in to get access.', 401)
);
}
// 2) Verification token
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
// 3) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next(
new AppError(
'The user belonging to this token does no longer exist.',
401
)
);
}
// 4) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next(
new AppError('User recently changed password! Please log in again.', 401)
);
}
// GRANT ACCESS TO PROTECTED ROUTE
req.user = currentUser;
res.locals.user = currentUser;
next();
});
// Only for rendered pages, no errors!
exports.isLoggedIn = async (req, res, next) => {
if (req.cookies.jwt) {
try {
// 1) verify token
const decoded = await promisify(jwt.verify)(
req.cookies.jwt,
process.env.JWT_SECRET
);
// 2) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next();
}
// 3) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next();
}
// THERE IS A LOGGED IN USER
res.locals.user = currentUser;
return next();
} catch (err) {
return next();
}
}
next();
};
exports.restrictTo = (...roles) => {
return (req, res, next) => {
// roles ['admin', 'lead-guide']. role='user'
if (!roles.includes(req.user.role)) {
return next(
new AppError('You do not have permission to perform this action', 403)
);
}
next();
};
};
exports.forgotPassword = catchAsync(async (req, res, next) => {
// 1) Get user based on POSTed email
const user = await User.findOne({ email: req.body.email });
if (!user) {
return next(new AppError('There is no user with email address.', 404));
}
// 2) Generate the random reset token
const resetToken = user.createPasswordResetToken();
await user.save({ validateBeforeSave: false });
// 3) Send it to user's email
try {
const resetURL = `${req.protocol}://${req.get(
'host'
)}/api/v1/users/resetPassword/${resetToken}`;
await new Email(user, resetURL).sendPasswordReset();
res.status(200).json({
status: 'success',
message: 'Token sent to email!'
});
} catch (err) {
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save({ validateBeforeSave: false });
return next(
new AppError('There was an error sending the email. Try again later!'),
500
);
}
});
exports.resetPassword = catchAsync(async (req, res, next) => {
// 1) Get user based on the token
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex');
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() }
});
// 2) If token has not expired, and there is user, set the new password
if (!user) {
return next(new AppError('Token is invalid or has expired', 400));
}
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
// 3) Update changedPasswordAt property for the user
// 4) Log the user in, send JWT
createSendToken(user, 200, req, res);
});
exports.updatePassword = catchAsync(async (req, res, next) => {
// 1) Get user from collection
const user = await User.findById(req.user.id).select('+password');
// 2) Check if POSTed current password is correct
if (!(await user.correctPassword(req.body.passwordCurrent, user.password))) {
return next(new AppError('Your current password is wrong.', 401));
}
// 3) If so, update password
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
await user.save();
// User.findByIdAndUpdate will NOT work as intended!
// 4) Log user in, send JWT
createSendToken(user, 200, req, res);
});

View File

@@ -0,0 +1,75 @@
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const Tour = require('../models/tourModel');
const User = require('../models/userModel');
const Booking = require('../models/bookingModel');
const catchAsync = require('../utils/catchAsync');
const factory = require('./handlerFactory');
exports.getCheckoutSession = catchAsync(async (req, res, next) => {
// 1) Get the currently booked tour
const tour = await Tour.findById(req.params.tourId);
// console.log(tour);
// 2) Create checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
// success_url: `${req.protocol}://${req.get('host')}/my-tours/?tour=${
// req.params.tourId
// }&user=${req.user.id}&price=${tour.price}`,
success_url: `${req.protocol}://${req.get('host')}/my-tours?alert=booking`,
cancel_url: `${req.protocol}://${req.get('host')}/tour/${tour.slug}`,
customer_email: req.user.email,
client_reference_id: req.params.tourId,
line_items: [
{
name: `${tour.name} Tour`,
description: tour.summary,
images: [
`${req.protocol}://${req.get('host')}/img/tours/${tour.imageCover}`
],
amount: tour.price * 100,
currency: 'usd',
quantity: 1
}
]
});
// 3) Create session as response
res.status(200).json({
status: 'success',
session
});
});
const createBookingCheckout = async session => {
const tour = session.client_reference_id;
const user = (await User.findOne({ email: session.customer_email })).id;
const price = session.display_items[0].amount / 100;
await Booking.create({ tour, user, price });
};
exports.webhookCheckout = (req, res, next) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (event.type === 'checkout.session.completed')
createBookingCheckout(event.data.object);
res.status(200).json({ received: true });
};
exports.createBooking = factory.createOne(Booking);
exports.getBooking = factory.getOne(Booking);
exports.getAllBookings = factory.getAll(Booking);
exports.updateBooking = factory.updateOne(Booking);
exports.deleteBooking = factory.deleteOne(Booking);

View File

@@ -0,0 +1,106 @@
const AppError = require('./../utils/appError');
const handleCastErrorDB = err => {
const message = `Invalid ${err.path}: ${err.value}.`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = err => {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
};
const handleValidationErrorDB = err => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
};
const handleJWTError = () =>
new AppError('Invalid token. Please log in again!', 401);
const handleJWTExpiredError = () =>
new AppError('Your token has expired! Please log in again.', 401);
const sendErrorDev = (err, req, res) => {
// A) API
if (req.originalUrl.startsWith('/api')) {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// B) RENDERED WEBSITE
console.error('ERROR 💥', err);
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: err.message
});
};
const sendErrorProd = (err, req, res) => {
// A) API
if (req.originalUrl.startsWith('/api')) {
// A) Operational, trusted error: send message to client
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// B) Programming or other unknown error: don't leak error details
// 1) Log error
console.error('ERROR 💥', err);
// 2) Send generic message
return res.status(500).json({
status: 'error',
message: 'Something went very wrong!'
});
}
// B) RENDERED WEBSITE
// A) Operational, trusted error: send message to client
if (err.isOperational) {
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: err.message
});
}
// B) Programming or other unknown error: don't leak error details
// 1) Log error
console.error('ERROR 💥', err);
// 2) Send generic message
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: 'Please try again later.'
});
};
module.exports = (err, req, res, next) => {
// console.log(err.stack);
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, req, res);
} else if (process.env.NODE_ENV === 'production') {
let error = { ...err };
error.message = err.message;
if (error.name === 'CastError') error = handleCastErrorDB(error);
if (error.code === 11000) error = handleDuplicateFieldsDB(error);
if (error.name === 'ValidationError')
error = handleValidationErrorDB(error);
if (error.name === 'JsonWebTokenError') error = handleJWTError();
if (error.name === 'TokenExpiredError') error = handleJWTExpiredError();
sendErrorProd(error, req, res);
}
};

View File

@@ -0,0 +1,90 @@
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const APIFeatures = require('./../utils/apiFeatures');
exports.deleteOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.findByIdAndDelete(req.params.id);
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(204).json({
status: 'success',
data: null
});
});
exports.updateOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: {
data: doc
}
});
});
exports.createOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.create(req.body);
res.status(201).json({
status: 'success',
data: {
data: doc
}
});
});
exports.getOne = (Model, popOptions) =>
catchAsync(async (req, res, next) => {
let query = Model.findById(req.params.id);
if (popOptions) query = query.populate(popOptions);
const doc = await query;
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: {
data: doc
}
});
});
exports.getAll = Model =>
catchAsync(async (req, res, next) => {
// To allow for nested GET reviews on tour (hack)
let filter = {};
if (req.params.tourId) filter = { tour: req.params.tourId };
const features = new APIFeatures(Model.find(filter), req.query)
.filter()
.sort()
.limitFields()
.paginate();
// const doc = await features.query.explain();
const doc = await features.query;
// SEND RESPONSE
res.status(200).json({
status: 'success',
results: doc.length,
data: {
data: doc
}
});
});

View File

@@ -0,0 +1,16 @@
const Review = require('./../models/reviewModel');
const factory = require('./handlerFactory');
// const catchAsync = require('./../utils/catchAsync');
exports.setTourUserIds = (req, res, next) => {
// Allow nested routes
if (!req.body.tour) req.body.tour = req.params.tourId;
if (!req.body.user) req.body.user = req.user.id;
next();
};
exports.getAllReviews = factory.getAll(Review);
exports.getReview = factory.getOne(Review);
exports.createReview = factory.createOne(Review);
exports.updateReview = factory.updateOne(Review);
exports.deleteReview = factory.deleteOne(Review);

View File

@@ -0,0 +1,223 @@
const multer = require('multer');
const sharp = require('sharp');
const Tour = require('./../models/tourModel');
const catchAsync = require('./../utils/catchAsync');
const factory = require('./handlerFactory');
const AppError = require('./../utils/appError');
const multerStorage = multer.memoryStorage();
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image! Please upload only images.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadTourImages = upload.fields([
{ name: 'imageCover', maxCount: 1 },
{ name: 'images', maxCount: 3 }
]);
// upload.single('image') req.file
// upload.array('images', 5) req.files
exports.resizeTourImages = catchAsync(async (req, res, next) => {
if (!req.files.imageCover || !req.files.images) return next();
// 1) Cover image
req.body.imageCover = `tour-${req.params.id}-${Date.now()}-cover.jpeg`;
await sharp(req.files.imageCover[0].buffer)
.resize(2000, 1333)
.toFormat('jpeg')
.jpeg({ quality: 90 })
.toFile(`public/img/tours/${req.body.imageCover}`);
// 2) Images
req.body.images = [];
await Promise.all(
req.files.images.map(async (file, i) => {
const filename = `tour-${req.params.id}-${Date.now()}-${i + 1}.jpeg`;
await sharp(file.buffer)
.resize(2000, 1333)
.toFormat('jpeg')
.jpeg({ quality: 90 })
.toFile(`public/img/tours/${filename}`);
req.body.images.push(filename);
})
);
next();
});
exports.aliasTopTours = (req, res, next) => {
req.query.limit = '5';
req.query.sort = '-ratingsAverage,price';
req.query.fields = 'name,price,ratingsAverage,summary,difficulty';
next();
};
exports.getAllTours = factory.getAll(Tour);
exports.getTour = factory.getOne(Tour, { path: 'reviews' });
exports.createTour = factory.createOne(Tour);
exports.updateTour = factory.updateOne(Tour);
exports.deleteTour = factory.deleteOne(Tour);
exports.getTourStats = catchAsync(async (req, res, next) => {
const stats = await Tour.aggregate([
{
$match: { ratingsAverage: { $gte: 4.5 } }
},
{
$group: {
_id: { $toUpper: '$difficulty' },
numTours: { $sum: 1 },
numRatings: { $sum: '$ratingsQuantity' },
avgRating: { $avg: '$ratingsAverage' },
avgPrice: { $avg: '$price' },
minPrice: { $min: '$price' },
maxPrice: { $max: '$price' }
}
},
{
$sort: { avgPrice: 1 }
}
// {
// $match: { _id: { $ne: 'EASY' } }
// }
]);
res.status(200).json({
status: 'success',
data: {
stats
}
});
});
exports.getMonthlyPlan = catchAsync(async (req, res, next) => {
const year = req.params.year * 1; // 2021
const plan = await Tour.aggregate([
{
$unwind: '$startDates'
},
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`)
}
}
},
{
$group: {
_id: { $month: '$startDates' },
numTourStarts: { $sum: 1 },
tours: { $push: '$name' }
}
},
{
$addFields: { month: '$_id' }
},
{
$project: {
_id: 0
}
},
{
$sort: { numTourStarts: -1 }
},
{
$limit: 12
}
]);
res.status(200).json({
status: 'success',
data: {
plan
}
});
});
// /tours-within/:distance/center/:latlng/unit/:unit
// /tours-within/233/center/34.111745,-118.113491/unit/mi
exports.getToursWithin = catchAsync(async (req, res, next) => {
const { distance, latlng, unit } = req.params;
const [lat, lng] = latlng.split(',');
const radius = unit === 'mi' ? distance / 3963.2 : distance / 6378.1;
if (!lat || !lng) {
next(
new AppError(
'Please provide latitutr and longitude in the format lat,lng.',
400
)
);
}
const tours = await Tour.find({
startLocation: { $geoWithin: { $centerSphere: [[lng, lat], radius] } }
});
res.status(200).json({
status: 'success',
results: tours.length,
data: {
data: tours
}
});
});
exports.getDistances = catchAsync(async (req, res, next) => {
const { latlng, unit } = req.params;
const [lat, lng] = latlng.split(',');
const multiplier = unit === 'mi' ? 0.000621371 : 0.001;
if (!lat || !lng) {
next(
new AppError(
'Please provide latitutr and longitude in the format lat,lng.',
400
)
);
}
const distances = await Tour.aggregate([
{
$geoNear: {
near: {
type: 'Point',
coordinates: [lng * 1, lat * 1]
},
distanceField: 'distance',
distanceMultiplier: multiplier
}
},
{
$project: {
distance: 1,
name: 1
}
}
]);
res.status(200).json({
status: 'success',
data: {
data: distances
}
});
});

View File

@@ -0,0 +1,111 @@
const multer = require('multer');
const sharp = require('sharp');
const User = require('./../models/userModel');
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const factory = require('./handlerFactory');
// const multerStorage = multer.diskStorage({
// destination: (req, file, cb) => {
// cb(null, 'public/img/users');
// },
// filename: (req, file, cb) => {
// const ext = file.mimetype.split('/')[1];
// cb(null, `user-${req.user.id}-${Date.now()}.${ext}`);
// }
// });
const multerStorage = multer.memoryStorage();
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image! Please upload only images.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadUserPhoto = upload.single('photo');
exports.resizeUserPhoto = catchAsync(async (req, res, next) => {
if (!req.file) return next();
req.file.filename = `user-${req.user.id}-${Date.now()}.jpeg`;
await sharp(req.file.buffer)
.resize(500, 500)
.toFormat('jpeg')
.jpeg({ quality: 90 })
.toFile(`public/img/users/${req.file.filename}`);
next();
});
const filterObj = (obj, ...allowedFields) => {
const newObj = {};
Object.keys(obj).forEach(el => {
if (allowedFields.includes(el)) newObj[el] = obj[el];
});
return newObj;
};
exports.getMe = (req, res, next) => {
req.params.id = req.user.id;
next();
};
exports.updateMe = catchAsync(async (req, res, next) => {
// 1) Create error if user POSTs password data
if (req.body.password || req.body.passwordConfirm) {
return next(
new AppError(
'This route is not for password updates. Please use /updateMyPassword.',
400
)
);
}
// 2) Filtered out unwanted fields names that are not allowed to be updated
const filteredBody = filterObj(req.body, 'name', 'email');
if (req.file) filteredBody.photo = req.file.filename;
// 3) Update user document
const updatedUser = await User.findByIdAndUpdate(req.user.id, filteredBody, {
new: true,
runValidators: true
});
res.status(200).json({
status: 'success',
data: {
user: updatedUser
}
});
});
exports.deleteMe = catchAsync(async (req, res, next) => {
await User.findByIdAndUpdate(req.user.id, { active: false });
res.status(204).json({
status: 'success',
data: null
});
});
exports.createUser = (req, res) => {
res.status(500).json({
status: 'error',
message: 'This route is not defined! Please use /signup instead'
});
};
exports.getUser = factory.getOne(User);
exports.getAllUsers = factory.getAll(User);
// Do NOT update passwords with this!
exports.updateUser = factory.updateOne(User);
exports.deleteUser = factory.deleteOne(User);

View File

@@ -0,0 +1,89 @@
const Tour = require('../models/tourModel');
const User = require('../models/userModel');
const Booking = require('../models/bookingModel');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');
exports.alerts = (req, res, next) => {
const { alert } = req.query;
if (alert === 'booking')
res.locals.alert =
"Your booking was successful! Please check your email for a confirmation. If your booking doesn't show up here immediatly, please come back later.";
next();
};
exports.getOverview = catchAsync(async (req, res, next) => {
// 1) Get tour data from collection
const tours = await Tour.find();
// 2) Build template
// 3) Render that template using tour data from 1)
res.status(200).render('overview', {
title: 'All Tours',
tours
});
});
exports.getTour = catchAsync(async (req, res, next) => {
// 1) Get the data, for the requested tour (including reviews and guides)
const tour = await Tour.findOne({ slug: req.params.slug }).populate({
path: 'reviews',
fields: 'review rating user'
});
if (!tour) {
return next(new AppError('There is no tour with that name.', 404));
}
// 2) Build template
// 3) Render template using data from 1)
res.status(200).render('tour', {
title: `${tour.name} Tour`,
tour
});
});
exports.getLoginForm = (req, res) => {
res.status(200).render('login', {
title: 'Log into your account'
});
};
exports.getAccount = (req, res) => {
res.status(200).render('account', {
title: 'Your account'
});
};
exports.getMyTours = catchAsync(async (req, res, next) => {
// 1) Find all bookings
const bookings = await Booking.find({ user: req.user.id });
// 2) Find tours with the returned IDs
const tourIDs = bookings.map(el => el.tour);
const tours = await Tour.find({ _id: { $in: tourIDs } });
res.status(200).render('overview', {
title: 'My Tours',
tours
});
});
exports.updateUserData = catchAsync(async (req, res, next) => {
const updatedUser = await User.findByIdAndUpdate(
req.user.id,
{
name: req.body.name,
email: req.body.email
},
{
new: true,
runValidators: true
}
);
res.status(200).render('account', {
title: 'Your account',
user: updatedUser
});
});

View File

@@ -0,0 +1,60 @@
const fs = require('fs');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const Tour = require('./../../models/tourModel');
const Review = require('./../../models/reviewModel');
const User = require('./../../models/userModel');
dotenv.config({ path: './config.env' });
const DB = process.env.DATABASE.replace(
'<PASSWORD>',
process.env.DATABASE_PASSWORD
);
mongoose
.connect(DB, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false
})
.then(() => console.log('DB connection successful!'));
// READ JSON FILE
const tours = JSON.parse(fs.readFileSync(`${__dirname}/tours.json`, 'utf-8'));
const users = JSON.parse(fs.readFileSync(`${__dirname}/users.json`, 'utf-8'));
const reviews = JSON.parse(
fs.readFileSync(`${__dirname}/reviews.json`, 'utf-8')
);
// IMPORT DATA INTO DB
const importData = async () => {
try {
await Tour.create(tours);
await User.create(users, { validateBeforeSave: false });
await Review.create(reviews);
console.log('Data successfully loaded!');
} catch (err) {
console.log(err);
}
process.exit();
};
// DELETE ALL DATA FROM DB
const deleteData = async () => {
try {
await Tour.deleteMany();
await User.deleteMany();
await Review.deleteMany();
console.log('Data successfully deleted!');
} catch (err) {
console.log(err);
}
process.exit();
};
if (process.argv[2] === '--import') {
importData();
} else if (process.argv[2] === '--delete') {
deleteData();
}

View File

@@ -0,0 +1,422 @@
[
{
"_id": "5c8a34ed14eb5c17645c9108",
"review": "Cras mollis nisi parturient mi nec aliquet suspendisse sagittis eros condimentum scelerisque taciti mattis praesent feugiat eu nascetur a tincidunt",
"rating": 5,
"user": "5c8a1dfa2f8fb814b56fa181",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a355b14eb5c17645c9109",
"review": "Tempus curabitur faucibus auctor bibendum duis gravida tincidunt litora himenaeos facilisis vivamus vehicula potenti semper fusce suspendisse sagittis!",
"rating": 4,
"user": "5c8a1dfa2f8fb814b56fa181",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a359914eb5c17645c910a",
"review": "Convallis turpis porttitor sapien ad urna efficitur dui vivamus in praesent nulla hac non potenti!",
"rating": 5,
"user": "5c8a1dfa2f8fb814b56fa181",
"tour": "5c88fa8cf4afda39709c295d"
},
{
"_id": "5c8a35b614eb5c17645c910b",
"review": "Habitasse scelerisque class quam primis convallis integer eros congue nulla proin nam faucibus parturient.",
"rating": 4,
"user": "5c8a1dfa2f8fb814b56fa181",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a364c14eb5c17645c910c",
"review": "Cras consequat fames faucibus ac aliquam dolor a euismod porttitor rhoncus venenatis himenaeos montes tristique pretium libero nisi!",
"rating": 5,
"user": "5c8a1e1a2f8fb814b56fa182",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a368c14eb5c17645c910d",
"review": "Laoreet justo volutpat per etiam donec at augue penatibus eu facilisis lorem phasellus ipsum tristique urna quam platea.",
"rating": 5,
"user": "5c8a1e1a2f8fb814b56fa182",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a36a014eb5c17645c910e",
"review": "Senectus lectus eleifend ex lobortis cras nam cursus accumsan tellus lacus faucibus himenaeos posuere!",
"rating": 5,
"user": "5c8a1e1a2f8fb814b56fa182",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a36b714eb5c17645c910f",
"review": "Pulvinar taciti etiam aenean lacinia natoque interdum fringilla suspendisse nam sapien urna!",
"rating": 4,
"user": "5c8a1e1a2f8fb814b56fa182",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a379a14eb5c17645c9110",
"review": "Pretium vel inceptos fringilla sit dui fusce varius gravida platea morbi semper erat elit porttitor potenti!",
"rating": 5,
"user": "5c8a24402f8fb814b56fa190",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a37b114eb5c17645c9111",
"review": "Ex a bibendum quis volutpat consequat euismod vulputate parturient laoreet diam sagittis amet at blandit.",
"rating": 4,
"user": "5c8a24402f8fb814b56fa190",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a37cb14eb5c17645c9112",
"review": "Auctor euismod interdum augue tristique senectus nascetur cras justo eleifend mattis libero id adipiscing amet placerat",
"rating": 5,
"user": "5c8a24402f8fb814b56fa190",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a37dd14eb5c17645c9113",
"review": "A facilisi justo ornare magnis velit diam dictumst parturient arcu nullam rhoncus nec!",
"rating": 4,
"user": "5c8a24402f8fb814b56fa190",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a37f114eb5c17645c9114",
"review": "Porttitor ullamcorper rutrum semper proin mus felis varius convallis conubia nisl erat lectus eget.",
"rating": 5,
"user": "5c8a24402f8fb814b56fa190",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a381714eb5c17645c9115",
"review": "Porttitor ullamcorper rutrum semper proin mus felis varius convallis conubia nisl erat lectus eget.",
"rating": 5,
"user": "5c8a1ec62f8fb814b56fa183",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a382d14eb5c17645c9116",
"review": "Semper blandit felis nostra facilisi sodales pulvinar habitasse diam sapien lobortis urna nunc ipsum orci.",
"rating": 5,
"user": "5c8a1ec62f8fb814b56fa183",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a384114eb5c17645c9117",
"review": "Neque amet vel integer placerat ex pretium elementum vitae quis ullamcorper nullam nunc habitant cursus justo!!!",
"rating": 5,
"user": "5c8a1ec62f8fb814b56fa183",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a385614eb5c17645c9118",
"review": "Sollicitudin sagittis ex ut fringilla enim condimentum et netus tristique.",
"rating": 5,
"user": "5c8a1ec62f8fb814b56fa183",
"tour": "5c88fa8cf4afda39709c295d"
},
{
"_id": "5c8a387214eb5c17645c9119",
"review": "Semper tempus curae at platea lobortis ullamcorper curabitur luctus maecenas nisl laoreet!",
"rating": 5,
"user": "5c8a1ec62f8fb814b56fa183",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a38ac14eb5c17645c911a",
"review": "Arcu adipiscing lobortis sem finibus consequat ac justo nisi pharetra ultricies facilisi!",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a38c714eb5c17645c911b",
"review": "Rutrum viverra turpis nunc ultricies dolor ornare metus habitant ex quis sociosqu nascetur pellentesque quam!",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a38da14eb5c17645c911c",
"review": "Elementum massa porttitor enim vitae eu ligula vivamus amet imperdiet urna tristique donec mattis mus erat.",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a38ed14eb5c17645c911d",
"review": "Fusce ullamcorper gravida libero nullam lacus litora class orci habitant sollicitudin...",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c295d"
},
{
"_id": "5c8a390d14eb5c17645c911e",
"review": "Varius potenti proin hendrerit felis sit convallis nunc non id facilisis aliquam platea elementum",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a391f14eb5c17645c911f",
"review": "Sem feugiat sed lorem vel dignissim platea habitasse dolor suscipit ultricies dapibus",
"rating": 5,
"user": "5c8a211f2f8fb814b56fa188",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a395b14eb5c17645c9120",
"review": "Sem feugiat sed lorem vel dignissim platea habitasse dolor suscipit ultricies dapibus",
"rating": 5,
"user": "5c8a20d32f8fb814b56fa187",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a399014eb5c17645c9121",
"review": "Tortor dolor sed vehicula neque ultrices varius orci feugiat dignissim auctor consequat.",
"rating": 4,
"user": "5c8a20d32f8fb814b56fa187",
"tour": "5c88fa8cf4afda39709c295d"
},
{
"_id": "5c8a39a214eb5c17645c9122",
"review": "Felis mauris aenean eu lectus fringilla habitasse nullam eros senectus ante etiam!",
"rating": 5,
"user": "5c8a20d32f8fb814b56fa187",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a39b614eb5c17645c9123",
"review": "Blandit varius nascetur est felis praesent lorem himenaeos pretium dapibus tellus bibendum consequat ac duis",
"rating": 3,
"user": "5c8a20d32f8fb814b56fa187",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a3a7014eb5c17645c9124",
"review": "Blandit varius nascetur est felis praesent lorem himenaeos pretium dapibus tellus bibendum consequat ac duis",
"rating": 5,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a3a8d14eb5c17645c9125",
"review": "Iaculis mauris eget sed nec lobortis rhoncus montes etiam dapibus suspendisse hendrerit quam pellentesque potenti sapien!",
"rating": 5,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a3a9914eb5c17645c9126",
"review": "Netus eleifend adipiscing ligula placerat fusce orci sollicitudin vivamus conubia.",
"rating": 5,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a3aaa14eb5c17645c9127",
"review": "Eleifend suspendisse ultricies platea primis ut ornare purus vel taciti faucibus justo nunc",
"rating": 4,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a3abc14eb5c17645c9128",
"review": "Malesuada consequat congue vel gravida eros conubia in sapien praesent diam!",
"rating": 4,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a3acf14eb5c17645c9129",
"review": "Curabitur maximus montes vestibulum nulla vel dictum cubilia himenaeos nunc hendrerit amet urna.",
"rating": 5,
"user": "5c8a23c82f8fb814b56fa18d",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a3b1e14eb5c17645c912a",
"review": "Curabitur maximus montes vestibulum nulla vel dictum cubilia himenaeos nunc hendrerit amet urna.",
"rating": 5,
"user": "5c8a23de2f8fb814b56fa18e",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a3b3214eb5c17645c912b",
"review": "Sociosqu eleifend tincidunt aenean condimentum gravida lorem arcu pellentesque felis dui feugiat nec.",
"rating": 5,
"user": "5c8a23de2f8fb814b56fa18e",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a3b4714eb5c17645c912c",
"review": "Ridiculus facilisis sem id aenean amet penatibus gravida phasellus a mus dui lacinia accumsan!!",
"rating": 1,
"user": "5c8a23de2f8fb814b56fa18e",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a3b6714eb5c17645c912e",
"review": "Venenatis molestie luctus cubilia taciti tempor faucibus nostra nisi curae integer.",
"rating": 5,
"user": "5c8a23de2f8fb814b56fa18e",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a3b7c14eb5c17645c912f",
"review": "Tempor pellentesque eu placerat auctor enim nam suscipit tincidunt natoque ipsum est.",
"rating": 5,
"user": "5c8a23de2f8fb814b56fa18e",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a3b9f14eb5c17645c9130",
"review": "Tempor pellentesque eu placerat auctor enim nam suscipit tincidunt natoque ipsum est.",
"rating": 5,
"user": "5c8a24282f8fb814b56fa18f",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a3bc414eb5c17645c9131",
"review": "Conubia semper efficitur rhoncus suspendisse taciti lectus ex sapien dolor molestie fusce class.",
"rating": 5,
"user": "5c8a24282f8fb814b56fa18f",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a3bdc14eb5c17645c9132",
"review": "Conubia pharetra pulvinar libero hac class congue curabitur mi porttitor!!",
"rating": 5,
"user": "5c8a24282f8fb814b56fa18f",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a3bf514eb5c17645c9133",
"review": "Nullam felis dictumst eros nulla torquent arcu inceptos mi faucibus ridiculus pellentesque gravida mauris.",
"rating": 5,
"user": "5c8a24282f8fb814b56fa18f",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a3c2514eb5c17645c9134",
"review": "Euismod suscipit ipsum efficitur rutrum dis mus dictumst laoreet lectus.",
"rating": 5,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a3c3b14eb5c17645c9135",
"review": "Massa orci lacus suspendisse maximus ad integer donec arcu parturient facilisis accumsan consectetur non",
"rating": 4,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c295a"
},
{
"_id": "5c8a3c5314eb5c17645c9136",
"review": "Blandit varius finibus imperdiet tortor hendrerit erat rhoncus dictumst inceptos massa in.",
"rating": 5,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a3c6814eb5c17645c9137",
"review": "Tristique semper proin pellentesque ipsum urna habitasse venenatis tincidunt morbi nisi at",
"rating": 4,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c295d"
},
{
"_id": "5c8a3c7814eb5c17645c9138",
"review": "Potenti etiam placerat mi metus ipsum curae eget nisl torquent pretium",
"rating": 4,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c2966"
},
{
"_id": "5c8a3c9014eb5c17645c9139",
"review": "Molestie non montes at fermentum cubilia quis dis placerat maecenas vulputate sapien facilisis",
"rating": 5,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a3ca314eb5c17645c913a",
"review": "Velit vulputate faucibus in nascetur praesent potenti primis pulvinar tempor",
"rating": 5,
"user": "5c8a245f2f8fb814b56fa191",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a3cdc14eb5c17645c913b",
"review": "Magna magnis tellus dui vivamus donec placerat vehicula erat turpis",
"rating": 5,
"user": "5c8a24822f8fb814b56fa192",
"tour": "5c88fa8cf4afda39709c2955"
},
{
"_id": "5c8a3cf414eb5c17645c913c",
"review": "Ligula lorem taciti fringilla himenaeos ex aliquam litora nam ad maecenas sit phasellus lectus!!",
"rating": 5,
"user": "5c8a24822f8fb814b56fa192",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a3d1e14eb5c17645c913d",
"review": "Nam ultrices quis leo viverra tristique curae facilisi luctus sapien eleifend fames orci lacinia pulvinar.",
"rating": 4,
"user": "5c8a24822f8fb814b56fa192",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a3d3a14eb5c17645c913e",
"review": "Ullamcorper ac nec id habitant a commodo eget libero cras congue!",
"rating": 4,
"user": "5c8a24822f8fb814b56fa192",
"tour": "5c88fa8cf4afda39709c2970"
},
{
"_id": "5c8a3d5314eb5c17645c913f",
"review": "Ultrices nam dui ex posuere velit varius himenaeos bibendum fermentum sollicitudin purus",
"rating": 5,
"user": "5c8a24822f8fb814b56fa192",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a3d8614eb5c17645c9140",
"review": "Ultrices nam dui ex posuere velit varius himenaeos bibendum fermentum sollicitudin purus",
"rating": 5,
"user": "5c8a24a02f8fb814b56fa193",
"tour": "5c88fa8cf4afda39709c2974"
},
{
"_id": "5c8a3d9b14eb5c17645c9141",
"review": "Vitae vulputate id quam metus orci cras mollis vivamus vehicula sapien et",
"rating": 2,
"user": "5c8a24a02f8fb814b56fa193",
"tour": "5c88fa8cf4afda39709c296c"
},
{
"_id": "5c8a3de514eb5c17645c9143",
"review": "Sem risus tempor auctor mattis netus montes tincidunt mollis lacinia natoque adipiscing",
"rating": 5,
"user": "5c8a24a02f8fb814b56fa193",
"tour": "5c88fa8cf4afda39709c2961"
},
{
"_id": "5c8a3e0714eb5c17645c9144",
"review": "Feugiat egestas ac pulvinar quis dui ligula tempor ad platea quisque scelerisque!",
"rating": 5,
"user": "5c8a24a02f8fb814b56fa193",
"tour": "5c88fa8cf4afda39709c2951"
},
{
"_id": "5c8a3e63e12c44188b4dbc5b",
"review": "Quisque egestas faucibus primis ridiculus mi felis tristique curabitur habitasse vehicula",
"rating": 4,
"user": "5c8a24a02f8fb814b56fa193",
"tour": "5c88fa8cf4afda39709c2966"
}
]

View File

@@ -0,0 +1,17 @@
/* eslint-disable */
const tour5 = {
id: 5,
name: 'The Sports Lover',
startLocation: 'California, USA',
nextStartDate: 'July 2021',
duration: 14,
maxGroupSize: 8,
difficulty: 'difficult',
avgRating: 4.7,
numReviews: 23,
regPrice: 2997,
shortDescription:
'Surfing, skating, parajumping, rock climbing and more, all in one tour',
longDescription:
'Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!'
};

View File

@@ -0,0 +1,137 @@
[
{
"id": 0,
"name": "The Forest Hiker",
"duration": 5,
"maxGroupSize": 25,
"difficulty": "easy",
"ratingsAverage": 4.7,
"ratingsQuantity": 37,
"price": 397,
"summary": "Breathtaking hike through the Canadian Banff National Park",
"description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"imageCover": "tour-1-cover.jpg",
"images": ["tour-1-1.jpg", "tour-1-2.jpg", "tour-1-3.jpg"],
"startDates": ["2021-04-25,10:00", "2021-07-20,10:00", "2021-10-05,10:00"]
},
{
"id": 1,
"name": "The Sea Explorer",
"duration": 7,
"maxGroupSize": 15,
"difficulty": "medium",
"ratingsAverage": 4.8,
"ratingsQuantity": 23,
"price": 497,
"summary": "Exploring the jaw-dropping US east coast by foot and by boat",
"description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"imageCover": "tour-2-cover.jpg",
"images": ["tour-2-1.jpg", "tour-2-2.jpg", "tour-2-3.jpg"],
"startDates": ["2021-06-19,10:00", "2021-07-20,10:00", "2021-08-18,10:00"]
},
{
"id": 2,
"name": "The Snow Adventurer",
"duration": 4,
"maxGroupSize": 10,
"difficulty": "difficult",
"ratingsAverage": 4.5,
"ratingsQuantity": 13,
"price": 997,
"summary": "Exciting adventure in the snow with snowboarding and skiing",
"description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
"imageCover": "tour-3-cover.jpg",
"images": ["tour-3-1.jpg", "tour-3-2.jpg", "tour-3-3.jpg"],
"startDates": ["2022-01-05,10:00", "2022-02-12,10:00", "2023-01-06,10:00"]
},
{
"id": 3,
"name": "The City Wanderer",
"duration": 9,
"maxGroupSize": 20,
"difficulty": "easy",
"ratingsAverage": 4.6,
"ratingsQuantity": 54,
"price": 1197,
"summary": "Living the life of Wanderlust in the US' most beatiful cities",
"description": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat lorem ipsum dolor sit amet.\nConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat!",
"imageCover": "tour-4-cover.jpg",
"images": ["tour-4-1.jpg", "tour-4-2.jpg", "tour-4-3.jpg"],
"startDates": ["2021-03-11,10:00", "2021-05-02,10:00", "2021-06-09,10:00"]
},
{
"id": 4,
"name": "The Park Camper",
"duration": 10,
"maxGroupSize": 15,
"difficulty": "medium",
"ratingsAverage": 4.9,
"ratingsQuantity": 19,
"price": 1497,
"summary": "Breathing in Nature in America's most spectacular National Parks",
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!",
"imageCover": "tour-5-cover.jpg",
"images": ["tour-5-1.jpg", "tour-5-2.jpg", "tour-5-3.jpg"],
"startDates": ["2021-08-05,10:00", "2022-03-20,10:00", "2022-08-12,10:00"]
},
{
"id": 5,
"name": "The Sports Lover",
"duration": 14,
"maxGroupSize": 8,
"difficulty": "difficult",
"ratingsAverage": 4.7,
"ratingsQuantity": 28,
"price": 2997,
"summary": "Surfing, skating, parajumping, rock climbing and more, all in one tour",
"description": "Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!",
"imageCover": "tour-6-cover.jpg",
"images": ["tour-6-1.jpg", "tour-6-2.jpg", "tour-6-3.jpg"],
"startDates": ["2021-07-19,10:00", "2021-09-06,10:00", "2022-03-18,10:00"]
},
{
"id": 6,
"name": "The Wine Taster",
"duration": 5,
"maxGroupSize": 8,
"difficulty": "easy",
"ratingsAverage": 4.5,
"ratingsQuantity": 35,
"price": 1997,
"summary": "Exquisite wines, scenic views, exclusive barrel tastings, and much more",
"description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"imageCover": "tour-7-cover.jpg",
"images": ["tour-7-1.jpg", "tour-7-2.jpg", "tour-7-3.jpg"],
"startDates": ["2021-02-12,10:00", "2021-04-14,10:00", "2021-09-01,10:00"]
},
{
"id": 7,
"name": "The Star Gazer",
"duration": 9,
"maxGroupSize": 8,
"difficulty": "medium",
"ratingsAverage": 4.7,
"ratingsQuantity": 28,
"price": 2997,
"summary": "The most remote and stunningly beautiful places for seeing the night sky",
"description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"imageCover": "tour-8-cover.jpg",
"images": ["tour-8-1.jpg", "tour-8-2.jpg", "tour-8-3.jpg"],
"startDates": ["2021-03-23,10:00", "2021-10-25,10:00", "2022-01-30,10:00"]
},
{
"id": 8,
"name": "The Northern Lights",
"duration": 3,
"maxGroupSize": 12,
"difficulty": "easy",
"ratingsAverage": 4.9,
"ratingsQuantity": 33,
"price": 1497,
"summary": "Enjoy the Northern Lights in one of the best places in the world",
"description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
"imageCover": "tour-9-cover.jpg",
"images": ["tour-9-1.jpg", "tour-9-2.jpg", "tour-9-3.jpg"],
"startDates": ["2021-12-16,10:00", "2022-01-16,10:00", "2022-12-12,10:00"]
}
]

View File

@@ -0,0 +1,470 @@
[
{
"startLocation": {
"description": "Miami, USA",
"type": "Point",
"coordinates": [-80.185942, 25.774772],
"address": "301 Biscayne Blvd, Miami, FL 33132, USA"
},
"ratingsAverage": 4.8,
"ratingsQuantity": 6,
"images": ["tour-2-1.jpg", "tour-2-2.jpg", "tour-2-3.jpg"],
"startDates": [
"2021-06-19T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
"2021-08-18T09:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2955",
"name": "The Sea Explorer",
"duration": 7,
"maxGroupSize": 15,
"difficulty": "medium",
"guides": ["5c8a22c62f8fb814b56fa18b", "5c8a1f4e2f8fb814b56fa185"],
"price": 497,
"summary": "Exploring the jaw-dropping US east coast by foot and by boat",
"description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"imageCover": "tour-2-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2959",
"description": "Lummus Park Beach",
"type": "Point",
"coordinates": [-80.128473, 25.781842],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c2958",
"description": "Islamorada",
"type": "Point",
"coordinates": [-80.647885, 24.909047],
"day": 2
},
{
"_id": "5c88fa8cf4afda39709c2957",
"description": "Sombrero Beach",
"type": "Point",
"coordinates": [-81.0784, 24.707496],
"day": 3
},
{
"_id": "5c88fa8cf4afda39709c2956",
"description": "West Key",
"type": "Point",
"coordinates": [-81.768719, 24.552242],
"day": 5
}
]
},
{
"startLocation": {
"description": "Banff, CAN",
"type": "Point",
"coordinates": [-115.570154, 51.178456],
"address": "224 Banff Ave, Banff, AB, Canada"
},
"ratingsAverage": 5,
"ratingsQuantity": 9,
"images": ["tour-1-1.jpg", "tour-1-2.jpg", "tour-1-3.jpg"],
"startDates": [
"2021-04-25T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
"2021-10-05T09:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2951",
"name": "The Forest Hiker",
"duration": 5,
"maxGroupSize": 25,
"difficulty": "easy",
"guides": [
"5c8a21d02f8fb814b56fa189",
"5c8a201e2f8fb814b56fa186",
"5c8a1f292f8fb814b56fa184"
],
"price": 397,
"summary": "Breathtaking hike through the Canadian Banff National Park",
"description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"imageCover": "tour-1-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2954",
"description": "Banff National Park",
"type": "Point",
"coordinates": [-116.214531, 51.417611],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c2953",
"description": "Jasper National Park",
"type": "Point",
"coordinates": [-118.076152, 52.875223],
"day": 3
},
{
"_id": "5c88fa8cf4afda39709c2952",
"description": "Glacier National Park of Canada",
"type": "Point",
"coordinates": [-117.490309, 51.261937],
"day": 5
}
]
},
{
"startLocation": {
"description": "Aspen, USA",
"type": "Point",
"coordinates": [-106.822318, 39.190872],
"address": "419 S Mill St, Aspen, CO 81611, USA"
},
"ratingsAverage": 4.5,
"ratingsQuantity": 6,
"images": ["tour-3-1.jpg", "tour-3-2.jpg", "tour-3-3.jpg"],
"startDates": [
"2022-01-05T10:00:00.000Z",
"2022-02-12T10:00:00.000Z",
"2023-01-06T10:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c295a",
"name": "The Snow Adventurer",
"duration": 4,
"maxGroupSize": 10,
"difficulty": "difficult",
"guides": [
"5c8a21d02f8fb814b56fa189",
"5c8a23412f8fb814b56fa18c",
"5c8a1f4e2f8fb814b56fa185"
],
"price": 997,
"summary": "Exciting adventure in the snow with snowboarding and skiing",
"description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
"imageCover": "tour-3-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c295c",
"description": "Aspen Highlands",
"type": "Point",
"coordinates": [-106.855385, 39.182677],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c295b",
"description": "Beaver Creek",
"type": "Point",
"coordinates": [-106.516623, 39.60499],
"day": 2
}
]
},
{
"startLocation": {
"description": "Las Vegas, USA",
"type": "Point",
"coordinates": [-115.172652, 36.110904],
"address": "3663 S Las Vegas Blvd, Las Vegas, NV 89109, USA"
},
"ratingsAverage": 4.7,
"ratingsQuantity": 7,
"images": ["tour-5-1.jpg", "tour-5-2.jpg", "tour-5-3.jpg"],
"startDates": [
"2021-08-05T09:00:00.000Z",
"2022-03-20T10:00:00.000Z",
"2022-08-12T09:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2961",
"name": "The Park Camper",
"duration": 10,
"maxGroupSize": 15,
"difficulty": "medium",
"guides": [
"5c8a21f22f8fb814b56fa18a",
"5c8a23412f8fb814b56fa18c",
"5c8a201e2f8fb814b56fa186"
],
"price": 1497,
"summary": "Breathing in Nature in America's most spectacular National Parks",
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!",
"imageCover": "tour-5-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2965",
"description": "Zion Canyon National Park",
"type": "Point",
"coordinates": [-112.987418, 37.198125],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c2964",
"description": "Antelope Canyon",
"type": "Point",
"coordinates": [-111.376161, 36.86438],
"day": 4
},
{
"_id": "5c88fa8cf4afda39709c2963",
"description": "Grand Canyon National Park",
"type": "Point",
"coordinates": [-112.115763, 36.058973],
"day": 5
},
{
"_id": "5c88fa8cf4afda39709c2962",
"description": "Joshua Tree National Park",
"type": "Point",
"coordinates": [-116.107963, 34.011646],
"day": 9
}
]
},
{
"startLocation": {
"description": "NYC, USA",
"type": "Point",
"coordinates": [-73.985141, 40.75894],
"address": "Manhattan, NY 10036, USA"
},
"ratingsAverage": 4.6,
"ratingsQuantity": 5,
"images": ["tour-4-1.jpg", "tour-4-2.jpg", "tour-4-3.jpg"],
"startDates": [
"2021-03-11T10:00:00.000Z",
"2021-05-02T09:00:00.000Z",
"2021-06-09T09:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c295d",
"name": "The City Wanderer",
"duration": 9,
"maxGroupSize": 20,
"difficulty": "easy",
"guides": ["5c8a22c62f8fb814b56fa18b", "5c8a201e2f8fb814b56fa186"],
"price": 1197,
"summary": "Living the life of Wanderlust in the US' most beatiful cities",
"description": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat lorem ipsum dolor sit amet.\nConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat!",
"imageCover": "tour-4-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2960",
"description": "New York",
"type": "Point",
"coordinates": [-73.967696, 40.781821],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c295f",
"description": "Los Angeles",
"type": "Point",
"coordinates": [-118.324396, 34.097984],
"day": 3
},
{
"_id": "5c88fa8cf4afda39709c295e",
"description": "San Francisco",
"type": "Point",
"coordinates": [-122.408865, 37.787825],
"day": 5
}
]
},
{
"startLocation": {
"description": "California, USA",
"type": "Point",
"coordinates": [-118.803461, 34.006072],
"address": "29130 Cliffside Dr, Malibu, CA 90265, USA"
},
"ratingsAverage": 3.9,
"ratingsQuantity": 7,
"images": ["tour-6-1.jpg", "tour-6-2.jpg", "tour-6-3.jpg"],
"startDates": [
"2021-07-19T09:00:00.000Z",
"2021-09-06T09:00:00.000Z",
"2022-03-18T10:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2966",
"name": "The Sports Lover",
"duration": 14,
"maxGroupSize": 8,
"difficulty": "difficult",
"guides": [
"5c8a21f22f8fb814b56fa18a",
"5c8a1f292f8fb814b56fa184",
"5c8a1f4e2f8fb814b56fa185"
],
"price": 2997,
"summary": "Surfing, skating, parajumping, rock climbing and more, all in one tour",
"description": "Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!",
"imageCover": "tour-6-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c296b",
"description": "Point Dume Beach",
"type": "Point",
"coordinates": [-118.809361, 34.003098],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c296a",
"description": "Venice Skate Park",
"type": "Point",
"coordinates": [-118.47549, 33.987367],
"day": 4
},
{
"_id": "5c88fa8cf4afda39709c2969",
"description": "San Diego Skydive",
"type": "Point",
"coordinates": [-116.830104, 33.022843],
"day": 6
},
{
"_id": "5c88fa8cf4afda39709c2968",
"description": "Kern River Rafting",
"type": "Point",
"coordinates": [-118.4547, 35.710359],
"day": 7
},
{
"_id": "5c88fa8cf4afda39709c2967",
"description": "Yosemite National Park",
"type": "Point",
"coordinates": [-119.600492, 37.742371],
"day": 10
}
]
},
{
"startLocation": {
"description": "Utah, USA",
"type": "Point",
"coordinates": [-109.55099, 37.283469],
"address": "Bluff, UT 84512, USA"
},
"ratingsAverage": 4.8,
"ratingsQuantity": 6,
"images": ["tour-8-1.jpg", "tour-8-2.jpg", "tour-8-3.jpg"],
"startDates": [
"2021-03-23T10:00:00.000Z",
"2021-10-25T09:00:00.000Z",
"2022-01-30T10:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2970",
"name": "The Star Gazer",
"duration": 9,
"maxGroupSize": 8,
"difficulty": "medium",
"guides": ["5c8a21d02f8fb814b56fa189", "5c8a1f292f8fb814b56fa184"],
"price": 2997,
"summary": "The most remote and stunningly beautiful places for seeing the night sky",
"description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"imageCover": "tour-8-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2973",
"description": "Natural Bridges National Monument",
"type": "Point",
"coordinates": [-109.99953, 37.629017],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c2972",
"description": "Horseshoe Bend",
"type": "Point",
"coordinates": [-111.50954, 36.883269],
"day": 3
},
{
"_id": "5c88fa8cf4afda39709c2971",
"description": "Death Valley National Park",
"type": "Point",
"coordinates": [-117.07399, 36.501435],
"day": 6
}
]
},
{
"startLocation": {
"description": "Yellowknife, CAN",
"type": "Point",
"coordinates": [-114.406097, 62.439943],
"address": "Yellowknife, NT X1A 2L2, Canada"
},
"ratingsAverage": 4.7,
"ratingsQuantity": 7,
"images": ["tour-9-1.jpg", "tour-9-2.jpg", "tour-9-3.jpg"],
"startDates": [
"2021-12-16T10:00:00.000Z",
"2022-01-16T10:00:00.000Z",
"2022-12-12T10:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c2974",
"name": "The Northern Lights",
"duration": 3,
"maxGroupSize": 12,
"difficulty": "easy",
"guides": [
"5c8a21f22f8fb814b56fa18a",
"5c8a201e2f8fb814b56fa186",
"5c8a23412f8fb814b56fa18c"
],
"price": 1497,
"summary": "Enjoy the Northern Lights in one of the best places in the world",
"description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!",
"imageCover": "tour-9-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c2975",
"description": "Yellowknife",
"type": "Point",
"coordinates": [-114.406097, 62.439943],
"day": 1
}
]
},
{
"startLocation": {
"description": "California, USA",
"type": "Point",
"coordinates": [-122.29286, 38.294065],
"address": "560 Jefferson St, Napa, CA 94559, USA"
},
"ratingsAverage": 4.4,
"ratingsQuantity": 7,
"images": ["tour-7-1.jpg", "tour-7-2.jpg", "tour-7-3.jpg"],
"startDates": [
"2021-02-12T10:00:00.000Z",
"2021-04-14T09:00:00.000Z",
"2021-09-01T09:00:00.000Z"
],
"_id": "5c88fa8cf4afda39709c296c",
"name": "The Wine Taster",
"duration": 5,
"maxGroupSize": 8,
"difficulty": "easy",
"guides": ["5c8a22c62f8fb814b56fa18b", "5c8a23412f8fb814b56fa18c"],
"price": 1997,
"summary": "Exquisite wines, scenic views, exclusive barrel tastings, and much more",
"description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"imageCover": "tour-7-cover.jpg",
"locations": [
{
"_id": "5c88fa8cf4afda39709c296f",
"description": "Beringer Vineyards",
"type": "Point",
"coordinates": [-122.479887, 38.510312],
"day": 1
},
{
"_id": "5c88fa8cf4afda39709c296e",
"description": "Clos Pegase Winery & Tasting Room",
"type": "Point",
"coordinates": [-122.582948, 38.585707],
"day": 3
},
{
"_id": "5c88fa8cf4afda39709c296d",
"description": "Raymond Vineyard and Cellar",
"type": "Point",
"coordinates": [-122.434697, 38.482181],
"day": 5
}
]
}
]

View File

@@ -0,0 +1,182 @@
[
{
"_id": "5c8a1d5b0190b214360dc057",
"name": "Jonas Schmedtmann",
"email": "admin@natours.io",
"role": "admin",
"active": true,
"photo": "user-1.jpg",
"password": "$2a$12$Q0grHjH9PXc6SxivC8m12.2mZJ9BbKcgFpwSG4Y1ZEII8HJVzWeyS"
},
{
"_id": "5c8a1dfa2f8fb814b56fa181",
"name": "Lourdes Browning",
"email": "loulou@example.com",
"role": "user",
"active": true,
"photo": "user-2.jpg",
"password": "$2a$12$hP1h2pnNp7wgyZNRwPsOTeZuNzWBv7vHmsR3DT/OaPSUBQT.y0S.."
},
{
"_id": "5c8a1e1a2f8fb814b56fa182",
"name": "Sophie Louise Hart",
"email": "sophie@example.com",
"role": "user",
"active": true,
"photo": "user-3.jpg",
"password": "$2a$12$9nFqToiTmjgfFVJiQvjmreLt4k8X4gGYCETGapSZOb2hHa55t0dDq"
},
{
"_id": "5c8a1ec62f8fb814b56fa183",
"name": "Ayla Cornell",
"email": "ayls@example.com",
"role": "user",
"active": true,
"photo": "user-4.jpg",
"password": "$2a$12$tm33.M/4pfEbZF64WbFuHuVFv85v4qEhi.ik8njbud7yaoqCZpjiy"
},
{
"_id": "5c8a1f292f8fb814b56fa184",
"name": "Leo Gillespie",
"email": "leo@example.com",
"role": "guide",
"active": true,
"photo": "user-5.jpg",
"password": "$2a$12$OOPr90tBEBF1Iho3ox0Jde0O/WXUR0VLA5xdh6tWcu7qb.qOCvSg2"
},
{
"_id": "5c8a1f4e2f8fb814b56fa185",
"name": "Jennifer Hardy",
"email": "jennifer@example.com",
"role": "guide",
"active": true,
"photo": "user-6.jpg",
"password": "$2a$12$XCXvvlhRBJ8CydKH09v1v.jpg0hB9gVVfMVEoz4MsxqL9zb5PrF42"
},
{
"_id": "5c8a201e2f8fb814b56fa186",
"name": "Kate Morrison",
"email": "kate@example.com",
"role": "guide",
"active": true,
"photo": "user-7.jpg",
"password": "$2a$12$II1F3aBSFDF3Xz7iB4rk/.a2dogwkClMN5gGCWrRlILrG1xtJG7q6"
},
{
"_id": "5c8a20d32f8fb814b56fa187",
"name": "Eliana Stout",
"email": "eliana@example.com",
"role": "user",
"active": true,
"photo": "user-8.jpg",
"password": "$2a$12$Jb/ILhdDV.ZpnPMu19xfe.NRh5ntE2LzNMNcsty05QWwRbmFFVMKO"
},
{
"_id": "5c8a211f2f8fb814b56fa188",
"name": "Cristian Vega",
"email": "chris@example.com",
"role": "user",
"active": true,
"photo": "user-9.jpg",
"password": "$2a$12$r7/jtdWtzNfrfC7zw3uS.eDJ3Bs.8qrO31ZdbMljL.lUY0TAsaAL6"
},
{
"_id": "5c8a21d02f8fb814b56fa189",
"name": "Steve T. Scaife",
"email": "steve@example.com",
"role": "lead-guide",
"active": true,
"photo": "user-10.jpg",
"password": "$2a$12$q7v9dm.S4DvqhAeBc4KwduedEDEkDe2GGFGzteW6xnHt120oRpkqm"
},
{
"_id": "5c8a21f22f8fb814b56fa18a",
"name": "Aarav Lynn",
"email": "aarav@example.com",
"role": "lead-guide",
"active": true,
"photo": "user-11.jpg",
"password": "$2a$12$lKWhzujFvQwG4m/X3mnTneOB3ib9IYETsOqQ8aN5QEWDjX6X2wJJm"
},
{
"_id": "5c8a22c62f8fb814b56fa18b",
"name": "Miyah Myles",
"email": "miyah@example.com",
"role": "lead-guide",
"active": true,
"photo": "user-12.jpg",
"password": "$2a$12$.XIvvmznHQSa9UOI639yhe4vzHKCYO1vpTUZc4d45oiT4GOZQe1kS"
},
{
"_id": "5c8a23412f8fb814b56fa18c",
"name": "Ben Hadley",
"email": "ben@example.com",
"role": "guide",
"active": true,
"photo": "user-13.jpg",
"password": "$2a$12$D3fyuS9ETdBBw5lOwceTMuZcDTyVq28ieeGUAanIuLMcSDz6bpfIe"
},
{
"_id": "5c8a23c82f8fb814b56fa18d",
"name": "Laura Wilson",
"email": "laura@example.com",
"role": "user",
"active": true,
"photo": "user-14.jpg",
"password": "$2a$12$VPYaAAOsI44uhq11WbZ5R.cHT4.fGdlI9gKJd95jmYw3.sAsmbvBq"
},
{
"_id": "5c8a23de2f8fb814b56fa18e",
"name": "Max Smith",
"email": "max@example.com",
"role": "user",
"active": true,
"photo": "user-15.jpg",
"password": "$2a$12$l5qamwqcqC2NlgN6o5A5..9Fxzr6X.bjx/8j3a9jYUHWGOL99oXlm"
},
{
"_id": "5c8a24282f8fb814b56fa18f",
"name": "Isabel Kirkland",
"email": "isabel@example.com",
"role": "user",
"active": true,
"photo": "user-16.jpg",
"password": "$2a$12$IUnwPH0MGFeMuz7g4gtfvOll.9wgLyxG.9C3TKlttfLtCQWEE6GIu"
},
{
"_id": "5c8a24402f8fb814b56fa190",
"name": "Alexander Jones",
"email": "alex@example.com",
"role": "user",
"active": true,
"photo": "user-17.jpg",
"password": "$2a$12$NnclhoYFNcSApoQ3ML8kk.b4B3gbpOmZJLfqska07miAnXukOgK6y"
},
{
"_id": "5c8a245f2f8fb814b56fa191",
"name": "Eduardo Hernandez",
"email": "edu@example.com",
"role": "user",
"active": true,
"photo": "user-18.jpg",
"password": "$2a$12$uB5H1OxLMOqDYTuTlptAoewlovENJvjrLwzsL1wUZ6OkAIByPPBGq"
},
{
"_id": "5c8a24822f8fb814b56fa192",
"name": "John Riley",
"email": "john@example.com",
"role": "user",
"active": true,
"photo": "user-19.jpg",
"password": "$2a$12$11JElTatQlAFo1Obw/dwd..vuVmQyYS7MT14pkl3lRvVPjGA00G8O"
},
{
"_id": "5c8a24a02f8fb814b56fa193",
"name": "Lisa Brown",
"email": "lisa@example.com",
"role": "user",
"active": true,
"photo": "user-20.jpg",
"password": "$2a$12$uA9FsDw63v6dkJKGlLQ/8ufYBs8euB7kqIQewyYlZXU5azEKeLEky"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -0,0 +1,77 @@
main.main
.user-view
nav.user-view__menu
ul.side-nav
li.side-nav--active
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-settings')
| Settings
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-briefcase')
| My bookings
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-star')
| My reviews
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-credit-card')
| Billing
.admin-nav
h5.admin-nav__heading Admin
ul.side-nav
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-map')
| Manage tours
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-users')
| Manage users
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-star')
| Manage reviews
li
a(href='#')
svg
use(xlink:href='img/icons.svg#icon-briefcase')
.user-view__content
.user-view__form-container
h2.heading-secondary.ma-bt-md Your account settings
form.form.form-user-data
.form__group
label.form__label(for='name') Name
input#name.form__input(type='text', value='Jonas Schmedtmann', required)
.form__group.ma-bt-md
label.form__label(for='email') Email address
input#email.form__input(type='email', value='admin@natours.io', required)
.form__group.form__photo-upload
img.form__user-photo(src='img/user.jpg', alt='User photo')
a.btn-text(href='') Choose new photo
.form__group.right
button.btn.btn--small.btn--green Save settings
.line &nbsp;
.user-view__form-container
h2.heading-secondary.ma-bt-md Password change
form.form.form-user-settings
.form__group
label.form__label(for='password-current') Current password
input#password-current.form__input(type='password', placeholder='••••••••', required, minlength='8')
.form__group
label.form__label(for='password') New password
input#password.form__input(type='password', placeholder='••••••••', required, minlength='8')
.form__group.ma-bt-lg
label.form__label(for='password-confirm') Confirm password
input#password-confirm.form__input(type='password', placeholder='••••••••', required, minlength='8')
.form__group.right
button.btn.btn--small.btn--green Save password

View File

@@ -0,0 +1,314 @@
//- Email template adapted from https://github.com/leemunroe/responsive-html-email-template
//- Converted from HTML using https://html2pug.now.sh/
doctype html
html
head
meta(name='viewport', content='width=device-width')
meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
title= subject
style.
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
.body {
background-color: #f6f6f6;
width: 100%;
}
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
}
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #55c57a;
text-decoration: underline;
}
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #55c57a;
border-radius: 5px;
box-sizing: border-box;
color: #55c57a;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #55c57a;
}
.btn-primary a {
background-color: #55c57a;
border-color: #55c57a;
color: #ffffff;
}
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
margin: 20px 0;
}
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #2e864b !important;
}
.btn-primary a:hover {
background-color: #2e864b !important;
border-color: #2e864b !important;
}
}
body
table.body(role='presentation', border='0', cellpadding='0', cellspacing='0')
tbody
tr
td
td.container
.content
// START CENTERED WHITE CONTAINER
table.main(role='presentation')
// START MAIN AREA
tbody
tr
td.wrapper
table(role='presentation', border='0', cellpadding='0', cellspacing='0')
tbody
tr
td
// CONTENT
p Hi NAME,
p Welcome to Natours, we're glad to have you 🎉🙏
p We're all a big familiy here, so make sure to upload your user photo so we get to know you a bit better!
table.btn.btn-primary(role='presentation', border='0', cellpadding='0', cellspacing='0')
tbody
tr
td(align='left')
table(role='presentation', border='0', cellpadding='0', cellspacing='0')
tbody
tr
td
a(href='#', target='_blank') Upload user photo
p If you need any help with booking your next tour, please don't hesitate to contact me!
p - Jonas Schmedtmann, CEO
// START FOOTER
.footer
table(role='presentation', border='0', cellpadding='0', cellspacing='0')
tbody
tr
td.content-block
span.apple-link Natours Inc, 123 Nowhere Road, San Francisco CA 99999
br
| Don't like these emails?
a(href='#') Unsubscribe
//- td  

View File

@@ -0,0 +1,6 @@
main.main
.error
.error__title
h2.heading-secondary.heading-secondary--error Uh oh! Something went wrong!
h2.error__emoji 😢 🤯
.error__msg Page not found!

View File

@@ -0,0 +1,12 @@
main.main
.login-form
h2.heading-secondary.ma-bt-lg Log into your account
form.form
.form__group
label.form__label(for='email') Email address
input#email.form__input(type='email', placeholder='you@example.com', required)
.form__group.ma-bt-md
label.form__label(for='password') Password
input#password.form__input(type='password', placeholder='••••••••', required, minlength='8')
.form__group
button.btn.btn--green Login

View File

@@ -0,0 +1,36 @@
.card
.card__header
.card__picture
.card__picture-overlay &nbsp;
img.card__picture-img(src='img/tour-1-cover.jpg', alt='Tour 1')
h3.heading-tertirary
span The Forest Hiker
.card__details
h4.card__sub-heading Easy 5-day tour
p.card__text Breathtaking hike through the Canadian Banff National Park
.card__data
svg.card__icon
use(xlink:href='img/icons.svg#icon-map-pin')
span Banff, Canada
.card__data
svg.card__icon
use(xlink:href='img/icons.svg#icon-calendar')
span April 2021
.card__data
svg.card__icon
use(xlink:href='img/icons.svg#icon-flag')
span 3 stops
.card__data
svg.card__icon
use(xlink:href='img/icons.svg#icon-user')
span 25 people
.card__footer
p
span.card__footer-value $297
span.card__footer-text per person
p.card__ratings
span.card__footer-value 4.9
span.card__footer-text rating (21)
a.btn.btn--green.btn--small(href='#') Details

View File

@@ -0,0 +1,109 @@
section.section-header
.header__hero
.header__hero-overlay &nbsp;
img.header__hero-img(src='/img/tour-5-cover.jpg', alt='Tour 5')
.heading-box
h1.heading-primary
span The Park Camper Tour
.heading-box__group
.heading-box__detail
svg.heading-box__icon
use(xlink:href='/img/icons.svg#icon-clock')
span.heading-box__text 10 days
.heading-box__detail
svg.heading-box__icon
use(xlink:href='/img/icons.svg#icon-map-pin')
span.heading-box__text Las Vegas, USA
section.section-description
.overview-box
div
.overview-box__group
h2.heading-secondary.ma-bt-lg Quick facts
.overview-box__detail
svg.overview-box__icon
use(xlink:href='/img/icons.svg#icon-calendar')
span.overview-box__label Next date
span.overview-box__text August 2021
.overview-box__detail
svg.overview-box__icon
use(xlink:href='/img/icons.svg#icon-trending-up')
span.overview-box__label Difficulty
span.overview-box__text Medium
.overview-box__detail
svg.overview-box__icon
use(xlink:href='/img/icons.svg#icon-user')
span.overview-box__label Participants
span.overview-box__text 10 people
.overview-box__detail
svg.overview-box__icon
use(xlink:href='/img/icons.svg#icon-star')
span.overview-box__label Rating
span.overview-box__text 4.9 / 5
.overview-box__group
h2.heading-secondary.ma-bt-lg Your tour guides
.overview-box__detail
img.overview-box__img(src='/img/users/user-19.jpg', alt='Lead guide')
span.overview-box__label Lead guide
span.overview-box__text Steven Miller
.overview-box__detail
img.overview-box__img(src='/img/users/user-18.jpg', alt='Tour guide')
span.overview-box__label Tour guide
span.overview-box__text Lisa Brown
.overview-box__detail
img.overview-box__img(src='/img/users/user-17.jpg', alt='Intern')
span.overview-box__label Intern
span.overview-box__text Max Smith
.description-box
h2.heading-secondary.ma-bt-lg About the park camper tour
p.description__text Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
p.description__text Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!
section.section-pictures
.picture-box
img.picture-box__img.picture-box__img--1(src='/img/tour-5-1.jpg', alt='The Park Camper Tour 1')
.picture-box
img.picture-box__img.picture-box__img--2(src='/img/tour-5-2.jpg', alt='The Park Camper Tour 1')
.picture-box
img.picture-box__img.picture-box__img--3(src='/img/tour-5-3.jpg', alt='The Park Camper Tour 1')
section.section-map
#map
section.section-reviews
.reviews
.reviews__card
.reviews__avatar
img.reviews__avatar-img(src='/img/users/user-7.jpg', alt='Jim Brown')
h6.reviews__user Jim Brown
p.reviews__text Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque dignissimos sint quo commodi corrupti accusantium veniam saepe numquam.
.reviews__rating
svg.reviews__star.reviews__star--active
use(xlink:href='/img/icons.svg#icon-star')
svg.reviews__star.reviews__star--active
use(xlink:href='/img/icons.svg#icon-star')
svg.reviews__star.reviews__star--active
use(xlink:href='/img/icons.svg#icon-star')
svg.reviews__star.reviews__star--active
use(xlink:href='/img/icons.svg#icon-star')
svg.reviews__star.reviews__star--active
use(xlink:href='/img/icons.svg#icon-star')
section.section-cta
.cta
.cta__img.cta__img--logo
img(src='/img/logo-white.png', alt='Natours logo')
img.cta__img.cta__img--1(src='/img/tour-5-2.jpg', alt='')
img.cta__img.cta__img--2(src='/img/tour-5-1.jpg', alt='')
.cta__content
h2.heading-secondary What are you waiting for?
p.cta__text 10 days. 1 adventure. Infinite memories. Make it yours today!
if user
button.btn.btn--green.span-all-rows Book tour now!
else
a.btn.btn--green.span-all-rows(href="/login") Login to book tour

View File

@@ -0,0 +1,38 @@
const mongoose = require('mongoose');
const bookingSchema = new mongoose.Schema({
tour: {
type: mongoose.Schema.ObjectId,
ref: 'Tour',
required: [true, 'Booking must belong to a Tour!']
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: [true, 'Booking must belong to a User!']
},
price: {
type: Number,
require: [true, 'Booking must have a price.']
},
createdAt: {
type: Date,
default: Date.now()
},
paid: {
type: Boolean,
default: true
}
});
bookingSchema.pre(/^find/, function(next) {
this.populate('user').populate({
path: 'tour',
select: 'name'
});
next();
});
const Booking = mongoose.model('Booking', bookingSchema);
module.exports = Booking;

View File

@@ -0,0 +1,103 @@
// review / rating / createdAt / ref to tour / ref to user
const mongoose = require('mongoose');
const Tour = require('./tourModel');
const reviewSchema = new mongoose.Schema(
{
review: {
type: String,
required: [true, 'Review can not be empty!']
},
rating: {
type: Number,
min: 1,
max: 5
},
createdAt: {
type: Date,
default: Date.now
},
tour: {
type: mongoose.Schema.ObjectId,
ref: 'Tour',
required: [true, 'Review must belong to a tour.']
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: [true, 'Review must belong to a user']
}
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
reviewSchema.index({ tour: 1, user: 1 }, { unique: true });
reviewSchema.pre(/^find/, function(next) {
// this.populate({
// path: 'tour',
// select: 'name'
// }).populate({
// path: 'user',
// select: 'name photo'
// });
this.populate({
path: 'user',
select: 'name photo'
});
next();
});
reviewSchema.statics.calcAverageRatings = async function(tourId) {
const stats = await this.aggregate([
{
$match: { tour: tourId }
},
{
$group: {
_id: '$tour',
nRating: { $sum: 1 },
avgRating: { $avg: '$rating' }
}
}
]);
// console.log(stats);
if (stats.length > 0) {
await Tour.findByIdAndUpdate(tourId, {
ratingsQuantity: stats[0].nRating,
ratingsAverage: stats[0].avgRating
});
} else {
await Tour.findByIdAndUpdate(tourId, {
ratingsQuantity: 0,
ratingsAverage: 4.5
});
}
};
reviewSchema.post('save', function() {
// this points to current review
this.constructor.calcAverageRatings(this.tour);
});
// findByIdAndUpdate
// findByIdAndDelete
reviewSchema.pre(/^findOneAnd/, async function(next) {
this.r = await this.findOne();
// console.log(this.r);
next();
});
reviewSchema.post(/^findOneAnd/, async function() {
// await this.findOne(); does NOT work here, query has already executed
await this.r.constructor.calcAverageRatings(this.r.tour);
});
const Review = mongoose.model('Review', reviewSchema);
module.exports = Review;

View File

@@ -0,0 +1,191 @@
const mongoose = require('mongoose');
const slugify = require('slugify');
// const User = require('./userModel');
// const validator = require('validator');
const tourSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'A tour must have a name'],
unique: true,
trim: true,
maxlength: [40, 'A tour name must have less or equal then 40 characters'],
minlength: [10, 'A tour name must have more or equal then 10 characters']
// validate: [validator.isAlpha, 'Tour name must only contain characters']
},
slug: String,
duration: {
type: Number,
required: [true, 'A tour must have a duration']
},
maxGroupSize: {
type: Number,
required: [true, 'A tour must have a group size']
},
difficulty: {
type: String,
required: [true, 'A tour must have a difficulty'],
enum: {
values: ['easy', 'medium', 'difficult'],
message: 'Difficulty is either: easy, medium, difficult'
}
},
ratingsAverage: {
type: Number,
default: 4.5,
min: [1, 'Rating must be above 1.0'],
max: [5, 'Rating must be below 5.0'],
set: val => Math.round(val * 10) / 10 // 4.666666, 46.6666, 47, 4.7
},
ratingsQuantity: {
type: Number,
default: 0
},
price: {
type: Number,
required: [true, 'A tour must have a price']
},
priceDiscount: {
type: Number,
validate: {
validator: function(val) {
// this only points to current doc on NEW document creation
return val < this.price;
},
message: 'Discount price ({VALUE}) should be below regular price'
}
},
summary: {
type: String,
trim: true,
required: [true, 'A tour must have a description']
},
description: {
type: String,
trim: true
},
imageCover: {
type: String,
required: [true, 'A tour must have a cover image']
},
images: [String],
createdAt: {
type: Date,
default: Date.now(),
select: false
},
startDates: [Date],
secretTour: {
type: Boolean,
default: false
},
startLocation: {
// GeoJSON
type: {
type: String,
default: 'Point',
enum: ['Point']
},
coordinates: [Number],
address: String,
description: String
},
locations: [
{
type: {
type: String,
default: 'Point',
enum: ['Point']
},
coordinates: [Number],
address: String,
description: String,
day: Number
}
],
guides: [
{
type: mongoose.Schema.ObjectId,
ref: 'User'
}
]
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
// tourSchema.index({ price: 1 });
tourSchema.index({ price: 1, ratingsAverage: -1 });
tourSchema.index({ slug: 1 });
tourSchema.index({ startLocation: '2dsphere' });
tourSchema.virtual('durationWeeks').get(function() {
return this.duration / 7;
});
// Virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
foreignField: 'tour',
localField: '_id'
});
// DOCUMENT MIDDLEWARE: runs before .save() and .create()
tourSchema.pre('save', function(next) {
this.slug = slugify(this.name, { lower: true });
next();
});
// tourSchema.pre('save', async function(next) {
// const guidesPromises = this.guides.map(async id => await User.findById(id));
// this.guides = await Promise.all(guidesPromises);
// next();
// });
// tourSchema.pre('save', function(next) {
// console.log('Will save document...');
// next();
// });
// tourSchema.post('save', function(doc, next) {
// console.log(doc);
// next();
// });
// QUERY MIDDLEWARE
// tourSchema.pre('find', function(next) {
tourSchema.pre(/^find/, function(next) {
this.find({ secretTour: { $ne: true } });
this.start = Date.now();
next();
});
tourSchema.pre(/^find/, function(next) {
this.populate({
path: 'guides',
select: '-__v -passwordChangedAt'
});
next();
});
// tourSchema.post(/^find/, function(docs, next) {
// console.log(`Query took ${Date.now() - this.start} milliseconds!`);
// next();
// });
// AGGREGATION MIDDLEWARE
// tourSchema.pre('aggregate', function(next) {
// this.pipeline().unshift({ $match: { secretTour: { $ne: true } } });
// console.log(this.pipeline());
// next();
// });
const Tour = mongoose.model('Tour', tourSchema);
module.exports = Tour;

View File

@@ -0,0 +1,117 @@
const crypto = require('crypto');
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please tell us your name!']
},
email: {
type: String,
required: [true, 'Please provide your email'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email']
},
photo: {
type: String,
default: 'default.jpg'
},
role: {
type: String,
enum: ['user', 'guide', 'lead-guide', 'admin'],
default: 'user'
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: 8,
select: false
},
passwordConfirm: {
type: String,
required: [true, 'Please confirm your password'],
validate: {
// This only works on CREATE and SAVE!!!
validator: function(el) {
return el === this.password;
},
message: 'Passwords are not the same!'
}
},
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date,
active: {
type: Boolean,
default: true,
select: false
}
});
userSchema.pre('save', async function(next) {
// Only run this function if password was actually modified
if (!this.isModified('password')) return next();
// Hash the password with cost of 12
this.password = await bcrypt.hash(this.password, 12);
// Delete passwordConfirm field
this.passwordConfirm = undefined;
next();
});
userSchema.pre('save', function(next) {
if (!this.isModified('password') || this.isNew) return next();
this.passwordChangedAt = Date.now() - 1000;
next();
});
userSchema.pre(/^find/, function(next) {
// this points to the current query
this.find({ active: { $ne: false } });
next();
});
userSchema.methods.correctPassword = async function(
candidatePassword,
userPassword
) {
return await bcrypt.compare(candidatePassword, userPassword);
};
userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {
if (this.passwordChangedAt) {
const changedTimestamp = parseInt(
this.passwordChangedAt.getTime() / 1000,
10
);
return JWTTimestamp < changedTimestamp;
}
// False means NOT changed
return false;
};
userSchema.methods.createPasswordResetToken = function() {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// console.log({ resetToken }, this.passwordResetToken);
this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
return resetToken;
};
const User = mongoose.model('User', userSchema);
module.exports = User;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "natours",
"version": "1.0.0",
"description": "Learning node, express and mongoDB",
"main": "app.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"start:prod": "NODE_ENV=production nodemon server.js",
"debug": "ndb server.js",
"watch:js": "parcel watch ./public/js/index.js --out-dir ./public/js --out-file bundle.js",
"build:js": "parcel build ./public/js/index.js --out-dir ./public/js --out-file bundle.js"
},
"author": "Jonas Schmedtmann",
"license": "ISC",
"dependencies": {
"@babel/polyfill": "^7.4.4",
"axios": "^0.18.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"dotenv": "^7.0.0",
"express": "^4.16.4",
"express-mongo-sanitize": "^1.3.2",
"express-rate-limit": "^3.5.0",
"helmet": "^3.16.0",
"hpp": "^0.2.2",
"html-to-text": "^5.1.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.5.2",
"morgan": "^1.9.1",
"multer": "^1.4.1",
"nodemailer": "^6.1.1",
"pug": "^2.0.3",
"sharp": "^0.22.1",
"slugify": "^1.3.4",
"stripe": "^7.0.0",
"validator": "^10.11.0",
"xss-clean": "^0.1.1"
},
"devDependencies": {
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"parcel-bundler": "^1.12.3",
"prettier": "^1.17.0"
},
"engines": {
"node": "^10"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Some files were not shown because too many files have changed in this diff Show More