├── .env ├── Dockerfile ├── Manifests └── dss.yml ├── README.md ├── app.js ├── cloudinary └── index.js ├── controllers ├── campgrounds.js ├── reviews.js └── users.js ├── docker-compose.yml ├── images ├── campgrounds.jpg ├── home.jpg └── register.jpg ├── middleware.js ├── models ├── campground.js ├── review.js └── user.js ├── package-lock.json ├── package.json ├── public ├── javascripts │ ├── clusterMap.js │ ├── showPageMap.js │ └── validateForms.js └── stylesheets │ ├── app.css │ ├── home.css │ └── stars.css ├── routes ├── campgrounds.js ├── reviews.js └── users.js ├── schemas.js ├── seeds ├── cities.js ├── index.js ├── seed-img │ ├── seed1.jpg │ └── seed2.jpg └── seedHelpers.js ├── utils ├── ExpressError.js └── catchAsync.js └── views ├── campgrounds ├── edit.ejs ├── index.ejs ├── new.ejs └── show.ejs ├── error.ejs ├── home.ejs ├── layouts └── boilerplate.ejs ├── partials ├── flash.ejs ├── footer.ejs └── navbar.ejs └── users ├── login.ejs └── register.ejs /.env: -------------------------------------------------------------------------------- 1 | CLOUDINARY_CLOUD_NAME= 2 | CLOUDINARY_KEY= 3 | CLOUDINARY_SECRET= 4 | MAPBOX_TOKEN= 5 | DB_URL=" " 6 | SECRET=devopsshack 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node 18 as parent image 2 | FROM node:18 3 | 4 | # Change the working directory on the Docker image to /app 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to the /app directory 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of project files into this image 14 | COPY . . 15 | 16 | # Expose application port 17 | EXPOSE 3000 18 | 19 | # Start the application 20 | CMD npm start 21 | -------------------------------------------------------------------------------- /Manifests/dss.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: yelp-camp-secrets 6 | type: Opaque 7 | data: 8 | CLOUDINARY_CLOUD_NAME: ZGlpMnJvenRw 9 | CLOUDINARY_KEY: NzU3NjQzMjU3ODkxMzY0 10 | CLOUDINARY_SECRET: SHpBeTdPU3VCZjJhSUFSUm5Fd0tIcEVtc01N 11 | MAPBOX_TOKEN: cGsuZXlKMUlqb2lZV1JwYW1GcGMzZGhiQ0lzSW1FaU9pSmpiSFl3TnpKemVHNHhaRzR5TW1wd1ltWm1OSFZ3YlhObUluMC5SVlFZc3UwUnZKZ0NGNTRrMjJkWDBB 12 | DB_URL: bW9uZ29kYitzcnY6Ly9qYWlzd2FsYWRpMjQ2OnNjN1ZHU3lkN2RRQ2NMeTFAY2x1c3RlcjAuaXE2bXV0aC5tb25nb2RiLm5ldC8/cmV0cnlXcml0ZXM9dHJ1ZSZ3PW1ham9yaXR5JmFwcE5hbWU9Q2x1c3RlcjA= 13 | SECRET: ZGV2b3Bzc2hhY2s= 14 | 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: yelp-camp-deployment 20 | spec: 21 | replicas: 1 22 | selector: 23 | matchLabels: 24 | app: yelp-camp 25 | template: 26 | metadata: 27 | labels: 28 | app: yelp-camp 29 | spec: 30 | containers: 31 | - name: yelp-camp-container 32 | image: adijaiswal/campa:latest 33 | ports: 34 | - containerPort: 3000 35 | env: 36 | - name: CLOUDINARY_CLOUD_NAME 37 | valueFrom: 38 | secretKeyRef: 39 | name: yelp-camp-secrets 40 | key: CLOUDINARY_CLOUD_NAME 41 | - name: CLOUDINARY_KEY 42 | valueFrom: 43 | secretKeyRef: 44 | name: yelp-camp-secrets 45 | key: CLOUDINARY_KEY 46 | - name: CLOUDINARY_SECRET 47 | valueFrom: 48 | secretKeyRef: 49 | name: yelp-camp-secrets 50 | key: CLOUDINARY_SECRET 51 | - name: MAPBOX_TOKEN 52 | valueFrom: 53 | secretKeyRef: 54 | name: yelp-camp-secrets 55 | key: MAPBOX_TOKEN 56 | - name: DB_URL 57 | valueFrom: 58 | secretKeyRef: 59 | name: yelp-camp-secrets 60 | key: DB_URL 61 | - name: SECRET 62 | valueFrom: 63 | secretKeyRef: 64 | name: yelp-camp-secrets 65 | key: SECRET 66 | livenessProbe: 67 | httpGet: 68 | path: / 69 | port: 3000 70 | initialDelaySeconds: 30 # Adjust the initial delay here 71 | readinessProbe: 72 | httpGet: 73 | path: / 74 | port: 3000 75 | initialDelaySeconds: 30 # Adjust the initial delay here 76 | 77 | --- 78 | apiVersion: v1 79 | kind: Service 80 | metadata: 81 | name: yelp-camp-service 82 | spec: 83 | selector: 84 | app: yelp-camp 85 | ports: 86 | - protocol: TCP 87 | port: 3000 88 | targetPort: 3000 89 | type: LoadBalancer 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yelp Camp Web Application 2 | 3 | This web application allows users to add, view, access, and rate campgrounds by location. It is based on "The Web Developer Bootcamp" by Colt Steele, but includes several modifications and bug fixes. The application leverages a variety of technologies and packages, such as: 4 | 5 | - **Node.js with Express**: Used for the web server. 6 | - **Bootstrap**: For front-end design. 7 | - **Mapbox**: Provides a fancy cluster map. 8 | - **MongoDB Atlas**: Serves as the database. 9 | - **Passport package with local strategy**: For authentication and authorization. 10 | - **Cloudinary**: Used for cloud-based image storage. 11 | - **Helmet**: Enhances application security. 12 | - ... 13 | 14 | ## Setup Instructions 15 | 16 | To get this application up and running, you'll need to set up accounts with Cloudinary, Mapbox, and MongoDB Atlas. Once these are set up, create a `.env` file in the same folder as `app.js`. This file should contain the following configurations: 17 | 18 | ```sh 19 | CLOUDINARY_CLOUD_NAME=[Your Cloudinary Cloud Name] 20 | CLOUDINARY_KEY=[Your Cloudinary Key] 21 | CLOUDINARY_SECRET=[Your Cloudinary Secret] 22 | MAPBOX_TOKEN=[Your Mapbox Token] 23 | DB_URL=[Your MongoDB Atlas Connection URL] 24 | SECRET=[Your Chosen Secret Key] # This can be any value you prefer 25 | ``` 26 | 27 | After configuring the .env file, you can start the project by running: 28 | ```sh 29 | docker compose up 30 | ``` 31 | 32 | ## Application Screenshots 33 | ![](./images/home.jpg) 34 | ![](./images/campgrounds.jpg) 35 | ![](./images/register.jpg) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== "production") { 2 | require('dotenv').config(); 3 | } 4 | 5 | const express = require('express'); 6 | const path = require('path'); 7 | const helmet = require('helmet'); 8 | const passport = require('passport'); 9 | const mongoose = require('mongoose'); 10 | const ejsMate = require('ejs-mate'); 11 | const session = require('express-session'); 12 | const flash = require('connect-flash'); 13 | const methodOverride = require('method-override'); 14 | const LocalStrategy = require('passport-local'); 15 | const mongoSanitize = require('express-mongo-sanitize'); 16 | const ExpressError = require('./utils/ExpressError'); 17 | const User = require('./models/user'); 18 | const userRoutes = require('./routes/users'); 19 | const campgroundRoutes = require('./routes/campgrounds'); 20 | const reviewRoutes = require('./routes/reviews'); 21 | 22 | const MongoDBStore = require("connect-mongo")(session); 23 | 24 | const dbUrl = process.env.DB_URL || 'mongodb://127.0.0.1:27017/yelp-camp'; 25 | 26 | mongoose.connect(dbUrl, { 27 | useNewUrlParser: true, 28 | useCreateIndex: true, 29 | useUnifiedTopology: true, 30 | useFindAndModify: false 31 | }); 32 | 33 | const db = mongoose.connection; 34 | db.on("error", console.error.bind(console, "connection error:")); 35 | db.once("open", () => { 36 | console.log("Database connected"); 37 | }); 38 | 39 | const app = express(); 40 | 41 | app.engine('ejs', ejsMate) 42 | app.set('view engine', 'ejs'); 43 | app.set('views', path.join(__dirname, 'views')) 44 | 45 | app.use(express.urlencoded({ extended: true })); 46 | app.use(methodOverride('_method')); 47 | app.use(express.static(path.join(__dirname, 'public'))) 48 | app.use(mongoSanitize({ 49 | replaceWith: '_' 50 | })) 51 | 52 | const secret = process.env.SECRET || 'thisshouldbeabettersecret!'; 53 | 54 | const store = new MongoDBStore({ 55 | url: dbUrl, 56 | secret, 57 | touchAfter: 24 * 60 * 60 // after 1 day, update the session, else, only update session when something change 58 | }); 59 | 60 | store.on("error", function (e) { 61 | console.log("SESSION STORE ERROR", e) 62 | }) 63 | 64 | const sessionConfig = { 65 | store, 66 | name: 'session', 67 | secret, 68 | resave: false, 69 | saveUninitialized: true, 70 | cookie: { 71 | httpOnly: true, 72 | // secure: true, 73 | expires: Date.now() + 1000 * 60 * 60 * 24 * 7, 74 | maxAge: 1000 * 60 * 60 * 24 * 7 75 | } 76 | } 77 | 78 | app.use(session(sessionConfig)); 79 | app.use(flash()); 80 | app.use(helmet({ contentSecurityPolicy: false })); 81 | 82 | 83 | const scriptSrcUrls = [ 84 | "https://stackpath.bootstrapcdn.com/", 85 | "https://api.tiles.mapbox.com/", 86 | "https://api.mapbox.com/", 87 | "https://kit.fontawesome.com/", 88 | "https://cdnjs.cloudflare.com/", 89 | "https://cdn.jsdelivr.net", 90 | ]; 91 | const styleSrcUrls = [ 92 | "https://kit-free.fontawesome.com/", 93 | "https://stackpath.bootstrapcdn.com/", 94 | "https://api.mapbox.com/", 95 | "https://api.tiles.mapbox.com/", 96 | "https://fonts.googleapis.com/", 97 | "https://use.fontawesome.com/", 98 | ]; 99 | const connectSrcUrls = [ 100 | "https://api.mapbox.com/", 101 | "https://a.tiles.mapbox.com/", 102 | "https://b.tiles.mapbox.com/", 103 | "https://events.mapbox.com/", 104 | ]; 105 | const fontSrcUrls = []; 106 | 107 | app.use( 108 | helmet.contentSecurityPolicy({ 109 | directives: { 110 | defaultSrc: [], 111 | connectSrc: ["'self'", ...connectSrcUrls], 112 | scriptSrc: ["'unsafe-inline'", "'self'", ...scriptSrcUrls], 113 | styleSrc: ["'self'", "'unsafe-inline'", ...styleSrcUrls], 114 | workerSrc: ["'self'", "blob:"], 115 | objectSrc: [], 116 | imgSrc: [ 117 | "'self'", 118 | "blob:", 119 | "data:", 120 | `https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/`, 121 | "https://images.unsplash.com/", 122 | ], 123 | fontSrc: ["'self'", ...fontSrcUrls], 124 | }, 125 | }) 126 | ); 127 | 128 | 129 | app.use(passport.initialize()); 130 | app.use(passport.session()); 131 | passport.use(new LocalStrategy(User.authenticate())); 132 | 133 | passport.serializeUser(User.serializeUser()); 134 | passport.deserializeUser(User.deserializeUser()); 135 | 136 | app.use((req, res, next) => { 137 | res.locals.currentUser = req.user; // req.user is user infomation in session that passport define for us 138 | res.locals.success = req.flash('success'); // message when success is invoked in route handler 139 | res.locals.error = req.flash('error'); 140 | next(); 141 | }) 142 | 143 | 144 | app.use('/', userRoutes); 145 | app.use('/campgrounds', campgroundRoutes) 146 | app.use('/campgrounds/:id/reviews', reviewRoutes) 147 | 148 | 149 | app.get('/', (req, res) => { 150 | res.render('home') 151 | }); 152 | 153 | 154 | app.all('*', (req, res, next) => { 155 | next(new ExpressError('Page Not Found', 404)) 156 | }) 157 | 158 | app.use((err, req, res, next) => { 159 | const { statusCode = 500 } = err; 160 | if (!err.message) err.message = 'Oh No, Something Went Wrong!' 161 | res.status(statusCode).render('error', { err }) 162 | }) 163 | 164 | const port = process.env.PORT || 3000; 165 | app.listen(port, () => { 166 | console.log(`Serving on port ${port}`) 167 | }) 168 | 169 | 170 | -------------------------------------------------------------------------------- /cloudinary/index.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 6 | api_key: process.env.CLOUDINARY_KEY, 7 | api_secret: process.env.CLOUDINARY_SECRET 8 | }); 9 | 10 | const storage = new CloudinaryStorage({ 11 | cloudinary, 12 | params: { 13 | folder: 'YelpCamp', 14 | allowedFormats: ['jpeg', 'png', 'jpg'] 15 | } 16 | }); 17 | 18 | module.exports = { 19 | cloudinary, 20 | storage 21 | } -------------------------------------------------------------------------------- /controllers/campgrounds.js: -------------------------------------------------------------------------------- 1 | const Campground = require('../models/campground'); 2 | const mbxGeocoding = require("@mapbox/mapbox-sdk/services/geocoding"); 3 | const mapBoxToken = process.env.MAPBOX_TOKEN; 4 | const geocoder = mbxGeocoding({ accessToken: mapBoxToken }); 5 | const { cloudinary } = require("../cloudinary"); 6 | const ExpressError = require('../utils/ExpressError'); 7 | 8 | 9 | module.exports.index = async (req, res) => { 10 | const campgrounds = await Campground.find({}).populate('popupText'); 11 | res.render('campgrounds/index', { campgrounds }) 12 | } 13 | 14 | module.exports.renderNewForm = (req, res) => { 15 | res.render('campgrounds/new'); 16 | } 17 | 18 | module.exports.createCampground = async (req, res, next) => { 19 | const geoData = await geocoder.forwardGeocode({ 20 | query: req.body.campground.location, 21 | limit: 1 22 | }).send() 23 | if (!geoData) { 24 | throw new ExpressError("Not found any location match your input, retry!", 422 ) 25 | } 26 | 27 | const campground = new Campground(req.body.campground); 28 | campground.geometry = geoData.body.features[0].geometry; 29 | campground.images = req.files.map(f => ({ url: f.path, filename: f.filename })); // req.files is created for us by multer 30 | campground.author = req.user._id; 31 | await campground.save(); 32 | console.log(campground); 33 | req.flash('success', 'Successfully made a new campground!'); 34 | res.redirect(`/campgrounds/${campground._id}`) 35 | } 36 | 37 | module.exports.showCampground = async (req, res,) => { 38 | const campground = await Campground.findById(req.params.id).populate({ 39 | path: 'reviews', 40 | populate: { 41 | path: 'author' 42 | } 43 | }).populate('author'); 44 | if (!campground) { 45 | req.flash('error', 'Cannot find that campground!'); 46 | return res.redirect('/campgrounds'); 47 | } 48 | res.render('campgrounds/show', { campground }); 49 | } 50 | 51 | module.exports.renderEditForm = async (req, res) => { 52 | const { id } = req.params; 53 | const campground = await Campground.findById(id) 54 | if (!campground) { 55 | req.flash('error', 'Cannot find that campground!'); 56 | return res.redirect('/campgrounds'); 57 | } 58 | res.render('campgrounds/edit', { campground }); 59 | } 60 | 61 | module.exports.updateCampground = async (req, res) => { 62 | const { id } = req.params; 63 | console.log(req.body); 64 | const campground = await Campground.findByIdAndUpdate(id, { ...req.body.campground }); 65 | const imgs = req.files.map(f => ({ url: f.path, filename: f.filename })); // req.files is created for us by multer 66 | campground.images.push(...imgs); 67 | await campground.save(); 68 | if (req.body.deleteImages) { 69 | for (let filename of req.body.deleteImages) { 70 | await cloudinary.uploader.destroy(filename); 71 | } 72 | await campground.updateOne({ $pull: { images: { filename: { $in: req.body.deleteImages } } } }) 73 | } 74 | req.flash('success', 'Successfully updated campground!'); 75 | res.redirect(`/campgrounds/${campground._id}`) 76 | } 77 | 78 | module.exports.deleteCampground = async (req, res) => { 79 | const { id } = req.params; 80 | await Campground.findByIdAndDelete(id); 81 | req.flash('success', 'Successfully deleted campground') 82 | res.redirect('/campgrounds'); 83 | } -------------------------------------------------------------------------------- /controllers/reviews.js: -------------------------------------------------------------------------------- 1 | const Campground = require('../models/campground'); 2 | const Review = require('../models/review'); 3 | 4 | module.exports.createReview = async (req, res) => { 5 | const campground = await Campground.findById(req.params.id); 6 | const review = new Review(req.body.review); 7 | review.author = req.user._id; 8 | campground.reviews.push(review); 9 | await review.save(); 10 | await campground.save(); 11 | req.flash('success', 'Created new review!'); 12 | res.redirect(`/campgrounds/${campground._id}`); 13 | } 14 | 15 | module.exports.deleteReview = async (req, res) => { 16 | const { id, reviewId } = req.params; 17 | await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } }); 18 | await Review.findByIdAndDelete(reviewId); 19 | req.flash('success', 'Successfully deleted review') 20 | res.redirect(`/campgrounds/${id}`); 21 | } 22 | -------------------------------------------------------------------------------- /controllers/users.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | module.exports.renderRegister = (req, res) => { 4 | res.render('users/register'); 5 | } 6 | 7 | module.exports.register = async (req, res, next) => { 8 | try { 9 | const { email, username, password } = req.body; 10 | const user = new User({ email, username }); 11 | const registeredUser = await User.register(user, password); 12 | req.login(registeredUser, err => { 13 | if (err) return next(err); 14 | req.flash('success', 'Welcome to Yelp Camp!'); 15 | res.redirect('/campgrounds'); 16 | }) 17 | } catch (e) { 18 | req.flash('error', e.message); 19 | res.redirect('register'); 20 | } 21 | } 22 | 23 | module.exports.renderLogin = (req, res) => { 24 | res.render('users/login'); 25 | } 26 | 27 | module.exports.login = (req, res) => { 28 | req.flash('success', 'welcome back!'); 29 | const redirectUrl = req.session.returnTo || '/campgrounds'; 30 | delete req.session.returnTo; 31 | res.redirect(redirectUrl); 32 | } 33 | 34 | module.exports.logout = (req, res) => { 35 | console.log("activvate logout"); 36 | req.logout(); 37 | // req.session.destroy(); 38 | req.flash('success', "Goodbye!"); 39 | res.redirect('/campgrounds'); 40 | // req.logout(function (err) { 41 | // if (err) { 42 | // return next(err); 43 | // } 44 | // req.flash('success', 'Goodbye!'); 45 | // res.redirect('/campgrounds'); 46 | // }); 47 | 48 | console.log("activvate logout end"); 49 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | nodejs: 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | -------------------------------------------------------------------------------- /images/campgrounds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaiswaladi246/3-Tier-Full-Stack/72baf740b67e09848240fa183316bfc1dbbf47c4/images/campgrounds.jpg -------------------------------------------------------------------------------- /images/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaiswaladi246/3-Tier-Full-Stack/72baf740b67e09848240fa183316bfc1dbbf47c4/images/home.jpg -------------------------------------------------------------------------------- /images/register.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaiswaladi246/3-Tier-Full-Stack/72baf740b67e09848240fa183316bfc1dbbf47c4/images/register.jpg -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | const { campgroundSchema, reviewSchema } = require('./schemas.js'); 2 | const ExpressError = require('./utils/ExpressError'); 3 | const Campground = require('./models/campground'); 4 | const Review = require('./models/review'); 5 | 6 | module.exports.validateCampground = (req, res, next) => { 7 | const { error } = campgroundSchema.validate(req.body); 8 | // console.log(req.body); 9 | if (error) { 10 | const msg = error.details.map(el => el.message).join(',') 11 | throw new ExpressError(msg, 400) 12 | } else { 13 | next(); 14 | } 15 | } 16 | 17 | module.exports.validateReview = (req, res, next) => { 18 | const { error } = reviewSchema.validate(req.body); 19 | if (error) { 20 | const msg = error.details.map(el => el.message).join(',') 21 | throw new ExpressError(msg, 400) 22 | } else { 23 | next(); 24 | } 25 | } 26 | 27 | module.exports.isLoggedIn = (req, res, next) => { 28 | if (!req.isAuthenticated()) { 29 | req.session.returnTo = req.originalUrl 30 | req.flash('error', 'You must be signed in first!'); 31 | return res.redirect('/login'); 32 | } 33 | next(); 34 | } 35 | 36 | module.exports.isAuthor = async (req, res, next) => { 37 | const { id } = req.params; 38 | const campground = await Campground.findById(id); 39 | if (!campground.author.equals(req.user._id)) { 40 | req.flash('error', 'You do not have permission to do that!'); 41 | return res.redirect(`/campgrounds/${id}`); 42 | } 43 | next(); 44 | } 45 | 46 | module.exports.isReviewAuthor = async (req, res, next) => { 47 | const { id, reviewId } = req.params; 48 | const review = await Review.findById(reviewId); 49 | if (!review.author.equals(req.user._id)) { 50 | req.flash('error', 'You do not have permission to do that!'); 51 | return res.redirect(`/campgrounds/${id}`); 52 | } 53 | next(); 54 | } 55 | -------------------------------------------------------------------------------- /models/campground.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Review = require('./review') 3 | const Schema = mongoose.Schema; 4 | const { cloudinary } = require("../cloudinary"); 5 | 6 | 7 | // https://res.cloudinary.com/douqbebwk/image/upload/w_300/v1600113904/YelpCamp/gxgle1ovzd2f3dgcpass.png 8 | 9 | const ImageSchema = new Schema({ 10 | url: String, 11 | filename: String 12 | }); 13 | 14 | ImageSchema.virtual('thumbnail').get(function () { 15 | return this.url.replace('/upload', '/upload/w_200'); 16 | }); 17 | 18 | ImageSchema.virtual('standardSize').get(function () { 19 | return this.url.replace('/upload', '/upload/w_200'); 20 | }); 21 | 22 | const opts = { toJSON: { virtuals: true } }; 23 | 24 | const CampgroundSchema = new Schema({ 25 | title: String, 26 | images: [ImageSchema], 27 | geometry: { 28 | type: { 29 | type: String, 30 | enum: ['Point'], 31 | required: true 32 | }, 33 | coordinates: { 34 | type: [Number], 35 | required: true 36 | } 37 | }, 38 | price: Number, 39 | description: String, 40 | location: String, 41 | author: { 42 | type: Schema.Types.ObjectId, 43 | ref: 'User' 44 | }, 45 | reviews: [ 46 | { 47 | type: Schema.Types.ObjectId, 48 | ref: 'Review' 49 | } 50 | ] 51 | }, opts); 52 | 53 | 54 | CampgroundSchema.virtual('properties.popUpMarkup').get(function () { 55 | return ` 56 | ${this.title} 57 |

