├── public ├── images │ ├── beach.jpg │ ├── compass.png │ ├── travel.jpeg │ ├── download.webp │ ├── gssoc_logo.png │ ├── travel_cover-1500x1000.jpeg │ ├── adult-adventure-backlit-915972.jpg │ ├── Hot-Air-Balloon-captions-for-Instagram-1.webp │ └── cappadocia-hot-air-balloons-colorful-specatcle.jpg ├── manifest.json ├── offline.html └── CSS │ ├── footer-theme.css │ ├── tailwind.input.css │ ├── loading.css │ └── holiday.css ├── postcss.config.js ├── utils ├── wrapAsync.js ├── ExpressError.js └── updateCoordinates.js ├── .gitignore ├── routes ├── phraseAssistant.js ├── newsletter.js ├── review.js ├── compare.js ├── listing.js ├── notifications.js ├── weather.js ├── packingList.js ├── user.js ├── safety.js └── currency.js ├── models ├── review.js ├── phrase.js ├── newsletter.js ├── searchLog.js ├── wishlist.js ├── listing.js └── badgeDefinition.js ├── tests ├── setup.js ├── utils │ ├── ExpressError.test.js │ └── wrapAsync.test.js ├── models │ ├── user.test.js │ ├── review.test.js │ └── listing.test.js ├── middleware.test.js ├── reviews.test.js ├── map.test.js ├── auth.test.js ├── listings.test.js ├── README.md └── schema.test.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_template.yml ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── stale.yml │ ├── issue-create-automate-message.yml │ ├── codeql.yml │ └── ci.yml ├── schema.js ├── views ├── includes │ ├── flash.ejs │ ├── category-bar.ejs │ ├── footer.ejs │ └── theme-toggle.ejs ├── users │ ├── login.ejs │ └── liked.ejs ├── phraseAssistant │ ├── _widget.ejs │ └── index.ejs ├── termCondition.ejs ├── privacy.ejs ├── error.ejs └── listings │ └── edit.ejs ├── .env.example ├── cloudConfig.js ├── LICENSE ├── tailwind.config.js ├── init └── index.js ├── config └── passport.js ├── SECURITY.md ├── locales ├── es.json ├── gu.json ├── mr.json ├── fr.json ├── pa.json ├── bn.json ├── ur.json ├── as.json ├── te.json ├── or.json ├── kn.json ├── ml.json └── ta.json ├── middleware.js ├── package.json ├── controllers ├── reviews.js └── newsletter.js ├── CODE_OF_CONDUCT.md └── services ├── badgeService.js ├── weatherService.js └── aiSummarizationService.js /public/images/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/beach.jpg -------------------------------------------------------------------------------- /public/images/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/compass.png -------------------------------------------------------------------------------- /public/images/travel.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/travel.jpeg -------------------------------------------------------------------------------- /public/images/download.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/download.webp -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/gssoc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/gssoc_logo.png -------------------------------------------------------------------------------- /utils/wrapAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | } 5 | } -------------------------------------------------------------------------------- /public/images/travel_cover-1500x1000.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/travel_cover-1500x1000.jpeg -------------------------------------------------------------------------------- /public/images/adult-adventure-backlit-915972.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/adult-adventure-backlit-915972.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | .vscode/ 4 | *.log 5 | .DS_Store 6 | Thumbs.db 7 | 8 | # Tailwind CSS compiled output 9 | public/CSS/tailwind.output.css 10 | -------------------------------------------------------------------------------- /public/images/Hot-Air-Balloon-captions-for-Instagram-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/Hot-Air-Balloon-captions-for-Instagram-1.webp -------------------------------------------------------------------------------- /public/images/cappadocia-hot-air-balloons-colorful-specatcle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koushik369mondal/WanderLust/HEAD/public/images/cappadocia-hot-air-balloons-colorful-specatcle.jpg -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /routes/phraseAssistant.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const phraseCtrl = require('../controllers/phraseAssistant'); 4 | 5 | // Render full page (optional) 6 | router.get('/', phraseCtrl.renderIndex); 7 | 8 | // API endpoints used by the widget 9 | router.post('/api/translate', phraseCtrl.translate); 10 | router.post('/api/save', phraseCtrl.saveFavorite); 11 | router.get('/api/favorites', phraseCtrl.getFavorites); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /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); 22 | -------------------------------------------------------------------------------- /models/phrase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const phraseSchema = new mongoose.Schema({ 4 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false }, 5 | sourceText: { type: String, required: true }, 6 | translatedText: { type: String, required: true }, 7 | transliteration: { type: String }, 8 | language: { type: String }, 9 | category: { type: String }, 10 | audioUrl: { type: String }, 11 | createdAt: { type: Date, default: Date.now } 12 | }); 13 | 14 | module.exports = mongoose.model('Phrase', phraseSchema); 15 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | // Test Setup and Configuration 2 | require('dotenv').config({ path: '.env.test' }); 3 | 4 | // Set test environment 5 | process.env.NODE_ENV = 'test'; 6 | 7 | // Mock console methods to reduce noise in test output 8 | global.console = { 9 | ...console, 10 | log: jest.fn(), 11 | debug: jest.fn(), 12 | info: jest.fn(), 13 | warn: jest.fn(), 14 | }; 15 | 16 | // Global test timeout 17 | jest.setTimeout(10000); 18 | 19 | // Clean up after all tests 20 | afterAll(async () => { 21 | // Close any open connections 22 | await new Promise(resolve => setTimeout(resolve, 500)); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 📚 Read the Documentation 4 | url: https://github.com/koushik369mondal/WanderLust/blob/main/README.md 5 | about: Check the README for setup instructions and project information 6 | - name: 💬 Ask a Question 7 | url: https://github.com/koushik369mondal/WanderLust/discussions 8 | about: Use GitHub Discussions for questions and general discussion 9 | - name: 🤝 Contributing Guidelines 10 | url: https://github.com/koushik369mondal/WanderLust/blob/main/CONTRIBUTION.md 11 | about: Learn how to contribute to this project 12 | -------------------------------------------------------------------------------- /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 | country: Joi.string().required(), 9 | price: Joi.number().required().min(0), 10 | image: Joi.string().allow("", null) 11 | }).required(), 12 | }); 13 | 14 | module.exports.reviewSchema = Joi.object({ 15 | review: Joi.object({ 16 | rating: Joi.number().required().min(1).max(5), 17 | comment: Joi.string().required(), 18 | }).required(), 19 | }); 20 | -------------------------------------------------------------------------------- /views/includes/flash.ejs: -------------------------------------------------------------------------------- 1 | <% if (typeof success !== 'undefined' && success && success.length) { %> 2 | 6 | <% } %> 7 | 8 | <% if (typeof error !== 'undefined' && error && error.length) { %> 9 | 13 | <% } %> -------------------------------------------------------------------------------- /routes/newsletter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const newsletterController = require("../controllers/newsletter.js"); 4 | const { isLoggedIn } = require("../middleware.js"); 5 | 6 | // Subscribe to newsletter 7 | router.post("/subscribe", newsletterController.subscribe); 8 | 9 | // Unsubscribe from newsletter 10 | router.post("/unsubscribe", newsletterController.unsubscribe); 11 | 12 | // Newsletter management page 13 | router.get("/", (req, res) => { 14 | res.render("newsletter", { title: "Newsletter" }); 15 | }); 16 | 17 | // Get newsletter statistics (admin only) 18 | router.get("/stats", isLoggedIn, newsletterController.getStats); 19 | 20 | module.exports = router; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # MongoDB Configuration 2 | ATLAS_DB_URL=your_mongodb_atlas_connection_string 3 | 4 | # Cloudinary Configuration (for image uploads) 5 | CLOUD_NAME=your_cloudinary_cloud_name 6 | CLOUD_API_KEY=your_cloudinary_api_key 7 | CLOUD_API_SECRET=your_cloudinary_api_secret 8 | 9 | # Mapbox Configuration (for maps) 10 | MAP_TOKEN=your_mapbox_access_token 11 | 12 | # Session Configuration 13 | SECRET=your_session_secret_key 14 | 15 | # Google OAuth (optional) 16 | GOOGLE_CLIENT_ID=your_google_client_id 17 | GOOGLE_CLIENT_SECRET=your_google_client_secret 18 | 19 | # Weather API Configuration 20 | WEATHER_API_KEY=your_openweathermap_api_key 21 | 22 | # Holiday API Configuration (optional) 23 | HOLIDAY_API_KEY=your_calendarific_api_key 24 | 25 | # Port Configuration 26 | PORT=8080 -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WanderLust - Travel Planner", 3 | "short_name": "WanderLust", 4 | "description": "Plan and manage your perfect trips with offline access", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#fe424d", 9 | "orientation": "portrait-primary", 10 | "scope": "/", 11 | "icons": [ 12 | { 13 | "src": "/images/travel_cover-1500x1000.jpeg", 14 | "sizes": "192x192", 15 | "type": "image/jpeg", 16 | "purpose": "any maskable" 17 | }, 18 | { 19 | "src": "/images/travel_cover-1500x1000.jpeg", 20 | "sizes": "512x512", 21 | "type": "image/jpeg", 22 | "purpose": "any maskable" 23 | } 24 | ], 25 | "categories": ["travel", "lifestyle", "productivity"], 26 | "lang": "en-US", 27 | "dir": "ltr" 28 | } 29 | -------------------------------------------------------------------------------- /tests/utils/ExpressError.test.js: -------------------------------------------------------------------------------- 1 | const ExpressError = require('../utils/ExpressError'); 2 | 3 | describe('ExpressError Utility', () => { 4 | it('should create error with status and message', () => { 5 | const error = new ExpressError(404, 'Not Found'); 6 | 7 | expect(error.statusCode).toBe(404); 8 | expect(error.message).toBe('Not Found'); 9 | expect(error).toBeInstanceOf(Error); 10 | }); 11 | 12 | it('should create error with custom status code', () => { 13 | const error = new ExpressError(500, 'Internal Server Error'); 14 | 15 | expect(error.statusCode).toBe(500); 16 | expect(error.message).toBe('Internal Server Error'); 17 | }); 18 | 19 | it('should create error with 400 Bad Request', () => { 20 | const error = new ExpressError(400, 'Bad Request'); 21 | 22 | expect(error.statusCode).toBe(400); 23 | expect(error.message).toBe('Bad Request'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /models/newsletter.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const newsletterSchema = new Schema({ 5 | email: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | lowercase: true, 10 | trim: true, 11 | match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email address'] 12 | }, 13 | subscribedAt: { 14 | type: Date, 15 | default: Date.now 16 | }, 17 | isActive: { 18 | type: Boolean, 19 | default: true 20 | }, 21 | source: { 22 | type: String, 23 | default: 'footer', 24 | enum: ['footer', 'popup', 'signup', 'newsletter-page'] 25 | } 26 | }); 27 | 28 | // Create indexes for better performance 29 | // Email index is automatically created due to unique: true 30 | newsletterSchema.index({ subscribedAt: -1 }); 31 | 32 | module.exports = mongoose.model("Newsletter", newsletterSchema); -------------------------------------------------------------------------------- /models/searchLog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const searchLogSchema = new Schema({ 5 | query: { 6 | type: String, 7 | required: true, 8 | trim: true, 9 | maxlength: 200 10 | }, 11 | resultsCount: { 12 | type: Number, 13 | required: true, 14 | min: 0 15 | }, 16 | userAgent: { 17 | type: String, 18 | default: '' 19 | }, 20 | ipAddress: { 21 | type: String, 22 | default: '' 23 | }, 24 | timestamp: { 25 | type: Date, 26 | default: Date.now 27 | }, 28 | category: { 29 | type: String, 30 | default: '' 31 | } 32 | }); 33 | 34 | // Create indexes for better performance 35 | searchLogSchema.index({ timestamp: -1 }); 36 | searchLogSchema.index({ query: 1 }); 37 | searchLogSchema.index({ resultsCount: 1 }); 38 | 39 | module.exports = mongoose.model("SearchLog", searchLogSchema); -------------------------------------------------------------------------------- /cloudConfig.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require("cloudinary").v2; 2 | const CloudinaryStorage = require("multer-storage-cloudinary"); 3 | 4 | // Check if required environment variables are set 5 | if (!process.env.CLOUD_NAME || !process.env.CLOUD_API_KEY || !process.env.CLOUD_API_SECRET) { 6 | console.warn("⚠️ Cloudinary environment variables are missing in cloudConfig.js."); 7 | console.warn("ℹ️ Image uploads will not work properly without valid Cloudinary credentials."); 8 | } 9 | 10 | cloudinary.config({ 11 | cloud_name: process.env.CLOUD_NAME || "dummy_cloud_name", 12 | api_key: process.env.CLOUD_API_KEY || "dummy_api_key", 13 | api_secret: process.env.CLOUD_API_SECRET || "dummy_api_secret", 14 | }); 15 | 16 | const storage = CloudinaryStorage({ 17 | cloudinary: cloudinary, 18 | params: { 19 | folder: 'wanderlust_DEV', 20 | allowedFormats: ['jpeg', 'png', 'jpg'], 21 | }, 22 | }); 23 | 24 | module.exports = { 25 | cloudinary, 26 | storage, 27 | } -------------------------------------------------------------------------------- /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 { 8 | validateReview, 9 | isLoggedIn, 10 | isReviewAuthor, 11 | } = require("../middleware.js"); 12 | 13 | const reviewController = require("../controllers/reviews.js"); 14 | 15 | // Reviews -> Post Route 16 | router.post( 17 | "/", 18 | isLoggedIn, 19 | validateReview, 20 | wrapAsync(reviewController.createReview) 21 | ); 22 | 23 | // Delete Review Route 24 | router.delete( 25 | "/:reviewId", 26 | isLoggedIn, 27 | isReviewAuthor, 28 | wrapAsync(reviewController.destroyReview) 29 | ); 30 | 31 | // Translate Review Route 32 | router.get( 33 | "/translate/:reviewId", 34 | wrapAsync(reviewController.translateReview) 35 | ); 36 | 37 | module.exports = router; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 koushik369mondal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./views/**/*.ejs", 5 | "./public/**/*.js", 6 | "./public/**/*.html" 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: '#007bff', 12 | secondary: '#6c757d', 13 | success: '#28a745', 14 | danger: '#dc3545', 15 | warning: '#ffc107', 16 | info: '#17a2b8', 17 | // Premium brand colors for glassmorphism 18 | brand1: '#0d1b2a', 19 | brand2: '#1b263b', 20 | gold: '#ffc371', 21 | coral: '#ff5f6d', 22 | aqua: '#00c8ff', 23 | glass: 'rgba(255, 255, 255, 0.1)', 24 | 'glass-dark': 'rgba(0, 0, 0, 0.2)', 25 | }, 26 | fontFamily: { 27 | sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'Arial', 'sans-serif'], 28 | }, 29 | backdropBlur: { 30 | xs: '2px', 31 | }, 32 | }, 33 | }, 34 | plugins: [], 35 | // Prefix to avoid conflicts with Bootstrap 36 | prefix: 'tw-', 37 | // Don't reset Bootstrap styles 38 | corePlugins: { 39 | preflight: false, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.yml: -------------------------------------------------------------------------------- 1 | name: 📝 Create an Issue 2 | description: Report a bug, request a feature, or suggest an improvement 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | ## 👋 Welcome to WanderLust! 8 | 9 | Thank you for contributing! Please provide clear information about your issue. 10 | 11 | - type: textarea 12 | attributes: 13 | label: 📋 Description 14 | description: Clearly describe what you want to report or suggest 15 | placeholder: | 16 | Be specific and clear: 17 | - What is the issue or feature? 18 | - What should happen? 19 | - What actually happens (if bug)? 20 | 21 | Examples: 22 | • "Fix typo 'accomodation' → 'accommodation' in navbar.ejs line 23" 23 | • "Add search bar to filter listings by location and price" 24 | • "Security: JWT token validation missing in /api/user routes" 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: 📸 Screenshots (Optional) 31 | description: Add screenshots, mockups, or error messages if helpful 32 | placeholder: Drag and drop images here -------------------------------------------------------------------------------- /models/wishlist.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const wishlistSchema = new Schema({ 5 | user: { 6 | type: Schema.Types.ObjectId, 7 | ref: "User", 8 | required: true 9 | }, 10 | listing: { 11 | type: Schema.Types.ObjectId, 12 | ref: "Listing", 13 | required: true 14 | }, 15 | addedAt: { 16 | type: Date, 17 | default: Date.now 18 | }, 19 | notes: { 20 | type: String, 21 | maxlength: 200, 22 | default: "" 23 | }, 24 | tags: [{ 25 | type: String, 26 | maxlength: 20 27 | }], 28 | priority: { 29 | type: String, 30 | enum: ['low', 'medium', 'high'], 31 | default: 'medium' 32 | }, 33 | isPrivate: { 34 | type: Boolean, 35 | default: false 36 | } 37 | }); 38 | 39 | // Create compound index to prevent duplicate entries 40 | wishlistSchema.index({ user: 1, listing: 1 }, { unique: true }); 41 | 42 | // Index for efficient queries 43 | wishlistSchema.index({ user: 1, addedAt: -1 }); 44 | wishlistSchema.index({ user: 1, priority: 1 }); 45 | 46 | module.exports = mongoose.model("Wishlist", wishlistSchema); -------------------------------------------------------------------------------- /init/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const initData = require("./data.js"); 3 | const Listing = require("../models/listing.js"); 4 | const { updateListingCoordinates } = require("../utils/updateCoordinates.js"); 5 | const dotenv = require("dotenv"); 6 | dotenv.config({ quiet: true }); 7 | 8 | const MONGO_URL = process.env.ATLAS_DB_URL; 9 | 10 | main() 11 | .then(() => { 12 | console.log("✅ Database connected"); 13 | }) 14 | .catch((err) => { 15 | console.error("❌ Database connection failed:", err.message); 16 | }); 17 | async function main() { 18 | await mongoose.connect(MONGO_URL); 19 | } 20 | 21 | const initDB = async () => { 22 | await Listing.deleteMany({}); 23 | initData.data = initData.data.map((obj) => ({ 24 | ...obj, 25 | owner: "68b03abbf434cdd259bd1032", 26 | // Don't set default coordinates here - let updateListingCoordinates handle it 27 | })); 28 | await Listing.insertMany(initData.data); 29 | console.log("✅ Sample data initialized"); 30 | 31 | // Now update all listings with proper coordinates 32 | console.log("🗺️ Updating listing coordinates..."); 33 | await updateListingCoordinates(); 34 | console.log("✅ All coordinates updated!"); 35 | }; 36 | 37 | initDB(); 38 | -------------------------------------------------------------------------------- /views/users/login.ejs: -------------------------------------------------------------------------------- 1 | <% layout('/layouts/boilerplate') %> 2 | 3 |
4 |
5 |
6 |

Login

7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 |

Don't have an account? Sign up here

19 |
20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /public/offline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WanderLust - Offline 7 | 8 | 34 | 35 | 36 |
37 |
38 |

You're Offline

39 |

It looks like you've lost your connection. This page hasn't been cached yet, but you can still access pages you've recently visited.

40 |
41 | 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "09:00" 9 | target-branch: "main" 10 | commit-message: 11 | prefix: "deps" 12 | prefix-development: "deps-dev" 13 | include: "scope" 14 | open-pull-requests-limit: 10 15 | reviewers: 16 | - "koushik369mondal" 17 | labels: 18 | - "dependencies" 19 | - "automated" 20 | - "GSSoC'25" 21 | allow: 22 | - dependency-type: "direct" 23 | - dependency-type: "indirect" 24 | ignore: 25 | # Ignore major version updates for stable packages 26 | - dependency-name: "express" 27 | update-types: ["version-update:semver-major"] 28 | - dependency-name: "mongoose" 29 | update-types: ["version-update:semver-major"] 30 | groups: 31 | security-updates: 32 | patterns: 33 | - "*" 34 | update-types: 35 | - "major" 36 | - "minor" 37 | - "patch" 38 | production-dependencies: 39 | patterns: 40 | - "express*" 41 | - "mongoose*" 42 | - "passport*" 43 | - "cloudinary*" 44 | - "multer*" 45 | - "ejs*" 46 | - "bootstrap*" 47 | update-types: 48 | - "minor" 49 | - "patch" 50 | development-dependencies: 51 | patterns: 52 | - "nodemon*" 53 | - "jest*" 54 | - "eslint*" 55 | - "prettier*" 56 | update-types: 57 | - "major" 58 | - "minor" 59 | - "patch" -------------------------------------------------------------------------------- /tests/models/user.test.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | describe('User Model', () => { 4 | describe('Schema Validation', () => { 5 | it('should create a valid user', () => { 6 | const validUser = new User({ 7 | email: 'test@example.com', 8 | username: 'testuser' 9 | }); 10 | 11 | const error = validUser.validateSync(); 12 | expect(error).toBeUndefined(); 13 | }); 14 | 15 | it('should fail without email', () => { 16 | const user = new User({ 17 | username: 'testuser' 18 | }); 19 | 20 | const error = user.validateSync(); 21 | expect(error.errors.email).toBeDefined(); 22 | }); 23 | 24 | it('should fail without username', () => { 25 | const user = new User({ 26 | email: 'test@example.com' 27 | }); 28 | 29 | const error = user.validateSync(); 30 | expect(error.errors.username).toBeDefined(); 31 | }); 32 | 33 | it('should fail with invalid email format', () => { 34 | const user = new User({ 35 | email: 'invalid-email', 36 | username: 'testuser' 37 | }); 38 | 39 | const error = user.validateSync(); 40 | expect(error.errors.email).toBeDefined(); 41 | }); 42 | 43 | it('should have default role as user', () => { 44 | const user = new User({ 45 | email: 'test@example.com', 46 | username: 'testuser' 47 | }); 48 | 49 | expect(user.role).toBe('user'); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /routes/compare.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Listing = require('../models/listing'); 4 | 5 | // GET /listings/compare 6 | router.get('/compare', async (req, res) => { 7 | try { 8 | console.log('Compare route accessed'); // Debug log 9 | const listingIds = req.query.ids ? req.query.ids.split(',') : []; 10 | 11 | if (listingIds.length < 2 || listingIds.length > 3) { 12 | req.flash('error', 'Please select 2-3 listings to compare.'); 13 | return res.redirect('/listings'); 14 | } 15 | 16 | const listings = await Listing.find({ 17 | '_id': { $in: listingIds } 18 | }).populate('reviews'); 19 | 20 | const processedListings = listings.map(listing => { 21 | const avgRating = listing.reviews.length > 0 22 | ? listing.reviews.reduce((sum, review) => sum + review.rating, 0) / listing.reviews.length 23 | : 0; 24 | 25 | return { 26 | ...listing.toObject(), 27 | avgRating: avgRating.toFixed(1), 28 | amenities: listing.amenities || [], 29 | priceWithTax: Math.round(listing.price * 1.18), 30 | reviewCount: listing.reviews.length 31 | }; 32 | }); 33 | 34 | res.render('listings/compare', { 35 | listings: processedListings, 36 | title: 'Compare Listings' 37 | }); 38 | } catch (error) { 39 | console.error('Comparison error:', error); 40 | req.flash('error', 'Error loading comparison page.'); 41 | res.redirect('/listings'); 42 | } 43 | }); 44 | 45 | module.exports = router; // Make sure this line is present -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy to Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "README.md" 9 | - "LICENSE" 10 | - "CODE_OF_CONDUCT.md" 11 | - "SECURITY.md" 12 | - ".gitignore" 13 | - "docs/**" 14 | - ".github/ISSUE_TEMPLATE/**" 15 | 16 | # Prevent multiple deployments from running simultaneously 17 | concurrency: 18 | group: deploy-production 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | deploy: 23 | runs-on: ubuntu-latest 24 | name: 🌐 Production Deployment 25 | 26 | steps: 27 | - name: 📥 Checkout Code 28 | uses: actions/checkout@v4 29 | 30 | - name: 🟢 Set up Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: "20.x" # Use stable LTS version for production 34 | cache: "npm" 35 | 36 | - name: 📦 Install Dependencies 37 | run: npm ci 38 | 39 | - name: 🔒 Run Security Audit 40 | run: npm audit --omit=dev --audit-level high 41 | 42 | - name: 🏗️ Build Application 43 | run: | 44 | echo "🚀 Building WanderLust for production deployment" 45 | echo "📝 Commit: ${{ github.sha }}" 46 | echo "👤 Author: ${{ github.actor }}" 47 | echo "🌿 Branch: ${{ github.ref_name }}" 48 | echo "" 49 | echo "✅ Dependencies installed successfully" 50 | echo "🔒 Security audit passed" 51 | echo "📦 Application ready for deployment" 52 | echo "" 53 | echo "🔗 Render will auto-deploy from this commit to:" 54 | echo " https://wanderlust-c8cb.onrender.com" 55 | echo "" 56 | echo "🎉 Deployment initiated successfully!" 57 | -------------------------------------------------------------------------------- /tests/utils/wrapAsync.test.js: -------------------------------------------------------------------------------- 1 | const wrapAsync = require('../utils/wrapAsync'); 2 | 3 | describe('wrapAsync Utility', () => { 4 | it('should wrap async function and catch errors', async () => { 5 | const asyncFn = async (req, res) => { 6 | throw new Error('Test error'); 7 | }; 8 | 9 | const wrappedFn = wrapAsync(asyncFn); 10 | const mockReq = {}; 11 | const mockRes = {}; 12 | const mockNext = jest.fn(); 13 | 14 | await wrappedFn(mockReq, mockRes, mockNext); 15 | 16 | expect(mockNext).toHaveBeenCalled(); 17 | expect(mockNext.mock.calls[0][0]).toBeInstanceOf(Error); 18 | expect(mockNext.mock.calls[0][0].message).toBe('Test error'); 19 | }); 20 | 21 | it('should call next with error when async function fails', async () => { 22 | const asyncFn = async () => { 23 | throw new Error('Async error'); 24 | }; 25 | 26 | const wrappedFn = wrapAsync(asyncFn); 27 | const mockNext = jest.fn(); 28 | 29 | await wrappedFn({}, {}, mockNext); 30 | 31 | expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); 32 | }); 33 | 34 | it('should not call next if no error occurs', async () => { 35 | const asyncFn = async (req, res) => { 36 | res.status(200).send('OK'); 37 | }; 38 | 39 | const wrappedFn = wrapAsync(asyncFn); 40 | const mockReq = {}; 41 | const mockRes = { status: jest.fn().mockReturnThis(), send: jest.fn() }; 42 | const mockNext = jest.fn(); 43 | 44 | await wrappedFn(mockReq, mockRes, mockNext); 45 | 46 | expect(mockRes.status).toHaveBeenCalledWith(200); 47 | expect(mockNext).not.toHaveBeenCalled(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const GoogleStrategy = require("passport-google-oauth20").Strategy; 3 | const User = require("../models/user"); 4 | 5 | // Google OAuth Strategy 6 | passport.use(new GoogleStrategy({ 7 | clientID: process.env.GOOGLE_CLIENT_ID || "dummy_client_id", 8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || "dummy_client_secret", 9 | callbackURL: "/auth/google/callback" 10 | }, async (accessToken, refreshToken, profile, done) => { 11 | try { 12 | // Check if user already exists with this Google ID 13 | let existingUser = await User.findOne({ googleId: profile.id }); 14 | 15 | if (existingUser) { 16 | return done(null, existingUser); 17 | } 18 | 19 | // Check if user exists with same email 20 | let user = await User.findOne({ email: profile.emails[0].value }); 21 | 22 | if (user) { 23 | // Link Google account to existing user 24 | user.googleId = profile.id; 25 | user.displayName = profile.displayName; 26 | user.profilePicture = profile.photos[0]?.value || ""; 27 | await user.save(); 28 | return done(null, user); 29 | } 30 | 31 | // Create new user 32 | const newUser = new User({ 33 | googleId: profile.id, 34 | email: profile.emails[0].value, 35 | displayName: profile.displayName, 36 | profilePicture: profile.photos[0]?.value || "", 37 | username: profile.emails[0].value.split('@')[0] // Use email prefix as username 38 | }); 39 | 40 | await newUser.save(); 41 | return done(null, newUser); 42 | } catch (error) { 43 | return done(error, null); 44 | } 45 | })); 46 | 47 | module.exports = passport; 48 | -------------------------------------------------------------------------------- /tests/middleware.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | 4 | describe('Middleware Tests', () => { 5 | describe('Error Handling Middleware', () => { 6 | it('should handle 404 errors', async () => { 7 | const res = await request(app) 8 | .get('/non-existent-route') 9 | .expect(404); 10 | 11 | expect(res.text).toContain('404'); 12 | }); 13 | }); 14 | 15 | describe('Session Middleware', () => { 16 | it('should set session cookies', async () => { 17 | const res = await request(app) 18 | .get('/listings'); 19 | 20 | expect(res.headers['set-cookie']).toBeDefined(); 21 | }); 22 | }); 23 | 24 | describe('Flash Messages', () => { 25 | it('should flash success messages', async () => { 26 | const agent = request.agent(app); 27 | 28 | // First request to set up session 29 | await agent.get('/listings'); 30 | 31 | // Flash messages should work after session is established 32 | const res = await agent.get('/login'); 33 | expect(res.status).toBe(200); 34 | }); 35 | }); 36 | 37 | describe('Security Headers', () => { 38 | it('should set security headers with Helmet', async () => { 39 | const res = await request(app) 40 | .get('/listings'); 41 | 42 | expect(res.headers['x-content-type-options']).toBeDefined(); 43 | }); 44 | }); 45 | 46 | describe('Method Override', () => { 47 | it('should support method override via query', async () => { 48 | const res = await request(app) 49 | .post('/listings/123456789012345678901234?_method=DELETE') 50 | .expect(302); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /views/phraseAssistant/_widget.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Local Language Assistant
4 | 5 |
6 | 7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 |
Translation
36 |

