├── .gitignore ├── utils ├── wrapAsync.js └── ExpressError.js ├── views ├── listings │ ├── error.ejs │ ├── new.ejs │ ├── edit.ejs │ ├── show.ejs │ └── index.ejs ├── includes │ ├── footer.ejs │ ├── flash.ejs │ └── navbar.ejs ├── users │ ├── login.ejs │ └── signup.ejs └── layouts │ └── boilerplate.ejs ├── public ├── js │ ├── map.js │ └── script.js └── css │ ├── style.css │ └── rating.css ├── models ├── user.js ├── review.js └── listing.js ├── cloudConfig.js ├── init ├── index.js └── data.js ├── controllers ├── reviews.js ├── users.js └── listings.js ├── routes ├── review.js ├── user.js └── listing.js ├── schema.js ├── package.json ├── middleware.js ├── app.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ -------------------------------------------------------------------------------- /utils/wrapAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | }; 5 | }; -------------------------------------------------------------------------------- /views/listings/error.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 |
4 | 7 |
-------------------------------------------------------------------------------- /utils/ExpressError.js: -------------------------------------------------------------------------------- 1 | class ExpressError extends Error { 2 | constructor(statusCode, message) { 3 | super(); 4 | this.statusCode = statusCode; 5 | this.message = message; 6 | } 7 | } 8 | 9 | module.exports = ExpressError; -------------------------------------------------------------------------------- /public/js/map.js: -------------------------------------------------------------------------------- 1 | maptilersdk.config.apiKey = mapToken ; 2 | 3 | const map = new maptilersdk.Map({ 4 | container: 'map', // container's id or the HTML element to render the map 5 | style: "basic-v2", 6 | // style: "maptilersdk.MapStyle.STREETS", 7 | center: [77.216721,28.644800], // starting position [lng, lat] 8 | zoom: 9, // starting zoom 9 | }); 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | }, 10 | }); 11 | 12 | userSchema.plugin(passportLocalMongoose); 13 | 14 | module.exports = mongoose.model("User", userSchema); -------------------------------------------------------------------------------- /models/review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const reviewSchema = new Schema ({ 5 | comment: String, 6 | rating: { 7 | type: Number, 8 | min: 1, 9 | max: 5, 10 | }, 11 | createdAt: { 12 | type: Date , 13 | default: Date.now(), 14 | }, 15 | author: { 16 | type: Schema.Types.ObjectId, 17 | ref: "User", 18 | }, 19 | }); 20 | 21 | module.exports = mongoose.model("Review", reviewSchema); -------------------------------------------------------------------------------- /views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict' 3 | 4 | // Fetch all the forms we want to apply custom Bootstrap validation styles to 5 | const forms = document.querySelectorAll('.needs-validation') 6 | 7 | // Loop over them and prevent submission 8 | Array.from(forms).forEach(form => { 9 | form.addEventListener('submit', event => { 10 | if (!form.checkValidity()) { 11 | event.preventDefault() 12 | event.stopPropagation() 13 | } 14 | 15 | form.classList.add('was-validated') 16 | }, false) 17 | }) 18 | })() -------------------------------------------------------------------------------- /cloudConfig.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUD_NAME, 6 | api_key: process.env.CLOUD_API_KEY, 7 | api_secret: process.env.CLOUD_API_SECRET 8 | }); 9 | 10 | const storage = new CloudinaryStorage({ 11 | cloudinary: cloudinary, 12 | params: { 13 | folder: 'wanderlust_DEV', 14 | allowerdFormats: ["png", "jpg", "jpeg", "webp"], 15 | }, 16 | }); 17 | 18 | module.exports = { 19 | cloudinary, 20 | storage, 21 | }; -------------------------------------------------------------------------------- /views/includes/flash.ejs: -------------------------------------------------------------------------------- 1 | <% if(success && success.length) { %> 2 | 11 | <% } %> 12 | 13 | 14 | <% if(error && error.length) { %> 15 | 24 | <% } %> 25 | -------------------------------------------------------------------------------- /init/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const initData = require("./data.js"); 3 | const Listing = require("../models/listing.js"); 4 | 5 | const MONGO_URL = "mongodb://127.0.0.1:27017/wanderlust"; 6 | 7 | main() 8 | .then(() => { 9 | console.log("connected to DB"); 10 | }) 11 | .catch((err) => { 12 | console.log(err); 13 | }); 14 | 15 | async function main() { 16 | await mongoose.connect(MONGO_URL); 17 | } 18 | 19 | const initDB = async () => { 20 | await Listing.deleteMany({}); 21 | initData.data = initData.data.map((obj) => ({ ...obj, owner: '6713930770b104d536516458'})); 22 | await Listing.insertMany(initData.data); 23 | console.log("data was initialized"); 24 | }; 25 | 26 | initDB(); 27 | -------------------------------------------------------------------------------- /controllers/reviews.js: -------------------------------------------------------------------------------- 1 | const Listing = require("../models/listing"); 2 | const Review = require("../models/review"); 3 | 4 | module.exports.createReview = async (req, res) => { 5 | let listing = await Listing.findById(req.params.id); 6 | let newReview = new Review(req.body.review); 7 | newReview.author = req.user._id; 8 | 9 | listing.reviews.push(newReview); 10 | 11 | await newReview.save(); 12 | await listing.save(); 13 | req.flash("success", "New Review Created!"); 14 | res.redirect(`/listings/${listing._id}`); 15 | }; 16 | 17 | module.exports.destroyReview = async (req,res) => { 18 | let { id, reviewId } = req.params; 19 | 20 | await Listing.findByIdAndUpdate(id, {$pull: {reviews: reviewId}}); 21 | await Review.findByIdAndDelete(reviewId); 22 | req.flash("success", "Review Deleted!"); 23 | res.redirect(`/listings/${id}`); 24 | }; -------------------------------------------------------------------------------- /routes/review.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router({ mergeParams: true}); 3 | const wrapAsync = require("../utils/wrapAsync.js"); 4 | const ExpressError = require("../utils/ExpressError.js"); 5 | const Review = require("../models/review.js"); 6 | const Listing = require("../models/listing.js"); 7 | const { validateReview, isLoggedIn,isReviewAuthor } = require("../middleware.js"); 8 | 9 | const reviewController = require("../controllers/reviews.js"); 10 | const review = require("../models/review.js"); 11 | 12 | //Post Route 13 | router.post( 14 | "/", 15 | isLoggedIn, 16 | validateReview, 17 | wrapAsync(reviewController.createReview) 18 | ); 19 | 20 | //Delete Review Route 21 | router.delete( 22 | "/:reviewId", 23 | isLoggedIn, 24 | isReviewAuthor, 25 | wrapAsync(reviewController.destroyReview) 26 | ); 27 | 28 | module.exports = router; -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const User = require("../models/user.js"); 4 | const wrapAsync = require("../utils/wrapAsync"); 5 | const passport = require("passport"); 6 | const { saveRedirectUrl,isReviewAuthor } = require("../middleware.js"); 7 | 8 | const userController = require("../controllers/users.js"); 9 | 10 | router 11 | .route("/signup") 12 | .get(userController.renderSignupForm) 13 | .post(wrapAsync(userController.signup)); 14 | 15 | router 16 | .route("/login") 17 | .get( userController.renderLoginForm) 18 | .post( 19 | saveRedirectUrl, 20 | passport.authenticate("local", { 21 | failureRedirect: "/login", 22 | failureFlash: true, 23 | }), 24 | userController.login 25 | ); 26 | 27 | router.get("/logout", userController.logout ); 28 | 29 | module.exports = router; -------------------------------------------------------------------------------- /schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | 3 | module.exports.listingSchema = Joi.object({ 4 | listing: Joi.object({ 5 | title: Joi.string().required(), 6 | description: Joi.string().required(), 7 | // location: Joi.string().required(), 8 | location: Joi.array().items(Joi.string()).required(), // Location as an array of strings 9 | // country: Joi.string().required(), 10 | country: Joi.array().items(Joi.string()).required(), 11 | price: Joi.number().required().min(0), 12 | // image: Joi.string().allow("", null) 13 | image: Joi.object({ 14 | url: Joi.string().allow("", null), // Allowing empty or null values 15 | filename: Joi.string().allow("", null) 16 | }).allow(null) // The whole object can be null 17 | }).required() 18 | }); 19 | 20 | module.exports.reviewSchema = Joi.object({ 21 | review: Joi.object({ 22 | rating: Joi.number().required().min(1).max(5), 23 | comment: Joi.string().required(), 24 | }).required(), 25 | }); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "21.6.1" 4 | }, 5 | "name": "wanderlust", 6 | "version": "1.0.0", 7 | "description": "", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@mapbox/mapbox-sdk": "^0.16.1", 17 | "@maptiler/sdk": "^2.5.0", 18 | "cloudinary": "^1.21.0", 19 | "connect-flash": "^0.1.1", 20 | "connect-mongo": "^5.1.0", 21 | "cookie-parser": "^1.4.7", 22 | "dotenv": "^16.4.5", 23 | "ejs": "^3.1.10", 24 | "ejs-mate": "^4.0.0", 25 | "express": "^4.19.2", 26 | "express-session": "^1.18.1", 27 | "joi": "^17.13.3", 28 | "method-override": "^3.0.0", 29 | "mongoose": "^8.4.0", 30 | "multer": "^1.4.5-lts.1", 31 | "multer-storage-cloudinary": "^4.0.0", 32 | "node-fetch": "^2.7.0", 33 | "passport": "^0.7.0", 34 | "passport-local": "^1.0.0", 35 | "passport-local-mongoose": "^8.0.0", 36 | "punycode": "^2.3.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /views/users/login.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 |
4 |

Login

5 |
6 |
7 |
8 | 9 | 16 |
17 | 18 |
19 | 20 | 27 |
28 | 29 |
30 |
31 |
-------------------------------------------------------------------------------- /routes/listing.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const wrapAsync = require("../utils/wrapAsync.js"); 4 | const Listing = require("../models/listing.js"); 5 | const { isLoggedIn, isOwner, validateListing } = require("../middleware.js"); 6 | const listingController = require("../controllers/listings.js"); 7 | const multer = require("multer"); 8 | const {storage} = require("../cloudConfig.js"); 9 | const upload = multer({ storage }); 10 | 11 | router 12 | .route("/") 13 | .get(wrapAsync(listingController.index)) 14 | .post( 15 | isLoggedIn, 16 | upload.single("listing[image][url]"), 17 | validateListing, 18 | wrapAsync(listingController.createListing) 19 | ); 20 | 21 | //New Route 22 | router.get("/new", isLoggedIn, listingController.renderNewForm); 23 | 24 | router 25 | .route("/:id") 26 | .get(wrapAsync(listingController.showListing)) 27 | .put( 28 | isLoggedIn, 29 | isOwner, 30 | upload.single("listing[image][url]"), 31 | validateListing, 32 | wrapAsync(listingController.updateListing) 33 | ) 34 | .delete(isLoggedIn, isOwner, wrapAsync(listingController.destroyListing)); 35 | 36 | //Edit Route 37 | router.get("/:id/edit", 38 | isLoggedIn, 39 | isOwner, 40 | wrapAsync(listingController.renderEditForm) 41 | ); 42 | 43 | module.exports = router; -------------------------------------------------------------------------------- /controllers/users.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | 3 | module.exports.renderSignupForm = (req, res) => { 4 | res.render("users/signup.ejs"); 5 | }; 6 | 7 | module.exports.signup = async(req, res) => { 8 | try { 9 | let {username, email, password} = req.body; 10 | const newUser = new User({email, username}); 11 | const registeredUser = await User.register(newUser, password); 12 | console.log(registeredUser); 13 | req.login(registeredUser, (err) => { 14 | if (err) { 15 | return next(err); 16 | } 17 | req.flash("success", "Welcome to Wanderlust!"); 18 | res.redirect("/listings"); 19 | }); 20 | } catch(e) { 21 | req.flash("error", e.message); 22 | res.redirect("/signup"); 23 | } 24 | }; 25 | 26 | module.exports.renderLoginForm = (req, res) => { 27 | res.render("users/login.ejs"); 28 | }; 29 | 30 | module.exports.login = async (req, res) => { 31 | req.flash("success", "Welcome back to Wanderlust!"); 32 | let redirectUrl = res.locals.redirectUrl || "/listings"; 33 | res.redirect(redirectUrl); 34 | }; 35 | 36 | module.exports.logout = (req, res, next) => { 37 | req.logout((err) => { 38 | if(err) { 39 | return next(err); 40 | } 41 | req.flash("success", "you are logged out!"); 42 | let redirectUrl = req.session.redirectUrl || "/listings"; 43 | res.redirect(redirectUrl); 44 | // res.redirect(req.session.redirectUrl); 45 | 46 | }); 47 | }; -------------------------------------------------------------------------------- /views/users/signup.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 |
4 |

Signup on Wanderlust

5 |
6 |
7 |
8 | 9 | 16 |
Looks good!!
17 |
18 | 19 |
20 | 21 | 28 |
29 | 30 |
31 | 32 | 39 |
40 | 41 |
42 |
43 |
-------------------------------------------------------------------------------- /models/listing.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const Review = require("./review.js"); 4 | 5 | // const listingSchema = new Schema({ 6 | // title: { 7 | // type: String, 8 | // required: true, 9 | // }, 10 | // description: String, 11 | // image: { 12 | // type: String, 13 | // default: 14 | // "https://images.unsplash.com/photo-1625505826533-5c80aca7d157?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGdvYXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 15 | // set: (v) => 16 | // v === "" 17 | // ? "https://images.unsplash.com/photo-1625505826533-5c80aca7d157?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGdvYXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60" 18 | // : v, 19 | // }, 20 | // price: Number, 21 | // location: String, 22 | // country: String, 23 | // }); 24 | 25 | const listingSchema = new Schema({ 26 | title: { 27 | type: 'string', 28 | required: true 29 | }, 30 | description: String, 31 | image: { 32 | url: String, 33 | filename: String, 34 | }, 35 | price: Number, 36 | location: [String], 37 | country: [String] , 38 | reviews: [ 39 | { 40 | type: Schema.Types.ObjectId, 41 | ref: "Review", 42 | }, 43 | ], 44 | owner: { 45 | type: Schema.Types.ObjectId, 46 | ref: "User", 47 | }, 48 | }); 49 | 50 | listingSchema.post("findOneAndDelete", async (listing) => { 51 | if (listing) { 52 | await Review.deleteMany({ _id: { $in: listing.reviews } }); 53 | } 54 | }); 55 | 56 | const Listing = mongoose.model("Listing", listingSchema); 57 | module.exports = Listing; 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /views/layouts/boilerplate.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 13 | " > 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <%- include("../includes/navbar.ejs") %> 27 | 28 |
29 | <%- include("../includes/flash.ejs") %> 30 | <%- body %> 31 |
32 | 33 | <%- include("../includes/footer.ejs") %> 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | const Listing = require("./models/listing"); 2 | const Review = require("./models/review"); 3 | const ExpressError = require("./utils/ExpressError.js"); 4 | const { listingSchema, reviewSchema } = require("./schema.js"); 5 | 6 | module.exports.isLoggedIn = (req, res, next) => { 7 | if (!req.isAuthenticated()) { 8 | req.session.redirectUrl = req.originalUrl; 9 | req.flash("error","you must be logged in to create listing!"); 10 | return res.redirect("/login"); 11 | } 12 | next(); 13 | }; 14 | 15 | module.exports.saveRedirectUrl = (req, res, next) => { 16 | if (req.session.redirectUrl) { 17 | res.locals.redirectUrl = req.session.redirectUrl; 18 | } 19 | next(); 20 | }; 21 | 22 | module.exports.isOwner = async (req, res, next) => { 23 | let { id } = req.params; 24 | let listing = await Listing.findById(id); 25 | if (!listing.owner.equals(res.locals.currUser._id)) { 26 | req.flash("error", "You are not the owner of this listing"); 27 | return res.redirect(`/listings/${id}`); 28 | } 29 | next(); 30 | }; 31 | 32 | module.exports.isReviewAuthor = async (req, res, next) => { 33 | let { id, reviewId } = req.params; 34 | let review = await Review.findById(reviewId); 35 | if (!review.author.equals(res.locals.currUser._id)) { 36 | req.flash('error', "You are not author of this review"); 37 | return res.redirect(`/listings/${id}`); 38 | } 39 | next(); 40 | }; 41 | 42 | module.exports.validateListing = (req, res, next) => { 43 | let {error} = listingSchema.validate(req.body); 44 | if (error) { 45 | let errMsg = error.details.map((el) => el.message).join(","); 46 | throw new ExpressError(400, errMsg); 47 | } else { 48 | next(); 49 | } 50 | }; 51 | 52 | module.exports.validateReview = (req, res, next) => { 53 | let {error} = reviewSchema.validate(req.body); 54 | if (error) { 55 | let errMsg = error.details.map((el) => el.message).join(","); 56 | throw new ExpressError(400, errMsg); 57 | } else { 58 | next(); 59 | } 60 | }; 61 | 62 | -------------------------------------------------------------------------------- /views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Plus Jakarta Sans', sans-serif !important; 3 | display: flex; 4 | flex-direction: column; 5 | min-height: 100vh; 6 | } 7 | 8 | .container { 9 | flex: 1; 10 | } 11 | 12 | /* Navbar */ 13 | .navbar { 14 | height: 5rem; 15 | background-color: white; 16 | } 17 | 18 | .fa-compass { 19 | color: #fe424d; 20 | font-size: 2rem; 21 | } 22 | 23 | nav-link { 24 | color: #222222 !important; 25 | } 26 | 27 | /* Footer */ 28 | .f-info-links a { 29 | text-decoration: none; 30 | color: #222222; 31 | } 32 | 33 | .f-info-links a:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | .f-info-links, 38 | .f-info-socials, .f-info-brand { 39 | width: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | } 44 | 45 | .f-info-socials i { 46 | font-size: 1.2rem; 47 | margin-right: 1rem; 48 | } 49 | 50 | .f-info { 51 | text-align: center; 52 | height: 8rem; 53 | background-color: #ebebeb; 54 | display: flex; 55 | flex-wrap: wrap; 56 | justify-content: center; 57 | align-items: space-evenly; 58 | } 59 | 60 | /* Cards */ 61 | .listing-card { 62 | border: none !important; 63 | margin-bottom: 2rem; 64 | } 65 | 66 | .card-img-top { 67 | border-radius: 1rem !important; 68 | width: 100% !important; 69 | object-fit: cover !important; 70 | } 71 | 72 | .card-body { 73 | padding: 0 !important; 74 | } 75 | 76 | .card-text p { 77 | font-weight: 400 ; 78 | } 79 | 80 | .listing-link{ 81 | text-decoration: none; 82 | } 83 | 84 | /* Card Effect */ 85 | .card-img-overlay:hover { 86 | opacity: 0.2; 87 | background-color: white; 88 | } 89 | 90 | /* New Page */ 91 | .add-btn { 92 | background-color: #fe424d !important; 93 | border: none !important; 94 | } 95 | 96 | /* Edit Page */ 97 | .edit-btn { 98 | background-color: #fe424d !important; 99 | border: none !important; 100 | } 101 | 102 | /* Show Page */ 103 | .show-img { 104 | height: 30vh; 105 | } 106 | 107 | .btns { 108 | display: flex; 109 | } 110 | 111 | /* Map */ 112 | #map { 113 | height: 400px; 114 | width: 80vh ; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /controllers/listings.js: -------------------------------------------------------------------------------- 1 | const Listing = require('../models/listing.js'); 2 | const mapToken = process.env.MAP_TOKEN ; 3 | 4 | // Middleware to set currUser in res.locals 5 | module.exports.setLocals = (req, res, next) => { 6 | res.locals.currUser = req.user; // Passport.js sets req.user when logged in 7 | next(); 8 | }; 9 | 10 | module.exports.index = async (req, res) => { 11 | const allListings = await Listing.find({}); 12 | res.render("listings/index.ejs", { allListings }); 13 | }; 14 | 15 | module.exports.renderNewForm = (req, res) => { 16 | res.render("listings/new.ejs"); 17 | }; 18 | 19 | module.exports.showListing = async (req, res) => { 20 | let { id } = req.params; 21 | const listing = await Listing.findById(id) 22 | .populate({ 23 | path: "reviews", 24 | populate: { 25 | path: "author", 26 | }, 27 | }) 28 | .populate("owner"); 29 | if(!listing) { 30 | req.flash("error", "Listing you requested for does not exist!"); 31 | res.redirect("/listings"); 32 | } 33 | res.render("listings/show.ejs", { listing }); 34 | }; 35 | 36 | 37 | module.exports.createListing = async (req, res, next) => { 38 | 39 | let url = req.file.path; 40 | let filename = req.file.filename; 41 | 42 | const newListing = new Listing(req.body.listing); 43 | newListing.owner = req.user._id; 44 | newListing.image = {url, filename}; 45 | 46 | await newListing.save(); 47 | req.flash("success", "New Listing Created!"); 48 | res.redirect("/listings"); 49 | }; 50 | 51 | module.exports.renderEditForm = async (req, res) => { 52 | let { id } = req.params; 53 | const listing = await Listing.findById(id); 54 | if(!listing) { 55 | req.flash("error", "Listing you requested for does not exist!"); 56 | res.redirect("/listings"); 57 | } 58 | 59 | let originalImageUrl = listing.image.url; 60 | originalImageUrl = originalImageUrl.replace("/upload", "/upload/,w_250"); 61 | res.render("listings/edit.ejs", { listing, originalImageUrl }); 62 | }; 63 | 64 | module.exports.updateListing = async (req, res) => { 65 | let { id } = req.params; 66 | let listing = await Listing.findByIdAndUpdate(id, { ...req.body.listing }); 67 | 68 | if(typeof req.file !== "undefined") { 69 | let url = req.file.path; 70 | let filename = req.file.filename; 71 | listing.image = { url, filename }; 72 | await listing.save(); 73 | } 74 | req.flash("success", "Listing Updated!"); 75 | res.redirect(`/listings/${id}`); 76 | }; 77 | 78 | module.exports.destroyListing = async (req, res) => { 79 | let { id } = req.params; 80 | let deletedListing = await Listing.findByIdAndDelete(id); 81 | console.log(deletedListing); 82 | req.flash("success", "Listing Deleted!"); 83 | res.redirect("/listings"); 84 | }; 85 | 86 | -------------------------------------------------------------------------------- /views/listings/new.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 |
3 |
4 | 5 |

Create a New Listing

6 |
13 | 14 |
15 | Title 16 | 23 |
Title looks good!
24 |
25 | 26 |
27 | Description 28 | 33 |
Please enter a short description
34 |
35 | 36 |
37 | Upload Listing Image 38 | 44 |
45 | 46 |
47 |
48 | Price 49 | 56 |
Price should be valid
57 |
58 | 59 |
60 | country 61 | 68 |
Country name should be valid
69 |
70 |
71 | 72 |
73 | Location 74 | 81 |
Location should be valid
82 |
83 | 84 |

85 | 86 |

87 |
88 |
89 |
-------------------------------------------------------------------------------- /views/listings/edit.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 |
3 |
4 |

Edit your Listing

5 |
10 |
11 | 12 | 19 |
Title looks good!
20 |
21 | 22 |
23 | 24 | 27 |
Please enter a short description
28 |
29 | 30 |
31 | Original Listing Image
32 | 33 |
34 | 35 |
36 | 37 | 42 |
43 | 44 |
45 |
46 | 47 | 54 |
Price should be valid
55 |
56 | 57 |
58 | 59 | 66 |
Country name should be valid
67 |
68 |
69 | 70 |
71 | 72 | 79 |
Location should be valid
80 |
81 | 82 | 83 |
84 |
85 |
86 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | if(process.env.NODE_ENV != "production") { 2 | require("dotenv").config(); 3 | } 4 | 5 | const express = require("express"); 6 | const app = express(); 7 | const mongoose = require("mongoose"); 8 | const path = require("path"); 9 | const methodOverride = require("method-override"); 10 | const ejsmate = require("ejs-mate"); 11 | const ExpressError = require("./utils/ExpressError.js"); 12 | const session = require("express-session"); 13 | const MongoStore = require("connect-mongo"); 14 | const flash = require("connect-flash"); 15 | const passport = require("passport"); 16 | const LocalStrategy = require("passport-local"); 17 | const User = require("./models/user.js"); 18 | 19 | const listingRouter = require("./routes/listing.js"); 20 | const reviewRouter = require("./routes/review.js"); 21 | const userRouter = require("./routes/user.js"); 22 | 23 | const dburl = process.env.ATLASDB_URL; 24 | 25 | main() 26 | .then(() => { 27 | console.log("connected to DB"); 28 | }) 29 | .catch((err) => { 30 | console.log(err); 31 | }); 32 | 33 | async function main() { 34 | await mongoose.connect(dburl); 35 | }; 36 | 37 | app.set("view engine", "ejs"); 38 | app.set("views", path.join(__dirname,"views")); 39 | app.use(express.urlencoded({extended: true})); 40 | app.use(methodOverride("_method")); 41 | app.engine("ejs", ejsmate); 42 | app.use(express.static(path.join(__dirname, "/public"))); 43 | 44 | const store = MongoStore.create({ 45 | mongoUrl: dburl, 46 | crypto: { 47 | secret: process.env.SECRET, 48 | }, 49 | touchAfter: 24*3600, 50 | }); 51 | 52 | store.on("error", () =>{ 53 | console.log("ERROR in MONGO SESSION STORE", err); 54 | }); 55 | 56 | const sessionOptions = { 57 | store, 58 | secret: process.env.SECRET, 59 | resave: false, 60 | saveUninitialized: true, 61 | cookie: { 62 | expires: Date.now() + 7 * 24 * 60 * 60 * 1000, 63 | maxAge: 7 * 24 * 60 * 60 * 1000, 64 | httpOnly: true, 65 | }, 66 | }; 67 | 68 | app.use(session(sessionOptions)); 69 | app.use(flash()); 70 | 71 | app.use(passport.initialize()); 72 | app.use(passport.session()); 73 | passport.use(new LocalStrategy(User.authenticate())); 74 | 75 | passport.serializeUser(User.serializeUser()); 76 | passport.deserializeUser(User.deserializeUser()); 77 | 78 | app.use((req, res, next) => { 79 | res.locals.success = req.flash("success"); 80 | res.locals.error = req.flash("error"); 81 | res.locals.currUser = req.user; 82 | next(); 83 | }); 84 | 85 | app.use("/listings", listingRouter); 86 | app.use("/listings/:id/reviews", reviewRouter); 87 | app.use("/", userRouter); 88 | 89 | // app.get("/testListing", async (req,res) => { 90 | // let sampleListing = new Listing({ 91 | // title: "My New Villa", 92 | // description: "By the beach", 93 | // price: 1200, 94 | // location: "calangute, Goa", 95 | // country: "India" , 96 | // }); 97 | 98 | // await sampleListing.save(); 99 | // console.log("sample was saved"); 100 | // res.send("successfull testing"); 101 | // }); 102 | 103 | app.all("*", (req, res, next) => { 104 | next(new ExpressError(404, "Page Not Found!")); 105 | }); 106 | 107 | app.use((err, req, res, next) => { 108 | let {statusCode = 500, message = "Something went wrong!"} = err; 109 | res.status(statusCode).render("./listings/error.ejs", { message }); 110 | // res.status(statusCode).send(message); 111 | }); 112 | 113 | app.listen(8080, () => { 114 | console.log("server is listening to port 8080"); 115 | }); 116 | 117 | -------------------------------------------------------------------------------- /views/listings/show.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 5 | 6 |
7 |
8 |

<%= listing.title %>

9 |
10 |
11 | listing_image 16 |
17 |

Owned by <%= listing.owner.username %>

18 | 19 |

<%= listing.description %>

20 |

21 | ₹ <%= listing.price.toLocaleString("en-IN") %> 22 |

23 |

<%= listing.location %>

24 |

<%= listing.country %>

25 |

26 |
27 |
28 | 29 |
30 | <% if(currUser && listing.owner._id.equals(currUser._id)) { %> 31 |
32 | Edit 37 | 38 |
39 | 40 |
41 |
42 |
43 | <% } %> 44 | 45 |
46 | <% if(currUser) { %> 47 |
48 |

Leave a Review

49 |
54 | 55 |
56 | 57 |
58 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 |
81 | 82 |
83 | 91 |
Please add some comments for review
92 |
93 | 94 |
95 |
96 | <% } %> 97 | 98 | 99 | 100 | <% if(listing.reviews.length > 0) { %> 101 |
102 |

All Reviews

103 | <% for(review of listing.reviews) { %> 104 |
105 |
106 |
@<%= review.author.username %>
107 |

111 |

<%= review.comment %>

112 | 113 |
118 | <% if(currUser) { %> 119 | 120 | <% } %> 121 |
122 |
123 |
124 | <% } %> 125 |
126 | <% } %> 127 |
128 | 129 |
130 |

Where you'll be

131 |
132 |
133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # NomadNook 3 | 4 | ## Overview 5 | 6 | NomadNook is a full-stack web application designed to facilitate the listing and booking of vacation rentals. The platform allows users to explore various accommodations, view detailed property information, and share their experiences through reviews and ratings. With an intuitive interface and robust backend, NomadNook provides a seamless user experience for both travelers and property owners. 7 | 8 | ## Key Features 9 | 10 | * **MVC Architecture:** Structured using the Model-View-Controller design pattern for organized and maintainable code. 11 | 12 | * **Data Validation:** Server-side and client-side validation to maintain data integrity and consistency. 13 | 14 | * **Error Handling:** Comprehensive error-handling mechanisms for a smoother user experience. 15 | 16 | * **Authorization:** Role-based access control ensuring only authorized users can modify or delete listings. 17 | 18 | * **User Authentication:** Secure registration and login system to protect user data. 19 | 20 | * **Interactive Map Integration:** Leverages MapTiler for geolocation and visualization of property locations. 21 | 22 | * **Cloudinary Integration:** Efficient image storage and management for property listings. 23 | 24 | * **Reliable Cloud Database:** Reliable and scalable cloud database for seamless data storage. 25 | 26 | ## Technology Stack 27 | 28 | * **Node.js:** Node.js is a runtime environment that allows running JavaScript on the server side. 29 | 30 | * **Express js:** Express js is a web framework built on Node.js that simplifies the creation of server-side applications and APIs. 31 | 32 | * **MongoDB:** MongoDB is a NoSQL database that stores data in flexible, JSON-like documents. 33 | 34 | * **JavaScript:** JavaScript adds interactivity and dynamic behavior to web pages, enabling client-side scripting. 35 | 36 | * **Bootstrap4:** Bootstrap4 is a front-end framework that provides pre-built responsive grid systems and components for faster web development. 37 | 38 | * **CSS3:** CSS3 styles and layouts the visual appearance of HTML elements. 39 | 40 | * **HTML5:** HTML5 defines the structure and semantic content of web pages. 41 | 42 | ## Use Cases 43 | 44 | * **Property Hosting:** Hosts can list their properties, set pricing, manage availability, and attract travelers effortlessly. 45 | 46 | * **Discover Unique Stays:** Guests can explore a wide range of curated properties, from city apartments to countryside retreats, using detailed filters. 47 | 48 | * **Trust and Feedback System:** A robust review and rating system allows guests to share their experiences, helping future travelers make informed decisions while enabling hosts to improve their offerings and build credibility. 49 | 50 | ## Benefits 51 | 52 | * **Fast and Scalable Backend:** Node.js and Express.js power a high-performance backend, ensuring smooth handling of increased traffic and user activity as the platform scales. 53 | 54 | * **Secure and Scalable Data Management:** The database offers a robust and cloud-based solution, ensuring efficient handling of user information, listings, and ratings, with strong security measures and scalability for future growth. 55 | 56 | * **Enhanced User Experience:** Leveraging HTML5, CSS3, and Bootstrap4 creates a modern, responsive, and user-friendly interface that adapts seamlessly across devices. 57 | 58 | * **Cost-Effective and Open-Source:** Leveraging open-source tools keeps development and operational costs low. 59 | 60 | * **Rapid Development and Updates:** The MERN stack’s full-stack JavaScript environment accelerates development cycles, making it easier to implement features and debug issues quickly. 61 | 62 | ## API Services 63 | 64 | ### MongoDB Atlas 65 | 66 | MongoDB Atlas serves as the database backbone of the application. As a cloud-based NoSQL database service, it ensures secure and efficient storage of critical application data such as user information, property listings, and reviews. 67 | [Learn more about MongoDB Atlas](https://www.mongodb.com/lp/cloud/atlas/try4-reg?utm_source=bing&utm_campaign=search_bs_pl_evergreen_atlas_core_prosp-brand_gic-null_apac-in_ps-all_desktop_eng_lead&utm_term=mongodb%20atlas%20database&utm_medium=cpc_paid_search&utm_ad=e&utm_ad_campaign_id=415204524&adgroup=1208363748749217&msclkid=8ade0753229d1d69119660c1229cadfd) 68 | 69 | ### Cloudinary 70 | 71 | Cloudinary is used for managing and optimizing media assets within the platform. It allows the efficient hosting of property images uploaded by users, along with dynamic transformations such as resizing and cropping to maintain consistency. 72 | [Learn more about Cloudinary](https://cloudinary.com/) 73 | 74 | ### MapTiler 75 | 76 | MapTiler provides interactive mapping and geolocation services that enhance the user experience by displaying property locations visually. This integration allows users to see the exact geographic coordinates of a property, making it easier to explore and evaluate rental options. 77 | [Learn more about MapTiler](https://www.maptiler.com/) 78 | 79 | ## Deployment 80 | 81 | ### Render 82 | Used for hosting and deploying the backend and frontend services. 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /views/listings/index.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 55 | 56 |
57 |
58 |
59 |

Trending

60 |
61 |
62 |
63 |

Rooms

64 |
65 |
66 |
67 |

Iconic cities

68 |
69 |
70 |
71 |

Mountain

72 |
73 |
74 |
75 |

Castles

76 |
77 |
78 |
79 |

Amazing Pools

80 |
81 |
82 |
83 |

Camping

84 |
85 |
86 |
87 |

Farms

88 |
89 |
90 |
91 |

Arctic

92 |
93 |
94 |
95 |

Domes

96 |
97 |
98 |
99 |

Boats

100 |
101 | 102 |
103 |
104 | 105 | 106 |
107 |
108 |
109 | 110 | 111 | 112 |
113 | <% for(let listing of allListings) { %> 114 | 115 |
116 | listing_image 122 |
123 |
124 |

125 | <%= listing.title %>
126 | ₹ <%= listing.price.toLocaleString("en-IN") %> / night 127 |    + 18% GST 128 |

129 |
130 |
131 |
132 | <% } %> 133 |
134 | 135 | 140 | 141 | 154 | -------------------------------------------------------------------------------- /public/css/rating.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-slot { 51 | display: block; 52 | position: relative; 53 | width: 150px; 54 | min-height: 60px; 55 | padding: 0; 56 | border: none; 57 | } 58 | 59 | .starability-slot > input { 60 | position: absolute; 61 | margin-right: -100%; 62 | opacity: 0; 63 | } 64 | 65 | .starability-slot > input:checked ~ label, 66 | .starability-slot > input:focus ~ label { 67 | background-position: 0 0; 68 | } 69 | 70 | .starability-slot > input:checked + label, 71 | .starability-slot > input:focus + label { 72 | background-position: 0 -30px; 73 | } 74 | 75 | .starability-slot > input[disabled]:hover + label { 76 | cursor: default; 77 | } 78 | 79 | .starability-slot > input:not([disabled]):hover ~ label { 80 | background-position: 0 0; 81 | } 82 | 83 | .starability-slot > input:not([disabled]):hover + label { 84 | background-position: 0 -30px; 85 | } 86 | 87 | .starability-slot > input:not([disabled]):hover + label::before { 88 | opacity: 1; 89 | } 90 | 91 | .starability-slot > input:focus + label { 92 | outline: 1px dotted #999; 93 | } 94 | 95 | .starability-slot .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-slot > .input-no-rate:focus ~ .starability-focus-ring { 106 | opacity: 1; 107 | } 108 | 109 | .starability-slot > 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-slot > 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-slot > label:nth-of-type(5)::before { 135 | width: 120px; 136 | left: -120px; 137 | } 138 | 139 | .starability-slot > label:nth-of-type(4)::before { 140 | width: 90px; 141 | left: -90px; 142 | } 143 | 144 | .starability-slot > label:nth-of-type(3)::before { 145 | width: 60px; 146 | left: -60px; 147 | } 148 | 149 | .starability-slot > label:nth-of-type(2)::before { 150 | width: 30px; 151 | left: -30px; 152 | } 153 | 154 | .starability-slot > 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-slot > 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-slot { 168 | width: auto; 169 | } 170 | .starability-slot > input { 171 | position: static; 172 | margin-right: 0; 173 | opacity: 1; 174 | } 175 | .starability-slot .input-no-rate { 176 | display: none; 177 | } 178 | .starability-slot > 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-slot > label::before, .starability-slot > label::after { 188 | display: none; 189 | } 190 | } 191 | 192 | .starability-slot > input:checked ~ label, 193 | .starability-slot > input:hover ~ label, 194 | .starability-slot > input:focus ~ label { 195 | -webkit-transition: background-position .7s; 196 | transition: background-position .7s; 197 | } -------------------------------------------------------------------------------- /init/data.js: -------------------------------------------------------------------------------- 1 | const sampleListings = [ 2 | // { 3 | // title: "Cozy Beachfront Cottage", 4 | // description: 5 | // "Escape to this charming beachfront cottage for a relaxing getaway. Enjoy stunning ocean views and easy access to the beach.", 6 | // image:"https://images.unsplash.com/photo-1552733407-5d5c46c3bb3b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fHRyYXZlbHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 7 | // price: 1500, 8 | // location: "Malibu", 9 | // country: "United States", 10 | // }, 11 | // { 12 | // title: "Modern Loft in Downtown", 13 | // description: 14 | // "Stay in the heart of the city in this stylish loft apartment. Perfect for urban explorers!", 15 | // image:"https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fHRyYXZlbHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 16 | 17 | // price: 1200, 18 | // location: "New York City", 19 | // country: "United States", 20 | // }, 21 | // { 22 | // title: "Mountain Retreat", 23 | // description: 24 | // "Unplug and unwind in this peaceful mountain cabin. Surrounded by nature, it's a perfect place to recharge.", 25 | // image:"https://images.unsplash.com/photo-1571896349842-33c89424de2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8aG90ZWxzfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 26 | 27 | // price: 1000, 28 | // location: "Aspen", 29 | // country: "United States", 30 | // }, 31 | // { 32 | // title: "Historic Villa in Tuscany", 33 | // description: 34 | // "Experience the charm of Tuscany in this beautifully restored villa. Explore the rolling hills and vineyards.", 35 | // image:"https://images.unsplash.com/photo-1566073771259-6a8506099945?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8aG90ZWxzfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 36 | 37 | // price: 2500, 38 | // location: "Florence", 39 | // country: "Italy", 40 | // }, 41 | // { 42 | // title: "Secluded Treehouse Getaway", 43 | // description: 44 | // "Live among the treetops in this unique treehouse retreat. A true nature lover's paradise.", 45 | // image:"https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTV8fGhvdGVsc3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 46 | 47 | // price: 800, 48 | // location: "Portland", 49 | // country: "United States", 50 | // }, 51 | // { 52 | // title: "Beachfront Paradise", 53 | // description: 54 | // "Step out of your door onto the sandy beach. This beachfront condo offers the ultimate relaxation.", 55 | // image:"https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjB8fGhvdGVsc3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 56 | 57 | // price: 2000, 58 | // location: "Cancun", 59 | // country: "Mexico", 60 | // }, 61 | // { 62 | // title: "Rustic Cabin by the Lake", 63 | // description: 64 | // "Spend your days fishing and kayaking on the serene lake. This cozy cabin is perfect for outdoor enthusiasts.", 65 | // image:"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fG1vdW50YWlufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 66 | 67 | // price: 900, 68 | // location: "Lake Tahoe", 69 | // country: "United States", 70 | // }, 71 | // { 72 | // title: "Luxury Penthouse with City Views", 73 | // description: 74 | // "Indulge in luxury living with panoramic city views from this stunning penthouse apartment.", 75 | // image:"https://images.unsplash.com/photo-1622396481328-9b1b78cdd9fd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8c2t5JTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 76 | 77 | // price: 3500, 78 | // location: "Los Angeles", 79 | // country: "United States", 80 | // }, 81 | // { 82 | // title: "Ski-In/Ski-Out Chalet", 83 | // description: 84 | // "Hit the slopes right from your doorstep in this ski-in/ski-out chalet in the Swiss Alps.", 85 | // image:"https://images.unsplash.com/photo-1502784444187-359ac186c5bb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fHNreSUyMHZhY2F0aW9ufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 86 | 87 | // price: 3000, 88 | // location: "Verbier", 89 | // country: "Switzerland", 90 | // }, 91 | // { 92 | // title: "Safari Lodge in the Serengeti", 93 | // description: 94 | // "Experience the thrill of the wild in a comfortable safari lodge. Witness the Great Migration up close.", 95 | // image:"https://images.unsplash.com/photo-1493246507139-91e8fad9978e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjl8fG1vdW50YWlufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 96 | 97 | // price: 4000, 98 | // location: "Serengeti National Park", 99 | // country: "Tanzania", 100 | // }, 101 | // { 102 | // title: "Historic Canal House", 103 | // description: 104 | // "Stay in a piece of history in this beautifully preserved canal house in Amsterdam's iconic district.", 105 | // image:"https://images.unsplash.com/photo-1504280390367-361c6d9f38f4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8Y2FtcGluZ3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 106 | 107 | // price: 1800, 108 | // location: "Amsterdam", 109 | // country: "Netherlands", 110 | // }, 111 | // { 112 | // title: "Private Island Retreat", 113 | // description: 114 | // "Have an entire island to yourself for a truly exclusive and unforgettable vacation experience.", 115 | // image:"https://images.unsplash.com/photo-1618140052121-39fc6db33972?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8bG9kZ2V8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 116 | 117 | // price: 10000, 118 | // location: "Fiji", 119 | // country: "Fiji", 120 | // }, 121 | // { 122 | // title: "Charming Cottage in the Cotswolds", 123 | // description: 124 | // "Escape to the picturesque Cotswolds in this quaint and charming cottage with a thatched roof.", 125 | // image:"https://images.unsplash.com/photo-1602088113235-229c19758e9f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8YmVhY2glMjB2YWNhdGlvbnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 126 | 127 | // price: 1200, 128 | // location: "Cotswolds", 129 | // country: "United Kingdom", 130 | // }, 131 | // { 132 | // title: "Historic Brownstone in Boston", 133 | // description: 134 | // "Step back in time in this elegant historic brownstone located in the heart of Boston.", 135 | // image:"https://images.unsplash.com/photo-1533619239233-6280475a633a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fHNreSUyMHZhY2F0aW9ufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 136 | 137 | // price: 2200, 138 | // location: "Boston", 139 | // country: "United States", 140 | // }, 141 | // { 142 | // title: "Beachfront Bungalow in Bali", 143 | // description: 144 | // "Relax on the sandy shores of Bali in this beautiful beachfront bungalow with a private pool.", 145 | // image:"https://images.unsplash.com/photo-1602391833977-358a52198938?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fGNhbXBpbmd8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 146 | 147 | // price: 1800, 148 | // location: "Bali", 149 | // country: "Indonesia", 150 | // }, 151 | // { 152 | // title: "Mountain View Cabin in Banff", 153 | // description: 154 | // "Enjoy breathtaking mountain views from this cozy cabin in the Canadian Rockies.", 155 | // image:"https://images.unsplash.com/photo-1521401830884-6c03c1c87ebb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 156 | 157 | // price: 1500, 158 | // location: "Banff", 159 | // country: "Canada", 160 | // }, 161 | // { 162 | // title: "Art Deco Apartment in Miami", 163 | // description: 164 | // "Step into the glamour of the 1920s in this stylish Art Deco apartment in South Beach.", 165 | // image:"https://plus.unsplash.com/premium_photo-1670963964797-942df1804579?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTZ8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 166 | 167 | // price: 1600, 168 | // location: "Miami", 169 | // country: "United States", 170 | // }, 171 | // { 172 | // title: "Tropical Villa in Phuket", 173 | // description: 174 | // "Escape to a tropical paradise in this luxurious villa with a private infinity pool in Phuket.", 175 | // image:"https://images.unsplash.com/photo-1470165301023-58dab8118cc9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 176 | 177 | // price: 3000, 178 | // location: "Phuket", 179 | // country: "Thailand", 180 | // }, 181 | // { 182 | // title: "Historic Castle in Scotland", 183 | // description: 184 | // "Live like royalty in this historic castle in the Scottish Highlands. Explore the rugged beauty of the area.", 185 | // image:"https://images.unsplash.com/photo-1585543805890-6051f7829f98?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGJlYWNoJTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 186 | 187 | // price: 4000, 188 | // location: "Scottish Highlands", 189 | // country: "United Kingdom", 190 | // }, 191 | // { 192 | // title: "Desert Oasis in Dubai", 193 | // description: 194 | // "Experience luxury in the middle of the desert in this opulent oasis in Dubai with a private pool.", 195 | // image:"https://images.unsplash.com/photo-1518684079-3c830dcef090?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZHViYWl8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 196 | 197 | // price: 5000, 198 | // location: "Dubai", 199 | // country: "United Arab Emirates", 200 | // }, 201 | // { 202 | // title: "Rustic Log Cabin in Montana", 203 | // description: 204 | // "Unplug and unwind in this cozy log cabin surrounded by the natural beauty of Montana.", 205 | // image:"https://images.unsplash.com/photo-1586375300773-8384e3e4916f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTN8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 206 | 207 | // price: 1100, 208 | // location: "Montana", 209 | // country: "United States", 210 | // }, 211 | // { 212 | // title: "Beachfront Villa in Greece", 213 | // description: 214 | // "Enjoy the crystal-clear waters of the Mediterranean in this beautiful beachfront villa on a Greek island.", 215 | // image:"https://images.unsplash.com/photo-1602343168117-bb8ffe3e2e9f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8dmlsbGF8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 216 | 217 | // price: 2500, 218 | // location: "Mykonos", 219 | // country: "Greece", 220 | // }, 221 | // { 222 | // title: "Eco-Friendly Treehouse Retreat", 223 | // description: 224 | // "Stay in an eco-friendly treehouse nestled in the forest. It's the perfect escape for nature lovers.", 225 | // image:"https://images.unsplash.com/photo-1488462237308-ecaa28b729d7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OXx8c2t5JTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 226 | 227 | // price: 750, 228 | // location: "Costa Rica", 229 | // country: "Costa Rica", 230 | // }, 231 | // { 232 | // title: "Historic Cottage in Charleston", 233 | // description: 234 | // "Experience the charm of historic Charleston in this beautifully restored cottage with a private garden.", 235 | // image:"https://images.unsplash.com/photo-1587381420270-3e1a5b9e6904?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 236 | 237 | // price: 1600, 238 | // location: "Charleston", 239 | // country: "United States", 240 | // }, 241 | // { 242 | // title: "Modern Apartment in Tokyo", 243 | // description: 244 | // "Explore the vibrant city of Tokyo from this modern and centrally located apartment.", 245 | // image:"https://images.unsplash.com/photo-1480796927426-f609979314bd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTV8fHRva3lvfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 246 | 247 | // price: 2000, 248 | // location: "Tokyo", 249 | // country: "Japan", 250 | // }, 251 | // { 252 | // title: "Lakefront Cabin in New Hampshire", 253 | // description: 254 | // "Spend your days by the lake in this cozy cabin in the scenic White Mountains of New Hampshire.", 255 | // image:"https://images.unsplash.com/photo-1578645510447-e20b4311e3ce?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDF8fGNhbXBpbmd8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 256 | 257 | // price: 1200, 258 | // location: "New Hampshire", 259 | // country: "United States", 260 | // }, 261 | // { 262 | // title: "Luxury Villa in the Maldives", 263 | // description: 264 | // "Indulge in luxury in this overwater villa in the Maldives with stunning views of the Indian Ocean.", 265 | // image:"https://images.unsplash.com/photo-1439066615861-d1af74d74000?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8bGFrZXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 266 | 267 | // price: 6000, 268 | // location: "Maldives", 269 | // country: "Maldives", 270 | // }, 271 | // { 272 | // title: "Ski Chalet in Aspen", 273 | // description: 274 | // "Hit the slopes in style with this luxurious ski chalet in the world-famous Aspen ski resort.", 275 | // image: "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGxha2V8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 276 | // price: 4000, 277 | // location: "Aspen", 278 | // country: "United States", 279 | // }, 280 | { 281 | title: "Cozy Beachfront Cottage", 282 | description: 283 | "Escape to this charming beachfront cottage for a relaxing getaway. Enjoy stunning ocean views and easy access to the beach.", 284 | image: { 285 | filename: "listingimage", 286 | url: "https://images.unsplash.com/photo-1552733407-5d5c46c3bb3b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fHRyYXZlbHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 287 | }, 288 | price: 1500, 289 | location: "Malibu", 290 | country: "United States", 291 | }, 292 | { 293 | title: "Modern Loft in Downtown", 294 | description: 295 | "Stay in the heart of the city in this stylish loft apartment. Perfect for urban explorers!", 296 | image: { 297 | filename: "listingimage", 298 | url: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fHRyYXZlbHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 299 | }, 300 | price: 1200, 301 | location: "New York City", 302 | country: "United States", 303 | }, 304 | { 305 | title: "Mountain Retreat", 306 | description: 307 | "Unplug and unwind in this peaceful mountain cabin. Surrounded by nature, it's a perfect place to recharge.", 308 | image: { 309 | filename: "listingimage", 310 | url: "https://images.unsplash.com/photo-1571896349842-33c89424de2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8aG90ZWxzfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 311 | }, 312 | price: 1000, 313 | location: "Aspen", 314 | country: "United States", 315 | }, 316 | { 317 | title: "Historic Villa in Tuscany", 318 | description: 319 | "Experience the charm of Tuscany in this beautifully restored villa. Explore the rolling hills and vineyards.", 320 | image: { 321 | filename: "listingimage", 322 | url: "https://images.unsplash.com/photo-1566073771259-6a8506099945?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8aG90ZWxzfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 323 | }, 324 | price: 2500, 325 | location: "Florence", 326 | country: "Italy", 327 | }, 328 | { 329 | title: "Secluded Treehouse Getaway", 330 | description: 331 | "Live among the treetops in this unique treehouse retreat. A true nature lover's paradise.", 332 | image: { 333 | filename: "listingimage", 334 | url: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTV8fGhvdGVsc3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 335 | }, 336 | price: 800, 337 | location: "Portland", 338 | country: "United States", 339 | }, 340 | { 341 | title: "Beachfront Paradise", 342 | description: 343 | "Step out of your door onto the sandy beach. This beachfront condo offers the ultimate relaxation.", 344 | image: { 345 | filename: "listingimage", 346 | url: "https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjB8fGhvdGVsc3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 347 | }, 348 | price: 2000, 349 | location: "Cancun", 350 | country: "Mexico", 351 | }, 352 | { 353 | title: "Rustic Cabin by the Lake", 354 | description: 355 | "Spend your days fishing and kayaking on the serene lake. This cozy cabin is perfect for outdoor enthusiasts.", 356 | image: { 357 | filename: "listingimage", 358 | url: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fG1vdW50YWlufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 359 | }, 360 | price: 900, 361 | location: "Lake Tahoe", 362 | country: "United States", 363 | }, 364 | { 365 | title: "Luxury Penthouse with City Views", 366 | description: 367 | "Indulge in luxury living with panoramic city views from this stunning penthouse apartment.", 368 | image: { 369 | filename: "listingimage", 370 | url: "https://images.unsplash.com/photo-1622396481328-9b1b78cdd9fd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8c2t5JTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 371 | }, 372 | price: 3500, 373 | location: "Los Angeles", 374 | country: "United States", 375 | }, 376 | { 377 | title: "Ski-In/Ski-Out Chalet", 378 | description: 379 | "Hit the slopes right from your doorstep in this ski-in/ski-out chalet in the Swiss Alps.", 380 | image: { 381 | filename: "listingimage", 382 | url: "https://images.unsplash.com/photo-1502784444187-359ac186c5bb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fHNreSUyMHZhY2F0aW9ufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 383 | }, 384 | price: 3000, 385 | location: "Verbier", 386 | country: "Switzerland", 387 | }, 388 | { 389 | title: "Safari Lodge in the Serengeti", 390 | description: 391 | "Experience the thrill of the wild in a comfortable safari lodge. Witness the Great Migration up close.", 392 | image: { 393 | filename: "listingimage", 394 | url: "https://images.unsplash.com/photo-1493246507139-91e8fad9978e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjl8fG1vdW50YWlufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 395 | }, 396 | price: 4000, 397 | location: "Serengeti National Park", 398 | country: "Tanzania", 399 | }, 400 | { 401 | title: "Historic Canal House", 402 | description: 403 | "Stay in a piece of history in this beautifully preserved canal house in Amsterdam's iconic district.", 404 | image: { 405 | filename: "listingimage", 406 | url: "https://images.unsplash.com/photo-1504280390367-361c6d9f38f4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8Y2FtcGluZ3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 407 | }, 408 | price: 1800, 409 | location: "Amsterdam", 410 | country: "Netherlands", 411 | }, 412 | { 413 | title: "Private Island Retreat", 414 | description: 415 | "Have an entire island to yourself for a truly exclusive and unforgettable vacation experience.", 416 | image: { 417 | filename: "listingimage", 418 | url: "https://images.unsplash.com/photo-1618140052121-39fc6db33972?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8bG9kZ2V8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 419 | }, 420 | price: 10000, 421 | location: "Fiji", 422 | country: "Fiji", 423 | }, 424 | { 425 | title: "Charming Cottage in the Cotswolds", 426 | description: 427 | "Escape to the picturesque Cotswolds in this quaint and charming cottage with a thatched roof.", 428 | image: { 429 | filename: "listingimage", 430 | url: "https://images.unsplash.com/photo-1602088113235-229c19758e9f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8YmVhY2glMjB2YWNhdGlvbnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 431 | }, 432 | price: 1200, 433 | location: "Cotswolds", 434 | country: "United Kingdom", 435 | }, 436 | { 437 | title: "Historic Brownstone in Boston", 438 | description: 439 | "Step back in time in this elegant historic brownstone located in the heart of Boston.", 440 | image: { 441 | filename: "listingimage", 442 | url: "https://images.unsplash.com/photo-1533619239233-6280475a633a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fHNreSUyMHZhY2F0aW9ufGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 443 | }, 444 | price: 2200, 445 | location: "Boston", 446 | country: "United States", 447 | }, 448 | { 449 | title: "Beachfront Bungalow in Bali", 450 | description: 451 | "Relax on the sandy shores of Bali in this beautiful beachfront bungalow with a private pool.", 452 | image: { 453 | filename: "listingimage", 454 | url: "https://images.unsplash.com/photo-1602391833977-358a52198938?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fGNhbXBpbmd8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 455 | }, 456 | price: 1800, 457 | location: "Bali", 458 | country: "Indonesia", 459 | }, 460 | { 461 | title: "Mountain View Cabin in Banff", 462 | description: 463 | "Enjoy breathtaking mountain views from this cozy cabin in the Canadian Rockies.", 464 | image: { 465 | filename: "listingimage", 466 | url: "https://images.unsplash.com/photo-1521401830884-6c03c1c87ebb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 467 | }, 468 | price: 1500, 469 | location: "Banff", 470 | country: "Canada", 471 | }, 472 | { 473 | title: "Art Deco Apartment in Miami", 474 | description: 475 | "Step into the glamour of the 1920s in this stylish Art Deco apartment in South Beach.", 476 | image: { 477 | filename: "listingimage", 478 | url: "https://plus.unsplash.com/premium_photo-1670963964797-942df1804579?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTZ8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 479 | }, 480 | price: 1600, 481 | location: "Miami", 482 | country: "United States", 483 | }, 484 | { 485 | title: "Tropical Villa in Phuket", 486 | description: 487 | "Escape to a tropical paradise in this luxurious villa with a private infinity pool in Phuket.", 488 | image: { 489 | filename: "listingimage", 490 | url: "https://images.unsplash.com/photo-1470165301023-58dab8118cc9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 491 | }, 492 | price: 3000, 493 | location: "Phuket", 494 | country: "Thailand", 495 | }, 496 | { 497 | title: "Historic Castle in Scotland", 498 | description: 499 | "Live like royalty in this historic castle in the Scottish Highlands. Explore the rugged beauty of the area.", 500 | image: { 501 | filename: "listingimage", 502 | url: "https://images.unsplash.com/photo-1585543805890-6051f7829f98?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGJlYWNoJTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 503 | }, 504 | price: 4000, 505 | location: "Scottish Highlands", 506 | country: "United Kingdom", 507 | }, 508 | { 509 | title: "Desert Oasis in Dubai", 510 | description: 511 | "Experience luxury in the middle of the desert in this opulent oasis in Dubai with a private pool.", 512 | image: { 513 | filename: "listingimage", 514 | url: "https://images.unsplash.com/photo-1518684079-3c830dcef090?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZHViYWl8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 515 | }, 516 | price: 5000, 517 | location: "Dubai", 518 | country: "United Arab Emirates", 519 | }, 520 | { 521 | title: "Rustic Log Cabin in Montana", 522 | description: 523 | "Unplug and unwind in this cozy log cabin surrounded by the natural beauty of Montana.", 524 | image: { 525 | filename: "listingimage", 526 | url: "https://images.unsplash.com/photo-1586375300773-8384e3e4916f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTN8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 527 | }, 528 | price: 1100, 529 | location: "Montana", 530 | country: "United States", 531 | }, 532 | { 533 | title: "Beachfront Villa in Greece", 534 | description: 535 | "Enjoy the crystal-clear waters of the Mediterranean in this beautiful beachfront villa on a Greek island.", 536 | image: { 537 | filename: "listingimage", 538 | url: "https://images.unsplash.com/photo-1602343168117-bb8ffe3e2e9f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8dmlsbGF8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 539 | }, 540 | price: 2500, 541 | location: "Mykonos", 542 | country: "Greece", 543 | }, 544 | { 545 | title: "Eco-Friendly Treehouse Retreat", 546 | description: 547 | "Stay in an eco-friendly treehouse nestled in the forest. It's the perfect escape for nature lovers.", 548 | image: { 549 | filename: "listingimage", 550 | url: "https://images.unsplash.com/photo-1488462237308-ecaa28b729d7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OXx8c2t5JTIwdmFjYXRpb258ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 551 | }, 552 | price: 750, 553 | location: "Costa Rica", 554 | country: "Costa Rica", 555 | }, 556 | { 557 | title: "Historic Cottage in Charleston", 558 | description: 559 | "Experience the charm of historic Charleston in this beautifully restored cottage with a private garden.", 560 | image: { 561 | filename: "listingimage", 562 | url: "https://images.unsplash.com/photo-1587381420270-3e1a5b9e6904?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGxvZGdlfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 563 | }, 564 | price: 1600, 565 | location: "Charleston", 566 | country: "United States", 567 | }, 568 | { 569 | title: "Modern Apartment in Tokyo", 570 | description: 571 | "Explore the vibrant city of Tokyo from this modern and centrally located apartment.", 572 | image: { 573 | filename: "listingimage", 574 | url: "https://images.unsplash.com/photo-1480796927426-f609979314bd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTV8fHRva3lvfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60", 575 | }, 576 | price: 2000, 577 | location: "Tokyo", 578 | country: "Japan", 579 | }, 580 | { 581 | title: "Lakefront Cabin in New Hampshire", 582 | description: 583 | "Spend your days by the lake in this cozy cabin in the scenic White Mountains of New Hampshire.", 584 | image: { 585 | filename: "listingimage", 586 | url: "https://images.unsplash.com/photo-1578645510447-e20b4311e3ce?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDF8fGNhbXBpbmd8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 587 | }, 588 | price: 1200, 589 | location: "New Hampshire", 590 | country: "United States", 591 | }, 592 | { 593 | title: "Luxury Villa in the Maldives", 594 | description: 595 | "Indulge in luxury in this overwater villa in the Maldives with stunning views of the Indian Ocean.", 596 | image: { 597 | filename: "listingimage", 598 | url: "https://images.unsplash.com/photo-1439066615861-d1af74d74000?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8bGFrZXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 599 | }, 600 | price: 6000, 601 | location: "Maldives", 602 | country: "Maldives", 603 | }, 604 | { 605 | title: "Ski Chalet in Aspen", 606 | description: 607 | "Hit the slopes in style with this luxurious ski chalet in the world-famous Aspen ski resort.", 608 | image: { 609 | filename: "listingimage", 610 | url: "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGxha2V8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=800&q=60", 611 | }, 612 | price: 4000, 613 | location: "Aspen", 614 | country: "United States", 615 | }, 616 | { 617 | title: "Secluded Beach House in Costa Rica", 618 | description: 619 | "Escape to a secluded beach house on the Pacific coast of Costa Rica. Surf, relax, and unwind.", 620 | image: { 621 | filename: "listingimage", 622 | url: "https://images.unsplash.com/photo-1499793983690-e29da59ef1c2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YmVhY2glMjBob3VzZXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60", 623 | }, 624 | price: 1800, 625 | location: "Costa Rica", 626 | country: "Costa Rica", 627 | }, 628 | ]; 629 | 630 | module.exports = { data: sampleListings }; --------------------------------------------------------------------------------