${this.description.substring(0, 20)}...

` 58 | }); 59 | 60 | 61 | CampgroundSchema.post('findOneAndDelete', async function (doc) { 62 | 63 | 64 | if (doc) { 65 | for (let image of doc.images) { 66 | await cloudinary.uploader.destroy(image.filename); 67 | } 68 | await Review.deleteMany({ 69 | _id: { 70 | $in: doc.reviews 71 | } 72 | }) 73 | } 74 | }) 75 | 76 | module.exports = mongoose.model('Campground', CampgroundSchema); -------------------------------------------------------------------------------- /models/review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const reviewSchema = new Schema({ 5 | body: String, 6 | rating: Number, 7 | author: { 8 | type: Schema.Types.ObjectId, 9 | ref: 'User' 10 | } 11 | }); 12 | 13 | module.exports = mongoose.model("Review", reviewSchema); 14 | 15 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const passportLocalMongoose = require('passport-local-mongoose'); 4 | 5 | const UserSchema = new Schema({ 6 | email: { 7 | type: String, 8 | required: true, 9 | unique: true 10 | } 11 | }); 12 | 13 | UserSchema.plugin(passportLocalMongoose); 14 | 15 | module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yelpcamp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"There are no test-cases\" && exit 0", 8 | "start": "node app.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@mapbox/mapbox-sdk": "^0.11.0", 15 | "cloudinary": "^1.23.0", 16 | "connect-flash": "^0.1.1", 17 | "connect-mongo": "^3.2.0", 18 | "dotenv": "^8.2.0", 19 | "ejs": "^3.1.5", 20 | "ejs-mate": "^3.0.0", 21 | "express": "^4.17.1", 22 | "express-mongo-sanitize": "^2.0.0", 23 | "express-session": "^1.17.1", 24 | "helmet": "^4.1.1", 25 | "joi": "^17.2.1", 26 | "method-override": "^3.0.0", 27 | "mongoose": "^5.10.4", 28 | "multer": "^1.4.2", 29 | "multer-storage-cloudinary": "^4.0.0", 30 | "passport": "^0.4.1", 31 | "passport-local": "^1.0.0", 32 | "passport-local-mongoose": "^6.0.1", 33 | "sanitize-html": "^1.27.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/javascripts/clusterMap.js: -------------------------------------------------------------------------------- 1 | // this file is modificated ver from https://docs.mapbox.com/mapbox-gl-js/example/cluster/ 2 | 3 | mapboxgl.accessToken = mapToken; 4 | const map = new mapboxgl.Map({ 5 | container: 'cluster-map', 6 | style: 'mapbox://styles/mapbox/light-v10', 7 | center: [-103.59179687498357, 40.66995747013945], 8 | zoom: 3 9 | }); 10 | 11 | map.addControl(new mapboxgl.NavigationControl()); 12 | 13 | 14 | map.on('load', function () { 15 | // Add a new source from our GeoJSON data and 16 | // set the 'cluster' option to true. GL-JS will 17 | // add the point_count property to your source data. 18 | map.addSource('campgrounds', { 19 | type: 'geojson', 20 | // Point to GeoJSON data. This example visualizes all M1.0+ earthquakes 21 | // from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program. 22 | data: campgrounds, 23 | cluster: true, 24 | clusterMaxZoom: 14, // Max zoom to cluster points on 25 | clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50) 26 | }); 27 | 28 | map.addLayer({ 29 | id: 'clusters', 30 | type: 'circle', 31 | source: 'campgrounds', 32 | filter: ['has', 'point_count'], 33 | paint: { 34 | // Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step) 35 | // with three steps to implement three types of circles: 36 | // * Blue, 20px circles when point count is less than 100 37 | // * Yellow, 30px circles when point count is between 100 and 750 38 | // * Pink, 40px circles when point count is greater than or equal to 750 39 | 'circle-color': [ 40 | 'step', 41 | ['get', 'point_count'], 42 | '#00BCD4', 43 | 10, 44 | '#2196F3', 45 | 30, 46 | '#3F51B5' 47 | ], 48 | 'circle-radius': [ 49 | 'step', 50 | ['get', 'point_count'], 51 | 15, 52 | 10, 53 | 20, 54 | 30, 55 | 25 56 | ] 57 | } 58 | }); 59 | 60 | map.addLayer({ 61 | id: 'cluster-count', 62 | type: 'symbol', 63 | source: 'campgrounds', 64 | filter: ['has', 'point_count'], 65 | layout: { 66 | 'text-field': '{point_count_abbreviated}', 67 | 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], 68 | 'text-size': 12 69 | } 70 | }); 71 | 72 | map.addLayer({ 73 | id: 'unclustered-point', 74 | type: 'circle', 75 | source: 'campgrounds', 76 | filter: ['!', ['has', 'point_count']], 77 | paint: { 78 | 'circle-color': '#11b4da', 79 | 'circle-radius': 4, 80 | 'circle-stroke-width': 1, 81 | 'circle-stroke-color': '#fff' 82 | } 83 | }); 84 | 85 | // inspect a cluster on click 86 | map.on('click', 'clusters', function (e) { 87 | const features = map.queryRenderedFeatures(e.point, { 88 | layers: ['clusters'] 89 | }); 90 | const clusterId = features[0].properties.cluster_id; 91 | map.getSource('campgrounds').getClusterExpansionZoom( 92 | clusterId, 93 | function (err, zoom) { 94 | if (err) return; 95 | 96 | map.easeTo({ 97 | center: features[0].geometry.coordinates, 98 | zoom: zoom 99 | }); 100 | } 101 | ); 102 | }); 103 | 104 | // When a click event occurs on a feature in 105 | // the unclustered-point layer, open a popup at 106 | // the location of the feature, with 107 | // description HTML from its properties. 108 | map.on('click', 'unclustered-point', function (e) { 109 | const { popUpMarkup } = e.features[0].properties; 110 | const coordinates = e.features[0].geometry.coordinates.slice(); 111 | 112 | // Ensure that if the map is zoomed out such that 113 | // multiple copies of the feature are visible, the 114 | // popup appears over the copy being pointed to. 115 | while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { 116 | coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; 117 | } 118 | 119 | new mapboxgl.Popup() 120 | .setLngLat(coordinates) 121 | .setHTML(popUpMarkup) 122 | .addTo(map); 123 | }); 124 | 125 | map.on('mouseenter', 'clusters', function () { 126 | map.getCanvas().style.cursor = 'pointer'; 127 | }); 128 | map.on('mouseleave', 'clusters', function () { 129 | map.getCanvas().style.cursor = ''; 130 | }); 131 | }); 132 | 133 | -------------------------------------------------------------------------------- /public/javascripts/showPageMap.js: -------------------------------------------------------------------------------- 1 | mapboxgl.accessToken = mapToken; 2 | const map = new mapboxgl.Map({ 3 | container: 'map', 4 | style: 'mapbox://styles/mapbox/light-v10', // stylesheet location 5 | center: campground.geometry.coordinates, // starting position [lng, lat] 6 | zoom: 10 // starting zoom 7 | }); 8 | 9 | map.addControl(new mapboxgl.NavigationControl()); 10 | 11 | 12 | new mapboxgl.Marker() 13 | .setLngLat(campground.geometry.coordinates) 14 | .setPopup( 15 | new mapboxgl.Popup({ offset: 25 }) 16 | .setHTML( 17 | `