37 |

38 |
39 |
40 | 41 |
42 |
Favorites
43 |
    44 |
    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /tests/reviews.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | 4 | describe('Review Routes', () => { 5 | describe('POST /listings/:id/reviews', () => { 6 | it('should redirect to login when not authenticated', async () => { 7 | const res = await request(app) 8 | .post('/listings/123456789012345678901234/reviews') 9 | .send({ 10 | rating: 5, 11 | comment: 'Great place!' 12 | }) 13 | .expect(302); 14 | 15 | expect(res.headers.location).toContain('/login'); 16 | }); 17 | 18 | it('should reject review with invalid rating', async () => { 19 | const res = await request(app) 20 | .post('/listings/123456789012345678901234/reviews') 21 | .send({ 22 | rating: 6, // Invalid rating (should be 1-5) 23 | comment: 'Test comment' 24 | }); 25 | 26 | expect(res.status).toBe(302); 27 | }); 28 | 29 | it('should reject review with missing comment', async () => { 30 | const res = await request(app) 31 | .post('/listings/123456789012345678901234/reviews') 32 | .send({ 33 | rating: 5, 34 | comment: '' 35 | }); 36 | 37 | expect(res.status).toBe(302); 38 | }); 39 | }); 40 | 41 | describe('DELETE /listings/:id/reviews/:reviewId', () => { 42 | it('should redirect to login when not authenticated', async () => { 43 | const res = await request(app) 44 | .delete('/listings/123456789012345678901234/reviews/123456789012345678901234') 45 | .expect(302); 46 | 47 | expect(res.headers.location).toContain('/login'); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /routes/listing.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const express = require("express"); 4 | const router = express.Router(); 5 | const wrapAsync = require("../utils/wrapAsync.js"); 6 | const Listing = require("../models/listing.js"); 7 | const { isLoggedIn, isOwner, validateListing } = require("../middleware.js"); 8 | const listingsController = require("../controllers/listings.js"); 9 | const multer = require("multer"); 10 | const { storage } = require("../cloudConfig.js"); 11 | const upload = multer({ storage }); 12 | 13 | router 14 | .route("/") 15 | .get(wrapAsync(listingsController.index)) 16 | .post( 17 | isLoggedIn, 18 | upload.single("listing[image]"), 19 | wrapAsync(listingsController.createListing) 20 | ); 21 | 22 | // Search suggestions API 23 | router.get("/search/suggestions", wrapAsync(listingsController.getSearchSuggestions)); 24 | 25 | // Advanced filtering API 26 | router.get("/api/filter", wrapAsync(listingsController.getFilteredListings)); 27 | 28 | // About route 29 | router.get("/about", (req, res) => { 30 | res.render("about", { title: "About Us" }); 31 | }); 32 | 33 | // New Route - This must come BEFORE the /:id route 34 | router.get("/new", isLoggedIn, listingsController.renderNewForm); 35 | 36 | router 37 | .route("/:id") 38 | .get(wrapAsync(listingsController.showListing)) 39 | .put( 40 | isLoggedIn, 41 | isOwner, 42 | upload.single("listing[image]"), 43 | validateListing, 44 | wrapAsync(listingsController.updateListing) 45 | ) 46 | .delete(isLoggedIn, isOwner, wrapAsync(listingsController.destroyListing)); 47 | 48 | // Edit Route 49 | router.get( 50 | "/:id/edit", 51 | isLoggedIn, 52 | isOwner, 53 | wrapAsync(listingsController.renderEditForm) 54 | ); 55 | 56 | // Like/Unlike routes 57 | router.post('/:id/like', isLoggedIn, wrapAsync(listingsController.likeListing)); 58 | router.post('/:id/unlike', isLoggedIn, wrapAsync(listingsController.unlikeListing)); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /public/CSS/footer-theme.css: -------------------------------------------------------------------------------- 1 | /* Footer Dark Mode Theme Support */ 2 | 3 | /* Light mode (default) footer variables */ 4 | :root { 5 | --footer-title-color: #ffffff; 6 | --footer-text-color: #ffffff; 7 | --footer-link-hover-color: #ffffff; 8 | --footer-link-hover-shadow: 0 0 8px rgba(254, 66, 77, 0.8); 9 | --footer-input-bg: rgba(255, 255, 255, 0.15); 10 | --footer-input-color: #ffffff; 11 | --footer-input-placeholder: rgba(255, 255, 255, 0.85); 12 | --footer-input-border: rgba(255, 255, 255, 0.3); 13 | --footer-input-focus-border: rgba(255, 255, 255, 0.6); 14 | --footer-input-focus-bg: rgba(255, 255, 255, 0.2); 15 | --footer-input-focus-shadow: 0 0 0 2px rgba(254, 66, 77, 0.3); 16 | --footer-button-hover: #d63641; 17 | --footer-social-color: rgba(255, 255, 255, 0.8); 18 | --footer-social-hover-color: #ffffff; 19 | --footer-social-hover-shadow: 0 0 12px rgba(254, 66, 77, 0.8); 20 | --footer-border-color: rgba(255, 255, 255, 0.2); 21 | --footer-bottom-color: rgba(255, 255, 255, 0.8); 22 | } 23 | 24 | /* Dark mode footer variables */ 25 | [data-theme="dark"] { 26 | --footer-title-color: var(--text-primary); 27 | --footer-text-color: var(--text-secondary); 28 | --footer-link-hover-color: var(--text-primary); 29 | --footer-link-hover-shadow: 0 0 8px rgba(254, 66, 77, 0.6); 30 | --footer-input-bg: rgba(255, 255, 255, 0.1); 31 | --footer-input-color: var(--text-primary); 32 | --footer-input-placeholder: var(--text-muted); 33 | --footer-input-border: rgba(255, 255, 255, 0.2); 34 | --footer-input-focus-border: rgba(255, 255, 255, 0.4); 35 | --footer-input-focus-bg: rgba(255, 255, 255, 0.15); 36 | --footer-input-focus-shadow: 0 0 0 2px rgba(254, 66, 77, 0.4); 37 | --footer-button-hover: #d63641; 38 | --footer-social-color: var(--text-secondary); 39 | --footer-social-hover-color: var(--text-primary); 40 | --footer-social-hover-shadow: 0 0 12px rgba(254, 66, 77, 0.6); 41 | --footer-border-color: rgba(255, 255, 255, 0.1); 42 | --footer-bottom-color: var(--text-muted); 43 | } -------------------------------------------------------------------------------- /public/CSS/tailwind.input.css: -------------------------------------------------------------------------------- 1 | /* Tailwind CSS Base Styles */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | /* Custom Tailwind Components */ 7 | @layer components { 8 | .tw-btn-primary { 9 | @apply tw-bg-blue-500 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-font-semibold hover:tw-bg-blue-600 tw-transition; 10 | } 11 | 12 | .tw-card { 13 | @apply tw-bg-white tw-rounded-lg tw-shadow-md tw-p-4 tw-transition hover:tw-shadow-lg; 14 | } 15 | 16 | .tw-badge { 17 | @apply tw-inline-block tw-px-2 tw-py-1 tw-text-xs tw-font-semibold tw-rounded-full; 18 | } 19 | 20 | /* Premium Glassmorphism Effects */ 21 | .tw-glass { 22 | @apply tw-backdrop-blur-md tw-bg-white/10 tw-border tw-border-white/20; 23 | } 24 | 25 | .tw-glass-dark { 26 | @apply tw-backdrop-blur-md tw-bg-black/20 tw-border tw-border-white/10; 27 | } 28 | 29 | /* Premium Gradient Buttons */ 30 | .tw-btn-gradient { 31 | @apply tw-bg-gradient-to-r tw-from-coral tw-to-gold tw-text-white tw-px-6 tw-py-3 tw-rounded-full tw-font-semibold tw-shadow-lg hover:tw-shadow-xl tw-transition-all tw-duration-300 hover:tw-scale-105; 32 | } 33 | 34 | .tw-btn-glass { 35 | @apply tw-glass tw-px-6 tw-py-3 tw-rounded-full tw-font-semibold tw-shadow-md hover:tw-shadow-lg tw-transition-all tw-duration-300 hover:tw-scale-105; 36 | } 37 | 38 | /* Category Pills */ 39 | .tw-category-pill { 40 | @apply tw-glass tw-px-4 tw-py-2 tw-rounded-full tw-text-sm tw-font-medium tw-whitespace-nowrap tw-transition-all tw-duration-300 hover:tw-scale-110 hover:tw-shadow-lg tw-cursor-pointer; 41 | } 42 | 43 | /* Search Bar Glass Effect */ 44 | .tw-search-glass { 45 | @apply tw-glass tw-rounded-full tw-shadow-inner tw-transition-all tw-duration-300 focus-within:tw-ring-2 focus-within:tw-ring-aqua focus-within:tw-shadow-xl; 46 | } 47 | } 48 | 49 | /* Custom Utilities */ 50 | @layer utilities { 51 | .tw-text-shadow { 52 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .tw-text-shadow-lg { 56 | text-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 57 | } 58 | } -------------------------------------------------------------------------------- /tests/models/review.test.js: -------------------------------------------------------------------------------- 1 | const Review = require('../models/review'); 2 | const mongoose = require('mongoose'); 3 | 4 | describe('Review Model', () => { 5 | describe('Schema Validation', () => { 6 | it('should create a valid review', () => { 7 | const validReview = new Review({ 8 | comment: 'Great place to stay!', 9 | rating: 5, 10 | author: new mongoose.Types.ObjectId() 11 | }); 12 | 13 | const error = validReview.validateSync(); 14 | expect(error).toBeUndefined(); 15 | }); 16 | 17 | it('should fail without comment', () => { 18 | const review = new Review({ 19 | rating: 5, 20 | author: new mongoose.Types.ObjectId() 21 | }); 22 | 23 | const error = review.validateSync(); 24 | expect(error.errors.comment).toBeDefined(); 25 | }); 26 | 27 | it('should fail without rating', () => { 28 | const review = new Review({ 29 | comment: 'Test comment', 30 | author: new mongoose.Types.ObjectId() 31 | }); 32 | 33 | const error = review.validateSync(); 34 | expect(error.errors.rating).toBeDefined(); 35 | }); 36 | 37 | it('should fail with rating less than 1', () => { 38 | const review = new Review({ 39 | comment: 'Test', 40 | rating: 0, 41 | author: new mongoose.Types.ObjectId() 42 | }); 43 | 44 | const error = review.validateSync(); 45 | expect(error.errors.rating).toBeDefined(); 46 | }); 47 | 48 | it('should fail with rating greater than 5', () => { 49 | const review = new Review({ 50 | comment: 'Test', 51 | rating: 6, 52 | author: new mongoose.Types.ObjectId() 53 | }); 54 | 55 | const error = review.validateSync(); 56 | expect(error.errors.rating).toBeDefined(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/map.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | 4 | describe('Map Integration Tests', () => { 5 | describe('Listing Show Page Map', () => { 6 | it('should load show page and contain map element', async () => { 7 | // This test will check if the show page loads and contains map element 8 | // Note: We need a valid listing ID for this test 9 | console.log('Testing map integration...'); 10 | 11 | // First get listings to find a valid ID 12 | const indexRes = await request(app) 13 | .get('/listings') 14 | .expect(200); 15 | 16 | // Check if the page contains map-related elements 17 | expect(indexRes.text).toContain('mapbox'); 18 | }); 19 | 20 | it('should have mapbox token available', () => { 21 | const mapToken = process.env.MAP_TOKEN; 22 | expect(mapToken).toBeDefined(); 23 | expect(mapToken).not.toBe(''); 24 | console.log('Map token exists:', !!mapToken); 25 | }); 26 | 27 | it('should have proper mapbox CSS and JS references', async () => { 28 | const res = await request(app) 29 | .get('/listings') 30 | .expect(200); 31 | 32 | // Check for Mapbox CSS 33 | expect(res.text).toContain('mapbox-gl.css'); 34 | // Check for Mapbox JS 35 | expect(res.text).toContain('mapbox-gl.js'); 36 | }); 37 | }); 38 | 39 | describe('Map JavaScript Logic', () => { 40 | it('should handle missing coordinates gracefully', () => { 41 | // Mock listing without coordinates 42 | const mockListing = { 43 | title: 'Test Listing', 44 | location: 'Test City', 45 | country: 'Test Country' 46 | }; 47 | 48 | // This would test the coordinate fallback logic 49 | expect(mockListing.location).toBeDefined(); 50 | expect(mockListing.country).toBeDefined(); 51 | }); 52 | }); 53 | }); -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of Wanderlust are currently supported with security updates: 6 | 7 | | Version | Supported | Notes | 8 | | ------- | ------------------ | ---------------------- | 9 | | 1.0.x | :white_check_mark: | Current stable release | 10 | | < 1.0 | :x: | Pre-release versions | 11 | 12 | _Note: This project is built with Node.js, Express.js, MongoDB, and EJS templating engine._ 13 | 14 | ## Reporting a Vulnerability 15 | 16 | We take security vulnerabilities seriously and appreciate your help in keeping Wanderlust secure. 17 | 18 | ### How to Report 19 | 20 | **Please do NOT report security vulnerabilities through public GitHub issues.** 21 | 22 | Instead, please report security vulnerabilities directly via email: 23 | 24 | - **Email:** koushik369mondal@gmail.com 25 | - **Subject:** [SECURITY] Wanderlust Vulnerability Report 26 | 27 | ### What to Include 28 | 29 | When reporting a security vulnerability, please include: 30 | 31 | - Description of the vulnerability 32 | - Steps to reproduce the issue 33 | - Potential impact assessment 34 | - Any suggested fixes or mitigation strategies 35 | - Your contact information for follow-up questions 36 | 37 | ### Response Timeline 38 | 39 | - **Initial Response:** Within 3 business days of receiving your report 40 | - **Status Updates:** We will keep you informed of our progress 41 | - **Resolution:** Timeline depends on complexity, but we prioritize security issues 42 | 43 | ### Our Commitment 44 | 45 | - We will acknowledge receipt of your vulnerability report promptly 46 | - We will investigate all legitimate reports and do our best to quickly fix the problem 47 | - We will notify you before any public disclosure of the vulnerability 48 | - We will credit you (if desired) for responsibly disclosing the issue 49 | 50 | ### Scope 51 | 52 | This security policy applies to: 53 | 54 | - The main Wanderlust application 55 | - All components including Node.js backend, Express routes, MongoDB integrations 56 | - EJS templates and client-side JavaScript 57 | - All dependencies and third-party packages 58 | 59 | Thank you for helping us maintain the security of Wanderlust! 60 | -------------------------------------------------------------------------------- /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: { type: String, required: true }, 7 | description: String, 8 | image: { 9 | url: String, 10 | filename: String, 11 | }, 12 | price: Number, 13 | location: String, 14 | country: String, 15 | 16 | // BADGE FIELDS 17 | isFeatured: { type: Boolean, default: false }, 18 | hasDiscount: { type: Boolean, default: false }, 19 | avgRating: { type: Number, default: 0 }, 20 | hasFeaturedReview: { type: Boolean, default: false }, 21 | discountPrice: { type: Number }, 22 | createdAt: { type: Date, default: Date.now }, 23 | 24 | category: { 25 | type: String, 26 | enum: ['Trending', 'Rooms', 'Iconic Cities', 'Mountains', 'Castles', 'Amazing pool', 'Camping', 'Farms', 'Arctic','Domes','Boats'], // This ensures only these values are accepted 27 | }, 28 | bestSeason: { 29 | type: String, 30 | }, 31 | travelTip: { 32 | type: String, 33 | }, 34 | 35 | reviews: [ 36 | { 37 | type: Schema.Types.ObjectId, 38 | ref: "Review", 39 | }, 40 | ], 41 | aiSummary: { 42 | type: String, 43 | default: null, 44 | }, 45 | aiSummaryLastUpdated: { 46 | type: Date, 47 | default: null, 48 | }, 49 | owner: { 50 | type: Schema.Types.ObjectId, 51 | ref: "User", 52 | }, 53 | geometry: { 54 | type: { 55 | type: String, // Don't do `{ location: { type: String } }` 56 | enum: ["Point"], // 'location.type' must be 'Point' 57 | required: false, 58 | }, 59 | coordinates: { 60 | type: [Number], 61 | required: false, 62 | }, 63 | }, 64 | // category: { 65 | // type: String, 66 | // enum: [ 67 | // "mountains", 68 | // "arctic", 69 | // "farms", 70 | // "deserts", 71 | // ] 72 | // } 73 | likes: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], 74 | }); 75 | 76 | listingSchema.post("findOneAndDelete", async (listing) => { 77 | if (listing) { 78 | await Review.deleteMany({ _id: { $in: listing.reviews } }); 79 | } 80 | }); 81 | 82 | const Listing = mongoose.model("Listing", listingSchema); 83 | module.exports = Listing; 84 | -------------------------------------------------------------------------------- /routes/notifications.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const notificationController = require('../controllers/notifications'); 4 | const { isLoggedIn } = require('../middleware'); 5 | 6 | // Simple rate limiting middleware 7 | const rateLimitMap = new Map(); 8 | const rateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => { 9 | return (req, res, next) => { 10 | const clientId = req.ip + ':' + (req.user ? req.user._id : 'anonymous'); 11 | const now = Date.now(); 12 | const windowStart = now - windowMs; 13 | 14 | if (!rateLimitMap.has(clientId)) { 15 | rateLimitMap.set(clientId, []); 16 | } 17 | 18 | const requests = rateLimitMap.get(clientId).filter(time => time > windowStart); 19 | 20 | if (requests.length >= maxRequests) { 21 | return res.status(429).json({ error: 'Too many requests. Please try again later.' }); 22 | } 23 | 24 | requests.push(now); 25 | rateLimitMap.set(clientId, requests); 26 | next(); 27 | }; 28 | }; 29 | 30 | // Middleware to ensure user is logged in for all notification routes 31 | router.use(isLoggedIn); 32 | // Add rate limiting to prevent abuse 33 | router.use(rateLimit(50, 15 * 60 * 1000)); // 50 requests per 15 minutes 34 | 35 | // Routes 36 | router.get('/', notificationController.getNotifications.bind(notificationController)); 37 | router.get('/count', notificationController.getUnreadCount.bind(notificationController)); 38 | router.get('/stats', notificationController.getStats.bind(notificationController)); 39 | router.get('/settings', notificationController.getSettings.bind(notificationController)); 40 | 41 | router.put('/settings', notificationController.updateSettings.bind(notificationController)); 42 | router.put('/:id/read', notificationController.markAsRead.bind(notificationController)); 43 | router.put('/read-all', notificationController.markAllAsRead.bind(notificationController)); 44 | router.delete('/:id', notificationController.dismissNotification.bind(notificationController)); 45 | 46 | // Test route (only in development) 47 | if (process.env.NODE_ENV !== 'production') { 48 | router.post('/test', notificationController.sendTestNotification.bind(notificationController)); 49 | } 50 | 51 | module.exports = router; -------------------------------------------------------------------------------- /tests/auth.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | 4 | describe('Authentication Routes', () => { 5 | describe('GET /signup', () => { 6 | it('should render signup page', async () => { 7 | const res = await request(app) 8 | .get('/signup') 9 | .expect('Content-Type', /html/) 10 | .expect(200); 11 | 12 | expect(res.text).toContain('Sign Up'); 13 | }); 14 | }); 15 | 16 | describe('GET /login', () => { 17 | it('should render login page', async () => { 18 | const res = await request(app) 19 | .get('/login') 20 | .expect('Content-Type', /html/) 21 | .expect(200); 22 | 23 | expect(res.text).toContain('Login'); 24 | }); 25 | }); 26 | 27 | describe('POST /signup', () => { 28 | it('should reject signup with missing fields', async () => { 29 | const res = await request(app) 30 | .post('/signup') 31 | .send({ 32 | username: '', 33 | email: '', 34 | password: '' 35 | }); 36 | 37 | expect(res.status).toBe(302); // Redirect on validation error 38 | }); 39 | 40 | it('should reject signup with invalid email', async () => { 41 | const res = await request(app) 42 | .post('/signup') 43 | .send({ 44 | username: 'testuser', 45 | email: 'invalid-email', 46 | password: 'password123' 47 | }); 48 | 49 | expect(res.status).toBe(302); 50 | }); 51 | }); 52 | 53 | describe('POST /login', () => { 54 | it('should reject login with invalid credentials', async () => { 55 | const res = await request(app) 56 | .post('/login') 57 | .send({ 58 | username: 'nonexistent', 59 | password: 'wrongpassword' 60 | }); 61 | 62 | expect(res.status).toBe(302); // Redirect on failed login 63 | }); 64 | }); 65 | 66 | describe('GET /logout', () => { 67 | it('should logout user and redirect', async () => { 68 | const res = await request(app) 69 | .get('/logout') 70 | .expect(302); 71 | 72 | expect(res.headers.location).toBe('/listings'); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/models/listing.test.js: -------------------------------------------------------------------------------- 1 | const Listing = require('../models/listing'); 2 | const mongoose = require('mongoose'); 3 | 4 | describe('Listing Model', () => { 5 | describe('Schema Validation', () => { 6 | it('should create a valid listing', () => { 7 | const validListing = new Listing({ 8 | title: 'Beach House', 9 | description: 'Beautiful beach house with ocean view', 10 | image: { url: 'https://example.com/image.jpg', filename: 'image.jpg' }, 11 | price: 200, 12 | location: 'Malibu', 13 | country: 'USA', 14 | owner: new mongoose.Types.ObjectId() 15 | }); 16 | 17 | const error = validListing.validateSync(); 18 | expect(error).toBeUndefined(); 19 | }); 20 | 21 | it('should fail without required title', () => { 22 | const listing = new Listing({ 23 | description: 'Test description', 24 | price: 100, 25 | location: 'Test', 26 | country: 'Test' 27 | }); 28 | 29 | const error = listing.validateSync(); 30 | expect(error.errors.title).toBeDefined(); 31 | }); 32 | 33 | it('should fail without required description', () => { 34 | const listing = new Listing({ 35 | title: 'Test Title', 36 | price: 100, 37 | location: 'Test', 38 | country: 'Test' 39 | }); 40 | 41 | const error = listing.validateSync(); 42 | expect(error.errors.description).toBeDefined(); 43 | }); 44 | 45 | it('should fail with negative price', () => { 46 | const listing = new Listing({ 47 | title: 'Test', 48 | description: 'Test', 49 | price: -100, 50 | location: 'Test', 51 | country: 'Test' 52 | }); 53 | 54 | const error = listing.validateSync(); 55 | expect(error.errors.price).toBeDefined(); 56 | }); 57 | 58 | it('should set default image if not provided', () => { 59 | const listing = new Listing({ 60 | title: 'Test', 61 | description: 'Test', 62 | price: 100, 63 | location: 'Test', 64 | country: 'Test' 65 | }); 66 | 67 | expect(listing.image.url).toContain('default'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "WanderLust", 3 | "explore_world": "Explora el mundo con nosotros", 4 | "search_destinations": "Buscar destinos...", 5 | "search": "Buscar", 6 | "home": "Inicio", 7 | "all_listings": "Todos los Listados", 8 | "add_new_listing": "Agregar Nuevo Listado", 9 | "login": "Iniciar Sesión", 10 | "signup": "Registrarse", 11 | "logout": "Cerrar Sesión", 12 | "profile": "Perfil", 13 | "language": "Idioma", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "spanish": "Español", 17 | "french": "Français", 18 | "share_your_journey": "Comparte tu Viaje", 19 | "your_words_inspire": "Tus palabras inspiran a otros viajeros", 20 | "rate_your_experience": "Califica tu Experiencia", 21 | "tell_your_story": "Cuenta tu Historia", 22 | "launch_review": "Enviar Reseña", 23 | "your_review_creates": "Tu reseña crea ondas de inspiración en la comunidad de viajeros", 24 | "traveler_stories": "Historias de Viajeros", 25 | "all_reviews": "Todas las Reseñas", 26 | "average_rating": "Calificación Promedio", 27 | "no_reviews_yet": "Aún no hay reseñas", 28 | "be_first_to_review": "¡Sé el primero en compartir tu experiencia en este lugar increíble!", 29 | "login_to_review": "Iniciar Sesión para Reseñar", 30 | "translate_review": "Traducir Reseña", 31 | "original_review": "Reseña Original", 32 | "translated_review": "Reseña Traducida", 33 | "translation_error": "Error en la traducción. Por favor, inténtalo de nuevo.", 34 | "poor_experience": "Experiencia Pobre", 35 | "below_expectations": "Por Debajo de las Expectativas", 36 | "good_experience": "Buena Experiencia", 37 | "amazing_experience": "Experiencia Increíble", 38 | "absolutely_perfect": "¡Absolutamente Perfecto!", 39 | "location": "Ubicación", 40 | "host": "Anfitrión", 41 | "vibe": "Ambiente", 42 | "value": "Valor", 43 | "what_made_special": "¿Qué hizo especial este lugar? Comparte la magia...", 44 | "location_breathtaking": "¡La ubicación era impresionante! ", 45 | "host_welcoming": "El anfitrión fue increíblemente acogedor. ", 46 | "perfect_relaxation": "Perfecto para relajación y paz. ", 47 | "great_value": "¡Excelente relación calidad-precio! ", 48 | "owned_by": "Propiedad de", 49 | "likes": "Me gusta", 50 | "like": "Me gusta", 51 | "unlike": "No me gusta", 52 | "add_to_wishlist": "Agregar a Lista de Deseos", 53 | "edit_listing": "Editar Listado", 54 | "delete_listing": "Eliminar Listado", 55 | "you_may_also_like": "También te puede gustar", 56 | "where_youll_be": "Donde estarás", 57 | "view_details": "Ver Detalles", 58 | "night": "noche", 59 | "recently": "Recientemente" 60 | } -------------------------------------------------------------------------------- /views/termCondition.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 |
    4 |

    Terms & Conditions – Wanderlust

    5 |

    Welcome to Wanderlust! By using our website, you agree to the following terms 6 | and conditions. Please read them carefully.

    7 | 8 |

    1. Acceptance of Terms

    9 |

    By accessing or using our website, you agree to comply with and be bound by these Terms & Conditions. 10 | If you do not agree, please do not use our site.

    11 | 12 |

    2. Use of Website

    13 | 18 | 19 |

    3. Accounts & Registration

    20 | 25 | 26 |

    4. Content & Intellectual Property

    27 | 32 | 33 |

    5. Third-Party Links

    34 |

    Our website may contain links to external websites. Wanderlust is not responsible for the content, services, or practices of these third-party sites.

    35 | 36 |

    6. Changes to Terms

    37 |

    We may update these Terms & Conditions at any time. Continued use of our website means you accept the updated terms.

    38 | 39 |
    40 | 41 | 42 | -------------------------------------------------------------------------------- /locales/gu.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "વંડરલસ્ટ", 3 | "explore_world": "અમારી સાથે વિશ્વનું અન્વેષણ કરો", 4 | "search_destinations": "ગંતવ્યો શોધો...", 5 | "search": "શોધો", 6 | "home": "હોમ", 7 | "all_listings": "બધી યાદીઓ", 8 | "add_new_listing": "નવી યાદી ઉમેરો", 9 | "login": "લોગિન", 10 | "signup": "સાઇનઅપ", 11 | "logout": "લોગઆઉટ", 12 | "profile": "પ્રોફાઇલ", 13 | "language": "ભાષા", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "તમારી યાત્રા શેર કરો", 28 | "your_words_inspire": "તમારા શબ્દો સાથી પ્રવાસીઓને પ્રેરણા આપે છે", 29 | "rate_your_experience": "તમારા અનુભવને રેટ કરો", 30 | "tell_your_story": "તમારી વાર્તા કહો", 31 | "launch_review": "રિવ્યૂ મોકલો", 32 | "your_review_creates": "તમારી રિવ્યૂ પ્રવાસ સમુદાયમાં પ્રેરણાની તરંગો બનાવે છે", 33 | "traveler_stories": "પ્રવાસીઓની વાર્તાઓ", 34 | "all_reviews": "બધી રિવ્યૂઓ", 35 | "average_rating": "સરેરાશ રેટિંગ", 36 | "no_reviews_yet": "હજુ સુધી કોઈ રિવ્યૂ નથી", 37 | "be_first_to_review": "આ અદ્ભુત સ્થળે તમારો અનુભવ શેર કરનાર પ્રથમ વ્યક્તિ બનો!", 38 | "login_to_review": "રિવ્યૂ માટે લોગિન કરો", 39 | "translate_review": "રિવ્યૂનું ભાષાંતર કરો", 40 | "original_review": "મૂળ રિવ્યૂ", 41 | "translated_review": "ભાષાંતરિત રિવ્યૂ", 42 | "translation_error": "ભાષાંતર નિષ્ફળ. કૃપા કરીને ફરી પ્રયાસ કરો.", 43 | "poor_experience": "ખરાબ અનુભવ", 44 | "below_expectations": "અપેક્ષાઓ કરતાં ઓછું", 45 | "good_experience": "સારો અનુભવ", 46 | "amazing_experience": "અદ્ભુત અનુભવ", 47 | "absolutely_perfect": "સંપૂર્ણ રીતે પરફેક્ટ!", 48 | "location": "સ્થાન", 49 | "host": "હોસ્ટ", 50 | "vibe": "વાતાવરણ", 51 | "value": "મૂલ્ય", 52 | "what_made_special": "આ સ્થળને શું ખાસ બનાવ્યું? જાદુ શેર કરો...", 53 | "location_breathtaking": "સ્થાન આશ્ચર્યજનક હતું! ", 54 | "host_welcoming": "હોસ્ટ અવિશ્વસનીય રીતે સ્વાગત કરતો હતો. ", 55 | "perfect_relaxation": "આરામ અને શાંતિ માટે પરફેક્ટ. ", 56 | "great_value": "પૈસા માટે શ્રેષ્ઠ મૂલ્ય! ", 57 | "owned_by": "માલિકી", 58 | "likes": "પસંદ", 59 | "like": "પસંદ કરો", 60 | "unlike": "નાપસંદ કરો", 61 | "add_to_wishlist": "વિશલિસ્ટમાં ઉમેરો", 62 | "edit_listing": "યાદી સંપાદિત કરો", 63 | "delete_listing": "યાદી કાઢી નાખો", 64 | "you_may_also_like": "તમને આ પણ ગમી શકે", 65 | "where_youll_be": "તમે ક્યાં હશો", 66 | "view_details": "વિગતો જુઓ", 67 | "night": "રાત", 68 | "recently": "તાજેતરમાં" 69 | } -------------------------------------------------------------------------------- /locales/mr.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "वंडरलस्ट", 3 | "explore_world": "आमच्यासोबत जगाचा शोध घ्या", 4 | "search_destinations": "गंतव्य शोधा...", 5 | "search": "शोधा", 6 | "home": "होम", 7 | "all_listings": "सर्व यादी", 8 | "add_new_listing": "नवीन यादी जोडा", 9 | "login": "लॉगिन", 10 | "signup": "साइनअप", 11 | "logout": "लॉगआउट", 12 | "profile": "प्रोफाइल", 13 | "language": "भाषा", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "तुमचा प्रवास शेअर करा", 28 | "your_words_inspire": "तुमचे शब्द सहप्रवाशांना प्रेरणा देतात", 29 | "rate_your_experience": "तुमच्या अनुभवाला रेटिंग द्या", 30 | "tell_your_story": "तुमची कहाणी सांगा", 31 | "launch_review": "रिव्यू पाठवा", 32 | "your_review_creates": "तुमचा रिव्यू प्रवास समुदायात प्रेरणेच्या लहरी निर्माण करतो", 33 | "traveler_stories": "प्रवाशांच्या कहाण्या", 34 | "all_reviews": "सर्व रिव्यू", 35 | "average_rating": "सरासरी रेटिंग", 36 | "no_reviews_yet": "अजून कोणतेही रिव्यू नाहीत", 37 | "be_first_to_review": "या अद्भुत ठिकाणी तुमचा अनुभव शेअर करणारे पहिले व्यक्ती व्हा!", 38 | "login_to_review": "रिव्यूसाठी लॉगिन करा", 39 | "translate_review": "रिव्यूचे भाषांतर करा", 40 | "original_review": "मूळ रिव्यू", 41 | "translated_review": "भाषांतरित रिव्यू", 42 | "translation_error": "भाषांतर अयशस्वी. कृपया पुन्हा प्रयत्न करा.", 43 | "poor_experience": "वाईट अनुभव", 44 | "below_expectations": "अपेक्षेपेक्षा कमी", 45 | "good_experience": "चांगला अनुभव", 46 | "amazing_experience": "अद्भुत अनुभव", 47 | "absolutely_perfect": "पूर्णपणे परिपूर्ण!", 48 | "location": "स्थान", 49 | "host": "यजमान", 50 | "vibe": "वातावरण", 51 | "value": "मूल्य", 52 | "what_made_special": "या ठिकाणाला काय खास बनवले? जादू शेअर करा...", 53 | "location_breathtaking": "स्थान चित्तथरारक होते! ", 54 | "host_welcoming": "यजमान आश्चर्यकारकपणे स्वागत करणारे होते. ", 55 | "perfect_relaxation": "विश्रांती आणि शांततेसाठी परिपूर्ण. ", 56 | "great_value": "पैशासाठी उत्तम मूल्य! ", 57 | "owned_by": "मालकी", 58 | "likes": "आवडी", 59 | "like": "आवडते", 60 | "unlike": "आवडत नाही", 61 | "add_to_wishlist": "विशलिस्टमध्ये जोडा", 62 | "edit_listing": "यादी संपादित करा", 63 | "delete_listing": "यादी हटवा", 64 | "you_may_also_like": "तुम्हाला हे देखील आवडू शकते", 65 | "where_youll_be": "तुम्ही कुठे असाल", 66 | "view_details": "तपशील पहा", 67 | "night": "रात्र", 68 | "recently": "अलीकडे" 69 | } -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "WanderLust", 3 | "explore_world": "Explorez le monde avec nous", 4 | "search_destinations": "Rechercher des destinations...", 5 | "search": "Rechercher", 6 | "home": "Accueil", 7 | "all_listings": "Toutes les Annonces", 8 | "add_new_listing": "Ajouter une Nouvelle Annonce", 9 | "login": "Se Connecter", 10 | "signup": "S'inscrire", 11 | "logout": "Se Déconnecter", 12 | "profile": "Profil", 13 | "language": "Langue", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "spanish": "Español", 17 | "french": "Français", 18 | "share_your_journey": "Partagez votre Voyage", 19 | "your_words_inspire": "Vos mots inspirent les autres voyageurs", 20 | "rate_your_experience": "Évaluez votre Expérience", 21 | "tell_your_story": "Racontez votre Histoire", 22 | "launch_review": "Envoyer l'Avis", 23 | "your_review_creates": "Votre avis crée des ondulations d'inspiration dans la communauté des voyageurs", 24 | "traveler_stories": "Histoires de Voyageurs", 25 | "all_reviews": "Tous les Avis", 26 | "average_rating": "Note Moyenne", 27 | "no_reviews_yet": "Aucun avis pour le moment", 28 | "be_first_to_review": "Soyez le premier à partager votre expérience dans cet endroit incroyable !", 29 | "login_to_review": "Se Connecter pour Évaluer", 30 | "translate_review": "Traduire l'Avis", 31 | "original_review": "Avis Original", 32 | "translated_review": "Avis Traduit", 33 | "translation_error": "Échec de la traduction. Veuillez réessayer.", 34 | "poor_experience": "Expérience Médiocre", 35 | "below_expectations": "En Dessous des Attentes", 36 | "good_experience": "Bonne Expérience", 37 | "amazing_experience": "Expérience Incroyable", 38 | "absolutely_perfect": "Absolument Parfait !", 39 | "location": "Emplacement", 40 | "host": "Hôte", 41 | "vibe": "Ambiance", 42 | "value": "Valeur", 43 | "what_made_special": "Qu'est-ce qui a rendu cet endroit spécial ? Partagez la magie...", 44 | "location_breathtaking": "L'emplacement était à couper le souffle ! ", 45 | "host_welcoming": "L'hôte était incroyablement accueillant. ", 46 | "perfect_relaxation": "Parfait pour la détente et la paix. ", 47 | "great_value": "Excellent rapport qualité-prix ! ", 48 | "owned_by": "Propriété de", 49 | "likes": "J'aime", 50 | "like": "J'aime", 51 | "unlike": "Je n'aime pas", 52 | "add_to_wishlist": "Ajouter à la Liste de Souhaits", 53 | "edit_listing": "Modifier l'Annonce", 54 | "delete_listing": "Supprimer l'Annonce", 55 | "you_may_also_like": "Vous pourriez aussi aimer", 56 | "where_youll_be": "Où vous serez", 57 | "view_details": "Voir les Détails", 58 | "night": "nuit", 59 | "recently": "Récemment" 60 | } -------------------------------------------------------------------------------- /locales/pa.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "ਵੰਡਰਲਸਟ", 3 | "explore_world": "ਸਾਡੇ ਨਾਲ ਦੁਨੀਆ ਦੀ ਖੋਜ ਕਰੋ", 4 | "search_destinations": "ਮੰਜ਼ਿਲਾਂ ਖੋਜੋ...", 5 | "search": "ਖੋਜੋ", 6 | "home": "ਘਰ", 7 | "all_listings": "ਸਾਰੀਆਂ ਸੂਚੀਆਂ", 8 | "add_new_listing": "ਨਵੀਂ ਸੂਚੀ ਸ਼ਾਮਲ ਕਰੋ", 9 | "login": "ਲਾਗਇਨ", 10 | "signup": "ਸਾਈਨਅਪ", 11 | "logout": "ਲਾਗਆਉਟ", 12 | "profile": "ਪ੍ਰੋਫਾਈਲ", 13 | "language": "ਭਾਸ਼ਾ", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "ਆਪਣਾ ਸਫ਼ਰ ਸਾਂਝਾ ਕਰੋ", 28 | "your_words_inspire": "ਤੁਹਾਡੇ ਸ਼ਬਦ ਸਾਥੀ ਯਾਤਰੀਆਂ ਨੂੰ ਪ੍ਰੇਰਿਤ ਕਰਦੇ ਹਨ", 29 | "rate_your_experience": "ਆਪਣੇ ਤਜਰਬੇ ਨੂੰ ਰੇਟ ਕਰੋ", 30 | "tell_your_story": "ਆਪਣੀ ਕਹਾਣੀ ਦੱਸੋ", 31 | "launch_review": "ਰਿਵਿਊ ਭੇਜੋ", 32 | "your_review_creates": "ਤੁਹਾਡਾ ਰਿਵਿਊ ਯਾਤਰਾ ਭਾਈਚਾਰੇ ਵਿੱਚ ਪ੍ਰੇਰਣਾ ਦੀਆਂ ਲਹਿਰਾਂ ਬਣਾਉਂਦਾ ਹੈ", 33 | "traveler_stories": "ਯਾਤਰੀਆਂ ਦੀਆਂ ਕਹਾਣੀਆਂ", 34 | "all_reviews": "ਸਾਰੇ ਰਿਵਿਊ", 35 | "average_rating": "ਔਸਤ ਰੇਟਿੰਗ", 36 | "no_reviews_yet": "ਅਜੇ ਤੱਕ ਕੋਈ ਰਿਵਿਊ ਨਹੀਂ", 37 | "be_first_to_review": "ਇਸ ਸ਼ਾਨਦਾਰ ਜਗ੍ਹਾ 'ਤੇ ਆਪਣਾ ਤਜਰਬਾ ਸਾਂਝਾ ਕਰਨ ਵਾਲੇ ਪਹਿਲੇ ਵਿਅਕਤੀ ਬਣੋ!", 38 | "login_to_review": "ਰਿਵਿਊ ਲਈ ਲਾਗਇਨ ਕਰੋ", 39 | "translate_review": "ਰਿਵਿਊ ਦਾ ਅਨੁਵਾਦ ਕਰੋ", 40 | "original_review": "ਅਸਲ ਰਿਵਿਊ", 41 | "translated_review": "ਅਨੁਵਾਦਿਤ ਰਿਵਿਊ", 42 | "translation_error": "ਅਨੁਵਾਦ ਅਸਫਲ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", 43 | "poor_experience": "ਮਾੜਾ ਤਜਰਬਾ", 44 | "below_expectations": "ਉਮੀਦਾਂ ਤੋਂ ਘੱਟ", 45 | "good_experience": "ਚੰਗਾ ਤਜਰਬਾ", 46 | "amazing_experience": "ਸ਼ਾਨਦਾਰ ਤਜਰਬਾ", 47 | "absolutely_perfect": "ਬਿਲਕੁਲ ਸੰਪੂਰਨ!", 48 | "location": "ਸਥਾਨ", 49 | "host": "ਮੇਜ਼ਬਾਨ", 50 | "vibe": "ਮਾਹੌਲ", 51 | "value": "ਮੁੱਲ", 52 | "what_made_special": "ਇਸ ਜਗ੍ਹਾ ਨੂੰ ਕੀ ਖਾਸ ਬਣਾਇਆ? ਜਾਦੂ ਸਾਂਝਾ ਕਰੋ...", 53 | "location_breathtaking": "ਸਥਾਨ ਸਾਹ ਲੈਣ ਵਾਲਾ ਸੀ! ", 54 | "host_welcoming": "ਮੇਜ਼ਬਾਨ ਅਵਿਸ਼ਵਾਸਯੋਗ ਤੌਰ 'ਤੇ ਸਵਾਗਤ ਕਰਨ ਵਾਲਾ ਸੀ। ", 55 | "perfect_relaxation": "ਆਰਾਮ ਅਤੇ ਸ਼ਾਂਤੀ ਲਈ ਸੰਪੂਰਨ। ", 56 | "great_value": "ਪੈਸੇ ਲਈ ਸ਼ਾਨਦਾਰ ਮੁੱਲ! ", 57 | "owned_by": "ਮਾਲਕੀ", 58 | "likes": "ਪਸੰਦ", 59 | "like": "ਪਸੰਦ ਕਰੋ", 60 | "unlike": "ਨਾਪਸੰਦ ਕਰੋ", 61 | "add_to_wishlist": "ਵਿਸ਼ਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ", 62 | "edit_listing": "ਸੂਚੀ ਸੰਪਾਦਿਤ ਕਰੋ", 63 | "delete_listing": "ਸੂਚੀ ਮਿਟਾਓ", 64 | "you_may_also_like": "ਤੁਹਾਨੂੰ ਇਹ ਵੀ ਪਸੰਦ ਆ ਸਕਦਾ ਹੈ", 65 | "where_youll_be": "ਤੁਸੀਂ ਕਿੱਥੇ ਹੋਵੋਗੇ", 66 | "view_details": "ਵੇਰਵੇ ਵੇਖੋ", 67 | "night": "ਰਾਤ", 68 | "recently": "ਹਾਲ ਹੀ ਵਿੱਚ" 69 | } -------------------------------------------------------------------------------- /locales/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "ওয়ান্ডারলাস্ট", 3 | "explore_world": "আমাদের সাথে বিশ্ব অন্বেষণ করুন", 4 | "search_destinations": "গন্তব্য খুঁজুন...", 5 | "search": "খুঁজুন", 6 | "home": "হোম", 7 | "all_listings": "সব তালিকা", 8 | "add_new_listing": "নতুন তালিকা যোগ করুন", 9 | "login": "লগইন", 10 | "signup": "সাইনআপ", 11 | "logout": "লগআউট", 12 | "profile": "প্রোফাইল", 13 | "language": "ভাষা", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "আপনার যাত্রা শেয়ার করুন", 28 | "your_words_inspire": "আপনার কথা সহযাত্রীদের অনুপ্রাণিত করে", 29 | "rate_your_experience": "আপনার অভিজ্ঞতা রেট করুন", 30 | "tell_your_story": "আপনার গল্প বলুন", 31 | "launch_review": "রিভিউ পাঠান", 32 | "your_review_creates": "আপনার রিভিউ ভ্রমণ সম্প্রদায়ে অনুপ্রেরণার তরঙ্গ সৃষ্টি করে", 33 | "traveler_stories": "ভ্রমণকারীর গল্প", 34 | "all_reviews": "সব রিভিউ", 35 | "average_rating": "গড় রেটিং", 36 | "no_reviews_yet": "এখনও কোন রিভিউ নেই", 37 | "be_first_to_review": "এই অসাধারণ জায়গায় আপনার অভিজ্ঞতা শেয়ার করা প্রথম ব্যক্তি হন!", 38 | "login_to_review": "রিভিউর জন্য লগইন করুন", 39 | "translate_review": "রিভিউ অনুবাদ করুন", 40 | "original_review": "মূল রিভিউ", 41 | "translated_review": "অনুবাদিত রিভিউ", 42 | "translation_error": "অনুবাদ ব্যর্থ। আবার চেষ্টা করুন।", 43 | "poor_experience": "খারাপ অভিজ্ঞতা", 44 | "below_expectations": "প্রত্যাশার চেয়ে কম", 45 | "good_experience": "ভাল অভিজ্ঞতা", 46 | "amazing_experience": "অসাধারণ অভিজ্ঞতা", 47 | "absolutely_perfect": "একদম নিখুঁত!", 48 | "location": "অবস্থান", 49 | "host": "হোস্ট", 50 | "vibe": "পরিবেশ", 51 | "value": "মূল্য", 52 | "what_made_special": "এই জায়গাটি কী বিশেষ করেছে? জাদু শেয়ার করুন...", 53 | "location_breathtaking": "অবস্থানটি মনমুগ্ধকর ছিল! ", 54 | "host_welcoming": "হোস্ট অবিশ্বাস্যভাবে স্বাগত জানিয়েছিল। ", 55 | "perfect_relaxation": "বিশ্রাম এবং শান্তির জন্য নিখুঁত। ", 56 | "great_value": "টাকার জন্য দুর্দান্ত মূল্য! ", 57 | "owned_by": "মালিকানা", 58 | "likes": "পছন্দ", 59 | "like": "পছন্দ করুন", 60 | "unlike": "অপছন্দ করুন", 61 | "add_to_wishlist": "উইশলিস্টে যোগ করুন", 62 | "edit_listing": "তালিকা সম্পাদনা করুন", 63 | "delete_listing": "তালিকা মুছুন", 64 | "you_may_also_like": "আপনি এটাও পছন্দ করতে পারেন", 65 | "where_youll_be": "আপনি কোথায় থাকবেন", 66 | "view_details": "বিস্তারিত দেখুন", 67 | "night": "রাত", 68 | "recently": "সম্প্রতি" 69 | } -------------------------------------------------------------------------------- /locales/ur.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "وینڈرلسٹ", 3 | "explore_world": "ہمارے ساتھ دنیا کی تلاش کریں", 4 | "search_destinations": "منزلیں تلاش کریں...", 5 | "search": "تلاش کریں", 6 | "home": "ہوم", 7 | "all_listings": "تمام فہرستیں", 8 | "add_new_listing": "نئی فہرست شامل کریں", 9 | "login": "لاگ ان", 10 | "signup": "سائن اپ", 11 | "logout": "لاگ آؤٹ", 12 | "profile": "پروفائل", 13 | "language": "زبان", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "اپنا سفر شیئر کریں", 28 | "your_words_inspire": "آپ کے الفاظ ساتھی مسافروں کو متاثر کرتے ہیں", 29 | "rate_your_experience": "اپنے تجربے کو ریٹ کریں", 30 | "tell_your_story": "اپنی کہانی بتائیں", 31 | "launch_review": "جائزہ بھیجیں", 32 | "your_review_creates": "آپ کا جائزہ سفری کمیونٹی میں تحریک کی لہریں پیدا کرتا ہے", 33 | "traveler_stories": "مسافروں کی کہانیاں", 34 | "all_reviews": "تمام جائزے", 35 | "average_rating": "اوسط ریٹنگ", 36 | "no_reviews_yet": "ابھی تک کوئی جائزہ نہیں", 37 | "be_first_to_review": "اس حیرت انگیز جگہ پر اپنا تجربہ شیئر کرنے والے پہلے شخص بنیں!", 38 | "login_to_review": "جائزے کے لیے لاگ ان کریں", 39 | "translate_review": "جائزے کا ترجمہ کریں", 40 | "original_review": "اصل جائزہ", 41 | "translated_review": "ترجمہ شدہ جائزہ", 42 | "translation_error": "ترجمہ ناکام۔ براہ کرم دوبارہ کوشش کریں۔", 43 | "poor_experience": "برا تجربہ", 44 | "below_expectations": "توقعات سے کم", 45 | "good_experience": "اچھا تجربہ", 46 | "amazing_experience": "حیرت انگیز تجربہ", 47 | "absolutely_perfect": "بالکل کامل!", 48 | "location": "مقام", 49 | "host": "میزبان", 50 | "vibe": "ماحول", 51 | "value": "قیمت", 52 | "what_made_special": "اس جگہ کو کیا خاص بنایا؟ جادو شیئر کریں...", 53 | "location_breathtaking": "مقام دم گھٹنے والا تھا! ", 54 | "host_welcoming": "میزبان ناقابل یقین حد تک خوش آمدید کہنے والا تھا۔ ", 55 | "perfect_relaxation": "آرام اور سکون کے لیے کامل۔ ", 56 | "great_value": "پیسے کے لیے بہترین قیمت! ", 57 | "owned_by": "ملکیت", 58 | "likes": "پسند", 59 | "like": "پسند کریں", 60 | "unlike": "ناپسند کریں", 61 | "add_to_wishlist": "خواہشات کی فہرست میں شامل کریں", 62 | "edit_listing": "فہرست میں ترمیم کریں", 63 | "delete_listing": "فہرست حذف کریں", 64 | "you_may_also_like": "آپ کو یہ بھی پسند آ سکتا ہے", 65 | "where_youll_be": "آپ کہاں ہوں گے", 66 | "view_details": "تفصیلات دیکھیں", 67 | "night": "رات", 68 | "recently": "حال ہی میں" 69 | } -------------------------------------------------------------------------------- /locales/as.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "ৱাণ্ডাৰলাষ্ট", 3 | "explore_world": "আমাৰ লগত পৃথিৱী অন্বেষণ কৰক", 4 | "search_destinations": "গন্তব্য বিচাৰক...", 5 | "search": "বিচাৰক", 6 | "home": "ঘৰ", 7 | "all_listings": "সকলো তালিকা", 8 | "add_new_listing": "নতুন তালিকা যোগ কৰক", 9 | "login": "লগইন", 10 | "signup": "চাইনআপ", 11 | "logout": "লগআউট", 12 | "profile": "প্ৰফাইল", 13 | "language": "ভাষা", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "আপোনাৰ যাত্ৰা ভাগ কৰক", 28 | "your_words_inspire": "আপোনাৰ কথাই সহযাত্ৰীসকলক অনুপ্ৰাণিত কৰে", 29 | "rate_your_experience": "আপোনাৰ অভিজ্ঞতা ৰেট কৰক", 30 | "tell_your_story": "আপোনাৰ কাহিনী কওক", 31 | "launch_review": "পৰ্যালোচনা পঠিয়াওক", 32 | "your_review_creates": "আপোনাৰ পৰ্যালোচনাই ভ্ৰমণ সম্প্ৰদায়ত অনুপ্ৰেৰণাৰ ঢৌ সৃষ্টি কৰে", 33 | "traveler_stories": "ভ্ৰমণকাৰীৰ কাহিনী", 34 | "all_reviews": "সকলো পৰ্যালোচনা", 35 | "average_rating": "গড় ৰেটিং", 36 | "no_reviews_yet": "এতিয়ালৈকে কোনো পৰ্যালোচনা নাই", 37 | "be_first_to_review": "এই আচৰিত ঠাইত আপোনাৰ অভিজ্ঞতা ভাগ কৰা প্ৰথম ব্যক্তি হওক!", 38 | "login_to_review": "পৰ্যালোচনাৰ বাবে লগইন কৰক", 39 | "translate_review": "পৰ্যালোচনা অনুবাদ কৰক", 40 | "original_review": "মূল পৰ্যালোচনা", 41 | "translated_review": "অনুবাদিত পৰ্যালোচনা", 42 | "translation_error": "অনুবাদ বিফল। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক।", 43 | "poor_experience": "বেয়া অভিজ্ঞতা", 44 | "below_expectations": "প্ৰত্যাশাতকৈ কম", 45 | "good_experience": "ভাল অভিজ্ঞতা", 46 | "amazing_experience": "আচৰিত অভিজ্ঞতা", 47 | "absolutely_perfect": "একেবাৰে নিখুঁত!", 48 | "location": "স্থান", 49 | "host": "আয়োজক", 50 | "vibe": "পৰিৱেশ", 51 | "value": "মূল্য", 52 | "what_made_special": "এই ঠাইক কিহে বিশেষ কৰিলে? যাদু ভাগ কৰক...", 53 | "location_breathtaking": "স্থানটো উশাহ লোৱাৰ দৰে আছিল! ", 54 | "host_welcoming": "আয়োজকে অবিশ্বাস্যভাৱে আদৰণি জনাইছিল। ", 55 | "perfect_relaxation": "বিশ্ৰাম আৰু শান্তিৰ বাবে নিখুঁত। ", 56 | "great_value": "টকাৰ বাবে উত্তম মূল্য! ", 57 | "owned_by": "মালিকানা", 58 | "likes": "পছন্দ", 59 | "like": "পছন্দ কৰক", 60 | "unlike": "অপছন্দ কৰক", 61 | "add_to_wishlist": "ইচ্ছা তালিকাত যোগ কৰক", 62 | "edit_listing": "তালিকা সম্পাদনা কৰক", 63 | "delete_listing": "তালিকা মচক", 64 | "you_may_also_like": "আপুনি ইয়াকো পছন্দ কৰিব পাৰে", 65 | "where_youll_be": "আপুনি ক'ত থাকিব", 66 | "view_details": "বিৱৰণ চাওক", 67 | "night": "ৰাতি", 68 | "recently": "সম্প্ৰতি" 69 | } -------------------------------------------------------------------------------- /locales/te.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "వాండర్‌లస్ట్", 3 | "explore_world": "మాతో ప్రపంచాన్ని అన్వేషించండి", 4 | "search_destinations": "గమ్యస్థానాలను వెతకండి...", 5 | "search": "వెతకండి", 6 | "home": "హోమ్", 7 | "all_listings": "అన్ని జాబితాలు", 8 | "add_new_listing": "కొత్త జాబితా జోడించండి", 9 | "login": "లాగిన్", 10 | "signup": "సైన్అప్", 11 | "logout": "లాగౌట్", 12 | "profile": "ప్రొఫైల్", 13 | "language": "భాష", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "మీ ప్రయాణాన్ని పంచుకోండి", 28 | "your_words_inspire": "మీ మాటలు తోటి ప్రయాణికులను ప్రేరేపిస్తాయి", 29 | "rate_your_experience": "మీ అనుభవాన్ని రేట్ చేయండి", 30 | "tell_your_story": "మీ కథ చెప్పండి", 31 | "launch_review": "రివ్యూ పంపండి", 32 | "your_review_creates": "మీ రివ్యూ ప్రయాణ సమాజంలో ప్రేరణ తరంగాలను సృష్టిస్తుంది", 33 | "traveler_stories": "ప్రయాణికుల కథలు", 34 | "all_reviews": "అన్ని రివ్యూలు", 35 | "average_rating": "సగటు రేటింగ్", 36 | "no_reviews_yet": "ఇంకా రివ్యూలు లేవు", 37 | "be_first_to_review": "ఈ అద్భుతమైన ప్రదేశంలో మీ అనుభవాన్ని పంచుకునే మొదటి వ్యక్తి అవ్వండి!", 38 | "login_to_review": "రివ్యూ కోసం లాగిన్ చేయండి", 39 | "translate_review": "రివ్యూ అనువదించండి", 40 | "original_review": "అసలు రివ్యూ", 41 | "translated_review": "అనువదించిన రివ్యూ", 42 | "translation_error": "అనువాదం విఫలమైంది. దయచేసి మళ్లీ ప్రయత్నించండి.", 43 | "poor_experience": "చెడు అనుభవం", 44 | "below_expectations": "అంచనాల కంటే తక్కువ", 45 | "good_experience": "మంచి అనుభవం", 46 | "amazing_experience": "అద్భుతమైన అనుభవం", 47 | "absolutely_perfect": "పూర్తిగా పరిపూర్ణం!", 48 | "location": "స్థానం", 49 | "host": "హోస్ట్", 50 | "vibe": "వాతావరణం", 51 | "value": "విలువ", 52 | "what_made_special": "ఈ ప్రదేశాన్ని ప్రత్యేకంగా చేసింది ఏమిటి? మాయాజాలాన్ని పంచుకోండి...", 53 | "location_breathtaking": "స్థానం ఆశ్చర్యకరంగా ఉంది! ", 54 | "host_welcoming": "హోస్ట్ అద్భుతంగా స్వాగతం పలికారు. ", 55 | "perfect_relaxation": "విశ్రాంతి మరియు శాంతికి పరిపూర్ణం. ", 56 | "great_value": "డబ్బుకు గొప్ప విలువ! ", 57 | "owned_by": "యాజమాన్యం", 58 | "likes": "ఇష్టాలు", 59 | "like": "ఇష్టపడండి", 60 | "unlike": "ఇష్టపడవద్దు", 61 | "add_to_wishlist": "విష్‌లిస్ట్‌కు జోడించండి", 62 | "edit_listing": "జాబితాను సవరించండి", 63 | "delete_listing": "జాబితాను తొలగించండి", 64 | "you_may_also_like": "మీరు దీన్ని కూడా ఇష్టపడవచ్చు", 65 | "where_youll_be": "మీరు ఎక్కడ ఉంటారు", 66 | "view_details": "వివరాలు చూడండి", 67 | "night": "రాత్రి", 68 | "recently": "ఇటీవల" 69 | } -------------------------------------------------------------------------------- /locales/or.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "ୱାଣ୍ଡରଲଷ୍ଟ", 3 | "explore_world": "ଆମ ସହିତ ବିଶ୍ୱ ଅନ୍ବେଷଣ କରନ୍ତୁ", 4 | "search_destinations": "ଗନ୍ତବ୍ୟ ସ୍ଥାନ ଖୋଜନ୍ତୁ...", 5 | "search": "ଖୋଜନ୍ତୁ", 6 | "home": "ଘର", 7 | "all_listings": "ସମସ୍ତ ତାଲିକା", 8 | "add_new_listing": "ନୂତନ ତାଲିକା ଯୋଗ କରନ୍ତୁ", 9 | "login": "ଲଗଇନ", 10 | "signup": "ସାଇନଅପ", 11 | "logout": "ଲଗଆଉଟ", 12 | "profile": "ପ୍ରୋଫାଇଲ", 13 | "language": "ଭାଷା", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "ଆପଣଙ୍କ ଯାତ୍ରା ସାଝା କରନ୍ତୁ", 28 | "your_words_inspire": "ଆପଣଙ୍କ ଶବ୍ଦ ସହଯାତ୍ରୀମାନଙ୍କୁ ପ୍ରେରଣା ଦିଏ", 29 | "rate_your_experience": "ଆପଣଙ୍କ ଅଭିଜ୍ଞତାକୁ ରେଟ କରନ୍ତୁ", 30 | "tell_your_story": "ଆପଣଙ୍କ କାହାଣୀ କୁହନ୍ତୁ", 31 | "launch_review": "ସମୀକ୍ଷା ପଠାନ୍ତୁ", 32 | "your_review_creates": "ଆପଣଙ୍କ ସମୀକ୍ଷା ଯାତ୍ରା ସମ୍ପ୍ରଦାୟରେ ପ୍ରେରଣାର ତରଙ୍ଗ ସୃଷ୍ଟି କରେ", 33 | "traveler_stories": "ଯାତ୍ରୀମାନଙ୍କ କାହାଣୀ", 34 | "all_reviews": "ସମସ୍ତ ସମୀକ୍ଷା", 35 | "average_rating": "ହାରାହାରି ରେଟିଂ", 36 | "no_reviews_yet": "ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ସମୀକ୍ଷା ନାହିଁ", 37 | "be_first_to_review": "ଏହି ଅଦ୍ଭୁତ ସ୍ଥାନରେ ଆପଣଙ୍କ ଅଭିଜ୍ଞତା ସାଝା କରୁଥିବା ପ୍ରଥମ ବ୍ୟକ୍ତି ହୁଅନ୍ତୁ!", 38 | "login_to_review": "ସମୀକ୍ଷା ପାଇଁ ଲଗଇନ କରନ୍ତୁ", 39 | "translate_review": "ସମୀକ୍ଷା ଅନୁବାଦ କରନ୍ତୁ", 40 | "original_review": "ମୂଳ ସମୀକ୍ଷା", 41 | "translated_review": "ଅନୁବାଦିତ ସମୀକ୍ଷା", 42 | "translation_error": "ଅନୁବାଦ ବିଫଳ। ଦୟାକରି ପୁନର୍ବାର ଚେଷ୍ଟା କରନ୍ତୁ।", 43 | "poor_experience": "ଖରାପ ଅଭିଜ୍ଞତା", 44 | "below_expectations": "ଆଶାଠାରୁ କମ", 45 | "good_experience": "ଭଲ ଅଭିଜ୍ଞତା", 46 | "amazing_experience": "ଅଦ୍ଭୁତ ଅଭିଜ୍ଞତା", 47 | "absolutely_perfect": "ସମ୍ପୂର୍ଣ୍ଣ ରୂପେ ସିଦ୍ଧ!", 48 | "location": "ସ୍ଥାନ", 49 | "host": "ଆୟୋଜକ", 50 | "vibe": "ପରିବେଶ", 51 | "value": "ମୂଲ୍ୟ", 52 | "what_made_special": "ଏହି ସ୍ଥାନକୁ କଣ ବିଶେଷ କଲା? ଯାଦୁ ସାଝା କରନ୍ତୁ...", 53 | "location_breathtaking": "ସ୍ଥାନଟି ନିଶ୍ୱାସ ବନ୍ଦ କରିଦେଉଥିଲା! ", 54 | "host_welcoming": "ଆୟୋଜକ ଅବିଶ୍ୱାସନୀୟ ଭାବରେ ସ୍ୱାଗତ କରିଥିଲେ। ", 55 | "perfect_relaxation": "ବିଶ୍ରାମ ଏବଂ ଶାନ୍ତି ପାଇଁ ସିଦ୍ଧ। ", 56 | "great_value": "ଟଙ୍କା ପାଇଁ ଉତ୍ତମ ମୂଲ୍ୟ! ", 57 | "owned_by": "ମାଲିକାନା", 58 | "likes": "ପସନ୍ଦ", 59 | "like": "ପସନ୍ଦ କରନ୍ତୁ", 60 | "unlike": "ନାପସନ୍ଦ କରନ୍ତୁ", 61 | "add_to_wishlist": "ଇଚ୍ଛା ତାଲିକାରେ ଯୋଗ କରନ୍ତୁ", 62 | "edit_listing": "ତାଲିକା ସମ୍ପାଦନା କରନ୍ତୁ", 63 | "delete_listing": "ତାଲିକା ଡିଲିଟ କରନ୍ତୁ", 64 | "you_may_also_like": "ଆପଣ ଏହା ମଧ୍ୟ ପସନ୍ଦ କରିପାରନ୍ତି", 65 | "where_youll_be": "ଆପଣ କେଉଁଠାରେ ରହିବେ", 66 | "view_details": "ବିବରଣୀ ଦେଖନ୍ତୁ", 67 | "night": "ରାତି", 68 | "recently": "ସମ୍ପ୍ରତି" 69 | } -------------------------------------------------------------------------------- /locales/kn.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "ವಾಂಡರ್ಲಸ್ಟ್", 3 | "explore_world": "ನಮ್ಮೊಂದಿಗೆ ಜಗತ್ತನ್ನು ಅನ್ವೇಷಿಸಿ", 4 | "search_destinations": "ಗಮ್ಯಸ್ಥಾನಗಳನ್ನು ಹುಡುಕಿ...", 5 | "search": "ಹುಡುಕಿ", 6 | "home": "ಮುಖ್ಯಪುಟ", 7 | "all_listings": "ಎಲ್ಲಾ ಪಟ್ಟಿಗಳು", 8 | "add_new_listing": "ಹೊಸ ಪಟ್ಟಿ ಸೇರಿಸಿ", 9 | "login": "ಲಾಗಿನ್", 10 | "signup": "ಸೈನ್ಅಪ್", 11 | "logout": "ಲಾಗ್ಔಟ್", 12 | "profile": "ಪ್ರೊಫೈಲ್", 13 | "language": "ಭಾಷೆ", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "ನಿಮ್ಮ ಪ್ರಯಾಣವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ", 28 | "your_words_inspire": "ನಿಮ್ಮ ಮಾತುಗಳು ಸಹ ಪ್ರಯಾಣಿಕರನ್ನು ಪ್ರೇರೇಪಿಸುತ್ತವೆ", 29 | "rate_your_experience": "ನಿಮ್ಮ ಅನುಭವವನ್ನು ರೇಟ್ ಮಾಡಿ", 30 | "tell_your_story": "ನಿಮ್ಮ ಕಥೆಯನ್ನು ಹೇಳಿ", 31 | "launch_review": "ವಿಮರ್ಶೆ ಕಳುಹಿಸಿ", 32 | "your_review_creates": "ನಿಮ್ಮ ವಿಮರ್ಶೆ ಪ್ರಯಾಣ ಸಮುದಾಯದಲ್ಲಿ ಪ್ರೇರಣೆಯ ಅಲೆಗಳನ್ನು ಸೃಷ್ಟಿಸುತ್ತದೆ", 33 | "traveler_stories": "ಪ್ರಯಾಣಿಕರ ಕಥೆಗಳು", 34 | "all_reviews": "ಎಲ್ಲಾ ವಿಮರ್ಶೆಗಳು", 35 | "average_rating": "ಸರಾಸರಿ ರೇಟಿಂಗ್", 36 | "no_reviews_yet": "ಇನ್ನೂ ಯಾವುದೇ ವಿಮರ್ಶೆಗಳಿಲ್ಲ", 37 | "be_first_to_review": "ಈ ಅದ್ಭುತ ಸ್ಥಳದಲ್ಲಿ ನಿಮ್ಮ ಅನುಭವವನ್ನು ಹಂಚಿಕೊಳ್ಳುವ ಮೊದಲ ವ್ಯಕ್ತಿಯಾಗಿರಿ!", 38 | "login_to_review": "ವಿಮರ್ಶೆಗಾಗಿ ಲಾಗಿನ್ ಮಾಡಿ", 39 | "translate_review": "ವಿಮರ್ಶೆಯನ್ನು ಅನುವಾದಿಸಿ", 40 | "original_review": "ಮೂಲ ವಿಮರ್ಶೆ", 41 | "translated_review": "ಅನುವಾದಿತ ವಿಮರ್ಶೆ", 42 | "translation_error": "ಅನುವಾದ ವಿಫಲವಾಗಿದೆ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", 43 | "poor_experience": "ಕಳಪೆ ಅನುಭವ", 44 | "below_expectations": "ನಿರೀಕ್ಷೆಗಳಿಗಿಂತ ಕಡಿಮೆ", 45 | "good_experience": "ಒಳ್ಳೆಯ ಅನುಭವ", 46 | "amazing_experience": "ಅದ್ಭುತ ಅನುಭವ", 47 | "absolutely_perfect": "ಸಂಪೂರ್ಣವಾಗಿ ಪರಿಪೂರ್ಣ!", 48 | "location": "ಸ್ಥಳ", 49 | "host": "ಆತಿಥೇಯ", 50 | "vibe": "ವಾತಾವರಣ", 51 | "value": "ಮೌಲ್ಯ", 52 | "what_made_special": "ಈ ಸ್ಥಳವನ್ನು ವಿಶೇಷವಾಗಿಸಿದ್ದು ಏನು? ಮಾಯಾಜಾಲವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ...", 53 | "location_breathtaking": "ಸ್ಥಳವು ಉಸಿರುಕಟ್ಟಿಸುವಂತಿತ್ತು! ", 54 | "host_welcoming": "ಆತಿಥೇಯರು ನಂಬಲಾಗದಷ್ಟು ಸ್ವಾಗತಿಸಿದರು. ", 55 | "perfect_relaxation": "ವಿಶ್ರಾಂತಿ ಮತ್ತು ಶಾಂತಿಗೆ ಪರಿಪೂರ್ಣ. ", 56 | "great_value": "ಹಣಕ್ಕೆ ಉತ್ತಮ ಮೌಲ್ಯ! ", 57 | "owned_by": "ಮಾಲೀಕತ್ವ", 58 | "likes": "ಇಷ್ಟಗಳು", 59 | "like": "ಇಷ್ಟ", 60 | "unlike": "ಇಷ್ಟವಿಲ್ಲ", 61 | "add_to_wishlist": "ವಿಶ್ಲಿಸ್ಟ್ಗೆ ಸೇರಿಸಿ", 62 | "edit_listing": "ಪಟ್ಟಿಯನ್ನು ಸಂಪಾದಿಸಿ", 63 | "delete_listing": "ಪಟ್ಟಿಯನ್ನು ಅಳಿಸಿ", 64 | "you_may_also_like": "ನೀವು ಇದನ್ನೂ ಇಷ್ಟಪಡಬಹುದು", 65 | "where_youll_be": "ನೀವು ಎಲ್ಲಿರುತ್ತೀರಿ", 66 | "view_details": "ವಿವರಗಳನ್ನು ವೀಕ್ಷಿಸಿ", 67 | "night": "ರಾತ್ರಿ", 68 | "recently": "ಇತ್ತೀಚೆಗೆ" 69 | } -------------------------------------------------------------------------------- /views/includes/category-bar.ejs: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 |
    6 | <% const categories=[ {name: 'Trending' , icon: 'fa-fire' , gradient: 'tw-from-coral tw-to-gold' }, 7 | {name: 'Rooms' , icon: 'fa-bed' , gradient: 'tw-from-blue-400 tw-to-purple-500' }, 8 | {name: 'Iconic Cities' , icon: 'fa-mountain-city' , gradient: 'tw-from-yellow-400 tw-to-orange-500' }, 9 | {name: 'Mountains' , icon: 'fa-mountain' , gradient: 'tw-from-green-400 tw-to-teal-500' }, 10 | {name: 'Castles' , icon: 'fa-chess-rook' , gradient: 'tw-from-purple-400 tw-to-pink-500' }, 11 | {name: 'Amazing pool' , icon: 'fa-person-swimming' , gradient: 'tw-from-aqua tw-to-blue-400' }, 12 | {name: 'Camping' , icon: 'fa-tents' , gradient: 'tw-from-green-500 tw-to-lime-500' }, {name: 'Farms' , 13 | icon: 'fa-tractor' , gradient: 'tw-from-amber-400 tw-to-orange-400' }, {name: 'Arctic' , 14 | icon: 'fa-snowflake' , gradient: 'tw-from-blue-200 tw-to-cyan-300' }, {name: 'Domes' , icon: 'fa-igloo' 15 | , gradient: 'tw-from-indigo-400 tw-to-blue-500' }, {name: 'Boats' , icon: 'fa-ship' , 16 | gradient: 'tw-from-blue-500 tw-to-teal-500' } ]; %> 17 | 18 | <% categories.forEach(cat=> { %> 19 | 21 |
    23 | 25 | 26 | <%= cat.name %> 27 | 28 |
    29 |
    30 | <% }); %> 31 |
    32 |
    33 |
    34 | 35 | -------------------------------------------------------------------------------- /locales/ml.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "വാണ്ടർലസ്റ്റ്", 3 | "explore_world": "ഞങ്ങളോടൊപ്പം ലോകം പര്യവേക്ഷണം ചെയ്യുക", 4 | "search_destinations": "ലക്ഷ്യസ്ഥാനങ്ങൾ തിരയുക...", 5 | "search": "തിരയുക", 6 | "home": "ഹോം", 7 | "all_listings": "എല്ലാ ലിസ്റ്റിംഗുകളും", 8 | "add_new_listing": "പുതിയ ലിസ്റ്റിംഗ് ചേർക്കുക", 9 | "login": "ലോഗിൻ", 10 | "signup": "സൈൻഅപ്പ്", 11 | "logout": "ലോഗൗട്ട്", 12 | "profile": "പ്രൊഫൈൽ", 13 | "language": "ഭാഷ", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "നിങ്ങളുടെ യാത്ര പങ്കിടുക", 28 | "your_words_inspire": "നിങ്ങളുടെ വാക്കുകൾ സഹയാത്രികരെ പ്രചോദിപ്പിക്കുന്നു", 29 | "rate_your_experience": "നിങ്ങളുടെ അനുഭവം റേറ്റ് ചെയ്യുക", 30 | "tell_your_story": "നിങ്ങളുടെ കഥ പറയുക", 31 | "launch_review": "റിവ്യൂ അയയ്ക്കുക", 32 | "your_review_creates": "നിങ്ങളുടെ റിവ്യൂ യാത്രാ സമൂഹത്തിൽ പ്രചോദന തരംഗങ്ങൾ സൃഷ്ടിക്കുന്നു", 33 | "traveler_stories": "യാത്രികരുടെ കഥകൾ", 34 | "all_reviews": "എല്ലാ റിവ്യൂകളും", 35 | "average_rating": "ശരാശരി റേറ്റിംഗ്", 36 | "no_reviews_yet": "ഇതുവരെ റിവ്യൂകളൊന്നുമില്ല", 37 | "be_first_to_review": "ഈ അത്ഭുതകരമായ സ്ഥലത്ത് നിങ്ങളുടെ അനുഭവം പങ്കിടുന്ന ആദ്യത്തെ വ്യക്തിയാകുക!", 38 | "login_to_review": "റിവ്യൂവിനായി ലോഗിൻ ചെയ്യുക", 39 | "translate_review": "റിവ്യൂ വിവർത്തനം ചെയ്യുക", 40 | "original_review": "യഥാർത്ഥ റിവ്യൂ", 41 | "translated_review": "വിവർത്തനം ചെയ്ത റിവ്യൂ", 42 | "translation_error": "വിവർത്തനം പരാജയപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക.", 43 | "poor_experience": "മോശം അനുഭവം", 44 | "below_expectations": "പ്രതീക്ഷകൾക്ക് താഴെ", 45 | "good_experience": "നല്ല അനുഭവം", 46 | "amazing_experience": "അത്ഭുതകരമായ അനുഭവം", 47 | "absolutely_perfect": "തികച്ചും പരിപൂർണ്ണം!", 48 | "location": "സ്ഥലം", 49 | "host": "ആതിഥേയൻ", 50 | "vibe": "അന്തരീക്ഷം", 51 | "value": "മൂല്യം", 52 | "what_made_special": "ഈ സ്ഥലത്തെ പ്രത്യേകമാക്കിയത് എന്താണ്? മാജിക് പങ്കിടുക...", 53 | "location_breathtaking": "സ്ഥലം ശ്വാസം മുട്ടിക്കുന്നതായിരുന്നു! ", 54 | "host_welcoming": "ആതിഥേയൻ അവിശ്വസനീയമായി സ്വാഗതം ചെയ്തു. ", 55 | "perfect_relaxation": "വിശ്രമത്തിനും സമാധാനത്തിനും പരിപൂർണ്ണം. ", 56 | "great_value": "പണത്തിന് മികച്ച മൂല്യം! ", 57 | "owned_by": "ഉടമസ്ഥത", 58 | "likes": "ഇഷ്ടങ്ങൾ", 59 | "like": "ഇഷ്ടം", 60 | "unlike": "ഇഷ്ടമല്ല", 61 | "add_to_wishlist": "വിഷ്ലിസ്റ്റിൽ ചേർക്കുക", 62 | "edit_listing": "ലിസ്റ്റിംഗ് എഡിറ്റ് ചെയ്യുക", 63 | "delete_listing": "ലിസ്റ്റിംഗ് ഇല്ലാതാക്കുക", 64 | "you_may_also_like": "നിങ്ങൾക്ക് ഇതും ഇഷ്ടപ്പെടാം", 65 | "where_youll_be": "നിങ്ങൾ എവിടെയായിരിക്കും", 66 | "view_details": "വിശദാംശങ്ങൾ കാണുക", 67 | "night": "രാത്രി", 68 | "recently": "അടുത്തിടെ" 69 | } -------------------------------------------------------------------------------- /routes/weather.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const weatherService = require('../services/weatherService'); 4 | const Listing = require('../models/listing'); 5 | 6 | // Main weather page 7 | router.get('/', async (req, res) => { 8 | try { 9 | // Fetch unique locations from listings for the dropdown 10 | let destinations = await Listing.distinct('location'); 11 | 12 | // Fallback destinations if no listings 13 | if (!destinations || destinations.length === 0) { 14 | destinations = [ 15 | 'Paris', 'Tokyo', 'New York', 'London', 'Sydney', 'Rome', 'Barcelona', 'Amsterdam', 16 | 'Dubai', 'Singapore', 'Bangkok', 'Mumbai', 'Cape Town', 'Rio de Janeiro', 'Toronto' 17 | ]; 18 | } 19 | 20 | res.render('weather', { 21 | title: 'Weather Information', 22 | currentUser: req.user, 23 | destinations: destinations.sort() // Sort alphabetically 24 | }); 25 | } catch (error) { 26 | console.error('Error fetching destinations:', error); 27 | // Fallback destinations on error 28 | const fallbackDestinations = [ 29 | 'Paris', 'Tokyo', 'New York', 'London', 'Sydney', 'Rome', 'Barcelona', 'Amsterdam', 30 | 'Dubai', 'Singapore', 'Bangkok', 'Mumbai', 'Cape Town', 'Rio de Janeiro', 'Toronto' 31 | ]; 32 | res.render('weather', { 33 | title: 'Weather Information', 34 | currentUser: req.user, 35 | destinations: fallbackDestinations.sort() 36 | }); 37 | } 38 | }); 39 | 40 | // Get weather for specific coordinates 41 | router.get('/current/:lat/:lon', async (req, res) => { 42 | try { 43 | const { lat, lon } = req.params; 44 | const weather = await weatherService.getCurrentWeather(parseFloat(lat), parseFloat(lon)); 45 | res.json(weather); 46 | } catch (error) { 47 | res.status(500).json({ error: 'Weather service unavailable' }); 48 | } 49 | }); 50 | 51 | // Get forecast for specific coordinates 52 | router.get('/forecast/:lat/:lon', async (req, res) => { 53 | try { 54 | const { lat, lon } = req.params; 55 | const forecast = await weatherService.getForecast(parseFloat(lat), parseFloat(lon)); 56 | res.json(forecast); 57 | } catch (error) { 58 | res.status(500).json({ error: 'Forecast service unavailable' }); 59 | } 60 | }); 61 | 62 | // Search weather by location name 63 | router.get('/search/:location', async (req, res) => { 64 | try { 65 | const { location } = req.params; 66 | const weather = await weatherService.getWeatherByLocation(location); 67 | res.json(weather); 68 | } catch (error) { 69 | res.status(500).json({ error: 'Weather search failed' }); 70 | } 71 | }); 72 | 73 | module.exports = router; 74 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | const Listing = require("./models/listing"); 2 | const Review = require("./models/review"); 3 | const { listingSchema, reviewSchema } = require("./schema.js"); 4 | const ExpressError = require("./utils/ExpressError.js"); 5 | module.exports.isLoggedIn = (req, res, next) => { 6 | if (!req.isAuthenticated()) { 7 | req.session.redirectUrl = req.originalUrl; 8 | req.flash("error", "You must be logged in to create a listing!"); 9 | return res.redirect("/login"); 10 | } 11 | next(); 12 | }; 13 | 14 | module.exports.saveRedirectUrl = (req, res, next) => { 15 | if (req.session.redirectUrl) { 16 | res.locals.redirectUrl = req.session.redirectUrl; 17 | } 18 | next(); 19 | }; 20 | 21 | module.exports.isOwner = async (req, res, next) => { 22 | let { id } = req.params; 23 | let listing = await Listing.findById(id); 24 | if (!listing.owner._id.equals(res.locals.currentUser._id)) { 25 | req.flash("error", "You are not the owner of this listing!"); 26 | return res.redirect(`/listings/${id}`); 27 | } 28 | next(); 29 | }; 30 | 31 | module.exports.validateListing = (req, res, next) => { 32 | let { error } = listingSchema.validate(req.body); 33 | if (error) { 34 | let errMsg = error.details.map((el) => el.message).join(","); 35 | throw new ExpressError(400, errMsg); 36 | } else { 37 | next(); 38 | } 39 | }; 40 | 41 | module.exports.validateReview = (req, res, next) => { 42 | let { error } = reviewSchema.validate(req.body); 43 | if (error) { 44 | let errMsg = error.details.map((el) => el.message).join(","); 45 | throw new ExpressError(400, errMsg); 46 | } else { 47 | next(); 48 | } 49 | }; 50 | 51 | module.exports.isReviewAuthor = async (req, res, next) => { 52 | let { id, reviewId } = req.params; 53 | let review = await Review.findById(reviewId); 54 | if (!review.author._id.equals(res.locals.currentUser._id)) { 55 | req.flash("error", "You are not the author of this review!"); 56 | return res.redirect(`/listings/${id}`); 57 | } 58 | next(); 59 | }; 60 | 61 | // Badge checking middleware - runs after user actions that might earn badges 62 | module.exports.checkForNewBadges = async (req, res, next) => { 63 | if (req.user) { 64 | try { 65 | const BadgeService = require('./services/badgeService'); 66 | const newBadges = await BadgeService.checkAndAwardBadges(req.user._id); 67 | 68 | if (newBadges && newBadges.length > 0) { 69 | const badgeNames = newBadges.join(', '); 70 | req.flash('success', `🎉 Congratulations! You earned new badge(s): ${badgeNames}`); 71 | } 72 | } catch (error) { 73 | console.error('Error checking for badges:', error); 74 | // Don't let badge errors break the main flow 75 | } 76 | } 77 | next(); 78 | }; 79 | -------------------------------------------------------------------------------- /tests/listings.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | 4 | describe('Listing Routes', () => { 5 | describe('GET /listings', () => { 6 | it('should return listings index page', async () => { 7 | const res = await request(app) 8 | .get('/listings') 9 | .expect('Content-Type', /html/) 10 | .expect(200); 11 | 12 | expect(res.text).toContain('All Listings'); 13 | }); 14 | }); 15 | 16 | describe('GET /listings/new', () => { 17 | it('should redirect to login when not authenticated', async () => { 18 | const res = await request(app) 19 | .get('/listings/new') 20 | .expect(302); 21 | 22 | expect(res.headers.location).toContain('/login'); 23 | }); 24 | }); 25 | 26 | describe('GET /listings/:id', () => { 27 | it('should return 404 for invalid listing ID', async () => { 28 | const res = await request(app) 29 | .get('/listings/invalid-id-123') 30 | .expect(500); // MongoDB will throw error for invalid ID 31 | }); 32 | }); 33 | 34 | describe('POST /listings', () => { 35 | it('should redirect to login when not authenticated', async () => { 36 | const res = await request(app) 37 | .post('/listings') 38 | .send({ 39 | title: 'Test Listing', 40 | description: 'Test Description', 41 | price: 100, 42 | location: 'Test Location', 43 | country: 'Test Country' 44 | }) 45 | .expect(302); 46 | 47 | expect(res.headers.location).toContain('/login'); 48 | }); 49 | 50 | it('should reject listing with missing required fields', async () => { 51 | const res = await request(app) 52 | .post('/listings') 53 | .send({ 54 | title: '', 55 | description: '' 56 | }); 57 | 58 | expect(res.status).toBe(302); 59 | }); 60 | }); 61 | 62 | describe('GET /listings/:id/edit', () => { 63 | it('should redirect to login when not authenticated', async () => { 64 | const res = await request(app) 65 | .get('/listings/123456789012345678901234/edit') 66 | .expect(302); 67 | 68 | expect(res.headers.location).toContain('/login'); 69 | }); 70 | }); 71 | 72 | describe('DELETE /listings/:id', () => { 73 | it('should redirect to login when not authenticated', async () => { 74 | const res = await request(app) 75 | .delete('/listings/123456789012345678901234') 76 | .expect(302); 77 | 78 | expect(res.headers.location).toContain('/login'); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /locales/ta.json: -------------------------------------------------------------------------------- 1 | { 2 | "wanderlust": "வாண்டர்லஸ்ட்", 3 | "explore_world": "எங்களுடன் உலகை ஆராயுங்கள்", 4 | "search_destinations": "இலக்குகளைத் தேடுங்கள்...", 5 | "search": "தேடுங்கள்", 6 | "home": "முகப்பு", 7 | "all_listings": "அனைத்து பட்டியல்கள்", 8 | "add_new_listing": "புதிய பட்டியல் சேர்க்கவும்", 9 | "login": "உள்நுழைவு", 10 | "signup": "பதிவு செய்யவும்", 11 | "logout": "வெளியேறு", 12 | "profile": "சுயவிவரம்", 13 | "language": "மொழி", 14 | "english": "English", 15 | "hindi": "हिंदी", 16 | "bengali": "বাংলা", 17 | "telugu": "తెలుగు", 18 | "marathi": "मराठी", 19 | "tamil": "தமிழ்", 20 | "gujarati": "ગુજરાતી", 21 | "kannada": "ಕನ್ನಡ", 22 | "malayalam": "മലയാളം", 23 | "punjabi": "ਪੰਜਾਬੀ", 24 | "odia": "ଓଡ଼ିଆ", 25 | "assamese": "অসমীয়া", 26 | "urdu": "اردو", 27 | "share_your_journey": "உங்கள் பயணத்தைப் பகிருங்கள்", 28 | "your_words_inspire": "உங்கள் வார்த்தைகள் சக பயணிகளை ஊக்குவிக்கின்றன", 29 | "rate_your_experience": "உங்கள் அனுபவத்தை மதிப்பிடுங்கள்", 30 | "tell_your_story": "உங்கள் கதையைச் சொல்லுங்கள்", 31 | "launch_review": "மதிப்பாய்வு அனுப்பவும்", 32 | "your_review_creates": "உங்கள் மதிப்பாய்வு பயண சமூகத்தில் உத்வேக அலைகளை உருவாக்குகிறது", 33 | "traveler_stories": "பயணிகளின் கதைகள்", 34 | "all_reviews": "அனைத்து மதிப்பாய்வுகள்", 35 | "average_rating": "சராசரி மதிப்பீடு", 36 | "no_reviews_yet": "இன்னும் மதிப்பாய்வுகள் இல்லை", 37 | "be_first_to_review": "இந்த அற்புதமான இடத்தில் உங்கள் அனுபவத்தைப் பகிரும் முதல் நபராக இருங்கள்!", 38 | "login_to_review": "மதிப்பாய்வுக்கு உள்நுழையவும்", 39 | "translate_review": "மதிப்பாய்வை மொழிபெயர்க்கவும்", 40 | "original_review": "அசல் மதிப்பாய்வு", 41 | "translated_review": "மொழிபெயர்க்கப்பட்ட மதிப்பாய்வு", 42 | "translation_error": "மொழிபெயர்ப்பு தோல்வியடைந்தது. மீண்டும் முயற்சிக்கவும்.", 43 | "poor_experience": "மோசமான அனுபவம்", 44 | "below_expectations": "எதிர்பார்ப்புகளுக்குக் கீழே", 45 | "good_experience": "நல்ல அனுபவம்", 46 | "amazing_experience": "அற்புதமான அனுபவம்", 47 | "absolutely_perfect": "முற்றிலும் சரியானது!", 48 | "location": "இடம்", 49 | "host": "புரவலர்", 50 | "vibe": "சூழல்", 51 | "value": "மதிப்பு", 52 | "what_made_special": "இந்த இடத்தை எது சிறப்பாக்கியது? மாயாஜாலத்தைப் பகிருங்கள்...", 53 | "location_breathtaking": "இடம் மூச்சடைக்கக்கூடியதாக இருந்தது! ", 54 | "host_welcoming": "புரவலர் நம்பமுடியாத அளவிற்கு வரவேற்பு அளித்தார். ", 55 | "perfect_relaxation": "ஓய்வு மற்றும் அமைதிக்கு சரியானது. ", 56 | "great_value": "பணத்திற்கு சிறந்த மதிப்பு! ", 57 | "owned_by": "உரிமையாளர்", 58 | "likes": "விருப்பங்கள்", 59 | "like": "விரும்பு", 60 | "unlike": "விரும்பாதே", 61 | "add_to_wishlist": "விருப்பப்பட்டியலில் சேர்க்கவும்", 62 | "edit_listing": "பட்டியலைத் திருத்தவும்", 63 | "delete_listing": "பட்டியலை நீக்கவும்", 64 | "you_may_also_like": "நீங்கள் இதையும் விரும்பலாம்", 65 | "where_youll_be": "நீங்கள் எங்கே இருப்பீர்கள்", 66 | "view_details": "விவரங்களைப் பார்க்கவும்", 67 | "night": "இரவு", 68 | "recently": "சமீபத்தில்" 69 | } -------------------------------------------------------------------------------- /public/CSS/loading.css: -------------------------------------------------------------------------------- 1 | /* Loading Overlay */ 2 | .loading-overlay { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: var(--loading-overlay-bg, rgba(0, 0, 0, 0.7)); 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | z-index: 9999; 13 | backdrop-filter: blur(3px); 14 | transition: background-color 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 15 | } 16 | 17 | [data-theme="dark"] .loading-overlay { 18 | --loading-overlay-bg: rgba(0, 0, 0, 0.85); 19 | } 20 | 21 | /* Loading Spinner */ 22 | .loading-spinner { 23 | width: 60px; 24 | height: 60px; 25 | border: 5px solid var(--loading-spinner-border, rgba(255, 255, 255, 0.3)); 26 | border-top: 5px solid var(--loading-spinner-top, #28a745); 27 | border-radius: 50%; 28 | animation: spin 1s linear infinite; 29 | box-shadow: 0 0 20px var(--loading-spinner-glow, rgba(40, 167, 69, 0.5)); 30 | transition: border-color 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), 31 | box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 32 | } 33 | 34 | [data-theme="dark"] .loading-spinner { 35 | --loading-spinner-border: rgba(255, 255, 255, 0.2); 36 | --loading-spinner-top: var(--accent-color); 37 | --loading-spinner-glow: rgba(254, 66, 77, 0.6); 38 | } 39 | 40 | @keyframes spin { 41 | 0% { transform: rotate(0deg); } 42 | 100% { transform: rotate(360deg); } 43 | } 44 | 45 | /* Button Loading State */ 46 | .btn-loading { 47 | position: relative; 48 | pointer-events: none; 49 | opacity: 0.8; 50 | cursor: not-allowed; 51 | } 52 | 53 | .btn-loading::before { 54 | content: ""; 55 | position: absolute; 56 | top: 0; 57 | left: 0; 58 | right: 0; 59 | bottom: 0; 60 | background-color: var(--btn-loading-overlay, rgba(0, 0, 0, 0.1)); 61 | border-radius: inherit; 62 | z-index: 1; 63 | transition: background-color 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 64 | } 65 | 66 | .btn-loading::after { 67 | content: ""; 68 | position: absolute; 69 | width: 18px; 70 | height: 18px; 71 | top: 50%; 72 | left: 50%; 73 | margin-left: -9px; 74 | margin-top: -9px; 75 | border: 2px solid var(--btn-loading-border, rgba(255, 255, 255, 0.3)); 76 | border-top-color: var(--btn-loading-top, #ffffff); 77 | border-radius: 50%; 78 | animation: button-loading-spinner 0.6s linear infinite; 79 | z-index: 2; 80 | transition: border-color 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 81 | } 82 | 83 | [data-theme="dark"] .btn-loading::before { 84 | --btn-loading-overlay: rgba(255, 255, 255, 0.1); 85 | } 86 | 87 | [data-theme="dark"] .btn-loading::after { 88 | --btn-loading-border: rgba(255, 255, 255, 0.2); 89 | --btn-loading-top: var(--text-primary); 90 | } 91 | 92 | @keyframes button-loading-spinner { 93 | 0% { transform: rotate(0deg); } 94 | 100% { transform: rotate(360deg); } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">=18.0.0" 4 | }, 5 | "name": "wanderlust", 6 | "version": "1.0.0", 7 | "main": "app.js", 8 | "scripts": { 9 | "test": "jest --coverage --detectOpenHandles", 10 | "test:watch": "jest --watch", 11 | "test:unit": "jest tests/models tests/utils", 12 | "test:integration": "jest tests/auth.test.js tests/listings.test.js tests/reviews.test.js", 13 | "start": "node app.js", 14 | "dev": "nodemon app.js", 15 | "build:css": "tailwindcss -i ./public/CSS/tailwind.input.css -o ./public/CSS/tailwind.output.css --minify", 16 | "watch:css": "tailwindcss -i ./public/CSS/tailwind.input.css -o ./public/CSS/tailwind.output.css --watch", 17 | "dev:all": "npm run watch:css & nodemon app.js" 18 | }, 19 | "keywords": [ 20 | "travel", 21 | "nodejs", 22 | "express", 23 | "mongodb", 24 | "ejs" 25 | ], 26 | "author": "Kaushik Mandal", 27 | "license": "MIT", 28 | "description": "Wanderlust - a travel experience sharing platform built with Node.js, Express, MongoDB, and EJS.", 29 | "dependencies": { 30 | "@google-cloud/translate": "^9.3.0", 31 | "@mapbox/mapbox-sdk": "^0.16.2", 32 | "@playwright/test": "^1.57.0", 33 | "axios": "^1.13.2", 34 | "cloudinary": "^2.8.0", 35 | "connect-flash": "^0.1.1", 36 | "connect-mongo": "^6.0.0", 37 | "cookie-parser": "^1.4.7", 38 | "dotenv": "^17.2.3", 39 | "ejs": "^3.1.10", 40 | "ejs-mate": "^4.0.0", 41 | "express": "^4.22.1", 42 | "express-session": "^1.18.1", 43 | "express-validator": "^7.3.1", 44 | "google-translate-api-x": "^10.7.2", 45 | "helmet": "^8.1.0", 46 | "i18n": "^0.15.3", 47 | "joi": "^18.0.2", 48 | "jspdf": "^3.0.4", 49 | "kill-port": "^2.0.1", 50 | "lodash": "^4.17.21", 51 | "method-override": "^3.0.0", 52 | "mongodb": "^7.0.0", 53 | "mongoose": "^8.20.2", 54 | "multer": "^2.0.2", 55 | "multer-storage-cloudinary": "^2.2.1", 56 | "node-cron": "^4.2.1", 57 | "node-fetch": "^3.3.2", 58 | "openai": "^6.10.0", 59 | "passport": "^0.7.0", 60 | "passport-google-oauth20": "^2.0.0", 61 | "passport-local": "^1.0.0", 62 | "passport-local-mongoose": "^9.0.1", 63 | "playwright": "^1.56.0", 64 | "socket.io": "^4.8.1" 65 | }, 66 | "devDependencies": { 67 | "autoprefixer": "^10.4.23", 68 | "babel-plugin-istanbul": "^7.0.1", 69 | "jest": "^30.2.0", 70 | "js-yaml": "^4.1.1", 71 | "nodemon": "^3.1.11", 72 | "postcss": "^8.5.6", 73 | "supertest": "^7.1.4", 74 | "tailwindcss": "^4.1.18" 75 | }, 76 | "jest": { 77 | "testEnvironment": "node", 78 | "setupFilesAfterEnv": [ 79 | "/tests/setup.js" 80 | ], 81 | "coverageDirectory": "coverage", 82 | "collectCoverageFrom": [ 83 | "**/*.js", 84 | "!node_modules/**", 85 | "!coverage/**", 86 | "!tests/**", 87 | "!jest.config.js" 88 | ], 89 | "testMatch": [ 90 | "**/tests/**/*.test.js" 91 | ], 92 | "verbose": true 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /controllers/reviews.js: -------------------------------------------------------------------------------- 1 | const Listing = require("../models/listing"); 2 | const Review = require("../models/review"); 3 | const aiSummarizationService = require("../services/aiSummarizationService"); 4 | const translate = require('google-translate-api-x'); 5 | 6 | module.exports.createReview = async (req, res) => { 7 | let listing = await Listing.findById(req.params.id); 8 | let newReview = new Review(req.body.review); 9 | newReview.author = req.user._id; 10 | 11 | listing.reviews.push(newReview); 12 | 13 | await newReview.save(); 14 | await listing.save(); 15 | 16 | // Regenerate AI summary after adding review 17 | try { 18 | const populatedListing = await Listing.findById(req.params.id).populate({ 19 | path: "reviews", 20 | populate: { path: "author" }, 21 | }); 22 | const newSummary = await aiSummarizationService.generateSummary(populatedListing.reviews, populatedListing.title); 23 | await Listing.findByIdAndUpdate(req.params.id, { 24 | aiSummary: newSummary, 25 | aiSummaryLastUpdated: new Date() 26 | }); 27 | console.log('AI summary updated after adding review'); 28 | } catch (error) { 29 | console.log('AI summary update failed after adding review:', error.message); 30 | } 31 | 32 | req.flash("success", "New review created!"); 33 | res.redirect(`/listings/${listing._id}`); 34 | }; 35 | 36 | module.exports.destroyReview = async (req, res) => { 37 | let { id, reviewId } = req.params; 38 | 39 | await Listing.findByIdAndUpdate(id, { $pull: { reviews: reviewId } }); 40 | await Review.findByIdAndDelete(reviewId); 41 | 42 | // Regenerate AI summary after removing review 43 | try { 44 | const populatedListing = await Listing.findById(id).populate({ 45 | path: "reviews", 46 | populate: { path: "author" }, 47 | }); 48 | const newSummary = await aiSummarizationService.generateSummary(populatedListing.reviews, populatedListing.title); 49 | await Listing.findByIdAndUpdate(id, { 50 | aiSummary: newSummary, 51 | aiSummaryLastUpdated: new Date() 52 | }); 53 | console.log('AI summary updated after removing review'); 54 | } catch (error) { 55 | console.log('AI summary update failed after removing review:', error.message); 56 | } 57 | 58 | req.flash("success", "Review deleted!"); 59 | res.redirect(`/listings/${id}`); 60 | }; 61 | 62 | module.exports.translateReview = async (req, res) => { 63 | try { 64 | const { reviewId } = req.params; 65 | const review = await Review.findById(reviewId); 66 | 67 | if (!review) { 68 | return res.status(404).json({ error: 'Review not found' }); 69 | } 70 | 71 | const targetLang = req.getLocale(); 72 | if (targetLang === 'en') { 73 | return res.json({ translatedText: review.comment }); 74 | } 75 | 76 | const result = await translate(review.comment, { to: targetLang }); 77 | res.json({ translatedText: result.text }); 78 | } catch (error) { 79 | console.error('Translation error:', error); 80 | res.status(500).json({ error: 'Translation failed' }); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /views/privacy.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 | 4 |
    5 |

    Privacy Policy

    6 |

    Last Updated: 7 | <%= new Date().toLocaleDateString() %> 8 |

    9 | 10 |

    11 | At Wanderlust, we value your trust and are committed to protecting your personal information. 12 | This Privacy Policy explains what data we collect, how we use it, and the choices you have regarding your 13 | information. 14 |

    15 | 16 |

    1. Information We Collect

    17 | 22 | 23 |

    2. How We Use Your Information

    24 | 29 | 30 |

    3. Sharing of Information

    31 |

    32 | We do not sell or rent your personal information. We may share limited data only with: 33 |

    34 | 38 | 39 |

    4. Data Protection

    40 |

    41 | We use reasonable security measures to protect your data. However, please note that no online system is completely 42 | secure. 43 |

    44 | 45 |

    5. Your Rights

    46 | 50 | 51 |

    6. Third-Party Links

    52 |

    53 | Our website may contain links to external websites. We are not responsible for their privacy practices. 54 |

    55 | 56 |

    7. Contact Us

    57 |
    58 |

    59 | If you have questions about this Privacy Policy, you can reach us at: 60 | 📧 wanderlust.support@example.com 61 |

    62 |
    63 |
    64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | 7 | name: "🗂️ Mark stale issues and pull requests" 8 | 9 | on: 10 | schedule: 11 | - cron: '0 12 * * *' # Run daily at 12:00 UTC 12 | workflow_dispatch: # Allow manual triggering 13 | 14 | jobs: 15 | stale: 16 | name: "Mark stale items" 17 | runs-on: ubuntu-latest 18 | permissions: 19 | issues: write 20 | pull-requests: write 21 | 22 | steps: 23 | - name: "Mark stale issues and PRs" 24 | uses: actions/stale@v9 25 | with: 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | # Issues configuration 29 | days-before-issue-stale: 60 30 | days-before-issue-close: 14 31 | stale-issue-message: | 32 | 👋 Hello! This issue has been automatically marked as stale because it has not had recent activity. 33 | 34 | 🏃‍♂️ **To keep this issue active:** 35 | - Add a comment with updates or questions 36 | - Reference it in a pull request 37 | - Add the `keep-open` label 38 | 39 | 🗑️ **If no activity occurs within 14 days, this issue will be automatically closed.** 40 | 41 | Thank you for contributing to WanderLust! 🌍✈️ 42 | close-issue-message: | 43 | 🔒 This issue has been automatically closed due to inactivity. 44 | 45 | If you believe this issue is still relevant, please feel free to reopen it or create a new issue with updated information. 46 | 47 | Thank you for your contribution to WanderLust! 🌍 48 | stale-issue-label: 'stale' 49 | 50 | # Pull requests configuration 51 | days-before-pr-stale: 30 52 | days-before-pr-close: 7 53 | stale-pr-message: | 54 | 👋 Hello! This pull request has been automatically marked as stale because it has not had recent activity. 55 | 56 | 🔄 **To keep this PR active:** 57 | - Push new commits 58 | - Add a comment with updates 59 | - Request a review 60 | - Add the `keep-open` label 61 | 62 | 🗑️ **If no activity occurs within 7 days, this PR will be automatically closed.** 63 | 64 | Thank you for contributing to WanderLust! 🌍✈️ 65 | close-pr-message: | 66 | 🔒 This pull request has been automatically closed due to inactivity. 67 | 68 | If you'd like to continue with this PR, please feel free to reopen it and address any feedback. 69 | 70 | Thank you for your contribution to WanderLust! 🌍 71 | stale-pr-label: 'stale' 72 | 73 | # Exempt labels 74 | exempt-issue-labels: 'keep-open,bug,enhancement,documentation,help-wanted,good-first-issue,priority-high' 75 | exempt-pr-labels: 'keep-open,work-in-progress,review-requested,priority-high' 76 | 77 | # Other options 78 | remove-stale-when-updated: true 79 | delete-branch: false 80 | -------------------------------------------------------------------------------- /routes/packingList.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { isLoggedIn } = require('../middleware'); 4 | const PackingListService = require('../services/packingListService'); 5 | const User = require('../models/user'); 6 | 7 | // GET /packing-list - Show packing list input form 8 | router.get('/', isLoggedIn, (req, res) => { 9 | res.render('packingList/form', { 10 | title: 'AI-Powered Packing List Generator', 11 | user: req.user 12 | }); 13 | }); 14 | 15 | // POST /packing-list/generate - Generate packing list using AI 16 | router.post('/generate', isLoggedIn, async (req, res) => { 17 | try { 18 | const { destination, duration, travelType, activities } = req.body; 19 | 20 | // Validate inputs 21 | const tripData = { 22 | destination: destination.trim(), 23 | duration: parseInt(duration), 24 | travelType: travelType.toLowerCase(), 25 | activities: Array.isArray(activities) ? activities : [activities] 26 | }; 27 | 28 | const validation = PackingListService.validateTripData(tripData); 29 | if (!validation.isValid) { 30 | return res.status(400).render('packingList/form', { 31 | title: 'AI-Powered Packing List Generator', 32 | errors: validation.errors, 33 | formData: req.body 34 | }); 35 | } 36 | 37 | // Generate packing list 38 | const packingList = await PackingListService.generatePackingList(tripData); 39 | 40 | res.render('packingList/result', { 41 | title: 'Your Personalized Packing List', 42 | packingList, 43 | user: req.user 44 | }); 45 | } catch (error) { 46 | console.error('Packing list generation error:', error); 47 | res.status(500).render('packingList/form', { 48 | title: 'AI-Powered Packing List Generator', 49 | errors: ['Failed to generate packing list. Please try again later.'], 50 | formData: req.body 51 | }); 52 | } 53 | }); 54 | 55 | // POST /packing-list/save - Save packing list to user's trip plans 56 | router.post('/save', isLoggedIn, async (req, res) => { 57 | try { 58 | const { packingList, tripDetails } = req.body; 59 | 60 | if (!packingList || !tripDetails) { 61 | return res.status(400).json({ error: 'Missing packing list or trip details' }); 62 | } 63 | 64 | const user = await User.findById(req.user._id); 65 | 66 | if (!user.tripPlans) { 67 | user.tripPlans = []; 68 | } 69 | 70 | // Add packing list to trip plan 71 | const newTripPlan = { 72 | destination: tripDetails.destination, 73 | startDate: tripDetails.startDate ? new Date(tripDetails.startDate) : new Date(), 74 | endDate: tripDetails.endDate ? new Date(tripDetails.endDate) : new Date(), 75 | travelers: tripDetails.travelers || 1, 76 | budgetType: tripDetails.budgetType || 'moderate', 77 | packingList: JSON.parse(packingList), 78 | total: tripDetails.total || 0, 79 | status: 'planned', 80 | createdAt: new Date() 81 | }; 82 | 83 | user.tripPlans.push(newTripPlan); 84 | await user.save(); 85 | 86 | res.json({ success: true, message: 'Packing list saved to your trips!' }); 87 | } catch (error) { 88 | console.error('Save packing list error:', error); 89 | res.status(500).json({ error: 'Failed to save packing list' }); 90 | } 91 | }); 92 | 93 | module.exports = router; 94 | -------------------------------------------------------------------------------- /.github/workflows/issue-create-automate-message.yml: -------------------------------------------------------------------------------- 1 | name: Smart Auto Comment on Issue 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | comment: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Generate Personalized Comment 15 | uses: actions/github-script@v7 16 | with: 17 | script: | 18 | const issueNumber = context.issue.number; 19 | const username = context.payload.issue.user.login; 20 | const title = context.payload.issue.title.toLowerCase(); 21 | const body = context.payload.issue.body ? context.payload.issue.body.toLowerCase() : ''; 22 | const labels = context.payload.issue.labels.map(label => label.name); 23 | 24 | let personalizedMessage = `### Hello @${username}! 👋\n\n`; 25 | 26 | // Detect issue type and personalize accordingly 27 | if (title.includes('bug') || title.includes('error') || title.includes('issue')) { 28 | personalizedMessage += `Thanks for reporting this bug! 🐛 Your detailed report helps us improve the project.\n\n`; 29 | personalizedMessage += `To help us resolve this quickly, please ensure you've included:\n`; 30 | personalizedMessage += `- Steps to reproduce the issue\n`; 31 | personalizedMessage += `- Expected vs actual behavior\n`; 32 | personalizedMessage += `- Your environment details\n\n`; 33 | } else if (title.includes('feature') || title.includes('enhancement') || title.includes('request')) { 34 | personalizedMessage += `Great feature suggestion! 💡 We appreciate community input on improving the project.\n\n`; 35 | personalizedMessage += `Your idea will be reviewed by our team. Meanwhile, feel free to:\n`; 36 | personalizedMessage += `- Check our [roadmap](ROADMAP.md) for planned features\n`; 37 | personalizedMessage += `- Join discussions with other contributors\n\n`; 38 | } else if (title.includes('question') || title.includes('help') || title.includes('how')) { 39 | personalizedMessage += `Thanks for your question! 🤔 We're here to help.\n\n`; 40 | personalizedMessage += `Before diving in, you might find answers in:\n`; 41 | personalizedMessage += `- Our [documentation](docs/)\n`; 42 | personalizedMessage += `- [FAQ section](FAQ.md)\n`; 43 | personalizedMessage += `- [Previous discussions](../../discussions)\n\n`; 44 | } else { 45 | personalizedMessage += `Thank you for opening this issue! We appreciate your contribution to the project.\n\n`; 46 | } 47 | 48 | // Add standard footer 49 | personalizedMessage += `> Please review our [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) before making any contributions.\n\n`; 50 | personalizedMessage += `We'll review this as soon as possible! ✨`; 51 | 52 | await github.rest.issues.createComment({ 53 | owner: context.repo.owner, 54 | repo: context.repo.repo, 55 | issue_number: issueNumber, 56 | body: personalizedMessage 57 | }); 58 | 59 | console.log('Personalized comment added successfully.'); -------------------------------------------------------------------------------- /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.js"); 5 | const passport = require("passport"); 6 | const { saveRedirectUrl, isLoggedIn } = 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 | // Google OAuth routes 30 | router.get("/auth/google", 31 | passport.authenticate("google", { scope: ["profile", "email"] }) 32 | ); 33 | 34 | router.get("/auth/google/callback", 35 | passport.authenticate("google", { failureRedirect: "/signup" }), 36 | userController.googleCallback 37 | ); 38 | 39 | 40 | router.get("/profile/likes", isLoggedIn, userController.showLikedListings); 41 | 42 | // Wishlist Routes (place before general profile routes) 43 | router.get("/profile/wishlist", isLoggedIn, wrapAsync(userController.showWishlist)); 44 | router.post("/profile/wishlist/:listingId", isLoggedIn, wrapAsync(userController.addToWishlist)); 45 | router.get("/profile/wishlist/add/:listingId", isLoggedIn, wrapAsync(userController.addToWishlist)); 46 | router.delete("/profile/wishlist/:listingId", isLoggedIn, wrapAsync(userController.removeFromWishlist)); 47 | 48 | // Vacation Slots Route 49 | router.get("/profile/vacation-slots", isLoggedIn, wrapAsync(userController.showVacationSlots)); 50 | 51 | // Enhanced Profile Routes 52 | router 53 | .route("/profile") 54 | .get(isLoggedIn, userController.renderProfile) 55 | .put(isLoggedIn, userController.updateProfile); 56 | 57 | // Travel Goals Routes 58 | router.post("/profile/travel-goals", isLoggedIn, wrapAsync(userController.addTravelGoal)); 59 | router.patch("/profile/travel-goals/:goalId/complete", isLoggedIn, wrapAsync(userController.completeTravelGoal)); 60 | router.delete("/profile/travel-goals/:goalId", isLoggedIn, wrapAsync(userController.deleteTravelGoal)); 61 | 62 | // Achievements Route 63 | router.get("/achievements", isLoggedIn, wrapAsync(userController.showAchievements)); 64 | 65 | // Leaderboard Route 66 | router.get("/leaderboard", isLoggedIn, wrapAsync(userController.showLeaderboard)); 67 | 68 | // Travel Journal Routes 69 | router.get("/profile/travel-journal", isLoggedIn, wrapAsync(userController.showTravelJournal)); 70 | router.post("/profile/travel-journal", isLoggedIn, wrapAsync(userController.addTravelMemory)); 71 | router.patch("/profile/travel-journal/:memoryId", isLoggedIn, wrapAsync(userController.updateTravelMemory)); 72 | router.delete("/profile/travel-journal/:memoryId", isLoggedIn, wrapAsync(userController.deleteTravelMemory)); 73 | 74 | // Smart Travel Recommendations Route 75 | router.get("/recommendations", (req, res) => { 76 | console.log("Direct route handler called"); 77 | res.send("Direct route handler working!"); 78 | }); 79 | 80 | // Root route - redirect to listings 81 | router.get("/", (req, res) => { 82 | res.redirect("/listings"); 83 | }); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /routes/safety.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const wrapAsync = require("../utils/wrapAsync.js"); 4 | const { isLoggedIn, isAdmin } = require("../middleware.js"); 5 | const scamsController = require("../controllers/scams.js"); 6 | const multer = require("multer"); 7 | const { storage } = require("../cloudConfig.js"); 8 | const upload = multer({ storage }); 9 | 10 | // Validation middleware 11 | const { body } = require('express-validator'); 12 | 13 | // Validation rules for scam report 14 | const validateScamReport = [ 15 | body('scamReport.title') 16 | .trim() 17 | .isLength({ min: 5, max: 200 }) 18 | .withMessage('Title must be between 5 and 200 characters'), 19 | body('scamReport.location') 20 | .trim() 21 | .isLength({ min: 2, max: 100 }) 22 | .withMessage('Location must be between 2 and 100 characters'), 23 | body('scamReport.country') 24 | .trim() 25 | .isLength({ min: 2, max: 100 }) 26 | .withMessage('Country is required'), 27 | body('scamReport.description') 28 | .trim() 29 | .isLength({ min: 20, max: 2000 }) 30 | .withMessage('Description must be between 20 and 2000 characters'), 31 | body('scamReport.category') 32 | .isIn(['Overpricing', 'Fake Guide', 'Fraud', 'Theft', 'Unsafe Area', 'Transportation Scam', 'Accommodation Scam', 'Tour Scam', 'Other']) 33 | .withMessage('Please select a valid category'), 34 | body('scamReport.severity') 35 | .isIn(['low', 'medium', 'high', 'critical']) 36 | .withMessage('Please select a valid severity level'), 37 | body('scamReport.incidentDate') 38 | .isISO8601() 39 | .withMessage('Please provide a valid incident date') 40 | .custom((value) => { 41 | const incidentDate = new Date(value); 42 | const now = new Date(); 43 | const oneYearAgo = new Date(); 44 | oneYearAgo.setFullYear(now.getFullYear() - 1); 45 | 46 | if (incidentDate > now) { 47 | throw new Error('Incident date cannot be in the future'); 48 | } 49 | if (incidentDate < oneYearAgo) { 50 | throw new Error('Incident date cannot be more than 1 year ago'); 51 | } 52 | return true; 53 | }) 54 | ]; 55 | 56 | // Routes 57 | router 58 | .route("/") 59 | .get(wrapAsync(scamsController.getSafetyAlerts)) // Safety alerts feed 60 | .post( 61 | isLoggedIn, 62 | upload.array("evidence", 5), // Allow up to 5 evidence files 63 | validateScamReport, 64 | wrapAsync(scamsController.createScamReport) 65 | ); 66 | 67 | // New report form 68 | router.get("/new", isLoggedIn, scamsController.renderNewForm); 69 | 70 | // Individual report routes 71 | router 72 | .route("/:id") 73 | .get(wrapAsync(scamsController.showScamReport)) 74 | .put( 75 | isLoggedIn, 76 | upload.array("evidence", 5), 77 | validateScamReport, 78 | wrapAsync(scamsController.updateScamReport) 79 | ) 80 | .delete(isLoggedIn, wrapAsync(scamsController.deleteScamReport)); 81 | 82 | // Edit form 83 | router.get("/:id/edit", isLoggedIn, wrapAsync(scamsController.renderEditForm)); 84 | 85 | // Voting routes 86 | router.post("/:id/upvote", isLoggedIn, wrapAsync(scamsController.upvoteReport)); 87 | router.post("/:id/downvote", isLoggedIn, wrapAsync(scamsController.downvoteReport)); 88 | 89 | // Admin routes - TODO: Implement admin verification UI 90 | // router.put("/:id/verify", isLoggedIn, isAdmin, wrapAsync(scamsController.verifyReport)); 91 | 92 | // API routes 93 | router.get("/api/location-alerts", wrapAsync(scamsController.getLocationAlerts)); 94 | 95 | module.exports = router; 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 🌍 Wanderlust — Code of Conduct 2 | 3 | ![Welcome Badge](https://img.shields.io/badge/Community-Welcoming-brightgreen?style=for-the-badge) 4 | ![Respect Badge](https://img.shields.io/badge/Respect-Essential-blue?style=for-the-badge) 5 | ![Inclusive Badge](https://img.shields.io/badge/Inclusive-Always-orange?style=for-the-badge) 6 | ![Safety Badge](https://img.shields.io/badge/Safe_Space-Guaranteed-purple?style=for-the-badge) 7 | 8 | --- 9 | 10 | ## ✨ **Welcome!** 11 | 12 | Hey there, awesome dev! 👋 13 | Welcome to **Wanderlust**—part of **GirlScript Summer of Code 2025**. 14 | 15 | We’re building a community that is: 16 | 17 | - **Friendly** 💛 18 | - **Inclusive** 🌈 19 | - **Supportive** 🤝 20 | 21 | Whether you’re a newbie or a pro, your **voice matters**. Let’s create something amazing **together**. 🚀 22 | 23 | --- 24 | 25 | ## 🌟 **Our Core Principles** 26 | 27 | We expect everyone in our community to: 28 | 29 | | Principle | Emoji | Why It Matters | 30 | | ------------------------------ | ----- | -------------------------------------------- | 31 | | **Respect Others** | 🙏 | Treat everyone kindly & considerately | 32 | | **Communicate Constructively** | 💬 | Help, guide, and provide feedback politely | 33 | | **Be Inclusive** | 🌏 | Welcome all backgrounds, skills & identities | 34 | | **Show Empathy** | ❤️ | Understand different perspectives | 35 | | **Stay Positive** | ✨ | Keep the vibes uplifting and motivating | 36 | 37 | > _"Kindness is contagious—spread it around!"_ 😎 38 | 39 | --- 40 | 41 | ## ❌ **Unacceptable Behavior** 42 | 43 | We take these seriously: 44 | 45 | - Harassment, discrimination, or exclusion of any kind 🚫 46 | - Personal attacks, insults, or derogatory comments 💢 47 | - Unwelcome sexual attention or advances ⚠️ 48 | - Intimidation, stalking, or threats 😨 49 | - Disruptive, trolling, or disrespectful behavior 🛑 50 | 51 | > **Pro Tip:** If it feels wrong in real life, don’t type it here. 🧠 52 | 53 | --- 54 | 55 | ## 📢 **Reporting Issues** 56 | 57 | If something bad happens, **don’t stress**—we’ve got your back! 58 | 59 | - Contact the team at: **[koushik369mondal@gmail.com](mailto:koushik369mondal@gmail.com)** 60 | - Your report will be **confidential & handled carefully** 🔒 61 | - Optionally, include screenshots or context for faster action 🖼️ 62 | 63 | ![Reporting GIF](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExaDFmZmR0amZtbzdyOXlyZmdpemN0NHo5YWNyazQ2bXhxeG1jcnh0aiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/L1R1tvI9svkIWwpVYr/giphy.gif) 64 | 65 | --- 66 | 67 | ## ⚖️ **Enforcement Responsibilities** 68 | 69 | Maintainers are responsible for: 70 | 71 | - Clarifying rules 📜 72 | - Taking fair & appropriate action ✅ 73 | - Ensuring a safe & inclusive environment for all 🌟 74 | 75 | --- 76 | 77 | ## 🌐 **Scope of This Code** 78 | 79 | This Code of Conduct applies to **all places related to Wanderlust**: 80 | 81 | - Repository & branches 82 | - Issues & Pull Requests 83 | - Discussions, chats, and community forums 84 | - Any contributor interactions 85 | 86 | > Think of it as your **safety net in the Wanderlust world** 🕸️ 87 | 88 | --- 89 | 90 | ## 💖 **Thank You!** 91 | 92 | Thanks for helping us make **Wanderlust** a **fun, safe, and inspiring space** for all contributors! 93 | 94 | Let’s code, create, and explore together. 🌟✨ 95 | 96 | ![Thank You GIF](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExNDdnYjJ6ZWE2Nzh4dzgzZWxmMm1zZ3pveGJ4bm1rd3A4Zzc5dWFmcyZlcD12MV9naWZzX3RyZW5kaW5nJmN0PWc/bWKy65WDyQ06GRnNC8/giphy.gif) 97 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | <% layout("/layouts/boilerplate") %> 2 | 3 |
    4 |
    5 |
    6 | 7 |
    8 | 9 |
    10 | 11 | 12 |

    404

    13 |

    Oops! You've wandered off the beaten path

    14 | 15 | 16 |
    17 |

    18 | It looks like the destination you're looking for doesn't exist on our map. 19 | Don't worry, even the best explorers sometimes take a wrong turn! 20 |

    21 |
    22 | 23 |

    24 | Let's get you back on track to discover amazing places. 25 |

    26 |
    27 | 28 |
    29 | 30 | 31 |
    32 | 33 | 34 | Explore Listings 35 | 36 | 40 |
    41 | 42 | 43 |
    44 |

    45 | 46 | Need help? Try searching for destinations or 47 | browse our listings 48 |

    49 |
    50 |
    51 |
    52 |
    53 | 54 | 55 | -------------------------------------------------------------------------------- /views/phraseAssistant/index.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layouts/boilerplate') %> 2 | 3 |
    4 |

    <%= title %>

    5 | 6 |
    7 |
    8 |
    9 |
    10 |
    Translate Custom Phrase
    11 | 12 |
    13 | 14 | 15 |
    16 | 17 |
    18 |
    19 | 20 | 21 |
    22 |
    23 | 24 | 31 |
    32 |
    33 | 34 | 35 | 36 | 47 |
    48 |
    49 | 50 |
    51 |
    52 |
    Quick Phrases
    53 |

    Select a category and language to see common phrases.

    54 | 55 |
    56 |
    57 | 58 | 65 |
    66 |
    67 | 68 | 69 |
    70 |
    71 | 72 | 73 | 74 |
    75 |
    76 |
    77 |
    78 | 79 |
    80 |
    81 |
    82 |
    My Travel Phrases
    83 |
    84 |
    85 |
    86 |
    87 |
    88 |
    89 | 90 | 91 | -------------------------------------------------------------------------------- /public/CSS/holiday.css: -------------------------------------------------------------------------------- 1 | /* Holiday Calendar Specific Styles */ 2 | .holiday-calendar { 3 | background: var(--bg-secondary); 4 | min-height: 100vh; 5 | padding: 20px 0; 6 | } 7 | 8 | /* Dark mode theme variables for holiday styles */ 9 | [data-theme="dark"] { 10 | --filter-chip-bg: rgba(254, 66, 77, 0.2); 11 | --filter-chip-border: rgba(254, 66, 77, 0.4); 12 | --export-btn-hover: #d63641; 13 | } 14 | 15 | .holiday-stats { 16 | display: grid; 17 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 18 | gap: 20px; 19 | margin-bottom: 30px; 20 | } 21 | 22 | .stat-card { 23 | background: var(--bg-primary); 24 | padding: 20px; 25 | border-radius: 10px; 26 | text-align: center; 27 | box-shadow: 0 2px 10px var(--shadow); 28 | } 29 | 30 | .stat-number { 31 | font-size: 2rem; 32 | font-weight: bold; 33 | color: var(--accent-color); 34 | } 35 | 36 | .stat-label { 37 | color: var(--text-secondary); 38 | font-size: 0.9rem; 39 | margin-top: 5px; 40 | } 41 | 42 | .filter-section { 43 | background: var(--bg-primary); 44 | padding: 20px; 45 | border-radius: 10px; 46 | margin-bottom: 20px; 47 | box-shadow: 0 2px 10px var(--shadow); 48 | } 49 | 50 | .filter-chips { 51 | display: flex; 52 | flex-wrap: wrap; 53 | gap: 10px; 54 | margin-top: 15px; 55 | } 56 | 57 | .filter-chip { 58 | background: var(--filter-chip-bg, rgba(254, 66, 77, 0.1)); 59 | color: var(--accent-color); 60 | border: 1px solid var(--filter-chip-border, rgba(254, 66, 77, 0.3)); 61 | padding: 8px 16px; 62 | border-radius: 20px; 63 | cursor: pointer; 64 | transition: all 0.3s ease; 65 | font-size: 0.9rem; 66 | } 67 | 68 | .filter-chip:hover, 69 | .filter-chip.active { 70 | background: var(--accent-color); 71 | color: white; 72 | } 73 | 74 | .holiday-timeline { 75 | position: relative; 76 | padding-left: 30px; 77 | } 78 | 79 | .holiday-timeline::before { 80 | content: ''; 81 | position: absolute; 82 | left: 15px; 83 | top: 0; 84 | bottom: 0; 85 | width: 2px; 86 | background: var(--accent-color); 87 | } 88 | 89 | .timeline-item { 90 | position: relative; 91 | margin-bottom: 30px; 92 | } 93 | 94 | .timeline-item::before { 95 | content: ''; 96 | position: absolute; 97 | left: -23px; 98 | top: 10px; 99 | width: 12px; 100 | height: 12px; 101 | border-radius: 50%; 102 | background: var(--accent-color); 103 | border: 3px solid var(--bg-primary); 104 | } 105 | 106 | .export-options { 107 | margin-top: 30px; 108 | text-align: center; 109 | } 110 | 111 | .export-btn { 112 | background: var(--accent-color); 113 | color: white; 114 | border: none; 115 | padding: 12px 24px; 116 | border-radius: 25px; 117 | margin: 0 10px; 118 | cursor: pointer; 119 | transition: all 0.3s ease; 120 | } 121 | 122 | .export-btn:hover { 123 | background: var(--export-btn-hover, #e63946); 124 | transform: translateY(-2px); 125 | } 126 | 127 | /* Mobile Responsiveness */ 128 | @media (max-width: 768px) { 129 | .holiday-grid { 130 | grid-template-columns: 1fr; 131 | } 132 | 133 | .filter-chips { 134 | justify-content: center; 135 | } 136 | 137 | .export-btn { 138 | display: block; 139 | width: 100%; 140 | margin: 10px 0; 141 | } 142 | } 143 | 144 | /* Dark theme support */ 145 | [data-theme="dark"] .holiday-calendar { 146 | background: var(--bg-primary); 147 | } 148 | 149 | [data-theme="dark"] .stat-card, 150 | [data-theme="dark"] .filter-section, 151 | [data-theme="dark"] .holiday-card { 152 | background: var(--bg-secondary); 153 | color: var(--text-primary); 154 | } 155 | 156 | [data-theme="dark"] .holiday-type, 157 | [data-theme="dark"] .stat-label { 158 | color: var(--text-secondary); 159 | } -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # WanderLust Test Suite 2 | 3 | This directory contains comprehensive test cases for the WanderLust application. 4 | 5 | ## Test Structure 6 | 7 | ``` 8 | tests/ 9 | ├── setup.js # Jest setup and configuration 10 | ├── auth.test.js # Authentication route tests 11 | ├── listings.test.js # Listing route tests 12 | ├── reviews.test.js # Review route tests 13 | ├── middleware.test.js # Middleware tests 14 | ├── schema.test.js # Joi schema validation tests 15 | ├── models/ 16 | │ ├── listing.test.js # Listing model tests 17 | │ ├── review.test.js # Review model tests 18 | │ └── user.test.js # User model tests 19 | └── utils/ 20 | ├── ExpressError.test.js # ExpressError utility tests 21 | └── wrapAsync.test.js # wrapAsync utility tests 22 | ``` 23 | 24 | ## Running Tests 25 | 26 | ### Run all tests 27 | 28 | ```bash 29 | npm test 30 | ``` 31 | 32 | ### Run tests in watch mode 33 | 34 | ```bash 35 | npm run test:watch 36 | ``` 37 | 38 | ### Run unit tests only 39 | 40 | ```bash 41 | npm run test:unit 42 | ``` 43 | 44 | ### Run integration tests only 45 | 46 | ```bash 47 | npm run test:integration 48 | ``` 49 | 50 | ### Run tests with coverage 51 | 52 | ```bash 53 | npm test -- --coverage 54 | ``` 55 | 56 | ## Test Categories 57 | 58 | ### 1. Route Tests 59 | 60 | - **auth.test.js**: Tests for signup, login, logout routes 61 | - **listings.test.js**: Tests for listing CRUD operations 62 | - **reviews.test.js**: Tests for review creation and deletion 63 | 64 | ### 2. Model Tests 65 | 66 | - **listing.test.js**: Tests for Listing model validation 67 | - **review.test.js**: Tests for Review model validation 68 | - **user.test.js**: Tests for User model validation 69 | 70 | ### 3. Utility Tests 71 | 72 | - **ExpressError.test.js**: Tests for custom error handling 73 | - **wrapAsync.test.js**: Tests for async error wrapper 74 | 75 | ### 4. Middleware Tests 76 | 77 | - **middleware.test.js**: Tests for error handling, session, flash messages, security headers 78 | 79 | ### 5. Schema Tests 80 | 81 | - **schema.test.js**: Tests for Joi validation schemas 82 | 83 | ## Test Coverage 84 | 85 | The test suite aims to cover: 86 | 87 | - ✅ Authentication flows 88 | - ✅ CRUD operations for listings and reviews 89 | - ✅ Model validations 90 | - ✅ Error handling 91 | - ✅ Middleware functionality 92 | - ✅ Schema validations 93 | - ✅ Utility functions 94 | 95 | ## Environment Variables 96 | 97 | Tests use `.env.test` file for configuration. Make sure to: 98 | 99 | 1. Copy `.env.test.example` to `.env.test` 100 | 2. Update with appropriate test database credentials 101 | 3. Never commit `.env.test` with real credentials 102 | 103 | ## Notes 104 | 105 | - Tests use `supertest` for HTTP assertions 106 | - Models are tested with validation mocks 107 | - Database operations are mocked in unit tests 108 | - Integration tests may require a test database connection 109 | 110 | ## Adding New Tests 111 | 112 | When adding new features: 113 | 114 | 1. Create corresponding test file in appropriate directory 115 | 2. Follow existing test patterns 116 | 3. Ensure tests are independent and isolated 117 | 4. Update this README with new test descriptions 118 | 119 | ## Continuous Integration 120 | 121 | Tests are automatically run on: 122 | 123 | - Pull requests 124 | - Push to main branch 125 | - Before deployment 126 | 127 | ## Troubleshooting 128 | 129 | ### Port Already in Use 130 | 131 | If you see `EADDRINUSE` error: 132 | 133 | ```bash 134 | npx kill-port 8080 135 | ``` 136 | 137 | ### Database Connection Issues 138 | 139 | - Ensure MongoDB is running 140 | - Check `.env.test` database URL 141 | - Verify test database exists 142 | 143 | ### Test Timeout 144 | 145 | Increase timeout in specific tests: 146 | 147 | ```javascript 148 | jest.setTimeout(15000); // 15 seconds 149 | ``` 150 | -------------------------------------------------------------------------------- /views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # CodeQL Security Analysis Workflow 2 | # This workflow performs security analysis using GitHub's CodeQL to detect 3 | # potential security vulnerabilities and coding errors in JavaScript/Node.js code 4 | 5 | name: "🔒 CodeQL Security Analysis" 6 | 7 | on: 8 | # Trigger on pushes to main branches 9 | push: 10 | branches: [ "main", "develop" ] 11 | paths: 12 | - '**.js' 13 | - '**.ts' 14 | - '**.json' 15 | - '.github/workflows/codeql.yml' 16 | 17 | # Trigger on pull requests targeting main branches 18 | pull_request: 19 | branches: [ "main", "develop" ] 20 | paths: 21 | - '**.js' 22 | - '**.ts' 23 | - '**.json' 24 | - '.github/workflows/codeql.yml' 25 | 26 | # Run weekly security scans (every Monday at 2 AM UTC) 27 | schedule: 28 | - cron: '0 2 * * 1' 29 | 30 | # Allow manual triggering of the workflow 31 | workflow_dispatch: 32 | 33 | jobs: 34 | analyze: 35 | name: 🛡️ Security Analysis 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 30 # Prevent workflow from running too long 38 | 39 | # Required permissions for CodeQL 40 | permissions: 41 | actions: read 42 | contents: read 43 | security-events: write 44 | 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Analyze JavaScript/TypeScript code 49 | language: [ 'javascript-typescript' ] 50 | include: 51 | - language: 'javascript-typescript' 52 | build-mode: none 53 | 54 | steps: 55 | # Step 1: Checkout the repository 56 | - name: 📥 Checkout Repository 57 | uses: actions/checkout@v4 58 | 59 | # Step 2: Initialize CodeQL analysis 60 | - name: 🔧 Initialize CodeQL 61 | uses: github/codeql-action/init@v2 62 | with: 63 | languages: ${{ matrix.language }} 64 | # Use built-in query suite for security analysis 65 | queries: +security-and-quality 66 | # Add config file if needed 67 | # config-file: ./.github/codeql/codeql-config.yml 68 | 69 | # Step 3: Set up Node.js for dependency analysis 70 | - name: 🟢 Setup Node.js 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: 'lts/*' 74 | cache: 'npm' 75 | 76 | # Step 4: Install dependencies (needed for complete analysis) 77 | - name: 📦 Install Dependencies 78 | run: | 79 | echo "Installing dependencies for comprehensive security analysis..." 80 | # Use npm ci for faster, more reliable builds in CI 81 | npm ci --ignore-scripts --only=production 82 | continue-on-error: true 83 | 84 | # Step 5: Build the application (helps with analysis) 85 | - name: 🏗️ Build Application 86 | run: | 87 | echo "Building application for enhanced CodeQL analysis..." 88 | # Only build if build script exists 89 | if npm run --silent build --dry-run 2>/dev/null; then 90 | npm run build 91 | else 92 | echo "No build script found, skipping build step" 93 | fi 94 | continue-on-error: true 95 | 96 | # Step 6: Perform CodeQL Analysis 97 | - name: 🔍 Perform CodeQL Analysis 98 | uses: github/codeql-action/analyze@v2 99 | with: 100 | category: "/language:${{matrix.language}}" 101 | # Upload results even if there are no findings 102 | upload: always 103 | 104 | # Step 7: Security scan summary 105 | - name: ✅ Security Scan Summary 106 | if: always() 107 | run: | 108 | echo "🔒 CodeQL Security Analysis completed!" 109 | echo "✓ Repository scanned for security vulnerabilities" 110 | echo "✓ Code quality issues analyzed" 111 | echo "✓ Results automatically uploaded to GitHub Security tab" 112 | echo "" 113 | echo "📊 Check the Security tab in your repository for detailed results" 114 | echo "🛡️ Any security issues found will appear as alerts" 115 | echo "🔗 View results at: https://github.com/${{ github.repository }}/security/code-scanning" -------------------------------------------------------------------------------- /tests/schema.test.js: -------------------------------------------------------------------------------- 1 | const { listingSchema, reviewSchema } = require('../schema'); 2 | 3 | describe('Schema Validation', () => { 4 | describe('Listing Schema', () => { 5 | it('should validate a valid listing', () => { 6 | const validListing = { 7 | listing: { 8 | title: 'Test Listing', 9 | description: 'Test Description', 10 | price: 100, 11 | location: 'Test Location', 12 | country: 'Test Country' 13 | } 14 | }; 15 | 16 | const { error } = listingSchema.validate(validListing); 17 | expect(error).toBeUndefined(); 18 | }); 19 | 20 | it('should reject listing without title', () => { 21 | const invalidListing = { 22 | listing: { 23 | description: 'Test', 24 | price: 100, 25 | location: 'Test', 26 | country: 'Test' 27 | } 28 | }; 29 | 30 | const { error } = listingSchema.validate(invalidListing); 31 | expect(error).toBeDefined(); 32 | }); 33 | 34 | it('should reject listing with negative price', () => { 35 | const invalidListing = { 36 | listing: { 37 | title: 'Test', 38 | description: 'Test', 39 | price: -100, 40 | location: 'Test', 41 | country: 'Test' 42 | } 43 | }; 44 | 45 | const { error } = listingSchema.validate(invalidListing); 46 | expect(error).toBeDefined(); 47 | }); 48 | 49 | it('should reject listing with empty description', () => { 50 | const invalidListing = { 51 | listing: { 52 | title: 'Test', 53 | description: '', 54 | price: 100, 55 | location: 'Test', 56 | country: 'Test' 57 | } 58 | }; 59 | 60 | const { error } = listingSchema.validate(invalidListing); 61 | expect(error).toBeDefined(); 62 | }); 63 | }); 64 | 65 | describe('Review Schema', () => { 66 | it('should validate a valid review', () => { 67 | const validReview = { 68 | review: { 69 | rating: 5, 70 | comment: 'Great place!' 71 | } 72 | }; 73 | 74 | const { error } = reviewSchema.validate(validReview); 75 | expect(error).toBeUndefined(); 76 | }); 77 | 78 | it('should reject review without rating', () => { 79 | const invalidReview = { 80 | review: { 81 | comment: 'Test' 82 | } 83 | }; 84 | 85 | const { error } = reviewSchema.validate(invalidReview); 86 | expect(error).toBeDefined(); 87 | }); 88 | 89 | it('should reject review with rating > 5', () => { 90 | const invalidReview = { 91 | review: { 92 | rating: 6, 93 | comment: 'Test' 94 | } 95 | }; 96 | 97 | const { error } = reviewSchema.validate(invalidReview); 98 | expect(error).toBeDefined(); 99 | }); 100 | 101 | it('should reject review with rating < 1', () => { 102 | const invalidReview = { 103 | review: { 104 | rating: 0, 105 | comment: 'Test' 106 | } 107 | }; 108 | 109 | const { error } = reviewSchema.validate(invalidReview); 110 | expect(error).toBeDefined(); 111 | }); 112 | 113 | it('should reject review without comment', () => { 114 | const invalidReview = { 115 | review: { 116 | rating: 5 117 | } 118 | }; 119 | 120 | const { error } = reviewSchema.validate(invalidReview); 121 | expect(error).toBeDefined(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /utils/updateCoordinates.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Listing = require('../models/listing'); 3 | require('dotenv').config(); 4 | 5 | // Country-based coordinate mappings 6 | const countryCoordinates = { 7 | 'india': [77.2090, 28.6139], 8 | 'united states': [-95.7129, 37.0902], 9 | 'usa': [-95.7129, 37.0902], 10 | 'italy': [12.5674, 41.8719], 11 | 'mexico': [-102.5528, 23.6345], 12 | 'switzerland': [8.2275, 46.8182], 13 | 'tanzania': [34.8888, -6.3690], 14 | 'netherlands': [5.2913, 52.1326], 15 | 'fiji': [179.4144, -16.5780], 16 | 'united kingdom': [-3.4360, 55.3781], 17 | 'uk': [-3.4360, 55.3781], 18 | 'indonesia': [113.9213, -0.7893], 19 | 'canada': [-106.3468, 56.1304], 20 | 'thailand': [100.9925, 15.8700], 21 | 'united arab emirates': [53.8478, 23.4241], 22 | 'greece': [21.8243, 39.0742], 23 | 'costa rica': [-83.7534, 9.7489], 24 | 'japan': [138.2529, 36.2048], 25 | 'maldives': [73.2207, 3.2028] 26 | }; 27 | 28 | // City-specific coordinates for better accuracy 29 | const cityCoordinates = { 30 | 'new york city': [-74.006, 40.7128], 31 | 'malibu': [-118.7798, 34.0259], 32 | 'aspen': [-106.8175, 39.1911], 33 | 'florence': [11.2558, 43.7696], 34 | 'portland': [-122.6784, 45.5152], 35 | 'cancun': [-86.8515, 21.1619], 36 | 'lake tahoe': [-120.0324, 39.0968], 37 | 'los angeles': [-118.2437, 34.0522], 38 | 'verbier': [7.2284, 46.0963], 39 | 'serengeti national park': [34.8333, -2.3333], 40 | 'amsterdam': [4.9041, 52.3676], 41 | 'fiji': [179.4144, -16.5780], 42 | 'cotswolds': [-1.8094, 51.8330], 43 | 'boston': [-71.0589, 42.3601], 44 | 'bali': [115.0920, -8.4095], 45 | 'banff': [-115.5708, 51.1784], 46 | 'miami': [-80.1918, 25.7617], 47 | 'phuket': [98.3923, 7.8804], 48 | 'scottish highlands': [-4.2026, 57.2707], 49 | 'dubai': [55.2708, 25.2048], 50 | 'montana': [-110.3626, 46.9219], 51 | 'mykonos': [25.3289, 37.4467], 52 | 'costa rica': [-83.7534, 9.7489], 53 | 'charleston': [-79.9311, 32.7765], 54 | 'tokyo': [139.6917, 35.6895], 55 | 'new hampshire': [-71.5376, 43.4525], 56 | 'maldives': [73.2207, 3.2028] 57 | }; 58 | 59 | async function updateListingCoordinates(shouldDisconnect = false) { 60 | try { 61 | // Only connect if not already connected 62 | if (mongoose.connection.readyState !== 1) { 63 | await mongoose.connect(process.env.ATLAS_DB_URL); 64 | console.log('✅ Connected to MongoDB'); 65 | } 66 | 67 | const listings = await Listing.find({}); 68 | let updateCount = 0; 69 | 70 | for (const listing of listings) { 71 | let coordinates = [77.2090, 28.6139]; // Default Delhi 72 | 73 | // Try city-specific coordinates first 74 | const locationKey = (listing.location || '').toLowerCase(); 75 | if (cityCoordinates[locationKey]) { 76 | coordinates = cityCoordinates[locationKey]; 77 | } else { 78 | // Fall back to country coordinates 79 | const countryKey = (listing.country || '').toLowerCase(); 80 | if (countryCoordinates[countryKey]) { 81 | coordinates = countryCoordinates[countryKey]; 82 | } 83 | } 84 | 85 | // Update the listing 86 | await Listing.findByIdAndUpdate(listing._id, { 87 | geometry: { 88 | type: 'Point', 89 | coordinates: coordinates 90 | } 91 | }); 92 | 93 | updateCount++; 94 | } 95 | 96 | console.log(`✅ Updated coordinates for ${updateCount} listings`); 97 | } catch (error) { 98 | console.error('❌ Error updating coordinates:', error); 99 | } finally { 100 | // Only disconnect if explicitly requested (e.g., when run as standalone script) 101 | if (shouldDisconnect) { 102 | await mongoose.disconnect(); 103 | console.log('✅ Disconnected from MongoDB'); 104 | } 105 | } 106 | } 107 | 108 | module.exports = { updateListingCoordinates }; 109 | 110 | // Run the update if script is executed directly 111 | if (require.main === module) { 112 | console.log('🚀 Starting coordinate update process...'); 113 | updateListingCoordinates(true).then(() => { 114 | console.log('✅ Update complete - all listings now have coordinates!'); 115 | process.exit(0); 116 | }).catch(err => { 117 | console.error('❌ Update failed:', err); 118 | process.exit(1); 119 | }); 120 | } -------------------------------------------------------------------------------- /views/users/liked.ejs: -------------------------------------------------------------------------------- 1 | <% layout('/layouts/boilerplate') %> 2 | 3 | 94 | 95 |
    96 |
    97 |
    98 |

    <%= name %>'s Liked Listings

    99 |

    All the places you've saved in one spot!

    100 |
    101 |
    102 | 103 |
    104 | <% if (likedListings && likedListings.length > 0) { %> 105 | <% for(let listing of likedListings) { %> 106 | 122 | <% } %> 123 | <% } else { %> 124 |
    125 |

    You haven't liked any listings yet. Start exploring to save your favorites!

    126 |
    127 | <% } %> 128 |
    129 |
    -------------------------------------------------------------------------------- /views/listings/edit.ejs: -------------------------------------------------------------------------------- 1 | <% layout('/layouts/boilerplate') %> 2 | 3 |
    4 |
    5 |

    Edit your Listing

    6 |
    7 | 8 |
    9 | 10 | 11 |
    Title looks good!
    12 |
    Please enter a title.
    13 |
    14 | 15 |
    16 | 17 | 18 |
    Description looks good!
    19 |
    Please enter a description.
    20 |
    21 | 22 |
    23 | 24 | 30 |
    Looks good!
    31 |
    Please select a category.
    32 |
    33 | 34 |
    35 |
    36 | 37 | 38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 | 45 |
    46 |
    Original Listing Image
    47 | 48 |
    49 | 50 |
    51 | 52 | 53 |
    54 | 55 |
    56 |
    57 | 58 | 59 |
    Looks good!
    60 |
    Please enter a valid price.
    61 |
    62 | 63 |
    64 | 65 | 66 |
    Looks good!
    67 |
    Please enter the country name.
    68 |
    69 |
    70 | 71 |
    72 | 73 | 74 |
    Looks good!
    75 |
    Please enter the location.
    76 |
    77 | 78 | 79 |

    80 |
    81 |
    82 |
    -------------------------------------------------------------------------------- /controllers/newsletter.js: -------------------------------------------------------------------------------- 1 | const Newsletter = require("../models/newsletter.js"); 2 | 3 | // Subscribe to newsletter 4 | module.exports.subscribe = async (req, res) => { 5 | try { 6 | const { email } = req.body; 7 | 8 | // Validate email 9 | if (!email) { 10 | req.flash("error", "Please provide an email address."); 11 | return res.redirect(req.get("Referrer") || "/"); 12 | } 13 | 14 | // Check if email already exists 15 | const existingSubscriber = await Newsletter.findOne({ email: email.toLowerCase() }); 16 | 17 | if (existingSubscriber) { 18 | if (existingSubscriber.isActive) { 19 | req.flash("error", "This email is already subscribed to our newsletter!"); 20 | return res.redirect(req.get("Referrer") || "/"); 21 | } else { 22 | // Reactivate subscription 23 | existingSubscriber.isActive = true; 24 | existingSubscriber.subscribedAt = new Date(); 25 | await existingSubscriber.save(); 26 | req.flash("success", "Welcome back! Your newsletter subscription has been reactivated."); 27 | return res.redirect(req.get("Referrer") || "/"); 28 | } 29 | } 30 | 31 | // Create new subscription 32 | const newSubscriber = new Newsletter({ 33 | email: email.toLowerCase(), 34 | source: req.body.source || 'footer' 35 | }); 36 | 37 | await newSubscriber.save(); 38 | req.flash("success", "🎉 Thank you for subscribing! You'll receive travel tips and exclusive deals."); 39 | res.redirect(req.get("Referrer") || "/"); 40 | 41 | } catch (error) { 42 | console.error("Newsletter subscription error:", error); 43 | 44 | if (error.name === 'ValidationError') { 45 | req.flash("error", "Please enter a valid email address."); 46 | } else if (error.code === 11000) { 47 | req.flash("error", "This email is already subscribed to our newsletter!"); 48 | } else { 49 | req.flash("error", "Something went wrong. Please try again later."); 50 | } 51 | 52 | res.redirect(req.get("Referrer") || "/"); 53 | } 54 | }; 55 | 56 | // Unsubscribe from newsletter 57 | module.exports.unsubscribe = async (req, res) => { 58 | try { 59 | const { email } = req.body; 60 | 61 | if (!email) { 62 | req.flash("error", "Please provide an email address."); 63 | return res.redirect(req.get("Referrer") || "/"); 64 | } 65 | 66 | const subscriber = await Newsletter.findOne({ email: email.toLowerCase() }); 67 | 68 | if (!subscriber) { 69 | req.flash("error", "Email address not found in our newsletter list."); 70 | return res.redirect(req.get("Referrer") || "/"); 71 | } 72 | 73 | if (!subscriber.isActive) { 74 | req.flash("error", "This email is already unsubscribed."); 75 | return res.redirect(req.get("Referrer") || "/"); 76 | } 77 | 78 | subscriber.isActive = false; 79 | await subscriber.save(); 80 | 81 | req.flash("success", "You have been successfully unsubscribed from our newsletter."); 82 | res.redirect(req.get("Referrer") || "/"); 83 | 84 | } catch (error) { 85 | console.error("Newsletter unsubscribe error:", error); 86 | req.flash("error", "Something went wrong. Please try again later."); 87 | res.redirect(req.get("Referrer") || "/"); 88 | } 89 | }; 90 | 91 | // Get newsletter statistics (admin only) 92 | module.exports.getStats = async (req, res) => { 93 | try { 94 | const totalSubscribers = await Newsletter.countDocuments({ isActive: true }); 95 | const recentSubscribers = await Newsletter.countDocuments({ 96 | isActive: true, 97 | subscribedAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } // Last 30 days 98 | }); 99 | 100 | const subscribersBySource = await Newsletter.aggregate([ 101 | { $match: { isActive: true } }, 102 | { $group: { _id: "$source", count: { $sum: 1 } } } 103 | ]); 104 | 105 | res.json({ 106 | totalSubscribers, 107 | recentSubscribers, 108 | subscribersBySource 109 | }); 110 | 111 | } catch (error) { 112 | console.error("Newsletter stats error:", error); 113 | res.status(500).json({ error: "Failed to fetch newsletter statistics" }); 114 | } 115 | }; -------------------------------------------------------------------------------- /services/badgeService.js: -------------------------------------------------------------------------------- 1 | const BadgeDefinition = require('../models/badgeDefinition'); 2 | const User = require('../models/user'); 3 | const Listing = require('../models/listing'); 4 | const Review = require('../models/review'); 5 | 6 | class BadgeService { 7 | static async checkAndAwardBadges(userId) { 8 | try { 9 | const user = await User.findById(userId).populate('badges'); 10 | if (!user) return false; 11 | 12 | const badges = await BadgeDefinition.find({ isActive: true }); 13 | let newBadgesAwarded = []; 14 | 15 | for (const badgeDefinition of badges) { 16 | // Check if user already has this badge 17 | const hasBadge = user.badges.some(badge => badge.name === badgeDefinition.name); 18 | if (hasBadge) continue; 19 | 20 | const meetsRequirement = await this.checkBadgeRequirement(user, badgeDefinition); 21 | if (meetsRequirement) { 22 | await user.awardBadge({ 23 | name: badgeDefinition.name, 24 | description: badgeDefinition.description, 25 | icon: badgeDefinition.icon, 26 | category: badgeDefinition.category 27 | }); 28 | newBadgesAwarded.push(badgeDefinition.name); 29 | } 30 | } 31 | 32 | return newBadgesAwarded; 33 | } catch (error) { 34 | console.error('Error checking badges:', error); 35 | return false; 36 | } 37 | } 38 | 39 | static async checkBadgeRequirement(user, badgeDefinition) { 40 | const { criteria } = badgeDefinition; 41 | 42 | switch (criteria.type) { 43 | case 'profile_completion': 44 | return user.profileCompletion >= criteria.threshold; 45 | 46 | case 'listing_count': 47 | const listingCount = await Listing.countDocuments({ owner: user._id }); 48 | return listingCount >= criteria.threshold; 49 | 50 | case 'review_count': 51 | const reviewCount = await Review.countDocuments({ author: user._id }); 52 | return reviewCount >= criteria.threshold; 53 | 54 | case 'destination_count': 55 | return user.favoriteDestinations.length >= criteria.threshold; 56 | 57 | case 'social_engagement': 58 | const socialLinks = user.socialLinks; 59 | const filledLinks = Object.values(socialLinks).filter(link => link && link.trim() !== '').length; 60 | return filledLinks >= criteria.threshold; 61 | 62 | case 'time_based': 63 | const daysSinceJoin = Math.floor((new Date() - user.joinDate) / (1000 * 60 * 60 * 24)); 64 | return daysSinceJoin >= criteria.threshold; 65 | 66 | default: 67 | return false; 68 | } 69 | } 70 | 71 | static async updateUserStats(userId) { 72 | try { 73 | const user = await User.findById(userId); 74 | if (!user) return false; 75 | 76 | // Count user's listings and reviews 77 | const [listingCount, reviewCount] = await Promise.all([ 78 | Listing.countDocuments({ owner: userId }), 79 | Review.countDocuments({ author: userId }) 80 | ]); 81 | 82 | // Update stats 83 | user.travelStats.totalListings = listingCount; 84 | user.travelStats.totalReviews = reviewCount; 85 | user.travelStats.countriesVisited = user.favoriteDestinations.length; 86 | 87 | await user.save(); 88 | return true; 89 | } catch (error) { 90 | console.error('Error updating user stats:', error); 91 | return false; 92 | } 93 | } 94 | 95 | static getBadgeRarityColor(rarity) { 96 | const colors = { 97 | common: '#6c757d', 98 | rare: '#17a2b8', 99 | epic: '#6f42c1', 100 | legendary: '#fd7e14' 101 | }; 102 | return colors[rarity] || colors.common; 103 | } 104 | 105 | static getBadgeRarityGradient(rarity) { 106 | const gradients = { 107 | common: 'linear-gradient(135deg, #6c757d, #adb5bd)', 108 | rare: 'linear-gradient(135deg, #17a2b8, #20c997)', 109 | epic: 'linear-gradient(135deg, #6f42c1, #e83e8c)', 110 | legendary: 'linear-gradient(135deg, #fd7e14, #ffc107)' 111 | }; 112 | return gradients[rarity] || gradients.common; 113 | } 114 | } 115 | 116 | module.exports = BadgeService; -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Build & Test Workflow 2 | # This workflow builds the application and runs tests to ensure everything works correctly 3 | # Uses npm ci for faster, reliable, reproducible builds 4 | 5 | name: "🏗️ Build & Test" 6 | 7 | # Trigger this workflow on push and pull requests to main branches 8 | on: 9 | push: 10 | branches: [ "main", "develop" ] 11 | pull_request: 12 | branches: [ "main", "develop" ] 13 | 14 | jobs: 15 | build-and-test: 16 | name: 🚀 Build and Test Application 17 | runs-on: ubuntu-latest 18 | 19 | # Test against multiple Node.js versions for compatibility 20 | strategy: 21 | matrix: 22 | node-version: [18.x, 20.x, 22.x] # Use LTS and current stable versions 23 | 24 | steps: 25 | # Step 1: Get the latest code from the repository 26 | - name: 📥 Checkout Repository 27 | uses: actions/checkout@v4 28 | 29 | # Step 2: Set up Node.js environment 30 | - name: 🟢 Setup Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | # Use npm cache for faster builds 35 | cache: 'npm' 36 | 37 | # Step 3: Clean install dependencies (faster and more reliable than npm install) 38 | - name: 📦 Install Dependencies 39 | run: | 40 | echo "Installing dependencies with npm ci for reproducible builds..." 41 | npm ci 42 | 43 | # Step 4: Build the application 44 | - name: 🏗️ Build Application 45 | run: | 46 | # Check if build script exists in package.json 47 | if npm run --silent build --dry-run 2>/dev/null; then 48 | echo "Building application..." 49 | npm run build 50 | else 51 | echo "⚠️ No build script found in package.json" 52 | echo "If your project needs compilation, add a build script to package.json" 53 | echo "Skipping build step..." 54 | fi 55 | 56 | # Step 5: Run tests 57 | - name: 🧪 Run Tests 58 | run: | 59 | # Check if test script exists in package.json 60 | if npm run --silent test --dry-run 2>/dev/null; then 61 | echo "Running tests..." 62 | npm test 63 | else 64 | echo "⚠️ No test script found in package.json" 65 | echo "Consider adding tests to improve code reliability" 66 | echo "You can use testing frameworks like Jest, Mocha, or Vitest" 67 | echo "Skipping test step..." 68 | fi 69 | 70 | # Step 6: Upload build artifacts (if build folder exists) 71 | - name: 📤 Upload Build Artifacts 72 | if: success() 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: build-files-node-${{ matrix.node-version }} 76 | path: | 77 | build/ 78 | dist/ 79 | public/ 80 | # Keep artifacts for 7 days 81 | retention-days: 7 82 | continue-on-error: true 83 | 84 | # Step 7: Show build summary 85 | - name: ✅ Build Summary 86 | if: always() 87 | run: | 88 | echo "🎉 Build and test workflow completed!" 89 | echo "✓ Node.js ${{ matrix.node-version }} environment ready" 90 | echo "✓ Dependencies installed with npm ci" 91 | echo "✓ Build process completed" 92 | echo "✓ Tests executed" 93 | echo "Ready for deployment! 🚀" 94 | 95 | # Summary job that consolidates all matrix results into a single status check 96 | ci-summary: 97 | name: "CI / Build and Test" 98 | runs-on: ubuntu-latest 99 | needs: build-and-test 100 | if: always() 101 | 102 | steps: 103 | - name: 📋 Check CI Status 104 | run: | 105 | echo "Checking overall CI status..." 106 | echo "Matrix job results: ${{ needs.build-and-test.result }}" 107 | 108 | if [ "${{ needs.build-and-test.result }}" = "success" ]; then 109 | echo "✅ All CI checks passed successfully!" 110 | echo "🎉 Build and tests completed across all Node.js versions" 111 | exit 0 112 | elif [ "${{ needs.build-and-test.result }}" = "failure" ]; then 113 | echo "❌ Some CI checks failed" 114 | echo "Please check the individual job results above" 115 | exit 1 116 | elif [ "${{ needs.build-and-test.result }}" = "cancelled" ]; then 117 | echo "⏹️ CI checks were cancelled" 118 | exit 1 119 | else 120 | echo "⚠️ CI checks completed with warnings or were skipped" 121 | echo "Job result: ${{ needs.build-and-test.result }}" 122 | exit 0 123 | fi -------------------------------------------------------------------------------- /services/weatherService.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Using built-in fetch instead of axios 4 | 5 | class WeatherService { 6 | constructor() { 7 | this.apiKey = process.env.WEATHER_API_KEY || 'demo_key'; 8 | this.baseUrl = 'https://api.openweathermap.org/data/2.5'; 9 | this.cache = new Map(); 10 | this.cacheTimeout = 10 * 60 * 1000; // 10 minutes 11 | } 12 | 13 | async getCurrentWeather(lat, lon) { 14 | const cacheKey = `current_${lat}_${lon}`; 15 | const cached = this.cache.get(cacheKey); 16 | 17 | if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { 18 | return cached.data; 19 | } 20 | 21 | try { 22 | const url = `${this.baseUrl}/weather?lat=${lat}&lon=${lon}&appid=${this.apiKey}&units=metric`; 23 | const response = await fetch(url); 24 | const data = await response.json(); 25 | 26 | const weatherData = { 27 | temperature: Math.round(data.main.temp), 28 | feelsLike: Math.round(data.main.feels_like), 29 | humidity: data.main.humidity, 30 | condition: data.weather[0].main, 31 | description: data.weather[0].description, 32 | icon: data.weather[0].icon, 33 | windSpeed: data.wind?.speed || 0 34 | }; 35 | 36 | this.cache.set(cacheKey, { 37 | data: weatherData, 38 | timestamp: Date.now() 39 | }); 40 | 41 | return weatherData; 42 | } catch (error) { 43 | console.error('Weather API error:', error.message); 44 | return this.getFallbackWeather(); 45 | } 46 | } 47 | 48 | async getForecast(lat, lon) { 49 | const cacheKey = `forecast_${lat}_${lon}`; 50 | const cached = this.cache.get(cacheKey); 51 | 52 | if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { 53 | return cached.data; 54 | } 55 | 56 | try { 57 | const url = `${this.baseUrl}/forecast?lat=${lat}&lon=${lon}&appid=${this.apiKey}&units=metric`; 58 | const response = await fetch(url); 59 | const data = await response.json(); 60 | 61 | const forecast = data.list.slice(0, 7).map(item => ({ 62 | date: new Date(item.dt * 1000).toLocaleDateString(), 63 | temperature: Math.round(item.main.temp), 64 | condition: item.weather[0].main, 65 | icon: item.weather[0].icon, 66 | description: item.weather[0].description 67 | })); 68 | 69 | this.cache.set(cacheKey, { 70 | data: forecast, 71 | timestamp: Date.now() 72 | }); 73 | 74 | return forecast; 75 | } catch (error) { 76 | console.error('Forecast API error:', error.message); 77 | return []; 78 | } 79 | } 80 | 81 | getBestTimeToVisit(location, country) { 82 | const seasonalData = { 83 | 'italy': 'Spring (Apr-Jun) & Fall (Sep-Oct)', 84 | 'france': 'Spring (Apr-Jun) & Fall (Sep-Nov)', 85 | 'japan': 'Spring (Mar-May) & Fall (Sep-Nov)', 86 | 'thailand': 'Cool Season (Nov-Feb)', 87 | 'india': 'Winter (Oct-Mar)', 88 | 'greece': 'Spring (Apr-Jun) & Fall (Sep-Oct)', 89 | 'spain': 'Spring (Mar-May) & Fall (Sep-Nov)', 90 | 'united states': 'Varies by region - Spring & Fall generally best', 91 | 'canada': 'Summer (Jun-Aug)', 92 | 'australia': 'Spring (Sep-Nov) & Fall (Mar-May)', 93 | 'default': 'Spring & Fall seasons typically ideal' 94 | }; 95 | 96 | const countryKey = country?.toLowerCase() || 'default'; 97 | return seasonalData[countryKey] || seasonalData['default']; 98 | } 99 | 100 | getFallbackWeather() { 101 | return { 102 | temperature: 22, 103 | feelsLike: 24, 104 | humidity: 65, 105 | condition: 'Clear', 106 | description: 'clear sky', 107 | icon: '01d', 108 | windSpeed: 3.5 109 | }; 110 | } 111 | 112 | getWeatherIcon(iconCode) { 113 | const iconMap = { 114 | '01d': '☀️', '01n': '🌙', 115 | '02d': '⛅', '02n': '☁️', 116 | '03d': '☁️', '03n': '☁️', 117 | '04d': '☁️', '04n': '☁️', 118 | '09d': '🌧️', '09n': '🌧️', 119 | '10d': '🌦️', '10n': '🌧️', 120 | '11d': '⛈️', '11n': '⛈️', 121 | '13d': '❄️', '13n': '❄️', 122 | '50d': '🌫️', '50n': '🌫️' 123 | }; 124 | return iconMap[iconCode] || '🌤️'; 125 | } 126 | } 127 | 128 | module.exports = new WeatherService(); -------------------------------------------------------------------------------- /services/aiSummarizationService.js: -------------------------------------------------------------------------------- 1 | const OpenAI = require('openai'); 2 | 3 | // Check for OpenAI API key 4 | let openai = null; 5 | if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== 'your_openai_api_key_here') { 6 | openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | } else { 10 | console.warn('⚠️ OPENAI_API_KEY not set! AI summarization will be disabled.'); 11 | } 12 | 13 | class AISummarizationService { 14 | /** 15 | * Generate AI summary from reviews 16 | * @param {Array} reviews - Array of review objects with comment and rating 17 | * @param {String} listingTitle - Title of the listing 18 | * @returns {String} - AI generated summary 19 | */ 20 | static async generateSummary(reviews, listingTitle) { 21 | if (!reviews || reviews.length === 0) { 22 | return "No reviews available yet. Be the first to share your experience!"; 23 | } 24 | 25 | if (reviews.length < 2) { 26 | return "Limited reviews available. More feedback needed for AI summary."; 27 | } 28 | 29 | // If OpenAI is not available, return fallback summary 30 | if (!openai) { 31 | console.log('Using fallback summary generation (OpenAI not configured)'); 32 | const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length; 33 | if (avgRating >= 4.5) { 34 | return `This destination has received excellent reviews with an average rating of ${avgRating.toFixed(1)}/5. Travelers consistently praise the experience and recommend it highly.`; 35 | } else if (avgRating >= 4.0) { 36 | return `This destination has good reviews with an average rating of ${avgRating.toFixed(1)}/5. Most travelers had positive experiences with some room for improvement.`; 37 | } else { 38 | return `This destination has mixed reviews with an average rating of ${avgRating.toFixed(1)}/5. Experiences vary among travelers.`; 39 | } 40 | } 41 | 42 | try { 43 | // Prepare reviews text for AI 44 | const reviewsText = reviews.map(review => 45 | `Rating: ${review.rating}/5\nComment: ${review.comment}` 46 | ).join('\n\n'); 47 | 48 | const prompt = `You are an AI travel assistant. Based on the following reviews for "${listingTitle}", create a concise, engaging summary (2-3 sentences) that highlights the key experiences, pros, and any common themes. Focus on what travelers loved and any important considerations. Keep it positive and informative. 49 | 50 | Reviews: 51 | ${reviewsText} 52 | 53 | Summary:`; 54 | 55 | const response = await openai.chat.completions.create({ 56 | model: 'gpt-3.5-turbo', 57 | messages: [ 58 | { 59 | role: 'system', 60 | content: 'You are a helpful travel assistant that creates engaging summaries from user reviews.' 61 | }, 62 | { 63 | role: 'user', 64 | content: prompt 65 | } 66 | ], 67 | max_tokens: 200, 68 | temperature: 0.7, 69 | }); 70 | 71 | const summary = response.choices[0].message.content.trim(); 72 | 73 | // Validate summary length 74 | if (summary.length < 20) { 75 | throw new Error('Summary too short'); 76 | } 77 | 78 | return summary; 79 | } catch (error) { 80 | console.error('AI Summarization Error:', error.message); 81 | 82 | // Fallback summary based on average rating 83 | const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length; 84 | if (avgRating >= 4.5) { 85 | return `This destination has received excellent reviews with an average rating of ${avgRating.toFixed(1)}/5. Travelers consistently praise the experience and recommend it highly.`; 86 | } else if (avgRating >= 4.0) { 87 | return `This destination has good reviews with an average rating of ${avgRating.toFixed(1)}/5. Most travelers had positive experiences with some room for improvement.`; 88 | } else { 89 | return `This destination has mixed reviews with an average rating of ${avgRating.toFixed(1)}/5. Experiences vary among travelers.`; 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Check if summary needs updating 96 | * @param {Date} lastUpdated - Last update timestamp 97 | * @param {Number} reviewCount - Current review count 98 | * @returns {Boolean} - Whether update is needed 99 | */ 100 | static needsUpdate(lastUpdated, reviewCount) { 101 | if (!lastUpdated) return true; 102 | 103 | const now = new Date(); 104 | const hoursSinceUpdate = (now - lastUpdated) / (1000 * 60 * 60); 105 | 106 | // Update if more than 24 hours old, or if significant new reviews (every 5 new reviews) 107 | return hoursSinceUpdate > 24 || (reviewCount > 0 && reviewCount % 5 === 0); 108 | } 109 | } 110 | 111 | module.exports = AISummarizationService; 112 | -------------------------------------------------------------------------------- /routes/currency.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | // Currency converter main page 5 | router.get("/", (req, res) => { 6 | const currencies = [ 7 | { code: 'USD', name: 'US Dollar', symbol: '$', flag: '🇺🇸' }, 8 | { code: 'EUR', name: 'Euro', symbol: '€', flag: '🇪🇺' }, 9 | { code: 'GBP', name: 'British Pound', symbol: '£', flag: '🇬🇧' }, 10 | { code: 'INR', name: 'Indian Rupee', symbol: '₹', flag: '🇮🇳' }, 11 | { code: 'JPY', name: 'Japanese Yen', symbol: '¥', flag: '🇯🇵' }, 12 | { code: 'CAD', name: 'Canadian Dollar', symbol: 'C$', flag: '🇨🇦' }, 13 | { code: 'AUD', name: 'Australian Dollar', symbol: 'A$', flag: '🇦🇺' }, 14 | { code: 'CHF', name: 'Swiss Franc', symbol: 'CHF', flag: '🇨🇭' }, 15 | { code: 'CNY', name: 'Chinese Yuan', symbol: '¥', flag: '🇨🇳' }, 16 | { code: 'KRW', name: 'South Korean Won', symbol: '₩', flag: '🇰🇷' }, 17 | { code: 'BRL', name: 'Brazilian Real', symbol: 'R$', flag: '🇧🇷' }, 18 | { code: 'MXN', name: 'Mexican Peso', symbol: '$', flag: '🇲🇽' }, 19 | { code: 'SGD', name: 'Singapore Dollar', symbol: 'S$', flag: '🇸🇬' }, 20 | { code: 'HKD', name: 'Hong Kong Dollar', symbol: 'HK$', flag: '🇭🇰' }, 21 | { code: 'NZD', name: 'New Zealand Dollar', symbol: 'NZ$', flag: '🇳🇿' }, 22 | { code: 'SEK', name: 'Swedish Krona', symbol: 'kr', flag: '🇸🇪' }, 23 | { code: 'NOK', name: 'Norwegian Krone', symbol: 'kr', flag: '🇳🇴' }, 24 | { code: 'DKK', name: 'Danish Krone', symbol: 'kr', flag: '🇩🇰' }, 25 | { code: 'PLN', name: 'Polish Złoty', symbol: 'zł', flag: '🇵🇱' }, 26 | { code: 'CZK', name: 'Czech Koruna', symbol: 'Kč', flag: '🇨🇿' } 27 | ]; 28 | 29 | res.render("currency/index", { 30 | title: "Currency Converter", 31 | currencies: currencies 32 | }); 33 | }); 34 | 35 | // API endpoint for currency conversion 36 | router.get("/api/convert", async (req, res) => { 37 | try { 38 | const { from, to, amount } = req.query; 39 | 40 | if (!from || !to || !amount) { 41 | return res.status(400).json({ 42 | success: false, 43 | error: 'Missing required parameters: from, to, amount' 44 | }); 45 | } 46 | 47 | const numAmount = parseFloat(amount); 48 | if (isNaN(numAmount) || numAmount < 0) { 49 | return res.status(400).json({ 50 | success: false, 51 | error: 'Invalid amount. Must be a positive number.' 52 | }); 53 | } 54 | 55 | // Use the existing trip planner service for exchange rates 56 | const tripPlannerService = require("../services/tripPlannerService"); 57 | const rates = await tripPlannerService.getExchangeRates(from); 58 | 59 | if (!rates || !rates[to]) { 60 | return res.status(400).json({ 61 | success: false, 62 | error: `Exchange rate not available for ${from} to ${to}` 63 | }); 64 | } 65 | 66 | const rate = rates[to]; 67 | const convertedAmount = numAmount * rate; 68 | 69 | res.json({ 70 | success: true, 71 | conversion: { 72 | from: from, 73 | to: to, 74 | amount: numAmount, 75 | rate: rate, 76 | result: Math.round(convertedAmount * 100) / 100, // Round to 2 decimal places 77 | timestamp: new Date().toISOString() 78 | } 79 | }); 80 | } catch (error) { 81 | console.error('Currency conversion error:', error); 82 | res.status(500).json({ 83 | success: false, 84 | error: 'Failed to convert currency' 85 | }); 86 | } 87 | }); 88 | 89 | // API endpoint for exchange rates 90 | router.get("/api/rates/:base", async (req, res) => { 91 | try { 92 | const { base } = req.params; 93 | 94 | const tripPlannerService = require("../services/tripPlannerService"); 95 | const rates = await tripPlannerService.getExchangeRates(base); 96 | 97 | if (!rates) { 98 | return res.status(400).json({ 99 | success: false, 100 | error: `Exchange rates not available for ${base}` 101 | }); 102 | } 103 | 104 | res.json({ 105 | success: true, 106 | base: base, 107 | rates: rates, 108 | timestamp: new Date().toISOString() 109 | }); 110 | } catch (error) { 111 | console.error('Exchange rates error:', error); 112 | res.status(500).json({ 113 | success: false, 114 | error: 'Failed to fetch exchange rates' 115 | }); 116 | } 117 | }); 118 | 119 | module.exports = router; 120 | -------------------------------------------------------------------------------- /views/includes/theme-toggle.ejs: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 13 |
    14 | 15 | -------------------------------------------------------------------------------- /models/badgeDefinition.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const badgeDefinitionSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | unique: true 9 | }, 10 | description: { 11 | type: String, 12 | required: true 13 | }, 14 | icon: { 15 | type: String, 16 | required: true 17 | }, 18 | category: { 19 | type: String, 20 | enum: ['explorer', 'reviewer', 'host', 'social', 'milestone'], 21 | required: true 22 | }, 23 | criteria: { 24 | type: { 25 | type: String, 26 | enum: ['listing_count', 'review_count', 'destination_count', 'profile_completion', 'social_engagement', 'time_based'], 27 | required: true 28 | }, 29 | threshold: { 30 | type: Number, 31 | required: true 32 | } 33 | }, 34 | rarity: { 35 | type: String, 36 | enum: ['common', 'rare', 'epic', 'legendary'], 37 | default: 'common' 38 | }, 39 | isActive: { 40 | type: Boolean, 41 | default: true 42 | }, 43 | createdAt: { 44 | type: Date, 45 | default: Date.now 46 | } 47 | }); 48 | 49 | // Define default badges 50 | const defaultBadges = [ 51 | { 52 | name: "Welcome Traveler", 53 | description: "Complete your profile", 54 | icon: "fa-user-check", 55 | category: "milestone", 56 | criteria: { type: "profile_completion", threshold: 80 }, 57 | rarity: "common" 58 | }, 59 | { 60 | name: "First Steps", 61 | description: "Create your first listing", 62 | icon: "fa-home", 63 | category: "host", 64 | criteria: { type: "listing_count", threshold: 1 }, 65 | rarity: "common" 66 | }, 67 | { 68 | name: "Rising Host", 69 | description: "Create 5 listings", 70 | icon: "fa-building", 71 | category: "host", 72 | criteria: { type: "listing_count", threshold: 5 }, 73 | rarity: "rare" 74 | }, 75 | { 76 | name: "Property Mogul", 77 | description: "Create 20 listings", 78 | icon: "fa-city", 79 | category: "host", 80 | criteria: { type: "listing_count", threshold: 20 }, 81 | rarity: "epic" 82 | }, 83 | { 84 | name: "Review Rookie", 85 | description: "Write your first review", 86 | icon: "fa-star", 87 | category: "reviewer", 88 | criteria: { type: "review_count", threshold: 1 }, 89 | rarity: "common" 90 | }, 91 | { 92 | name: "Seasoned Critic", 93 | description: "Write 10 reviews", 94 | icon: "fa-edit", 95 | category: "reviewer", 96 | criteria: { type: "review_count", threshold: 10 }, 97 | rarity: "rare" 98 | }, 99 | { 100 | name: "Review Master", 101 | description: "Write 50 reviews", 102 | icon: "fa-award", 103 | category: "reviewer", 104 | criteria: { type: "review_count", threshold: 50 }, 105 | rarity: "epic" 106 | }, 107 | { 108 | name: "Globe Trotter", 109 | description: "Add 10 favorite destinations", 110 | icon: "fa-globe", 111 | category: "explorer", 112 | criteria: { type: "destination_count", threshold: 10 }, 113 | rarity: "rare" 114 | }, 115 | { 116 | name: "World Explorer", 117 | description: "Add 25 favorite destinations", 118 | icon: "fa-map", 119 | category: "explorer", 120 | criteria: { type: "destination_count", threshold: 25 }, 121 | rarity: "epic" 122 | }, 123 | { 124 | name: "Travel Legend", 125 | description: "Add 50 favorite destinations", 126 | icon: "fa-compass", 127 | category: "explorer", 128 | criteria: { type: "destination_count", threshold: 50 }, 129 | rarity: "legendary" 130 | }, 131 | { 132 | name: "Social Butterfly", 133 | description: "Complete all social links", 134 | icon: "fa-users", 135 | category: "social", 136 | criteria: { type: "social_engagement", threshold: 4 }, 137 | rarity: "rare" 138 | }, 139 | { 140 | name: "Veteran Member", 141 | description: "Member for 1 year", 142 | icon: "fa-calendar-alt", 143 | category: "milestone", 144 | criteria: { type: "time_based", threshold: 365 }, 145 | rarity: "epic" 146 | } 147 | ]; 148 | 149 | // Static method to initialize default badges 150 | badgeDefinitionSchema.statics.initializeDefaults = async function() { 151 | for (const badge of defaultBadges) { 152 | await this.findOneAndUpdate( 153 | { name: badge.name }, 154 | badge, 155 | { upsert: true, new: true } 156 | ); 157 | } 158 | }; 159 | 160 | module.exports = mongoose.model("BadgeDefinition", badgeDefinitionSchema); --------------------------------------------------------------------------------