This commit is contained in:
2022-03-23 20:28:57 -05:00
commit 20011f687a
39 changed files with 14785 additions and 0 deletions

5
.env Normal file
View File

@@ -0,0 +1,5 @@
CLOUDINARY_KEY=925997211682149
CLOUDINARY_SECRET=rSbEA0F5GppHGXGKFiVDCXUgrIs
CLOUDINARY_CLOUD_NAME=dtgt3xx2r
MONGODB_URL=mongodb+srv://patrick:EiuJSBqTcGABVWO9@yelpcamp.pj9io.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
MAPBOX_TOKEN=pk.eyJ1IjoiMG5ud2VlIiwiYSI6ImNsMTJoNDdqYzFtb3MzYnFoZmRqYTBhcWoifQ.ZziQHzVEcmfPmqA8BaBblw

158
app.js Normal file
View File

@@ -0,0 +1,158 @@
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
const express = require('express');
const path = require('path');
const mongoose = require('mongoose');
const ejsMate = require('ejs-mate');
const session = require('express-session');
const flash = require('connect-flash');
const ExpressError = require('./utils/ExpressError');
const methodOverride = require('method-override');
const passport = require('passport');
const LocalStrategy = require('passport-local');
const User = require('./models/user');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const userRoutes = require('./routes/users');
const campgroundRoutes = require('./routes/campgrounds');
const reviewRoutes = require('./routes/reviews');
const MongoDBStore = require('connect-mongo')(session);
const dbUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017/yelp-camp';
mongoose.connect(dbUrl, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('Database connected');
});
const app = express();
app.engine('ejs', ejsMate);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.urlencoded({ extended: true }));
app.use(methodOverride('_method'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(
mongoSanitize({
replaceWith: '_',
})
);
const secret = process.env.SECRET || 'thisshouldbeabettersecret!';
const store = new MongoDBStore({
url: dbUrl,
secret,
touchAfter: 24 * 60 * 60,
});
store.on('error', function (e) {
console.log('SESSION STORE ERROR', e);
});
const sessionConfig = {
store,
name: 'session',
secret,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
// secure: true,
expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
maxAge: 1000 * 60 * 60 * 24 * 7,
},
};
app.use(session(sessionConfig));
app.use(flash());
app.use(helmet());
const scriptSrcUrls = [
'https://stackpath.bootstrapcdn.com',
'https://api.tiles.mapbox.com',
'https://api.mapbox.com',
'https://kit.fontawesome.com',
'https://cdnjs.cloudflare.com',
'https://cdn.jsdelivr.net',
];
const styleSrcUrls = [
'https://kit-free.fontawesome.com',
'https://stackpath.bootstrapcdn.com',
'https://api.mapbox.com',
'https://api.tiles.mapbox.com',
'https://fonts.googleapis.com',
'https://use.fontawesome.com',
];
const connectSrcUrls = ['https://api.mapbox.com', 'https://*.tiles.mapbox.com', 'https://events.mapbox.com'];
const fontSrcUrls = [];
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: [],
connectSrc: ["'self'", ...connectSrcUrls],
scriptSrc: ["'unsafe-inline'", "'self'", ...scriptSrcUrls],
styleSrc: ["'self'", "'unsafe-inline'", ...styleSrcUrls],
workerSrc: ["'self'", 'blob:'],
childSrc: ['blob:'],
objectSrc: [],
imgSrc: [
"'self'",
'blob:',
'data:',
'https://res.cloudinary.com/dtgt3xx2r/', //SHOULD MATCH YOUR CLOUDINARY ACCOUNT!
'https://images.unsplash.com',
],
fontSrc: ["'self'", ...fontSrcUrls],
},
})
);
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
app.use((req, res, next) => {
res.locals.currentUser = req.user;
res.locals.success = req.flash('success');
res.locals.error = req.flash('error');
next();
});
app.use('/', userRoutes);
app.use('/campgrounds', campgroundRoutes);
app.use('/campgrounds/:id/reviews', reviewRoutes);
app.get('/', (req, res) => {
res.render('home');
});
app.all('*', (req, res, next) => {
next(new ExpressError('Page Not Found', 404));
});
app.use((err, req, res, next) => {
const { statusCode = 500 } = err;
if (!err.message) err.message = 'Oh No, Something Went Wrong!';
res.status(statusCode).render('error', { err });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Serving on port ${port}`);
});

21
cloudinary/index.js Normal file
View File

@@ -0,0 +1,21 @@
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_KEY,
api_secret: process.env.CLOUDINARY_SECRET
});
const storage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'YelpCamp',
allowedFormats: ['jpeg', 'png', 'jpg']
}
});
module.exports = {
cloudinary,
storage
}

View File

@@ -0,0 +1,81 @@
const Campground = require('../models/campground');
const mbxGeocoding = require('@mapbox/mapbox-sdk/services/geocoding');
const mapBoxToken = process.env.MAPBOX_TOKEN;
const geocoder = mbxGeocoding({ accessToken: mapBoxToken });
const { cloudinary } = require('../cloudinary');
module.exports.index = async (req, res) => {
const campgrounds = await Campground.find({}).populate('popupText');
res.render('campgrounds/index', { campgrounds });
};
module.exports.renderNewForm = (req, res) => {
res.render('campgrounds/new');
};
module.exports.createCampground = async (req, res, next) => {
const geoData = await geocoder
.forwardGeocode({
query: req.body.campground.location,
limit: 1,
})
.send();
const campground = new Campground(req.body.campground);
campground.geometry = geoData.body.features[0].geometry;
campground.images = req.files.map(f => ({ url: f.path, filename: f.filename }));
campground.author = req.user._id;
await campground.save();
console.log(campground);
req.flash('success', 'Successfully made a new campground!');
res.redirect(`/campgrounds/${campground._id}`);
};
module.exports.showCampground = async (req, res) => {
const campground = await Campground.findById(req.params.id)
.populate({
path: 'reviews',
populate: {
path: 'author',
},
})
.populate('author');
if (!campground) {
req.flash('error', 'Cannot find that campground!');
return res.redirect('/campgrounds');
}
res.render('campgrounds/show', { campground });
};
module.exports.renderEditForm = async (req, res) => {
const { id } = req.params;
const campground = await Campground.findById(id);
if (!campground) {
req.flash('error', 'Cannot find that campground!');
return res.redirect('/campgrounds');
}
res.render('campgrounds/edit', { campground });
};
module.exports.updateCampground = async (req, res) => {
const { id } = req.params;
console.log(req.body);
const campground = await Campground.findByIdAndUpdate(id, { ...req.body.campground });
const imgs = req.files.map(f => ({ url: f.path, filename: f.filename }));
campground.images.push(...imgs);
await campground.save();
if (req.body.deleteImages) {
for (let filename of req.body.deleteImages) {
await cloudinary.uploader.destroy(filename);
}
await campground.updateOne({ $pull: { images: { filename: { $in: req.body.deleteImages } } } });
}
req.flash('success', 'Successfully updated campground!');
res.redirect(`/campgrounds/${campground._id}`);
};
module.exports.deleteCampground = async (req, res) => {
const { id } = req.params;
await Campground.findByIdAndDelete(id);
req.flash('success', 'Successfully deleted campground');
res.redirect('/campgrounds');
};

21
controllers/reviews.js Normal file
View File

@@ -0,0 +1,21 @@
const Campground = require('../models/campground');
const Review = require('../models/review');
module.exports.createReview = async (req, res) => {
const campground = await Campground.findById(req.params.id);
const review = new Review(req.body.review);
review.author = req.user._id;
campground.reviews.push(review);
await review.save();
await campground.save();
req.flash('success', 'Created new review!');
res.redirect(`/campgrounds/${campground._id}`);
};
module.exports.deleteReview = async (req, res) => {
const { id, reviewId } = req.params;
await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } });
await Review.findByIdAndDelete(reviewId);
req.flash('success', 'Successfully deleted review');
res.redirect(`/campgrounds/${id}`);
};

39
controllers/users.js Normal file
View File

@@ -0,0 +1,39 @@
const User = require('../models/user');
module.exports.renderRegister = (req, res) => {
res.render('users/register');
};
module.exports.register = async (req, res, next) => {
try {
const { email, username, password } = req.body;
const user = new User({ email, username });
const registeredUser = await User.register(user, password);
req.login(registeredUser, err => {
if (err) return next(err);
req.flash('success', 'Welcome to YelpCamp!');
res.redirect('/campgrounds');
});
} catch (e) {
req.flash('error', e.message);
res.redirect('register');
}
};
module.exports.renderLogin = (req, res) => {
res.render('users/login');
};
module.exports.login = (req, res) => {
req.flash('success', 'welcome back!');
const redirectUrl = req.session.returnTo || '/campgrounds';
delete req.session.returnTo;
res.redirect(redirectUrl);
};
module.exports.logout = (req, res) => {
req.logout();
// req.session.destroy();
req.flash('success', 'Goodbye!');
res.redirect('/campgrounds');
};

54
middleware.js Normal file
View File

@@ -0,0 +1,54 @@
const { campgroundSchema, reviewSchema } = require('./schemas.js');
const ExpressError = require('./utils/ExpressError');
const Campground = require('./models/campground');
const Review = require('./models/review');
module.exports.isLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
req.session.returnTo = req.originalUrl;
req.flash('error', 'You must be signed in first!');
return res.redirect('/login');
}
next();
};
module.exports.validateCampground = (req, res, next) => {
const { error } = campgroundSchema.validate(req.body);
console.log(req.body);
if (error) {
const msg = error.details.map(el => el.message).join(',');
throw new ExpressError(msg, 400);
} else {
next();
}
};
module.exports.isAuthor = async (req, res, next) => {
const { id } = req.params;
const campground = await Campground.findById(id);
if (!campground.author.equals(req.user._id)) {
req.flash('error', 'You do not have permission to do that!');
return res.redirect(`/campgrounds/${id}`);
}
next();
};
module.exports.isReviewAuthor = async (req, res, next) => {
const { id, reviewId } = req.params;
const review = await Review.findById(reviewId);
if (!review.author.equals(req.user._id)) {
req.flash('error', 'You do not have permission to do that!');
return res.redirect(`/campgrounds/${id}`);
}
next();
};
module.exports.validateReview = (req, res, next) => {
const { error } = reviewSchema.validate(req.body);
if (error) {
const msg = error.details.map(el => el.message).join(',');
throw new ExpressError(msg, 400);
} else {
next();
}
};

66
models/campground.js Normal file
View File

@@ -0,0 +1,66 @@
const mongoose = require('mongoose');
const Review = require('./review');
const Schema = mongoose.Schema;
// https://res.cloudinary.com/douqbebwk/image/upload/w_300/v1600113904/YelpCamp/gxgle1ovzd2f3dgcpass.png
const ImageSchema = new Schema({
url: String,
filename: String,
});
ImageSchema.virtual('thumbnail').get(function () {
return this.url.replace('/upload', '/upload/w_200');
});
const opts = { toJSON: { virtuals: true } };
const CampgroundSchema = new Schema(
{
title: String,
images: [ImageSchema],
geometry: {
type: {
type: String,
enum: ['Point'],
required: true,
},
coordinates: {
type: [Number],
required: true,
},
},
price: Number,
description: String,
location: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
},
reviews: [
{
type: Schema.Types.ObjectId,
ref: 'Review',
},
],
},
opts
);
CampgroundSchema.virtual('properties.popUpMarkup').get(function () {
return `
<strong><a href="/campgrounds/${this._id}">${this.title}</a><strong>
<p>${this.description.substring(0, 20)}...</p>`;
});
CampgroundSchema.post('findOneAndDelete', async function (doc) {
if (doc) {
await Review.deleteMany({
_id: {
$in: doc.reviews,
},
});
}
});
module.exports = mongoose.model('Campground', CampgroundSchema);

13
models/review.js Normal file
View File

@@ -0,0 +1,13 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const reviewSchema = new Schema({
body: String,
rating: Number,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
},
});
module.exports = mongoose.model('Review', reviewSchema);

15
models/user.js Normal file
View File

@@ -0,0 +1,15 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const passportLocalMongoose = require('passport-local-mongoose');
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true,
},
});
UserSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model('User', UserSchema);

4196
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "yelpcamp",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mapbox/mapbox-sdk": "^0.11.0",
"cloudinary": "^1.23.0",
"connect-flash": "^0.1.1",
"connect-mongo": "^3.2.0",
"dotenv": "^8.2.0",
"ejs": "^3.1.5",
"ejs-mate": "^3.0.0",
"express": "^4.17.1",
"express-mongo-sanitize": "^2.0.0",
"express-session": "^1.17.1",
"helmet": "^4.1.1",
"joi": "^17.2.1",
"method-override": "^3.0.0",
"mongoose": "^5.10.4",
"multer": "^1.4.2",
"multer-storage-cloudinary": "^4.0.0",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^6.0.1",
"sanitize-html": "^1.27.4"
}
}

View File

@@ -0,0 +1,106 @@
mapboxgl.accessToken = mapToken;
const map = new mapboxgl.Map({
container: 'cluster-map',
style: 'mapbox://styles/mapbox/light-v10',
center: [-103.59179687498357, 40.66995747013945],
zoom: 3,
});
map.addControl(new mapboxgl.NavigationControl());
map.on('load', function () {
// Add a new source from our GeoJSON data and
// set the 'cluster' option to true. GL-JS will
// add the point_count property to your source data.
map.addSource('campgrounds', {
type: 'geojson',
// Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
// from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
data: campgrounds,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50, // Radius of each cluster when clustering points (defaults to 50)
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'campgrounds',
filter: ['has', 'point_count'],
paint: {
// Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
'circle-color': ['step', ['get', 'point_count'], '#00BCD4', 10, '#2196F3', 30, '#3F51B5'],
'circle-radius': ['step', ['get', 'point_count'], 15, 10, 20, 30, 25],
},
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'campgrounds',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
},
});
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'campgrounds',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
},
});
// inspect a cluster on click
map.on('click', 'clusters', function (e) {
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters'],
});
const clusterId = features[0].properties.cluster_id;
map.getSource('campgrounds').getClusterExpansionZoom(clusterId, function (err, zoom) {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom,
});
});
});
// When a click event occurs on a feature in
// the unclustered-point layer, open a popup at
// the location of the feature, with
// description HTML from its properties.
map.on('click', 'unclustered-point', function (e) {
const { popUpMarkup } = e.features[0].properties;
const coordinates = e.features[0].geometry.coordinates.slice();
// Ensure that if the map is zoomed out such that
// multiple copies of the feature are visible, the
// popup appears over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
new mapboxgl.Popup().setLngLat(coordinates).setHTML(popUpMarkup).addTo(map);
});
map.on('mouseenter', 'clusters', function () {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', function () {
map.getCanvas().style.cursor = '';
});
});

View File

@@ -0,0 +1,14 @@
mapboxgl.accessToken = mapToken;
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10', // stylesheet location
center: campground.geometry.coordinates, // starting position [lng, lat]
zoom: 10, // starting zoom
});
map.addControl(new mapboxgl.NavigationControl());
new mapboxgl.Marker()
.setLngLat(campground.geometry.coordinates)
.setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(`<h3>${campground.title}</h3><p>${campground.location}</p>`))
.addTo(map);

View File

@@ -0,0 +1,24 @@
(function () {
'use strict';
bsCustomFileInput.init();
// Fetch all the forms we want to apply custom Bootstrap validation styles to
const forms = document.querySelectorAll('.validated-form');
// Loop over them and prevent submission
Array.from(forms).forEach(function (form) {
form.addEventListener(
'submit',
function (event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
},
false
);
});
})();

View File

@@ -0,0 +1,9 @@
#cluster-map {
width: 100%;
height: 500px;
}
#map {
width: 100%;
height: 300px;
}

View File

@@ -0,0 +1,36 @@
body {
height: 100vh;
background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
url('https://images.unsplash.com/photo-1559521783-1d1599583485?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80');
background-size: cover;
background-position: center;
text-shadow: 0 0.05rem 0.1rem rgba(0, 0, 0, 0.5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.5);
}
.cover-container {
max-width: 60vw;
}
.nav-link {
padding: 0.25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
margin-left: 1rem;
border-bottom: 0.25rem solid transparent;
}
.nav-link:hover {
color: rgba(255, 255, 255, 0.5);
border-bottom-color: rgba(255, 255, 255, 0.5);
}
.nav-link.active {
color: white;
border-bottom-color: white;
}
.btn-secondary,
.btn-secondary:hover {
color: #333;
text-shadow: none;
}

View File

@@ -0,0 +1,191 @@
.starability-result {
position: relative;
width: 150px;
height: 30px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAA8CAMAAABGivqtAAAAxlBMVEUAAACZmZn2viTHuJ72viOampqampr1viSampr3vySampqdnZ34wiX1vSSampr1vSOZmZmampr1viT2vSOampr2viT2viSampr2viSampr2vyX4vyWbm5v3vSSdnZ32wSadnZ36wCWcnJyZmZn/wSr/2ySampr2vSP2viSZmZn2vSSZmZn2vST2viSampr2viSbm5ubm5uZmZn1vSSampqbm5v2vSWampqampr3vSf5wiT5vyagoKD/xCmkpKT/yCSZmZn1vSO4V2dEAAAAQHRSTlMA+vsG9fO6uqdgRSIi7+3q39XVqZWVgnJyX09HPDw1NTAwKRkYB+jh3L6+srKijY2Ef2lpYllZUU5CKigWFQ4Oneh1twAAAZlJREFUOMuV0mdzAiEQBmDgWq4YTWIvKRqT2Htv8P//VJCTGfYQZnw/3fJ4tyO76KE0m1b2fZu+U/pu4QGlA7N+Up5PIz9d+cmkbSrSNr9seT3GKeNYIyeO5j16S28exY5suK0U/QKmmeCCX6xs22hJLVkitMImxCvEs8EG3SCRCN/ViFPqnq5epIzZ07QJJvkM9Tkz1xnkmXbfSvR7f4H8AtXBkLGj74mMvjM1+VHZpAZ4LM4K/LBWEI9jwP71v1ZEQ6dyvQMf8A/1pmdZnKce/VH1iIsdte4U8VEtY23xOujxtFpWDgKbfjD2YeEhY0OzfjGeLyO/XfnNpAcmcjDwKOXRfU1IyiTRyEkaiz67pb9oJHJb9vVqKfgjLBPyF5Sq9T0KmSUhQmtiQrJGPHVi0DoSabj31G2gW3buHd0pY85lNdcCk8xlNDPXMuSyNiwl+theIb9C7RLIpKvviYy+M6H8qGwSAp6Is19+GP6KxwnggJ/kq6Jht5rnRQA4z9zyRRaXssvyqp5I6Vutv0vkpJaJtnjpz/8B19ytIayazLoAAAAASUVORK5CYII=');
font-size: 0.1em;
color: transparent;
}
.starability-result:after {
content: ' ';
position: absolute;
left: 0;
height: 30px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAA8CAMAAABGivqtAAAAxlBMVEUAAACZmZn2viTHuJ72viOampqampr1viSampr3vySampqdnZ34wiX1vSSampr1vSOZmZmampr1viT2vSOampr2viT2viSampr2viSampr2vyX4vyWbm5v3vSSdnZ32wSadnZ36wCWcnJyZmZn/wSr/2ySampr2vSP2viSZmZn2vSSZmZn2vST2viSampr2viSbm5ubm5uZmZn1vSSampqbm5v2vSWampqampr3vSf5wiT5vyagoKD/xCmkpKT/yCSZmZn1vSO4V2dEAAAAQHRSTlMA+vsG9fO6uqdgRSIi7+3q39XVqZWVgnJyX09HPDw1NTAwKRkYB+jh3L6+srKijY2Ef2lpYllZUU5CKigWFQ4Oneh1twAAAZlJREFUOMuV0mdzAiEQBmDgWq4YTWIvKRqT2Htv8P//VJCTGfYQZnw/3fJ4tyO76KE0m1b2fZu+U/pu4QGlA7N+Up5PIz9d+cmkbSrSNr9seT3GKeNYIyeO5j16S28exY5suK0U/QKmmeCCX6xs22hJLVkitMImxCvEs8EG3SCRCN/ViFPqnq5epIzZ07QJJvkM9Tkz1xnkmXbfSvR7f4H8AtXBkLGj74mMvjM1+VHZpAZ4LM4K/LBWEI9jwP71v1ZEQ6dyvQMf8A/1pmdZnKce/VH1iIsdte4U8VEtY23xOujxtFpWDgKbfjD2YeEhY0OzfjGeLyO/XfnNpAcmcjDwKOXRfU1IyiTRyEkaiz67pb9oJHJb9vVqKfgjLBPyF5Sq9T0KmSUhQmtiQrJGPHVi0DoSabj31G2gW3buHd0pY85lNdcCk8xlNDPXMuSyNiwl+theIb9C7RLIpKvviYy+M6H8qGwSAp6Is19+GP6KxwnggJ/kq6Jht5rnRQA4z9zyRRaXssvyqp5I6Vutv0vkpJaJtnjpz/8B19ytIayazLoAAAAASUVORK5CYII=');
background-position: 0 -30px;
}
.starability-result[data-rating='5']::after {
width: 150px;
}
.starability-result[data-rating='4']::after {
width: 120px;
}
.starability-result[data-rating='3']::after {
width: 90px;
}
.starability-result[data-rating='2']::after {
width: 60px;
}
.starability-result[data-rating='1']::after {
width: 30px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2), screen and (min-resolution: 192dpi) {
.starability-result {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAB4CAMAAACZ62E6AAABAlBMVEUAAACZmZmampr2vSObm5v/yiufn5+ampr1viP1viSZmZn2viOZmZmampqampr2viSampqampqcnJz5vyScnJz3wSf/wyn/xiujo6Oqqqr/0C/1vSOampr2viP2viOampr2viP2vST2viOampqampqampr1vyP3viSampr2vyT4vyX3viSbm5ubm5v5wCT8xSmgoKCampqampr3vyb2wiWenp72viOampqZmZmampr2viP2viP1viSampqbm5v2vyT3viObm5v4vyadnZ34wSSbm5v2viSZmZn2viP2vST2viP2viT1viOZmZn2viT2viX3viT3vyb2vyOZmZn1vSOZmZlNN+fKAAAAVHRSTlMA9uz4PQwS8O7r5+fTw4yMelw2MB0dFRELBgbS+/Hfu7uxqKWdg4N7ZmZMPi8pKRgPs0w7Nhb14drKw6Gck21tXkNDIyMZ1rDLycTBtaqVknlfV0sGP8ZwAAADW0lEQVRYw9zWvYqDQBSG4TPDoCAqKhYKQgoVLFaIgZCkiCBBUqVazv3fyu4aEXWdM85Uy779A+LP58AfTQgw73AwtxFiZIwbxMbUfuB3H4b49YNfZrbGodoI52+cm9hH9sbZwwAXOFbo2zjDsSzWxnecuuvaM8MpdtbEPs7y9azF5phZWrjERaWOPdpLbB81cICrgv3W4mvMLbU6RmFQeA5u5HhFEEbHLdWLsMxvHJXxW16Goh+ZqPyny1Az5j79SsCJoWHsBNAxQ9sNF26bWFuMC8v1LY+mmeTadjaqtaNnnXoxWBcde1nNWnzdb68xrOqvu22/MTzuPutujpJ122NvluSb8tTWk85CclDZQwLS0oa2TQpEKacsJy0kSJaQOKJxROKKxhWJ7zS+k9ijsUdim8Y2ZWNUFBP4pMKfOv8onX9WrsI5gd3VVLXtatxcuU0znGUHCUAS2DgrS6mT6hTzrXEjfIZj5Dk2xKkihqm4wKlQfQRqalhUP9UHo3FIPAG/Et44JVLsDDf0JHmB3OEByOwZES8hSAsviGjBdh3ylh6plmMnW4IyAUVJWcE/76vTell1EIaiMBwIAcWBA9GC0lIdKFXQQUsHVVCklN7ojf3+z3JOxYqK2TH555+K6CJJQtRbr9XtDmCnjH0AX9Va8J+liIMvDtRsCk2pEs6hKVexR2g7KuDihwt5a9MfprY0fkLXU9ZmFLpoJolN6GXKWWfZx0tHCocwKJSxC22ItYUEjmBUJHFjfYz1xQxlfaLiZsBExq2IPtbkNbLtOwwuGgjTLkH43mYtSzam7+1Bsr3nm5uExBQUozEh9V7N7uvmwZcqdpm0C6vJW63bZEuXtbrV2zpDzhrpYLBWMnY1mjV7JWFtMio7zbWniWFxvHnWm1yGxXmOPXP+L3YV2ysjnNhaZNeMcHPvuL27BMnVMaujljBAYyje4niH4g2ONyh+4PiB4gOODyjWcKxh1gZBNoJjEY4R/BLhF4IDEQ4QPBoEoyxH4+bxrUsHyxwxQlg0WHXqYifVLmo67cKY/UtaXFxBV26TLjuHrkp8BPJTMij1xQejdkgO24nf7dBOCRcbzQuNOR9Qs64GzzrfQa8It2oFAA6Zrga9xEeq1KHmLUHIiCAWInsg1x/MLqkMsItF8QAAAABJRU5ErkJggg==');
background-size: 30px auto;
}
.starability-result:after {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAB4CAMAAACZ62E6AAABAlBMVEUAAACZmZmampr2vSObm5v/yiufn5+ampr1viP1viSZmZn2viOZmZmampqampr2viSampqampqcnJz5vyScnJz3wSf/wyn/xiujo6Oqqqr/0C/1vSOampr2viP2viOampr2viP2vST2viOampqampqampr1vyP3viSampr2vyT4vyX3viSbm5ubm5v5wCT8xSmgoKCampqampr3vyb2wiWenp72viOampqZmZmampr2viP2viP1viSampqbm5v2vyT3viObm5v4vyadnZ34wSSbm5v2viSZmZn2viP2vST2viP2viT1viOZmZn2viT2viX3viT3vyb2vyOZmZn1vSOZmZlNN+fKAAAAVHRSTlMA9uz4PQwS8O7r5+fTw4yMelw2MB0dFRELBgbS+/Hfu7uxqKWdg4N7ZmZMPi8pKRgPs0w7Nhb14drKw6Gck21tXkNDIyMZ1rDLycTBtaqVknlfV0sGP8ZwAAADW0lEQVRYw9zWvYqDQBSG4TPDoCAqKhYKQgoVLFaIgZCkiCBBUqVazv3fyu4aEXWdM85Uy779A+LP58AfTQgw73AwtxFiZIwbxMbUfuB3H4b49YNfZrbGodoI52+cm9hH9sbZwwAXOFbo2zjDsSzWxnecuuvaM8MpdtbEPs7y9azF5phZWrjERaWOPdpLbB81cICrgv3W4mvMLbU6RmFQeA5u5HhFEEbHLdWLsMxvHJXxW16Goh+ZqPyny1Az5j79SsCJoWHsBNAxQ9sNF26bWFuMC8v1LY+mmeTadjaqtaNnnXoxWBcde1nNWnzdb68xrOqvu22/MTzuPutujpJ122NvluSb8tTWk85CclDZQwLS0oa2TQpEKacsJy0kSJaQOKJxROKKxhWJ7zS+k9ijsUdim8Y2ZWNUFBP4pMKfOv8onX9WrsI5gd3VVLXtatxcuU0znGUHCUAS2DgrS6mT6hTzrXEjfIZj5Dk2xKkihqm4wKlQfQRqalhUP9UHo3FIPAG/Et44JVLsDDf0JHmB3OEByOwZES8hSAsviGjBdh3ylh6plmMnW4IyAUVJWcE/76vTell1EIaiMBwIAcWBA9GC0lIdKFXQQUsHVVCklN7ojf3+z3JOxYqK2TH555+K6CJJQtRbr9XtDmCnjH0AX9Va8J+liIMvDtRsCk2pEs6hKVexR2g7KuDihwt5a9MfprY0fkLXU9ZmFLpoJolN6GXKWWfZx0tHCocwKJSxC22ItYUEjmBUJHFjfYz1xQxlfaLiZsBExq2IPtbkNbLtOwwuGgjTLkH43mYtSzam7+1Bsr3nm5uExBQUozEh9V7N7uvmwZcqdpm0C6vJW63bZEuXtbrV2zpDzhrpYLBWMnY1mjV7JWFtMio7zbWniWFxvHnWm1yGxXmOPXP+L3YV2ysjnNhaZNeMcHPvuL27BMnVMaujljBAYyje4niH4g2ONyh+4PiB4gOODyjWcKxh1gZBNoJjEY4R/BLhF4IDEQ4QPBoEoyxH4+bxrUsHyxwxQlg0WHXqYifVLmo67cKY/UtaXFxBV26TLjuHrkp8BPJTMij1xQejdkgO24nf7dBOCRcbzQuNOR9Qs64GzzrfQa8It2oFAA6Zrga9xEeq1KHmLUHIiCAWInsg1x/MLqkMsItF8QAAAABJRU5ErkJggg==');
background-size: 30px auto;
}
}
.starability-basic {
display: block;
position: relative;
width: 150px;
min-height: 60px;
padding: 0;
border: none;
}
.starability-basic > input {
position: absolute;
margin-right: -100%;
opacity: 0;
}
.starability-basic > input:checked ~ label,
.starability-basic > input:focus ~ label {
background-position: 0 0;
}
.starability-basic > input:checked + label,
.starability-basic > input:focus + label {
background-position: 0 -30px;
}
.starability-basic > input[disabled]:hover + label {
cursor: default;
}
.starability-basic > input:not([disabled]):hover ~ label {
background-position: 0 0;
}
.starability-basic > input:not([disabled]):hover + label {
background-position: 0 -30px;
}
.starability-basic > input:not([disabled]):hover + label::before {
opacity: 1;
}
.starability-basic > input:focus + label {
outline: 1px dotted #999;
}
.starability-basic .starability-focus-ring {
position: absolute;
left: 0;
width: 100%;
height: 30px;
outline: 2px dotted #999;
pointer-events: none;
opacity: 0;
}
.starability-basic > .input-no-rate:focus ~ .starability-focus-ring {
opacity: 1;
}
.starability-basic > label {
position: relative;
display: inline-block;
float: left;
width: 30px;
height: 30px;
font-size: 0.1em;
color: transparent;
cursor: pointer;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAA8CAMAAABGivqtAAAAxlBMVEUAAACZmZn2viTHuJ72viOampqampr1viSampr3vySampqdnZ34wiX1vSSampr1vSOZmZmampr1viT2vSOampr2viT2viSampr2viSampr2vyX4vyWbm5v3vSSdnZ32wSadnZ36wCWcnJyZmZn/wSr/2ySampr2vSP2viSZmZn2vSSZmZn2vST2viSampr2viSbm5ubm5uZmZn1vSSampqbm5v2vSWampqampr3vSf5wiT5vyagoKD/xCmkpKT/yCSZmZn1vSO4V2dEAAAAQHRSTlMA+vsG9fO6uqdgRSIi7+3q39XVqZWVgnJyX09HPDw1NTAwKRkYB+jh3L6+srKijY2Ef2lpYllZUU5CKigWFQ4Oneh1twAAAZlJREFUOMuV0mdzAiEQBmDgWq4YTWIvKRqT2Htv8P//VJCTGfYQZnw/3fJ4tyO76KE0m1b2fZu+U/pu4QGlA7N+Up5PIz9d+cmkbSrSNr9seT3GKeNYIyeO5j16S28exY5suK0U/QKmmeCCX6xs22hJLVkitMImxCvEs8EG3SCRCN/ViFPqnq5epIzZ07QJJvkM9Tkz1xnkmXbfSvR7f4H8AtXBkLGj74mMvjM1+VHZpAZ4LM4K/LBWEI9jwP71v1ZEQ6dyvQMf8A/1pmdZnKce/VH1iIsdte4U8VEtY23xOujxtFpWDgKbfjD2YeEhY0OzfjGeLyO/XfnNpAcmcjDwKOXRfU1IyiTRyEkaiz67pb9oJHJb9vVqKfgjLBPyF5Sq9T0KmSUhQmtiQrJGPHVi0DoSabj31G2gW3buHd0pY85lNdcCk8xlNDPXMuSyNiwl+theIb9C7RLIpKvviYy+M6H8qGwSAp6Is19+GP6KxwnggJ/kq6Jht5rnRQA4z9zyRRaXssvyqp5I6Vutv0vkpJaJtnjpz/8B19ytIayazLoAAAAASUVORK5CYII=');
background-repeat: no-repeat;
background-position: 0 -30px;
}
.starability-basic > label::before {
content: '';
position: absolute;
display: block;
height: 30px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAA8CAMAAABGivqtAAAAxlBMVEUAAACZmZn2viTHuJ72viOampqampr1viSampr3vySampqdnZ34wiX1vSSampr1vSOZmZmampr1viT2vSOampr2viT2viSampr2viSampr2vyX4vyWbm5v3vSSdnZ32wSadnZ36wCWcnJyZmZn/wSr/2ySampr2vSP2viSZmZn2vSSZmZn2vST2viSampr2viSbm5ubm5uZmZn1vSSampqbm5v2vSWampqampr3vSf5wiT5vyagoKD/xCmkpKT/yCSZmZn1vSO4V2dEAAAAQHRSTlMA+vsG9fO6uqdgRSIi7+3q39XVqZWVgnJyX09HPDw1NTAwKRkYB+jh3L6+srKijY2Ef2lpYllZUU5CKigWFQ4Oneh1twAAAZlJREFUOMuV0mdzAiEQBmDgWq4YTWIvKRqT2Htv8P//VJCTGfYQZnw/3fJ4tyO76KE0m1b2fZu+U/pu4QGlA7N+Up5PIz9d+cmkbSrSNr9seT3GKeNYIyeO5j16S28exY5suK0U/QKmmeCCX6xs22hJLVkitMImxCvEs8EG3SCRCN/ViFPqnq5epIzZ07QJJvkM9Tkz1xnkmXbfSvR7f4H8AtXBkLGj74mMvjM1+VHZpAZ4LM4K/LBWEI9jwP71v1ZEQ6dyvQMf8A/1pmdZnKce/VH1iIsdte4U8VEtY23xOujxtFpWDgKbfjD2YeEhY0OzfjGeLyO/XfnNpAcmcjDwKOXRfU1IyiTRyEkaiz67pb9oJHJb9vVqKfgjLBPyF5Sq9T0KmSUhQmtiQrJGPHVi0DoSabj31G2gW3buHd0pY85lNdcCk8xlNDPXMuSyNiwl+theIb9C7RLIpKvviYy+M6H8qGwSAp6Is19+GP6KxwnggJ/kq6Jht5rnRQA4z9zyRRaXssvyqp5I6Vutv0vkpJaJtnjpz/8B19ytIayazLoAAAAASUVORK5CYII=');
background-position: 0 30px;
pointer-events: none;
opacity: 0;
}
.starability-basic > label:nth-of-type(5)::before {
width: 120px;
left: -120px;
}
.starability-basic > label:nth-of-type(4)::before {
width: 90px;
left: -90px;
}
.starability-basic > label:nth-of-type(3)::before {
width: 60px;
left: -60px;
}
.starability-basic > label:nth-of-type(2)::before {
width: 30px;
left: -30px;
}
.starability-basic > label:nth-of-type(1)::before {
width: 0px;
left: 0px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2), screen and (min-resolution: 192dpi) {
.starability-basic > label {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAB4CAMAAACZ62E6AAABAlBMVEUAAACZmZmampr2vSObm5v/yiufn5+ampr1viP1viSZmZn2viOZmZmampqampr2viSampqampqcnJz5vyScnJz3wSf/wyn/xiujo6Oqqqr/0C/1vSOampr2viP2viOampr2viP2vST2viOampqampqampr1vyP3viSampr2vyT4vyX3viSbm5ubm5v5wCT8xSmgoKCampqampr3vyb2wiWenp72viOampqZmZmampr2viP2viP1viSampqbm5v2vyT3viObm5v4vyadnZ34wSSbm5v2viSZmZn2viP2vST2viP2viT1viOZmZn2viT2viX3viT3vyb2vyOZmZn1vSOZmZlNN+fKAAAAVHRSTlMA9uz4PQwS8O7r5+fTw4yMelw2MB0dFRELBgbS+/Hfu7uxqKWdg4N7ZmZMPi8pKRgPs0w7Nhb14drKw6Gck21tXkNDIyMZ1rDLycTBtaqVknlfV0sGP8ZwAAADW0lEQVRYw9zWvYqDQBSG4TPDoCAqKhYKQgoVLFaIgZCkiCBBUqVazv3fyu4aEXWdM85Uy779A+LP58AfTQgw73AwtxFiZIwbxMbUfuB3H4b49YNfZrbGodoI52+cm9hH9sbZwwAXOFbo2zjDsSzWxnecuuvaM8MpdtbEPs7y9azF5phZWrjERaWOPdpLbB81cICrgv3W4mvMLbU6RmFQeA5u5HhFEEbHLdWLsMxvHJXxW16Goh+ZqPyny1Az5j79SsCJoWHsBNAxQ9sNF26bWFuMC8v1LY+mmeTadjaqtaNnnXoxWBcde1nNWnzdb68xrOqvu22/MTzuPutujpJ122NvluSb8tTWk85CclDZQwLS0oa2TQpEKacsJy0kSJaQOKJxROKKxhWJ7zS+k9ijsUdim8Y2ZWNUFBP4pMKfOv8onX9WrsI5gd3VVLXtatxcuU0znGUHCUAS2DgrS6mT6hTzrXEjfIZj5Dk2xKkihqm4wKlQfQRqalhUP9UHo3FIPAG/Et44JVLsDDf0JHmB3OEByOwZES8hSAsviGjBdh3ylh6plmMnW4IyAUVJWcE/76vTell1EIaiMBwIAcWBA9GC0lIdKFXQQUsHVVCklN7ojf3+z3JOxYqK2TH555+K6CJJQtRbr9XtDmCnjH0AX9Va8J+liIMvDtRsCk2pEs6hKVexR2g7KuDihwt5a9MfprY0fkLXU9ZmFLpoJolN6GXKWWfZx0tHCocwKJSxC22ItYUEjmBUJHFjfYz1xQxlfaLiZsBExq2IPtbkNbLtOwwuGgjTLkH43mYtSzam7+1Bsr3nm5uExBQUozEh9V7N7uvmwZcqdpm0C6vJW63bZEuXtbrV2zpDzhrpYLBWMnY1mjV7JWFtMio7zbWniWFxvHnWm1yGxXmOPXP+L3YV2ysjnNhaZNeMcHPvuL27BMnVMaujljBAYyje4niH4g2ONyh+4PiB4gOODyjWcKxh1gZBNoJjEY4R/BLhF4IDEQ4QPBoEoyxH4+bxrUsHyxwxQlg0WHXqYifVLmo67cKY/UtaXFxBV26TLjuHrkp8BPJTMij1xQejdkgO24nf7dBOCRcbzQuNOR9Qs64GzzrfQa8It2oFAA6Zrga9xEeq1KHmLUHIiCAWInsg1x/MLqkMsItF8QAAAABJRU5ErkJggg==');
background-size: 30px auto;
}
}
@media screen and (-ms-high-contrast: active) {
.starability-basic {
width: auto;
}
.starability-basic > input {
position: static;
margin-right: 0;
opacity: 1;
}
.starability-basic .input-no-rate {
display: none;
}
.starability-basic > label {
display: inline;
float: none;
width: auto;
height: auto;
font-size: 1em;
color: inherit;
background: none;
}
.starability-basic > label::before,
.starability-basic > label::after {
display: none;
}
}

24
routes/campgrounds.js Normal file
View File

@@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const campgrounds = require('../controllers/campgrounds');
const catchAsync = require('../utils/catchAsync');
const { isLoggedIn, isAuthor, validateCampground } = require('../middleware');
const multer = require('multer');
const { storage } = require('../cloudinary');
const upload = multer({ storage });
const Campground = require('../models/campground');
router.route('/').get(catchAsync(campgrounds.index)).post(isLoggedIn, upload.array('image'), validateCampground, catchAsync(campgrounds.createCampground));
router.get('/new', isLoggedIn, campgrounds.renderNewForm);
router
.route('/:id')
.get(catchAsync(campgrounds.showCampground))
.put(isLoggedIn, isAuthor, upload.array('image'), validateCampground, catchAsync(campgrounds.updateCampground))
.delete(isLoggedIn, isAuthor, catchAsync(campgrounds.deleteCampground));
router.get('/:id/edit', isLoggedIn, isAuthor, catchAsync(campgrounds.renderEditForm));
module.exports = router;

14
routes/reviews.js Normal file
View File

@@ -0,0 +1,14 @@
const express = require('express');
const router = express.Router({ mergeParams: true });
const { validateReview, isLoggedIn, isReviewAuthor } = require('../middleware');
const Campground = require('../models/campground');
const Review = require('../models/review');
const reviews = require('../controllers/reviews');
const ExpressError = require('../utils/ExpressError');
const catchAsync = require('../utils/catchAsync');
router.post('/', isLoggedIn, validateReview, catchAsync(reviews.createReview));
router.delete('/:reviewId', isLoggedIn, isReviewAuthor, catchAsync(reviews.deleteReview));
module.exports = router;

17
routes/users.js Normal file
View File

@@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const passport = require('passport');
const catchAsync = require('../utils/catchAsync');
const User = require('../models/user');
const users = require('../controllers/users');
router.route('/register').get(users.renderRegister).post(catchAsync(users.register));
router
.route('/login')
.get(users.renderLogin)
.post(passport.authenticate('local', { failureFlash: true, failureRedirect: '/login' }), users.login);
router.get('/logout', users.logout);
module.exports = router;

41
schemas.js Normal file
View File

@@ -0,0 +1,41 @@
const BaseJoi = require('joi');
const sanitizeHtml = require('sanitize-html');
const extension = joi => ({
type: 'string',
base: joi.string(),
messages: {
'string.escapeHTML': '{{#label}} must not include HTML!',
},
rules: {
escapeHTML: {
validate(value, helpers) {
const clean = sanitizeHtml(value, {
allowedTags: [],
allowedAttributes: {},
});
if (clean !== value) return helpers.error('string.escapeHTML', { value });
return clean;
},
},
},
});
const Joi = BaseJoi.extend(extension);
module.exports.campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required().escapeHTML(),
price: Joi.number().required().min(0),
location: Joi.string().required().escapeHTML(),
description: Joi.string().required().escapeHTML(),
}).required(),
deleteImages: Joi.array(),
});
module.exports.reviewSchema = Joi.object({
review: Joi.object({
rating: Joi.number().required().min(1).max(5),
body: Joi.string().required().escapeHTML(),
}).required(),
});

9002
seeds/cities.js Normal file

File diff suppressed because it is too large Load Diff

55
seeds/index.js Normal file
View File

@@ -0,0 +1,55 @@
const mongoose = require('mongoose');
const cities = require('./cities');
const { places, descriptors } = require('./seedHelpers');
const Campground = require('../models/campground');
mongoose.connect('mongodb://localhost:27017/yelp-camp', {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('Database connected');
});
const sample = array => array[Math.floor(Math.random() * array.length)];
const seedDB = async () => {
await Campground.deleteMany({});
for (let i = 0; i < 300; i++) {
const random1000 = Math.floor(Math.random() * 1000);
const price = Math.floor(Math.random() * 20) + 10;
const camp = new Campground({
//YOUR USER ID
author: '5f5c330c2cd79d538f2c66d9',
location: `${cities[random1000].city}, ${cities[random1000].state}`,
title: `${sample(descriptors)} ${sample(places)}`,
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam dolores vero perferendis laudantium, consequuntur voluptatibus nulla architecto, sit soluta esse iure sed labore ipsam a cum nihil atque molestiae deserunt!',
price,
geometry: {
type: 'Point',
coordinates: [cities[random1000].longitude, cities[random1000].latitude],
},
images: [
{
url: 'https://res.cloudinary.com/douqbebwk/image/upload/v1600060601/YelpCamp/ahfnenvca4tha00h2ubt.png',
filename: 'YelpCamp/ahfnenvca4tha00h2ubt',
},
{
url: 'https://res.cloudinary.com/douqbebwk/image/upload/v1600060601/YelpCamp/ruyoaxgf72nzpi4y6cdi.png',
filename: 'YelpCamp/ruyoaxgf72nzpi4y6cdi',
},
],
});
await camp.save();
}
};
seedDB().then(() => {
mongoose.connection.close();
});

44
seeds/seedHelpers.js Normal file
View File

@@ -0,0 +1,44 @@
module.exports.descriptors = [
'Forest',
'Ancient',
'Petrified',
'Roaring',
'Cascade',
'Tumbling',
'Silent',
'Redwood',
'Bullfrog',
'Maple',
'Misty',
'Elk',
'Grizzly',
'Ocean',
'Sea',
'Sky',
'Dusty',
'Diamond',
];
module.exports.places = [
'Flats',
'Village',
'Canyon',
'Pond',
'Group Camp',
'Horse Camp',
'Ghost Town',
'Camp',
'Dispersed Camp',
'Backcountry',
'River',
'Creek',
'Creekside',
'Bay',
'Spring',
'Bayshore',
'Sands',
'Mule Camp',
'Hunting Camp',
'Cliffs',
'Hollow',
];

9
utils/ExpressError.js Normal file
View File

@@ -0,0 +1,9 @@
class ExpressError extends Error {
constructor(message, statusCode) {
super();
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = ExpressError;

5
utils/catchAsync.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = func => {
return (req, res, next) => {
func(req, res, next).catch(next);
};
};

View File

@@ -0,0 +1,66 @@
<% layout('layouts/boilerplate')%>
<div class="row">
<h1 class="text-center">Edit Campground</h1>
<div class="col-md-6 offset-md-3">
<form action="/campgrounds/<%=campground._id%>?_method=PUT" method="POST" novalidate class="validated-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label" for="title">Title</label>
<input class="form-control" type="text" id="title" name="campground[title]" value="<%=campground.title %>" required />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="location">Location</label>
<input class="form-control" type="text" id="location" name="campground[location]" value="<%=campground.location %>" required />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="price">Campground Price</label>
<div class="input-group">
<span class="input-group-text" id="price-label">$</span>
<input
type="text"
class="form-control"
id="price"
placeholder="0.00"
aria-label="price"
aria-describedby="price-label"
name="campground[price]"
value="<%=campground.price %>"
required
/>\
<div class="valid-feedback">Looks good!</div>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="description">Description</label>
<textarea class="form-control" type="text" id="description" name="campground[description]" required><%= campground.description%></textarea>
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<div class="form-file custom-file">
<input type="file" class="form-file-input" id="image" name="image" multiple />
<label class="form-file-label" for="image">
<span class="form-file-text custom-file-label">Add more image(s)...</span>
<span class="form-file-button">Browse</span>
</label>
</div>
</div>
<div class="mb-3">
<% campground.images.forEach(function(img, i) { %>
<img src="<%=img.thumbnail %>" class="img-thumbnail" alt="" />
<div class="form-check-inline">
<input type="checkbox" id="image-<%=i%>" name="deleteImages[]" value="<%=img.filename%>" />
</div>
<label for="image-<%=i%>">Delete?</label>
<% })%>
</div>
<div class="mb-3">
<button class="btn btn-info">Update Campground</button>
</div>
</form>
<a href="/campgrounds/<%= campground._id%>">Back To Campground</a>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<% layout('layouts/boilerplate')%>
<div id="cluster-map"></div>
<div class="container">
<h1>All Campgrounds</h1>
<% for (let campground of campgrounds){%>
<div class="card mb-3">
<div class="row">
<div class="col-md-4">
<%if(campground.images.length) {%>
<img class="img-fluid" alt="" src="<%=campground.images[0].url%>" />
<% }else {%>
<img class="img-fluid" alt="" src="https://res.cloudinary.com/douqbebwk/image/upload/v1600103881/YelpCamp/lz8jjv2gyynjil7lswf4.png" />
<% } %>
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title"><%= campground.title %></h5>
<p class="card-text"><%= campground.description %></p>
<p class="card-text">
<small class="text-muted"><%= campground.location%></small>
</p>
<a class="btn btn-primary" href="/campgrounds/<%=campground._id%>">View <%=campground.title%></a>
</div>
</div>
</div>
</div>
<% }%>
</div>
<script>
const mapToken = '<%-process.env.MAPBOX_TOKEN%>';
const campgrounds = { features: <%- JSON.stringify(campgrounds) %>}
</script>
<script src="/javascripts/clusterMap.js"></script>

62
views/campgrounds/new.ejs Normal file
View File

@@ -0,0 +1,62 @@
<% layout('layouts/boilerplate')%>
<div class="row">
<h1 class="text-center">New Campground</h1>
<div class="col-md-6 offset-md-3">
<form action="/campgrounds" method="POST" novalidate class="validated-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label" for="title">Title</label>
<input class="form-control" type="text" id="title" name="campground[title]" required />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="location">Location</label>
<input class="form-control" type="text" id="location" name="campground[location]" required />
<div class="valid-feedback">Looks good!</div>
</div>
<!-- <div class="mb-3">
<label class="form-label" for="image">Image Url</label>
<input class="form-control" type="text" id="image" name="campground[image]" required>
<div class="valid-feedback">
Looks good!
</div>
</div> -->
<div class="mb-3">
<label class="form-label" for="price">Campground Price</label>
<div class="input-group">
<span class="input-group-text" id="price-label">$</span>
<input
type="text"
class="form-control"
id="price"
placeholder="0.00"
aria-label="price"
aria-describedby="price-label"
name="campground[price]"
required
/>
</div>
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="description">Description</label>
<textarea class="form-control" type="text" id="description" name="campground[description]" required></textarea>
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<div class="form-file custom-file">
<input type="file" class="form-file-input" id="image" name="image" multiple />
<label class="form-file-label" for="image">
<span class="form-file-text custom-file-label">Choose image(s)...</span>
<span class="form-file-button">Browse</span>
</label>
</div>
</div>
<div class="mb-3">
<button class="btn btn-success">Add Campground</button>
</div>
</form>
<a href="/campgrounds">All Campgrounds</a>
</div>
</div>

View File

@@ -0,0 +1,99 @@
<% layout('layouts/boilerplate')%>
<link rel="stylesheet" href="/stylesheets/stars.css" />
<div class="row">
<div class="col-6">
<div id="campgroundCarousel" class="carousel slide" data-ride="carousel">
<div class="carousel-inner">
<% campground.images.forEach((img, i) => { %>
<div class="carousel-item <%= i === 0 ? 'active' : ''%>">
<img src="<%= img.url%>" class="d-block w-100" alt="" />
</div>
<% }) %>
</div>
<% if(campground.images.length > 1) {%>
<a class="carousel-control-prev" href="#campgroundCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#campgroundCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
<% } %>
</div>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><%= campground.title%></h5>
<p class="card-text"><%= campground.description%></p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item text-muted"><%= campground.location%></li>
<li class="list-group-item">Submitted by <%= campground.author.username%></li>
<li class="list-group-item">$<%= campground.price%>/night</li>
</ul>
<% if( currentUser && campground.author.equals(currentUser._id)) {%>
<div class="card-body">
<a class="card-link btn btn-info" href="/campgrounds/<%=campground._id%>/edit">Edit</a>
<form class="d-inline" action="/campgrounds/<%=campground._id%>?_method=DELETE" method="POST">
<button class="btn btn-danger">Delete</button>
</form>
</div>
<% } %>
<div class="card-footer text-muted">2 days ago</div>
</div>
</div>
<div class="col-6">
<div id="map"></div>
<% if(currentUser){ %>
<h2>Leave a Review</h2>
<form action="/campgrounds/<%=campground._id%>/reviews" method="POST" class="mb-3 validated-form" novalidate>
<!-- <div class="mb-3">
<label class="form-label" for="rating">Rating</label>
<input class="form-range" type="range" min="1" max="5" name="review[rating]" id="rating">
</div> -->
<fieldset class="starability-basic">
<input type="radio" id="no-rate" class="input-no-rate" name="review[rating]" value="1" checked aria-label="No rating." />
<input type="radio" id="first-rate1" name="review[rating]" value="1" />
<label for="first-rate1" title="Terrible">1 star</label>
<input type="radio" id="first-rate2" name="review[rating]" value="2" />
<label for="first-rate2" title="Not good">2 stars</label>
<input type="radio" id="first-rate3" name="review[rating]" value="3" />
<label for="first-rate3" title="Average">3 stars</label>
<input type="radio" id="first-rate4" name="review[rating]" value="4" />
<label for="first-rate4" title="Very good">4 stars</label>
<input type="radio" id="first-rate5" name="review[rating]" value="5" />
<label for="first-rate5" title="Amazing">5 stars</label>
</fieldset>
<div class="mb-3">
<label class="form-label" for="body">Review Text</label>
<textarea class="form-control" name="review[body]" id="body" cols="30" rows="3" required></textarea>
<div class="valid-feedback">Looks good!</div>
</div>
<button class="btn btn-success">Submit</button>
</form>
<% } %> <% for(let review of campground.reviews) { %>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><%= review.author.username%></h5>
<p class="starability-result" data-rating="<%=review.rating%>">Rated: <%= review.rating %> stars</p>
<!-- <h6 class="card-subtitle mb-2 text-muted">By <%= review.author.username%></h6> -->
<p class="card-text">Review: <%= review.body %></p>
<% if( currentUser && review.author.equals(currentUser._id)) {%>
<form action="/campgrounds/<%=campground._id%>/reviews/<%=review._id%>?_method=DELETE" method="POST">
<button class="btn btn-sm btn-danger">Delete</button>
</form>
<% } %>
</div>
</div>
<% } %>
</div>
</div>
<script>
const mapToken = '<%-process.env.MAPBOX_TOKEN%>';
const campground = <%- JSON.stringify(campground) %>
</script>
<script src="/javascripts/showPageMap.js"></script>

11
views/error.ejs Normal file
View File

@@ -0,0 +1,11 @@
<% layout('layouts/boilerplate')%>
<div class="row">
<div class="col-6 offset-3">
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading"><%=err.message%></h4>
<% if (process.env.NODE_ENV !== "production") { %>
<p><%= err.stack %></p>
<% } %>
</div>
</div>
</div>

59
views/home.ejs Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YelpCamp</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/stylesheets/home.css" />
</head>
<body class="d-flex text-center text-white bg-dark">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="mb-auto">
<div>
<h3 class="float-md-left mb-0">YelpCamp</h3>
<nav class="nav nav-masthead justify-content-center float-md-right">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="/campgrounds">Campgrounds</a>
<% if(!currentUser) { %>
<a class="nav-link" href="/login">Login</a>
<a class="nav-link" href="/register">Register</a>
<% } else { %>
<a class="nav-link" href="/logout">Logout</a>
<% } %>
</nav>
</div>
</header>
<main class="px-3">
<h1>YelpCamp</h1>
<p class="lead">
Welcome to YelpCamp! <br />
Jump right in and explore our many campgrounds. <br />
Feel free to share some of your own and comment on others!
</p>
<a href="/campgrounds" class="btn btn-lg btn-secondary font-weight-bold border-white bg-white">View Campgrounds</a>
</main>
<footer class="mt-auto text-white-50">
<p>&copy; 2020</p>
</footer>
</div>
<script
src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js"
integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/"
crossorigin="anonymous"
></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YelpCamp</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I"
crossorigin="anonymous"
/>
<script src="https://cdn.jsdelivr.net/npm/bs-custom-file-input/dist/bs-custom-file-input.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet" />
<link rel="stylesheet" href="/stylesheets/app.css" />
</head>
<body class="d-flex flex-column vh-100">
<%- include('../partials/navbar')%>
<main class="container mt-5"><%- include('../partials/flash')%> <%- body %></main>
<%- include('../partials/footer')%>
<script
src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js"
integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/"
crossorigin="anonymous"
></script>
<script src="/javascripts/validateForms.js"></script>
</body>
</html>

15
views/partials/flash.ejs Normal file
View File

@@ -0,0 +1,15 @@
<% if(success && success.length) {%>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<%= success %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<% } %> <% if(error && error.length) {%>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<%= error %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<% } %>

View File

@@ -0,0 +1,5 @@
<footer class="footer bg-dark py-3 mt-auto">
<div class="container">
<span class="text-muted">&copy; YelpCamp 2020</span>
</div>
</footer>

31
views/partials/navbar.ejs Normal file
View File

@@ -0,0 +1,31 @@
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">YelpCamp</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link" href="/">Home</a>
<a class="nav-link" href="/campgrounds">Campgrounds</a>
<a class="nav-link" href="/campgrounds/new">New Campground</a>
</div>
<div class="navbar-nav ml-auto">
<% if(!currentUser) {%>
<a class="nav-link" href="/login">Login</a>
<a class="nav-link" href="/register">Register</a>
<% } else {%>
<a class="nav-link" href="/logout">Logout</a>
<% } %>
</div>
</div>
</div>
</nav>

32
views/users/login.ejs Normal file
View File

@@ -0,0 +1,32 @@
<% layout('layouts/boilerplate')%>
<div class="container d-flex justify-content-center align-items-center mt-5">
<div class="row">
<div class="col-md-6 offset-md-3 col-xl-4 offset-xl-4">
<div class="card shadow">
<img
src="https://images.unsplash.com/photo-1571863533956-01c88e79957e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1267&q=80"
alt=""
class="card-img-top"
/>
<div class="card-body">
<h5 class="card-title">Login</h5>
<form action="/login" method="POST" class="validated-form" novalidate>
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input class="form-control" type="text" id="username" name="username" autofocus required />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input class="form-control" type="password" id="password" name="password" required />
<div class="valid-feedback">Looks good!</div>
</div>
<button class="btn btn-success btn-block">Login</button>
</form>
</div>
</div>
</div>
</div>
</div>

35
views/users/register.ejs Normal file
View File

@@ -0,0 +1,35 @@
<% layout('layouts/boilerplate')%>
<div class="container d-flex justify-content-center align-items-center mt-5">
<div class="row">
<div class="col-md-6 offset-md-3 col-xl-4 offset-xl-4">
<div class="card shadow">
<img
src="https://images.unsplash.com/photo-1571863533956-01c88e79957e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1267&q=80"
alt=""
class="card-img-top"
/>
<div class="card-body">
<h5 class="card-title">Register</h5>
<form action="/register" method="POST" class="validated-form" novalidate>
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input class="form-control" type="text" id="username" name="username" required autofocus />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email</label>
<input class="form-control" type="email" id="email" name="email" required />
<div class="valid-feedback">Looks good!</div>
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input class="form-control" type="password" id="password" name="password" required />
<div class="valid-feedback">Looks good!</div>
</div>
<button class="btn btn-success btn-block">Register</button>
</form>
</div>
</div>
</div>
</div>
</div>