init
This commit is contained in:
5
.env
Normal file
5
.env
Normal 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
158
app.js
Normal 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
21
cloudinary/index.js
Normal 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
|
||||
}
|
||||
81
controllers/campgrounds.js
Normal file
81
controllers/campgrounds.js
Normal 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
21
controllers/reviews.js
Normal 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
39
controllers/users.js
Normal 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
54
middleware.js
Normal 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
66
models/campground.js
Normal 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
13
models/review.js
Normal 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
15
models/user.js
Normal 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
4196
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
106
public/javascripts/clusterMap.js
Normal file
106
public/javascripts/clusterMap.js
Normal 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 = '';
|
||||
});
|
||||
});
|
||||
14
public/javascripts/showPageMap.js
Normal file
14
public/javascripts/showPageMap.js
Normal 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);
|
||||
24
public/javascripts/validateForms.js
Normal file
24
public/javascripts/validateForms.js
Normal 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
|
||||
);
|
||||
});
|
||||
})();
|
||||
9
public/stylesheets/app.css
Normal file
9
public/stylesheets/app.css
Normal file
@@ -0,0 +1,9 @@
|
||||
#cluster-map {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
36
public/stylesheets/home.css
Normal file
36
public/stylesheets/home.css
Normal 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;
|
||||
}
|
||||
191
public/stylesheets/stars.css
Normal file
191
public/stylesheets/stars.css
Normal 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
24
routes/campgrounds.js
Normal 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
14
routes/reviews.js
Normal 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
17
routes/users.js
Normal 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
41
schemas.js
Normal 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
9002
seeds/cities.js
Normal file
File diff suppressed because it is too large
Load Diff
55
seeds/index.js
Normal file
55
seeds/index.js
Normal 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
44
seeds/seedHelpers.js
Normal 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
9
utils/ExpressError.js
Normal 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
5
utils/catchAsync.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = func => {
|
||||
return (req, res, next) => {
|
||||
func(req, res, next).catch(next);
|
||||
};
|
||||
};
|
||||
66
views/campgrounds/edit.ejs
Normal file
66
views/campgrounds/edit.ejs
Normal 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>
|
||||
36
views/campgrounds/index.ejs
Normal file
36
views/campgrounds/index.ejs
Normal 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
62
views/campgrounds/new.ejs
Normal 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>
|
||||
99
views/campgrounds/show.ejs
Normal file
99
views/campgrounds/show.ejs
Normal 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
11
views/error.ejs
Normal 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
59
views/home.ejs
Normal 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>© 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>
|
||||
38
views/layouts/boilerplate.ejs
Normal file
38
views/layouts/boilerplate.ejs
Normal 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
15
views/partials/flash.ejs
Normal 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">×</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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<% } %>
|
||||
5
views/partials/footer.ejs
Normal file
5
views/partials/footer.ejs
Normal file
@@ -0,0 +1,5 @@
|
||||
<footer class="footer bg-dark py-3 mt-auto">
|
||||
<div class="container">
|
||||
<span class="text-muted">© YelpCamp 2020</span>
|
||||
</div>
|
||||
</footer>
|
||||
31
views/partials/navbar.ejs
Normal file
31
views/partials/navbar.ejs
Normal 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
32
views/users/login.ejs
Normal 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
35
views/users/register.ejs
Normal 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>
|
||||
Reference in New Issue
Block a user