${campground.title}

${campground.location}

` 18 | ) 19 | ) 20 | .addTo(map) 21 | 22 | -------------------------------------------------------------------------------- /public/javascripts/validateForms.js: -------------------------------------------------------------------------------- 1 | 2 | // A self-invoking function, also known as an immediately invoked function expression (IIFE), 3 | // is a JavaScript function that is defined and executed immediately after it is created. 4 | // search more for wtf is self-invoking function in JS, very clunky 5 | 6 | (function () { 7 | 'use strict' 8 | 9 | bsCustomFileInput.init() 10 | 11 | // Fetch all the forms we want to apply custom Bootstrap validation styles to 12 | const forms = document.querySelectorAll('.validated-form') 13 | 14 | // Loop over them and prevent submission 15 | Array.from(forms) 16 | .forEach(function (form) { 17 | form.addEventListener('submit', function (event) { 18 | if (!form.checkValidity()) { 19 | event.preventDefault() 20 | event.stopPropagation() 21 | } 22 | 23 | form.classList.add('was-validated') 24 | }, false) 25 | }) 26 | })() 27 | 28 | -------------------------------------------------------------------------------- /public/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | #cluster-map { 2 | width: 100%; 3 | height: 500px; 4 | } 5 | 6 | #map { 7 | width: 100%; 8 | height: 300px; 9 | } 10 | 11 | 12 | .thumbnail-image { 13 | width: 500px; 14 | height: 250px; 15 | object-fit: cover; 16 | } 17 | 18 | .carousel-size { 19 | width: 100%; 20 | height: 50%; 21 | } 22 | 23 | .centered-image { 24 | width: 100%; 25 | height: 380px; 26 | object-fit: cover; 27 | } -------------------------------------------------------------------------------- /public/stylesheets/home.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), 4 | url("https://images.unsplash.com/photo-1559521783-1d1599583485?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80"); 5 | background-size: cover; 6 | background-position: center; 7 | text-shadow: 0 0.05rem 0.1rem rgba(0, 0, 0, 0.5); 8 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.5); 9 | } 10 | .cover-container { 11 | max-width: 60vw; 12 | } 13 | 14 | .nav-link { 15 | padding: 0.25rem 0; 16 | font-weight: 700; 17 | color: rgba(255,255,255,0.5); 18 | margin-left: 1rem; 19 | border-bottom: 0.25rem solid transparent; 20 | } 21 | 22 | .nav-link:hover{ 23 | color: rgba(255,255,255,0.5); 24 | border-bottom-color:rgba(255,255,255,0.5); 25 | } 26 | 27 | .nav-link.active { 28 | color: white; 29 | border-bottom-color:white; 30 | 31 | } 32 | 33 | .btn-secondary, .btn-secondary:hover{ 34 | color: #333; 35 | text-shadow: none; 36 | } -------------------------------------------------------------------------------- /public/stylesheets/stars.css: -------------------------------------------------------------------------------- 1 | .starability-result { 2 | position: relative; 3 | width: 150px; 4 | height: 30px; 5 | 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="); 6 | font-size: 0.1em; 7 | color: transparent; 8 | } 9 | 10 | .starability-result:after { 11 | content: ' '; 12 | position: absolute; 13 | left: 0; 14 | height: 30px; 15 | 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="); 16 | background-position: 0 -30px; 17 | } 18 | 19 | .starability-result[data-rating="5"]::after { 20 | width: 150px; 21 | } 22 | 23 | .starability-result[data-rating="4"]::after { 24 | width: 120px; 25 | } 26 | 27 | .starability-result[data-rating="3"]::after { 28 | width: 90px; 29 | } 30 | 31 | .starability-result[data-rating="2"]::after { 32 | width: 60px; 33 | } 34 | 35 | .starability-result[data-rating="1"]::after { 36 | width: 30px; 37 | } 38 | 39 | @media screen and (-webkit-min-device-pixel-ratio: 2), screen and (min-resolution: 192dpi) { 40 | .starability-result { 41 | 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=="); 42 | background-size: 30px auto; 43 | } 44 | .starability-result:after { 45 | 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=="); 46 | background-size: 30px auto; 47 | } 48 | } 49 | 50 | .starability-basic { 51 | display: block; 52 | position: relative; 53 | width: 150px; 54 | min-height: 60px; 55 | padding: 0; 56 | border: none; 57 | } 58 | 59 | .starability-basic > input { 60 | position: absolute; 61 | margin-right: -100%; 62 | opacity: 0; 63 | } 64 | 65 | .starability-basic > input:checked ~ label, 66 | .starability-basic > input:focus ~ label { 67 | background-position: 0 0; 68 | } 69 | 70 | .starability-basic > input:checked + label, 71 | .starability-basic > input:focus + label { 72 | background-position: 0 -30px; 73 | } 74 | 75 | .starability-basic > input[disabled]:hover + label { 76 | cursor: default; 77 | } 78 | 79 | .starability-basic > input:not([disabled]):hover ~ label { 80 | background-position: 0 0; 81 | } 82 | 83 | .starability-basic > input:not([disabled]):hover + label { 84 | background-position: 0 -30px; 85 | } 86 | 87 | .starability-basic > input:not([disabled]):hover + label::before { 88 | opacity: 1; 89 | } 90 | 91 | .starability-basic > input:focus + label { 92 | outline: 1px dotted #999; 93 | } 94 | 95 | .starability-basic .starability-focus-ring { 96 | position: absolute; 97 | left: 0; 98 | width: 100%; 99 | height: 30px; 100 | outline: 2px dotted #999; 101 | pointer-events: none; 102 | opacity: 0; 103 | } 104 | 105 | .starability-basic > .input-no-rate:focus ~ .starability-focus-ring { 106 | opacity: 1; 107 | } 108 | 109 | .starability-basic > label { 110 | position: relative; 111 | display: inline-block; 112 | float: left; 113 | width: 30px; 114 | height: 30px; 115 | font-size: 0.1em; 116 | color: transparent; 117 | cursor: pointer; 118 | 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="); 119 | background-repeat: no-repeat; 120 | background-position: 0 -30px; 121 | } 122 | 123 | .starability-basic > label::before { 124 | content: ''; 125 | position: absolute; 126 | display: block; 127 | height: 30px; 128 | 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="); 129 | background-position: 0 30px; 130 | pointer-events: none; 131 | opacity: 0; 132 | } 133 | 134 | .starability-basic > label:nth-of-type(5)::before { 135 | width: 120px; 136 | left: -120px; 137 | } 138 | 139 | .starability-basic > label:nth-of-type(4)::before { 140 | width: 90px; 141 | left: -90px; 142 | } 143 | 144 | .starability-basic > label:nth-of-type(3)::before { 145 | width: 60px; 146 | left: -60px; 147 | } 148 | 149 | .starability-basic > label:nth-of-type(2)::before { 150 | width: 30px; 151 | left: -30px; 152 | } 153 | 154 | .starability-basic > label:nth-of-type(1)::before { 155 | width: 0px; 156 | left: 0px; 157 | } 158 | 159 | @media screen and (-webkit-min-device-pixel-ratio: 2), screen and (min-resolution: 192dpi) { 160 | .starability-basic > label { 161 | 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=="); 162 | background-size: 30px auto; 163 | } 164 | } 165 | 166 | @media screen and (-ms-high-contrast: active) { 167 | .starability-basic { 168 | width: auto; 169 | } 170 | .starability-basic > input { 171 | position: static; 172 | margin-right: 0; 173 | opacity: 1; 174 | } 175 | .starability-basic .input-no-rate { 176 | display: none; 177 | } 178 | .starability-basic > label { 179 | display: inline; 180 | float: none; 181 | width: auto; 182 | height: auto; 183 | font-size: 1em; 184 | color: inherit; 185 | background: none; 186 | } 187 | .starability-basic > label::before, .starability-basic > label::after { 188 | display: none; 189 | } 190 | } -------------------------------------------------------------------------------- /routes/campgrounds.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const campgrounds = require('../controllers/campgrounds'); 4 | const catchAsync = require('../utils/catchAsync'); 5 | const { isLoggedIn, isAuthor, validateCampground } = require('../middleware'); 6 | const multer = require('multer'); 7 | const { storage } = require('../cloudinary'); 8 | const upload = multer({ storage }); 9 | 10 | const Campground = require('../models/campground'); 11 | 12 | router.route('/') 13 | .get(catchAsync(campgrounds.index)) 14 | .post(isLoggedIn, upload.array('image'), validateCampground, catchAsync(campgrounds.createCampground)) 15 | // image is a field specified in form 16 | // similarly , we have upload.single() for upload one img 17 | 18 | 19 | router.get('/new', isLoggedIn, campgrounds.renderNewForm) 20 | 21 | router.route('/:id') 22 | .get(catchAsync(campgrounds.showCampground)) 23 | .put(isLoggedIn, isAuthor, upload.array('image'), validateCampground, catchAsync(campgrounds.updateCampground)) 24 | .delete(isLoggedIn, isAuthor, catchAsync(campgrounds.deleteCampground)); 25 | // image is a field specified in form 26 | // similarly , we have upload.single() for upload one img 27 | 28 | router.get('/:id/edit', isLoggedIn, isAuthor, catchAsync(campgrounds.renderEditForm)) 29 | 30 | 31 | 32 | module.exports = router; -------------------------------------------------------------------------------- /routes/reviews.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router({ mergeParams: true }); 3 | const { validateReview, isLoggedIn, isReviewAuthor } = require('../middleware'); 4 | const Campground = require('../models/campground'); 5 | const Review = require('../models/review'); 6 | const reviews = require('../controllers/reviews'); 7 | const ExpressError = require('../utils/ExpressError'); 8 | const catchAsync = require('../utils/catchAsync'); 9 | 10 | router.post('/', isLoggedIn, validateReview, catchAsync(reviews.createReview)) 11 | 12 | router.delete('/:reviewId', isLoggedIn, isReviewAuthor, catchAsync(reviews.deleteReview)) 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const passport = require('passport'); 4 | const catchAsync = require('../utils/catchAsync'); 5 | const User = require('../models/user'); 6 | const users = require('../controllers/users'); 7 | 8 | router.route('/register') 9 | .get(users.renderRegister) 10 | .post(catchAsync(users.register)); 11 | 12 | router.route('/login') 13 | .get(users.renderLogin) 14 | .post(passport.authenticate('local', { failureFlash: true, failureRedirect: '/login' }), users.login) 15 | 16 | router.get('/logout', users.logout) 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /schemas.js: -------------------------------------------------------------------------------- 1 | const BaseJoi = require('joi'); 2 | const sanitizeHtml = require('sanitize-html'); 3 | 4 | const extension = (joi) => ({ 5 | type: 'string', 6 | base: joi.string(), 7 | messages: { 8 | 'string.escapeHTML': '{{#label}} must not include HTML!' 9 | }, 10 | rules: { 11 | escapeHTML: { 12 | validate(value, helpers) { 13 | const clean = sanitizeHtml(value, { 14 | allowedTags: [], 15 | allowedAttributes: {}, 16 | }); 17 | if (clean !== value) return helpers.error('string.escapeHTML', { value }) 18 | return clean; 19 | } 20 | } 21 | } 22 | }); 23 | 24 | const Joi = BaseJoi.extend(extension) 25 | 26 | module.exports.campgroundSchema = Joi.object({ 27 | campground: Joi.object({ 28 | title: Joi.string().required().escapeHTML(), 29 | price: Joi.number().required().min(0), 30 | location: Joi.string().required().escapeHTML(), 31 | description: Joi.string().required().escapeHTML() 32 | }).required(), 33 | deleteImages: Joi.array() 34 | }); 35 | 36 | module.exports.reviewSchema = Joi.object({ 37 | review: Joi.object({ 38 | rating: Joi.number().required().min(1).max(5), 39 | body: Joi.string().required().escapeHTML() 40 | }).required() 41 | }) 42 | 43 | -------------------------------------------------------------------------------- /seeds/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const cities = require('./cities'); 3 | const { places, descriptors } = require('./seedHelpers'); 4 | const Campground = require('../models/campground'); 5 | const User = require('../models/user'); 6 | const cloudinary = require('cloudinary').v2; 7 | 8 | 9 | if (process.env.NODE_ENV !== "production") { 10 | require('dotenv').config(); 11 | } 12 | 13 | 14 | cloudinary.config({ 15 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 16 | api_key: process.env.CLOUDINARY_KEY, 17 | api_secret: process.env.CLOUDINARY_SECRET 18 | }); 19 | 20 | const dbUrl = process.env.DB_URL || 'mongodb://127.0.0.1:27017/yelp-camp'; 21 | console.log(process.env.DB_URL); 22 | mongoose.connect(dbUrl, { 23 | useNewUrlParser: true, 24 | useCreateIndex: true, 25 | useUnifiedTopology: true 26 | }); 27 | 28 | const db = mongoose.connection; 29 | db.on("error", console.error.bind(console, "connection error:")); 30 | db.once("open", () => { 31 | console.log("Database connected"); 32 | }); 33 | 34 | 35 | const sample = array => array[Math.floor(Math.random() * array.length)]; 36 | 37 | 38 | const seedDB = async () => { 39 | // clear all cur users and campgrounds (along with its comment) 40 | // ! hàm này chưa hoàn thành 41 | 42 | const user = new User({ 43 | email: "trancongquang2002@gmail.com", 44 | username: "qang" 45 | }) 46 | 47 | const registeredUser = await User.register(user, "qang"); 48 | 49 | await cloudinary.v2.uploader 50 | .upload("/seed-img/seed1.jpg", { 51 | folder: "seed-img/", 52 | public_id: "seed1", 53 | use_filename: true, 54 | unique_filename: false 55 | }) 56 | .then(result => console.log(result)); 57 | 58 | 59 | for (let i = 0; i < 50; i++) { 60 | const random1000 = Math.floor(Math.random() * 1000); 61 | const price = Math.floor(Math.random() * 20) + 10; 62 | const camp = new Campground({ 63 | //YOUR USER ID 64 | author: '653d0786717b3f3e7877aeec', 65 | location: `${cities[random1000].city}, ${cities[random1000].state}`, 66 | title: `${sample(descriptors)} ${sample(places)}`, 67 | 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!', 68 | price, 69 | geometry: { 70 | type: "Point", 71 | coordinates: [ 72 | cities[random1000].longitude, 73 | cities[random1000].latitude, 74 | ] 75 | }, 76 | images: [ 77 | { 78 | url: 'https://res.cloudinary.com/dgo51ltyy/image/upload/v1698575538/YelpCamp/rk5cfgnwmre1gimvf7zi.jpg', 79 | filename: 'YelpCamp/653e34b29d3f6818cc73d9ca' 80 | }, 81 | { 82 | url: 'https://res.cloudinary.com/dgo51ltyy/image/upload/v1698575597/YelpCamp/tumktwmasbbenzhw67ph.jpg', 83 | filename: 'YelpCamp/tumktwmasbbenzhw67ph' 84 | } 85 | ] 86 | }) 87 | await camp.save(); 88 | console.log("add success") 89 | } 90 | } 91 | 92 | seedDB().then(() => { 93 | mongoose.connection.close(); 94 | }) -------------------------------------------------------------------------------- /seeds/seed-img/seed1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaiswaladi246/3-Tier-Full-Stack/72baf740b67e09848240fa183316bfc1dbbf47c4/seeds/seed-img/seed1.jpg -------------------------------------------------------------------------------- /seeds/seed-img/seed2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaiswaladi246/3-Tier-Full-Stack/72baf740b67e09848240fa183316bfc1dbbf47c4/seeds/seed-img/seed2.jpg -------------------------------------------------------------------------------- /seeds/seedHelpers.js: -------------------------------------------------------------------------------- 1 | module.exports.descriptors = [ 2 | 'Forest', 3 | 'Ancient', 4 | 'Petrified', 5 | 'Roaring', 6 | 'Cascade', 7 | 'Tumbling', 8 | 'Silent', 9 | 'Redwood', 10 | 'Bullfrog', 11 | 'Maple', 12 | 'Misty', 13 | 'Elk', 14 | 'Grizzly', 15 | 'Ocean', 16 | 'Sea', 17 | 'Sky', 18 | 'Dusty', 19 | 'Diamond' 20 | ] 21 | 22 | module.exports.places = [ 23 | 'Flats', 24 | 'Village', 25 | 'Canyon', 26 | 'Pond', 27 | 'Group Camp', 28 | 'Horse Camp', 29 | 'Ghost Town', 30 | 'Camp', 31 | 'Dispersed Camp', 32 | 'Backcountry', 33 | 'River', 34 | 'Creek', 35 | 'Creekside', 36 | 'Bay', 37 | 'Spring', 38 | 'Bayshore', 39 | 'Sands', 40 | 'Mule Camp', 41 | 'Hunting Camp', 42 | 'Cliffs', 43 | 'Hollow' 44 | ] 45 | 46 | -------------------------------------------------------------------------------- /utils/ExpressError.js: -------------------------------------------------------------------------------- 1 | class ExpressError extends Error { 2 | constructor(message, statusCode) { 3 | super(); 4 | this.message = message; 5 | this.statusCode = statusCode; 6 | } 7 | } 8 | 9 | module.exports = ExpressError; -------------------------------------------------------------------------------- /utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = func => { 2 | return (req, res, next) => { 3 | func(req, res, next).catch(next); 4 | } 5 | } -------------------------------------------------------------------------------- /views/campgrounds/edit.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 |
3 |

Edit Campground

4 |
5 |
7 |
8 | 9 | 11 |
12 | Looks good! 13 |
14 |
15 |
16 | 17 | 19 |
20 | Looks good! 21 |
22 |
23 | 24 |
25 | 26 |
27 | $ 28 | \
31 | Looks good! 32 |
33 |
34 |
35 | 36 |
37 | 38 | 40 |
41 | Looks good! 42 |
43 |
44 |
45 |
46 | 47 | 48 | 52 |
53 |
54 |
55 | <% campground.images.forEach(function(img, i) { %> 56 | 57 | 58 |
59 | 60 | 61 |
62 | 63 | <% })%> 64 |
65 |
66 | 67 |
68 |
69 | Back To Campground 70 | 71 |
72 |
-------------------------------------------------------------------------------- /views/campgrounds/index.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 |
3 |
4 |

All Campgrounds

5 | 6 | <% for (let campground of campgrounds){%> 7 |
8 |
9 |
10 | <%if(campground.images.length) {%> 11 | 12 | <% }else {%> 13 | 15 | <% } %> 16 |
17 |
18 |
19 |
<%= campground.title %>
20 | 21 |

<%= campground.description %>

22 |

23 | <%= campground.location%> 24 |

25 | View <%=campground.title%> 26 |
27 |
28 |
29 |
30 | <% }%> 31 |
32 | 37 | 38 | -------------------------------------------------------------------------------- /views/campgrounds/new.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 |
3 |

New Campground

4 |
5 |
6 |
7 | 8 | 9 |
10 | Looks good! 11 |
12 |
13 |
14 | 15 | 16 |
17 | Looks good! 18 |
19 |
20 | 21 | 28 |
29 | 30 |
31 | $ 32 | 34 |
35 |
36 | Looks good! 37 |
38 |
39 | 40 |
41 | 42 | 44 |
45 | Looks good! 46 |
47 |
48 |
49 |
50 | 51 | 52 | 56 |
57 |
58 |
59 | 60 |
61 |
62 | All Campgrounds 63 |
64 |
-------------------------------------------------------------------------------- /views/campgrounds/show.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 | 3 |
4 |
5 | 25 | 26 |
27 |
28 |
29 | <%= campground.title%> 30 |
31 |

32 | <%= campground.description%> 33 |

34 |
35 |
    36 |
  • 37 | <%= campground.location%> 38 |
  • 39 |
  • Submitted by <%= campground.author.username%> 40 |
  • 41 |
  • $<%= campground.price%>/night
  • 42 |
43 | <% if( currentUser && campground.author.equals(currentUser._id)) {%> 44 |
45 | Edit 46 |
47 | 48 |
49 |
50 | <% } %> 51 | 54 |
55 | 56 |
57 |
58 |
59 | 60 | <% if(currentUser){ %> 61 |

Leave a Review

62 |
64 | 68 |
69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 | 84 | 86 |
87 | Looks good! 88 |
89 |
90 | 91 |
92 | <% } %> 93 | <% for(let review of campground.reviews) { %> 94 |
95 |
96 |
97 | <%= review.author.username%> 98 |
99 |

100 | Rated: <%= review.rating %> stars 101 |

102 | 103 |

Review: <%= review.body %> 104 |

105 | <% if( currentUser && review.author.equals(currentUser._id)) {%> 106 |
109 | 110 |
111 | <% } %> 112 |
113 |
114 | <% } %> 115 |
116 |
117 | 118 | 122 | 123 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 |
3 |
4 | 10 |
11 |
-------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | YelpCamp 8 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

YelpCamp

18 | 28 |
29 |
30 |
31 |

YelpCamp

32 |

Welcome to YelpCamp!
Jump right in and explore our many campgrounds.
33 | Feel free to share some of your own and comment on others!

34 | View 35 | Campgrounds 36 |
37 | 38 |
39 |

© 2020

40 |
41 | 42 | 43 |
44 | 45 | 46 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /views/layouts/boilerplate.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | YelpCamp 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <%- include('../partials/navbar')%> 22 |
23 | <%- include('../partials/flash')%> 24 | <%- body %> 25 |
26 | <%- include('../partials/footer')%> 27 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /views/partials/flash.ejs: -------------------------------------------------------------------------------- 1 | <% if(success && success.length) {%> 2 | 8 | <% } %> 9 | 10 | <% if(error && error.length) {%> 11 | 17 | <% } %> -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | © YelpCamp 2020 4 |
5 |
-------------------------------------------------------------------------------- /views/partials/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/users/login.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 | 3 |
4 |
5 |
6 |
7 | 9 |
10 |
Login
11 |
12 |
13 | 14 | 15 |
16 | Looks good! 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 | Looks good! 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
-------------------------------------------------------------------------------- /views/users/register.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate')%> 2 |
3 |
4 |
5 |
6 | 8 |
9 |
Register
10 |
11 |
12 | 13 | 14 |
15 | Looks good! 16 |
17 |
18 |
19 | 20 | 21 |
22 | Looks good! 23 |
24 |
25 |
26 | 27 | 28 |
29 | Looks good! 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------