├── utils ├── wrapAsync.js ├── ExpressError.js └── hereMaps.js ├── public ├── images │ └── image-1681900961930-333556675.png ├── js │ └── validate-form.js └── css │ ├── home.css │ └── stars.css ├── views ├── layouts │ ├── partials │ │ ├── footer.ejs │ │ ├── alert.ejs │ │ └── navbar.ejs │ └── app.ejs ├── error.ejs ├── auth │ ├── login.ejs │ └── register.ejs ├── home.ejs └── places │ ├── create.ejs │ ├── index.ejs │ ├── edit.ejs │ └── show.ejs ├── middlewares ├── isAuth.js ├── validateReview.js ├── validatePlace.js ├── isValidObjectId.js └── isAuthor.js ├── schemas ├── review.js └── place.js ├── models ├── review.js ├── user.js └── place.js ├── package.json ├── routes ├── reviews.js ├── user.js └── places.js ├── controllers ├── reviews.js ├── auth.js └── places.js ├── configs └── multer.js ├── app.js ├── .gitignore └── seeds └── place.js /utils/wrapAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = func => { 2 | return (req, res, next) => { 3 | func(req, res, next).catch(next); 4 | } 5 | } -------------------------------------------------------------------------------- /public/images/image-1681900961930-333556675.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunadiotic/yelpclone/HEAD/public/images/image-1681900961930-333556675.png -------------------------------------------------------------------------------- /views/layouts/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /middlewares/isAuth.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | if (!req.isAuthenticated()) { 3 | req.flash('error_msg', 'You are not logged in'); 4 | return res.redirect('/login'); 5 | } 6 | next(); 7 | } -------------------------------------------------------------------------------- /schemas/review.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi') 2 | 3 | module.exports.reviewSchema = Joi.object({ 4 | review: Joi.object({ 5 | rating: Joi.number().min(1).required(), 6 | body: Joi.string().required(), 7 | }).required() 8 | }) 9 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |
4 | 9 |
-------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /schemas/place.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi') 2 | 3 | module.exports.placeSchema = Joi.object({ 4 | place: Joi.object({ 5 | title: Joi.string().required(), 6 | description: Joi.string().required(), 7 | location: Joi.string().required(), 8 | price: Joi.number().min(0).required(), 9 | // image: Joi.string().required() 10 | }).required() 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 | unique: true 10 | } 11 | }) 12 | 13 | userSchema.plugin(passportLocalMongoose); 14 | 15 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /middlewares/validateReview.js: -------------------------------------------------------------------------------- 1 | const ExpressError = require('../utils/ExpressError'); 2 | const { reviewSchema } = require('../schemas/review'); 3 | 4 | module.exports = (req, res, next) => { 5 | const { error } = reviewSchema.validate(req.body); 6 | if (error) { 7 | const msg = error.details.map(el => el.message).join(',') 8 | return next(new ExpressError(msg, 400)) 9 | } else { 10 | next(); 11 | } 12 | } -------------------------------------------------------------------------------- /middlewares/validatePlace.js: -------------------------------------------------------------------------------- 1 | 2 | const { placeSchema } = require('../schemas/place'); 3 | const ExpressError = require('../utils/ExpressError'); 4 | 5 | module.exports = (req, res, next) => { 6 | const { error } = placeSchema.validate(req.body); 7 | if (error) { 8 | const msg = error.details.map(el => el.message).join(',') 9 | return next(new ExpressError(msg, 400)) 10 | } else { 11 | next(); 12 | } 13 | } -------------------------------------------------------------------------------- /views/layouts/partials/alert.ejs: -------------------------------------------------------------------------------- 1 | <% if(success_msg && success_msg.length ) {%> 2 | 6 | <% } %> 7 | 8 | <% if(error_msg && error_msg.length ) {%> 9 | 13 | <% } %> -------------------------------------------------------------------------------- /middlewares/isValidObjectId.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | module.exports = (url) => { 4 | return (req, res, next) => { 5 | const paramId = ['id', 'place_id', 'review_id'].find(param => req.params[param]); 6 | 7 | if (!paramId) { 8 | return next(); 9 | } 10 | 11 | const id = req.params[paramId]; 12 | if (!mongoose.Types.ObjectId.isValid(id)) { 13 | req.flash('error_msg', 'Invalid ID / Data tidak ditemukan'); 14 | return res.redirect(url || '/'); 15 | } 16 | 17 | next(); 18 | }; 19 | }; -------------------------------------------------------------------------------- /public/js/validate-form.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('.validated-form') 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 | })() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yelpclone", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "connect-flash": "^0.1.1", 13 | "ejs": "^3.1.9", 14 | "ejs-mate": "^4.0.0", 15 | "express": "^4.18.2", 16 | "express-session": "^1.17.3", 17 | "joi": "^17.9.1", 18 | "method-override": "^3.0.0", 19 | "mongoose": "^7.0.3", 20 | "multer": "^1.4.5-lts.1", 21 | "passport": "^0.6.0", 22 | "passport-local-mongoose": "^8.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /middlewares/isAuthor.js: -------------------------------------------------------------------------------- 1 | const Place = require("../models/place"); 2 | const Review = require("../models/review"); 3 | 4 | module.exports.isAuthorPlace = async (req, res, next) => { 5 | const { id } = req.params; 6 | const place = await Place.findById(id); 7 | 8 | if (!place.author.equals(req.user._id)) { 9 | req.flash('error_msg', 'Not Authorized!'); 10 | return res.redirect(`/places/${id}`); 11 | } 12 | next(); 13 | } 14 | 15 | module.exports.isAuthorReview = async (req, res, next) => { 16 | const { place_id, review_id } = req.params; 17 | const place = await Review.findById(review_id); 18 | 19 | if (!place.author.equals(req.user._id)) { 20 | req.flash('error_msg', 'Not Authorized!'); 21 | return res.redirect(`/places/${place_id}`); 22 | } 23 | next(); 24 | } -------------------------------------------------------------------------------- /routes/reviews.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const wrapAsync = require('../utils/wrapAsync'); 3 | const Place = require('../models/place'); 4 | const Review = require('../models/review'); 5 | const ReviewController = require('../controllers/reviews'); 6 | const isValidObjectId = require('../middlewares/isValidObjectId'); 7 | const isAuth = require('../middlewares/isAuth'); 8 | const validateReview = require('../middlewares/validateReview'); 9 | const { isAuthorReview } = require('../middlewares/isAuthor'); 10 | const router = express.Router({ mergeParams: true }); 11 | 12 | router.post('/', isAuth, isValidObjectId('/places'), validateReview, wrapAsync(ReviewController.store)) 13 | 14 | router.delete('/:review_id', isAuth, isAuthorReview, isValidObjectId('/places'), wrapAsync(ReviewController.destroy)) 15 | 16 | module.exports = router; -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const User = require('../models/user'); 4 | const AuthController = require('../controllers/auth') 5 | const wrapAsync = require('../utils/wrapAsync'); 6 | const passport = require('passport'); 7 | 8 | router.route('/register') 9 | .get((req, res) => { 10 | res.render('auth/register'); 11 | }) 12 | .post(wrapAsync(AuthController.register)) 13 | 14 | router.route('/login') 15 | .get(AuthController.loginForm) 16 | .post(passport.authenticate('local', { 17 | failureRedirect: '/login', 18 | failureFlash: { 19 | type: 'error_msg', 20 | msg: 'Invalid username or password' 21 | } 22 | }), AuthController.login) 23 | 24 | router.post('/logout', AuthController.logout) 25 | 26 | 27 | module.exports = router; -------------------------------------------------------------------------------- /controllers/reviews.js: -------------------------------------------------------------------------------- 1 | const Place = require("../models/place"); 2 | const Review = require("../models/review"); 3 | 4 | module.exports.store = async (req, res) => { 5 | const { place_id } = req.params; 6 | 7 | const review = new Review(req.body.review); 8 | review.author = req.user._id 9 | 10 | const place = await Place.findById(place_id); 11 | place.reviews.push(review); 12 | 13 | await review.save(); 14 | await place.save(); 15 | 16 | req.flash('success_msg', 'Review Created!'); 17 | res.redirect(`/places/${place_id}`); 18 | } 19 | 20 | module.exports.destroy = async (req, res) => { 21 | const { place_id, review_id } = req.params; 22 | await Place.findByIdAndUpdate(place_id, { $pull: { reviews: review_id } }); 23 | await Review.findByIdAndDelete(review_id); 24 | req.flash('success_msg', 'Review Deleted!'); 25 | res.redirect(`/places/${place_id}`); 26 | } -------------------------------------------------------------------------------- /configs/multer.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'); 2 | const path = require('path'); 3 | 4 | const storage = multer.diskStorage({ 5 | destination: function (req, file, cb) { 6 | cb(null, 'public/images/'); // direktori penyimpanan gambar dalam folder public 7 | }, 8 | filename: function (req, file, cb) { 9 | const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) 10 | cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); // format nama file 11 | } 12 | }); 13 | 14 | const upload = multer({ 15 | storage: storage, 16 | fileFilter: function (req, file, cb) { 17 | // fungsi untuk memeriksa format file yang diizinkan 18 | if (file.mimetype.startsWith('image/')) { 19 | cb(null, true); 20 | } else { 21 | cb(new Error('Only images are allowed.')); 22 | } 23 | } 24 | }); 25 | 26 | module.exports = upload; 27 | -------------------------------------------------------------------------------- /utils/hereMaps.js: -------------------------------------------------------------------------------- 1 | const ExpressError = require("./ExpressError"); 2 | const baseUrl = 'https://geocode.search.hereapi.com/v1' 3 | const apiKey = 'cKKA1D0ftrVJs4QkN-6rCiVWIOiCgsvi3UEVAjhWqAs' 4 | 5 | const geocode = async (address) => { 6 | const url = `${baseUrl}/geocode?q=${address}&apiKey=${apiKey}`; 7 | try { 8 | const response = await fetch(url); 9 | const data = await response.json(); 10 | const lat = data.items[0].position.lat; 11 | const lng = data.items[0].position.lng; 12 | return { lat, lng } 13 | } catch (err) { 14 | new ExpressError(err.message, 500) 15 | } 16 | } 17 | 18 | module.exports.geometry = async (address) => { 19 | try { 20 | const position = await geocode(address); 21 | return { 22 | type: 'Point', 23 | coordinates: [position.lng, position.lat] 24 | } 25 | } catch (err) { 26 | new ExpressError(err.message, 500) 27 | } 28 | } -------------------------------------------------------------------------------- /views/auth/login.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |
4 |

Login

5 |
6 |
7 |
8 | 9 | 10 |
11 | Looks good! 12 |
13 |
14 |
15 | 16 | 17 |
18 | Looks good! 19 |
20 |
21 | 22 |
23 |
24 |
-------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | module.exports.register = async (req, res) => { 4 | try { 5 | const { email, username, password } = req.body; 6 | const user = new User({ email, username }); 7 | const registerUser = await User.register(user, password); 8 | req.login(registerUser, err => { 9 | if (err) return next(err); 10 | req.flash('success_msg', 'You are now registered and logged in'); 11 | res.redirect('/places'); 12 | }) 13 | } catch (error) { 14 | req.flash('error_msg', error.message); 15 | res.redirect('/register'); 16 | } 17 | } 18 | 19 | module.exports.loginForm = (req, res) => { 20 | res.render('auth/login'); 21 | } 22 | 23 | module.exports.login = (req, res) => { 24 | req.flash('success_msg', 'You are now logged in'); 25 | res.redirect('/places'); 26 | } 27 | 28 | module.exports.logout = (req, res) => { 29 | req.logout(function (err) { 30 | if (err) { return next(err); } 31 | req.flash('success_msg', 'You are now logged out'); 32 | res.redirect('/places'); 33 | }); 34 | } -------------------------------------------------------------------------------- /models/place.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = mongoose.Schema 3 | const Review = require('./review') 4 | 5 | const placeSchema = new Schema({ 6 | title: String, 7 | price: Number, 8 | description: String, 9 | location: String, 10 | geometry: { 11 | type: { 12 | type: String, 13 | enum: ['Point'], 14 | required: true 15 | }, 16 | coordinates: { 17 | type: [Number], 18 | required: true 19 | } 20 | }, 21 | images: [ 22 | { 23 | url: String, 24 | filename: String 25 | } 26 | ], 27 | author: { 28 | type: Schema.Types.ObjectId, 29 | ref: 'User' 30 | }, 31 | reviews: [ 32 | { 33 | type: Schema.Types.ObjectId, 34 | ref: 'Review' 35 | } 36 | ] 37 | }) 38 | 39 | placeSchema.post('findOneAndDelete', async function (doc) { 40 | if (doc) { 41 | await Review.deleteMany({ 42 | _id: { 43 | $in: doc.reviews 44 | } 45 | }) 46 | } 47 | }) 48 | 49 | module.exports = mongoose.model('Place', placeSchema) -------------------------------------------------------------------------------- /routes/places.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const wrapAsync = require('../utils/wrapAsync'); 3 | const PlaceController = require('../controllers/places') 4 | const isValidObjectId = require('../middlewares/isValidObjectId'); 5 | const isAuth = require('../middlewares/isAuth'); 6 | const validatePlace = require('../middlewares/validatePlace'); 7 | const { isAuthorPlace } = require('../middlewares/isAuthor'); 8 | const upload = require('../configs/multer'); 9 | const router = express.Router(); 10 | 11 | router.route('/') 12 | .get(wrapAsync(PlaceController.index)) 13 | .post(isAuth, upload.array('image', 5), validatePlace, wrapAsync(PlaceController.store)) 14 | 15 | router.get('/create', isAuth, PlaceController.create) 16 | 17 | router.route('/:id') 18 | .get(isValidObjectId('/places'), wrapAsync(PlaceController.show)) 19 | .put(isAuth, isAuthorPlace, isValidObjectId('/places'), upload.array('image', 5), validatePlace, wrapAsync(PlaceController.update)) 20 | .delete(isAuth, isAuthorPlace, isValidObjectId('/places'), wrapAsync(PlaceController.destroy)) 21 | 22 | router.get('/:id/edit', isAuth, isAuthorPlace, isValidObjectId('/places'), wrapAsync(PlaceController.edit)) 23 | 24 | router.delete('/:id/images', wrapAsync(PlaceController.destroyImages)) 25 | 26 | module.exports = router; -------------------------------------------------------------------------------- /views/auth/register.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |
4 |

Register

5 |
6 |
7 |
8 | 9 | 10 |
11 | Looks good! 12 |
13 |
14 |
15 | 16 | 17 |
18 | Looks good! 19 |
20 |
21 |
22 | 23 | 24 |
25 | Looks good! 26 |
27 |
28 | 29 |
30 |
31 |
-------------------------------------------------------------------------------- /views/layouts/partials/navbar.ejs: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /public/css/home.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color: transparent !important; 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: 100; 8 | } 9 | 10 | .navbar-brand { 11 | color: #fff; 12 | font-size: 24px; 13 | font-weight: bold; 14 | } 15 | 16 | .navbar-nav { 17 | justify-content: flex-end; 18 | } 19 | 20 | .nav-item { 21 | margin-left: 10px; 22 | } 23 | 24 | .hero-section { 25 | background-image: url('https://wallpapercave.com/wp/wp4069427.jpg'); 26 | background-size: cover; 27 | background-position: center; 28 | height: 100vh; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | backdrop-filter: blur(5px); /* Tambahkan backdrop-filter */ 33 | } 34 | 35 | .hero-section::before { 36 | content: ""; 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | right: 0; 41 | bottom: 0; 42 | background-color: rgba(0, 0, 0, 0.5); /* Menambahkan warna gelap dan transparan */ 43 | z-index: -1; 44 | } 45 | 46 | .hero-section h1, 47 | .hero-section h4 { 48 | color: #fff; 49 | text-align: center; 50 | } 51 | 52 | .btn-outline-light { 53 | color: #fff; 54 | border-color: #fff; 55 | } 56 | 57 | .btn-group-center { 58 | display: flex; 59 | justify-content: center; 60 | } 61 | 62 | .btn-group-center .btn { 63 | margin: 0 5px; 64 | } 65 | 66 | .navbar-nav .nav-link { 67 | color: #fff; 68 | } 69 | 70 | .navbar-nav .nav-link:hover { 71 | text-decoration: underline; 72 | padding-bottom: 2px; 73 | } -------------------------------------------------------------------------------- /views/layouts/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BestPoint 8 | 9 | 11 | 13 | 15 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | <%- include('partials/navbar') %> 25 | 26 |
27 | <%- include('partials/alert') %> 28 | 29 | <%- body %> 30 |
31 | 32 | <%- include('partials/footer') %> 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const ejsMate = require('ejs-mate') 2 | const express = require('express'); 3 | const session = require('express-session'); 4 | const flash = require('connect-flash'); 5 | const ExpressError = require('./utils/ExpressError'); 6 | const methodOverride = require('method-override'); 7 | const mongoose = require('mongoose'); 8 | const path = require('path'); 9 | const app = express(); 10 | const passport = require('passport') 11 | const LocalStrategy = require('passport-local') 12 | const User = require('./models/user'); 13 | const hereMaps = require('./utils/hereMaps'); 14 | 15 | // connect to mongodb 16 | mongoose.connect('mongodb://127.0.0.1/yelp_clone') 17 | .then((result) => { 18 | console.log('connected to mongodb') 19 | }).catch((err) => { 20 | console.log(err) 21 | }); 22 | 23 | // view engine 24 | app.engine('ejs', ejsMate) 25 | app.set('view engine', 'ejs'); 26 | app.set('views', path.join(__dirname, 'views')); 27 | 28 | // middleware 29 | app.use(express.urlencoded({ extended: true })); 30 | app.use(methodOverride('_method')); 31 | app.use(express.static(path.join(__dirname, 'public'))); 32 | app.use(session({ 33 | secret: 'this-is-a-secret-key', 34 | resave: false, 35 | saveUninitialized: false, 36 | cookie: { 37 | httpOnly: true, 38 | expires: Date.now() + 1000 * 60 * 60 * 24 * 7, 39 | maxAge: 1000 * 60 * 60 * 24 * 7 40 | } 41 | })) 42 | app.use(flash()); 43 | app.use(passport.initialize()); 44 | app.use(passport.session()); 45 | 46 | passport.use(new LocalStrategy(User.authenticate())) 47 | passport.serializeUser(User.serializeUser()) 48 | passport.deserializeUser(User.deserializeUser()) 49 | 50 | app.use((req, res, next) => { 51 | res.locals.currentUser = req.user; 52 | res.locals.success_msg = req.flash('success_msg'); 53 | res.locals.error_msg = req.flash('error_msg'); 54 | res.locals.error = req.flash('error'); 55 | next(); 56 | }) 57 | 58 | app.get('/', async (req, res) => { 59 | res.render('home'); 60 | }); 61 | 62 | // places routes 63 | app.use('/', require('./routes/user')) 64 | app.use('/places', require('./routes/places')); 65 | app.use('/places/:place_id/reviews', require('./routes/reviews')); 66 | 67 | 68 | app.all('*', (req, res, next) => { 69 | next(new ExpressError('Page not found', 404)); 70 | }) 71 | 72 | app.use((err, req, res, next) => { 73 | const { statusCode = 500 } = err; 74 | if (!err.message) err.message = 'Oh No, Something Went Wrong!' 75 | res.status(statusCode).render('error', { err }); 76 | }) 77 | 78 | app.listen(3000, () => { 79 | console.log(`server is running on http://127.0.0.1:3000`); 80 | }); 81 | -------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Website 7 | 8 | 9 | 10 | 11 | 12 |
13 | 45 |
46 |
47 |
48 |

BestPoints

49 |

Discover the Best Points for Your Perfect Vacation

50 | 51 |
52 | Jelajahi Tempat 53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /views/places/create.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |
4 |

Add Place

5 |
6 |
7 |
8 | 9 | 10 |
11 | Looks good! 12 |
13 |
14 | Please fill out this field. 15 |
16 |
17 |
18 | 19 | 20 |
21 | Looks good! 22 |
23 |
24 | Please fill out this field. 25 |
26 |
27 |
28 | 29 | 30 |
31 | Looks good! 32 |
33 |
34 | Please fill out this field. 35 |
36 |
37 |
38 | 39 | 40 |
41 | Looks good! 42 |
43 |
44 | Please fill out this field. 45 |
46 |
47 |
48 | 49 | 50 |
51 | Looks good! 52 |
53 |
54 | Please fill out this field. 55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 | -------------------------------------------------------------------------------- /views/places/index.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |

Places

4 |
5 | <% for(const place of places) { %> 6 |
7 |
8 |
9 | <% if(place.images.length){ %> 10 | 11 | <% } else { %> 12 | 13 | <% } %> 14 |
15 |
16 |
17 |
<%= place.title %>
18 |

<%= place.description %>

19 |

20 | <%= place.location %> 21 |

22 | View <%= place.title %> 23 |
24 |
25 |
26 |
27 | <% } %> 28 | 29 | -------------------------------------------------------------------------------- /controllers/places.js: -------------------------------------------------------------------------------- 1 | const Place = require("../models/place"); 2 | const fs = require('fs'); 3 | const hereMaps = require('../utils/hereMaps'); 4 | 5 | module.exports.index = async (req, res) => { 6 | const places = await Place.find(); 7 | const clusterPlaces = places.map(place => { 8 | return { 9 | lat: place.geometry.coordinates[1], 10 | lng: place.geometry.coordinates[0] 11 | } 12 | }) 13 | const cluster = JSON.stringify(clusterPlaces) 14 | res.render('places/index', { places, cluster }); 15 | } 16 | 17 | module.exports.create = (req, res) => { 18 | res.render('places/create'); 19 | } 20 | 21 | module.exports.store = async (req, res, next) => { 22 | const images = req.files.map(file => ({ url: file.path, filename: file.filename })); 23 | const geoData = await hereMaps.geometry(req.body.place.location); 24 | 25 | const place = new Place(req.body.place); 26 | place.author = req.user._id 27 | place.images = images; 28 | place.geometry = geoData 29 | 30 | await place.save(); 31 | 32 | req.flash('success_msg', 'Place Created!'); 33 | res.redirect('/places'); 34 | } 35 | 36 | 37 | module.exports.show = async (req, res) => { 38 | const place = await Place.findById(req.params.id) 39 | .populate({ 40 | path: 'reviews', 41 | populate: { 42 | path: 'author' 43 | } 44 | }) 45 | .populate('author'); 46 | res.render('places/show', { place }); 47 | } 48 | 49 | module.exports.edit = async (req, res) => { 50 | const place = await Place.findById(req.params.id); 51 | res.render('places/edit', { place }); 52 | } 53 | 54 | module.exports.update = async (req, res) => { 55 | const { id } = req.params; 56 | const place = await Place.findByIdAndUpdate(id, { ...req.body.place }); 57 | 58 | if (req.files && req.files.length > 0) { 59 | place.images.forEach(image => { 60 | fs.unlinkSync(image.url); 61 | }); 62 | 63 | const images = req.files.map(file => ({ url: file.path, filename: file.filename })); 64 | place.images = images 65 | await place.save(); 66 | } 67 | 68 | req.flash('success_msg', 'Place Updated!'); 69 | res.redirect(`/places/${place._id}`); 70 | } 71 | 72 | module.exports.destroy = async (req, res) => { 73 | const { id } = req.params 74 | const place = await Place.findById(id); 75 | 76 | if (place.images.length > 0) { 77 | place.images.forEach(image => { 78 | fs.unlinkSync(image.url); 79 | }); 80 | } 81 | 82 | await Place.findByIdAndDelete(req.params.id); 83 | req.flash('success_msg', 'Place Deleted!'); 84 | res.redirect('/places'); 85 | } 86 | 87 | module.exports.destroyImages = async (req, res) => { 88 | try { 89 | const { id } = req.params 90 | const { images } = req.body 91 | 92 | // Cek apakah model Place ditemukan berdasarkan ID-nya 93 | const place = await Place.findById(id); 94 | if (!place) { 95 | req.flash('error_msg', 'Place not found'); 96 | return res.redirect(`/places/${id}/edit`); 97 | } 98 | 99 | if (!images || images.length === 0) { 100 | req.flash('error_msg', 'Please select at least one image'); 101 | return res.redirect(`/places/${id}/edit`); 102 | } 103 | 104 | // Hapus file gambar dari sistem file 105 | images.forEach(image => { 106 | fs.unlinkSync(image); 107 | }); 108 | 109 | // Hapus data gambar dari model Place 110 | await Place.findByIdAndUpdate( 111 | id, 112 | { $pull: { images: { url: { $in: images } } } }, 113 | { new: true } 114 | ); 115 | 116 | req.flash('success_msg', 'Successfully deleted images'); 117 | return res.redirect(`/places/${id}/edit`); 118 | } catch (err) { 119 | console.error(err); 120 | req.flash('error_msg', 'Failed to delete images'); 121 | return res.redirect(`/places/${id}/edit`); 122 | } 123 | } -------------------------------------------------------------------------------- /views/places/edit.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 |
4 |

Update Place

5 |
6 |
7 |
8 | 9 | 10 |
11 | Looks good! 12 |
13 |
14 | Please fill out this field. 15 |
16 |
17 |
18 | 19 | 20 |
21 | Looks good! 22 |
23 |
24 | Please fill out this field. 25 |
26 |
27 |
28 | 29 | 30 |
31 | Looks good! 32 |
33 |
34 | Please fill out this field. 35 |
36 |
37 |
38 | 39 | 40 |
41 | Looks good! 42 |
43 |
44 | Please fill out this field. 45 |
46 |
47 |
48 | 49 | 50 |
51 | Looks good! 52 |
53 |
54 | Please fill out this field. 55 |
56 |
57 | 58 |
59 |
60 | <% if(place.images.length > 0) {%> 61 |
62 |
Delete Images
63 |
64 | 65 | 66 |
67 |
68 | <% place.images.forEach((image, index) => {%> 69 |
70 |
71 | 72 |
73 | 77 |
78 |
79 |
80 | <% }) %> 81 |
82 | 83 |
84 |
85 | <% } %> 86 |
87 | 88 | -------------------------------------------------------------------------------- /seeds/place.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Place = require('../models/place'); 3 | const hereMaps = require('../utils/hereMaps'); 4 | 5 | mongoose.connect('mongodb://127.0.0.1/yelp_clone') 6 | .then((result) => { 7 | console.log('connected to mongodb') 8 | }).catch((err) => { 9 | console.log(err) 10 | }); 11 | 12 | async function seedPlaces() { 13 | const places = [ 14 | { 15 | title: 'Taman Mini Indonesia Indah', 16 | price: 20000, 17 | description: 'Taman hiburan keluarga dengan berbagai replika bangunan dari seluruh Indonesia', 18 | location: 'Taman Mini Indonesia Indah, Jakarta', 19 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 20 | }, 21 | { 22 | title: 'Pantai Kuta', 23 | price: 0, 24 | description: 'Pantai yang terkenal di Bali dengan pemandangan sunset yang indah', 25 | location: 'Pantai Kuta, Kuta, Badung Regency, Bali', 26 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 27 | }, 28 | { 29 | title: 'Borobudur', 30 | price: 0, 31 | description: 'Candi Buddha terbesar di dunia yang terletak di Magelang, Jawa Tengah', 32 | location: 'Borobudur, Magelang, Central Java', 33 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 34 | }, 35 | { 36 | title: 'Kawah Putih', 37 | price: 0, 38 | description: 'Kawah vulkanik dengan danau berwarna putih di Bandung, Jawa Barat', 39 | location: 'Kawah Putih, Ciwidey, West Java', 40 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 41 | }, 42 | { 43 | title: 'Malioboro', 44 | price: 0, 45 | description: 'Jalan utama di Yogyakarta dengan berbagai toko dan kuliner khas', 46 | location: 'Jl. Malioboro, Yogyakarta City, Special Region of Yogyakarta', 47 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 48 | }, 49 | { 50 | title: 'Pantai Tanjung Aan', 51 | price: 10000, 52 | description: 'Pantai dengan pasir berwarna putih dan air laut yang jernih di Lombok, Nusa Tenggara Barat', 53 | location: 'Pantai Tanjung Aan, Lombok, West Nusa Tenggara', 54 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 55 | }, 56 | { 57 | title: 'Bukit Bintang', 58 | price: 0, 59 | description: 'Kawasan perbelanjaan dan hiburan di Kuala Lumpur, Malaysia', 60 | location: 'Bukit Bintang, Kuala Lumpur, Federal Territory of Kuala Lumpur, Malaysia', 61 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 62 | }, 63 | { 64 | title: 'Candi Prambanan', 65 | price: 25000, 66 | description: 'Candi Hindu terbesar di Indonesia yang terletak di Yogyakarta', 67 | location: 'Candi Prambanan, Sleman, Special Region of Yogyakarta', 68 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 69 | }, 70 | { 71 | title: 'Danau Toba', 72 | price: 0, 73 | description: 'Danau vulkanik terbesar di Indonesia yang terletak di Sumatera Utara', 74 | location: 'Danau Toba, North Sumatra', 75 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 76 | }, 77 | { 78 | title: 'Kawah Ijen', 79 | price: 100000, 80 | description: 'Kawah vulkanik dengan fenomena blue fire di Banyuwangi, Jawa Timur', 81 | location: 'Kawah Ijen, Banyuwangi, East Java', 82 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 83 | }, 84 | { 85 | title: 'Pantai Sanur', 86 | price: 0, 87 | description: 'Pantai di Bali yang cocok untuk berenang dan melihat matahari terbit', 88 | location: 'Pantai Sanur, Denpasar, Bali', 89 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 90 | }, 91 | 92 | { 93 | title: 'Candi Borobudur', 94 | price: 25000, 95 | description: 'Candi Buddha terbesar di dunia yang terletak di Magelang, Jawa Tengah', 96 | location: 'Candi Borobudur, Borobudur, Magelang, Central Java', 97 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 98 | }, 99 | { 100 | title: 'Pulau Komodo', 101 | price: 5000000, 102 | description: 'Pulau di Indonesia yang terkenal dengan komodo, hewan terbesar di dunia', 103 | location: 'Pulau Komodo, East Nusa Tenggara', 104 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 105 | }, 106 | { 107 | title: 'Taman Nasional Gunung Rinjani', 108 | price: 150000, 109 | description: 'Taman nasional yang terletak di Lombok dan memiliki gunung tertinggi kedua di Indonesia', 110 | location: 'Taman Nasional Gunung Rinjani, Lombok, West Nusa Tenggara', 111 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 112 | }, 113 | { 114 | title: 'Bukit Tinggi', 115 | price: 0, 116 | description: 'Kota kecil yang terletak di Sumatera Barat dengan arsitektur khas Eropa', 117 | location: 'Bukit Tinggi, West Sumatra', 118 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 119 | }, 120 | { 121 | title: 'Pulau Weh', 122 | price: 0, 123 | description: 'Pulau yang terletak di ujung barat Indonesia dengan keindahan bawah laut yang luar biasa', 124 | location: 'Pulau Weh, Sabang, Aceh', 125 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 126 | }, 127 | { 128 | title: 'Taman Safari Indonesia', 129 | price: 0, 130 | description: 'Taman hiburan keluarga dengan berbagai satwa liar di Cisarua, Bogor', 131 | location: 'Taman Safari Indonesia, Cisarua, West Java', 132 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 133 | }, 134 | { 135 | title: 'Gunung Merbabu', 136 | price: 50000, 137 | description: 'Gunung yang terletak di Jawa Tengah dengan pemandangan matahari terbit yang indah', 138 | location: 'Gunung Merbabu, Central Java', 139 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 140 | }, 141 | { 142 | title: 'Pulau Lombok', 143 | price: 0, 144 | description: 'Pulau di Indonesia yang terkenal dengan keindahan pantainya', 145 | location: 'Pulau Lombok, West Nusa Tenggara', 146 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 147 | }, 148 | { 149 | title: 'Tanjung Lesung', 150 | price: 100000, 151 | description: 'Kawasan wisata pantai di Banten yang cocok untuk bersantai dan berenang', 152 | location: 'Tanjung Lesung, Pandeglang, Banten', 153 | image: 'https://source.unsplash.com/collection/2349781/1280x720' 154 | } 155 | ] 156 | 157 | const newPlace = await Promise.all(places.map(async (place) => { 158 | let geoData = await hereMaps.geometry(place.location); 159 | if (!geoData) { 160 | geoData = { type: 'Point', coordinates: [116.32883, -8.90952] } 161 | } 162 | return { 163 | ...place, 164 | author: '643d36579773b789e91ef660', 165 | images: { 166 | url: 'public\\images\\image-1681876521153-260851838.jpg', 167 | filename: 'image-1681876521153-260851838.jpg' 168 | }, 169 | geometry: { ...geoData } 170 | } 171 | })) 172 | 173 | try { 174 | await Place.deleteMany({}); 175 | await Place.insertMany(newPlace); 176 | console.log('Data berhasil disimpan'); 177 | } catch (err) { 178 | console.log('Terjadi kesalahan saat menyimpan data:', err); 179 | } finally { 180 | mongoose.disconnect(); 181 | } 182 | } 183 | 184 | seedPlaces(); -------------------------------------------------------------------------------- /views/places/show.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/app') %> 2 | 3 | 4 |
5 |
6 | 31 |
32 |
33 |
<%= place.title %>
34 |

<%= place.description %>

35 |
36 |
    37 |
  • <%= place.location %>
  • 38 |
  • Submitted by: <%= place.author.username %>
  • 39 |
  • <%= place.price %>
  • 40 |
41 | <% if(currentUser && place.author.equals(currentUser._id)) {%> 42 |
43 | Edit 44 |
45 | 46 |
47 |
48 | <% } %> 49 |
50 |
51 |
52 |
53 | <% if(currentUser) {%> 54 |

Leave a review

55 |
56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 |
72 | 73 | 74 |
75 | Looks good! 76 |
77 |
78 | Please fill out this field. 79 |
80 |
81 | 82 |
83 | <% } %> 84 | <% for(const review of place.reviews) { %> 85 |
86 |
87 |
Rating: <%= review.rating %>
88 |

89 | Rated: <%= review.rating %> stars 90 |

91 |
<%= review.author.username %>
92 |

<%= review.body %>

93 | <% if(currentUser && review.author.equals(currentUser._id)) {%> 94 |
95 | 96 |
97 | <% } %> 98 |
99 |
100 | <% } %> 101 |
102 |
103 | 104 | 108 | 109 | -------------------------------------------------------------------------------- /public/css/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 | } --------------------------------------------------------------------------------