├── Frontend └── Roomzy │ ├── src │ ├── index.css │ ├── utils │ │ └── constants.js │ ├── assets │ │ ├── login1.jpg │ │ ├── account.png │ │ └── react.svg │ ├── components │ │ ├── Body │ │ │ ├── Body.css │ │ │ └── Body.jsx │ │ ├── forgotPassword │ │ │ ├── ForgotPasswordModal.jsx │ │ │ ├── ForgotPasswordModal.css │ │ │ ├── ForgotPassword.css │ │ │ └── ForgotPassword.jsx │ │ ├── Listing │ │ │ ├── listing2.css │ │ │ └── listing2.jsx │ │ ├── OtpSender │ │ │ ├── OTPSender.css │ │ │ └── OTPSender.jsx │ │ ├── SignupScreen │ │ │ ├── SignupScreen.css │ │ │ └── SignupScreen.jsx │ │ ├── Search_Section │ │ │ ├── Search_section.jsx │ │ │ └── Search_section.css │ │ ├── reviewScreen │ │ │ ├── Review.css │ │ │ └── Review.jsx │ │ ├── loginScreen │ │ │ ├── LoginScreen.css │ │ │ └── LoginScreen.jsx │ │ ├── feed │ │ │ ├── feed.css │ │ │ └── feed.jsx │ │ ├── notificationCenter │ │ │ ├── NotificationCenter.css │ │ │ └── NotificationCenter.jsx │ │ └── NavBar │ │ │ ├── Navbar.css │ │ │ └── Navbar.jsx │ ├── main.jsx │ ├── App.css │ └── App.jsx │ ├── .gitignore │ ├── vite.config.js │ ├── index.html │ ├── README.md │ ├── package.json │ ├── eslint.config.js │ └── public │ └── vite.svg ├── Backend ├── .gitignore ├── combined.log ├── src │ ├── controllers │ │ ├── info-controller.js │ │ ├── authController.js │ │ └── room-controller.js │ ├── config │ │ ├── cloudinaryConfig.js │ │ ├── server-config.js │ │ └── logger-config.js │ ├── routes │ │ ├── index.js │ │ ├── authRoutes.js │ │ └── room-routes.js │ ├── middleware2 │ │ ├── cloudinaryStorage.js │ │ └── auth.js │ ├── index.js │ ├── models │ │ ├── Usermodel.js │ │ └── roommodel.js │ ├── utils │ │ └── sendEmail.js │ ├── middlewares │ │ └── validator.js │ ├── services │ │ ├── room-service.js │ │ └── authService.js │ └── repositories │ │ ├── user-repository.js │ │ └── room-repository.js ├── .env ├── package.json └── package-lock.json └── README.md /Frontend/Roomzy/src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | combined.log 3 | .env -------------------------------------------------------------------------------- /Frontend/Roomzy/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const BASE_URL = "http://localhost:8000"; -------------------------------------------------------------------------------- /Frontend/Roomzy/src/assets/login1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Princy2909/RoomZy/HEAD/Frontend/Roomzy/src/assets/login1.jpg -------------------------------------------------------------------------------- /Frontend/Roomzy/src/assets/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Princy2909/RoomZy/HEAD/Frontend/Roomzy/src/assets/account.png -------------------------------------------------------------------------------- /Backend/combined.log: -------------------------------------------------------------------------------- 1 | 2025-02-05 14:30:11 :[right now!] :info :Successfully started the server 2 | 2025-02-05 14:37:08 :[right now!] :info :Successfully started the server 3 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Body/Body.css: -------------------------------------------------------------------------------- 1 | .body-container > * { 2 | margin-bottom: 20px; /* or whatever space you prefer */ 3 | /* background-color: aquamarine; */ 4 | } 5 | -------------------------------------------------------------------------------- /Backend/src/controllers/info-controller.js: -------------------------------------------------------------------------------- 1 | 2 | const {StatusCodes}=require('http-status-codes'); 3 | const info=(req,res)=>{ 4 | return res.json({ 5 | success:true, 6 | message:"Success", 7 | error:{} 8 | 9 | }) 10 | }; 11 | module.exports =info; 12 | 13 | -------------------------------------------------------------------------------- /Backend/src/config/cloudinaryConfig.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require("cloudinary").v2; 2 | 3 | cloudinary.config({ 4 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 5 | api_key: process.env.CLOUDINARY_API_KEY, 6 | api_secret: process.env.CLOUDINARY_API_SECRET, 7 | }); 8 | 9 | module.exports = cloudinary; 10 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import './index.css' 5 | import App from './App.jsx' 6 | 7 | createRoot(document.getElementById('root')).render( 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /Frontend/Roomzy/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Frontend/Roomzy/vite.config.js: -------------------------------------------------------------------------------- 1 | // import { defineConfig } from 'vite' 2 | // import tailwindcss from '@tailwindcss/vite' 3 | // export default defineConfig({ 4 | // plugins: [ 5 | // tailwindcss(), 6 | // ], 7 | // }) 8 | 9 | import { defineConfig } from 'vite' 10 | import react from '@vitejs/plugin-react' 11 | 12 | // https://vite.dev/config/ 13 | export default defineConfig({ 14 | plugins: [react()], 15 | }) 16 | -------------------------------------------------------------------------------- /Frontend/Roomzy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Backend/src/config/server-config.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | require("dotenv").config(); 3 | 4 | const connectDB = async () => { 5 | try { 6 | await mongoose.connect(process.env.MONGO_URI); 7 | console.log("✅ MongoDB connected..."); 8 | } catch (error) { 9 | console.error("❌ MongoDB connection error:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectDB; 15 | -------------------------------------------------------------------------------- /Backend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | // Import individual route files 5 | const authRoutes = require("./authRoutes"); 6 | const roomRoutes = require("./room-routes"); 7 | 8 | // Use the routes 9 | router.use("/auth", authRoutes); // All auth-related routes (signup, login, etc.) 10 | router.use("/rooms", roomRoutes); // All room-related routes (CRUD, search, etc.) 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /Frontend/Roomzy/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Backend/src/middleware2/cloudinaryStorage.js: -------------------------------------------------------------------------------- 1 | const multer = require("multer"); 2 | const { CloudinaryStorage } = require("multer-storage-cloudinary"); 3 | const cloudinary=require("../config/cloudinaryConfig"); 4 | 5 | // file: middleware/upload.js 6 | 7 | const storage = new CloudinaryStorage({ 8 | cloudinary, 9 | params: { 10 | folder: "room-photos", 11 | allowed_formats: ["jpg", "jpeg", "png"], 12 | }, 13 | }); 14 | 15 | const upload = multer({ storage }); 16 | 17 | module.exports = upload; 18 | -------------------------------------------------------------------------------- /Backend/.env: -------------------------------------------------------------------------------- 1 | # MONGO_URI="mongodb+srv://prashant:Prashant@cluster0.tzoqr.mongodb.net/room_rental_db?retryWrites=true&w=majority&appName=Cluster0" 2 | MONGO_URI="mongodb+srv://suunnyshekhar:Jwi0zz03E4CY85po@cluster-neww.o20pa0c.mongodb.net/Roomzy" 3 | 4 | JWT_SECRET=mysecurejwtsecret 5 | PORT=3000 6 | BASE_URL=http://localhost:3000 7 | EMAIL_USER=rajsachin805130@gmail.com 8 | EMAIL_PASS=kyiu ophp diog gbna 9 | CLOUDINARY_API_SECRET=jRyuMC6tGD0ls1FwzLqhbvbMMhY 10 | CLOUDINARY_API_KEY=122737239598829 11 | CLOUDINARY_CLOUD_NAME=dshhafwmb -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Body/Body.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Feed from "../feed/feed"; 3 | import SearchSection from "../Search_Section/Search_section"; 4 | import Navbar from "../NavBar/Navbar"; 5 | import "./Body.css"; 6 | import { Outlet } from "react-router-dom"; 7 | 8 | const Body = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Body; -------------------------------------------------------------------------------- /Backend/src/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authController = require("../controllers/authController"); 4 | // const {validateToken} =require("../middleware2/auth"); 5 | 6 | router.post("/signup", authController.signup); 7 | router.post("/verify-otp", authController.verifyOTP); 8 | router.post("/login", authController.login); 9 | router.post("/forgot-password", authController.forgotPassword); 10 | router.post("/reset-password", authController.resetPassword); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /Backend/src/config/logger-config.js: -------------------------------------------------------------------------------- 1 | const {createLogger,format, transports}=require("winston"); 2 | const {combine,timestamp,label,printf}=format; 3 | 4 | 5 | const customFormat=printf(({level,message,label,timestamp})=>{ 6 | return `${timestamp} :[${label}] :${level} :${message}`; 7 | }) 8 | 9 | const logger = createLogger({ 10 | format: combine( 11 | label({ label: 'right now!' }), 12 | timestamp({format:'YYYY-MM-dd HH:mm:ss'}), 13 | customFormat 14 | ), 15 | transports: [new transports.Console(), new transports.File({filename:'combined.log'})] 16 | }); 17 | module.exports =logger; -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/forgotPassword/ForgotPasswordModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './ForgotPasswordModal.css'; 3 | import ForgotPassword from './ForgotPassword'; 4 | 5 | const ForgotPasswordModal = ({ isOpen, onClose }) => { 6 | if (!isOpen) return null; 7 | 8 | return ( 9 |
10 |
e.stopPropagation()}> 11 | 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default ForgotPasswordModal; -------------------------------------------------------------------------------- /Backend/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cookieParser=require("cookie-parser"); 3 | const connectDB = require("./config/server-config"); 4 | const dotenv = require("dotenv"); 5 | const authRoutes = require("./routes"); 6 | const cors = require("cors"); 7 | 8 | dotenv.config(); 9 | connectDB(); 10 | const app = express(); 11 | app.use(cookieParser()); 12 | app.use(express.json()); 13 | app.use(cors({ 14 | origin: "http://localhost:5173", 15 | credentials: true 16 | })); 17 | app.use("/api", authRoutes); 18 | port=8000; 19 | app.listen(port, () => console.log(`Server running on port ${port}`)); 20 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/forgotPassword/ForgotPasswordModal.css: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background: rgba(0, 0, 0, 0.5); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | -webkit-backdrop-filter: blur(5px); /* Safari support */ 12 | backdrop-filter: blur(5px); /* Standard syntax */ 13 | } 14 | 15 | .forgot-password-modal { 16 | background: #F4EDD3; 17 | border-radius: 10px; 18 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); 19 | width: 90%; 20 | max-width: 400px; 21 | } -------------------------------------------------------------------------------- /Backend/src/middleware2/auth.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/Usermodel"); 2 | const jwt=require("jsonwebtoken"); 3 | 4 | const validateToken=async (req,res,next)=>{ 5 | try{ 6 | const {token}=req.cookies; 7 | const decodedData=jwt.verify(token,process.env.JWT_SECRET); 8 | const {id}=decodedData; 9 | const user= await User.findById(id); 10 | if(!user) throw new Error("Token Invalid !!"); 11 | req.user=user; 12 | next(); 13 | }catch(err){ 14 | res.status(401).json({error : "Unauthorized"+err.message}); 15 | } 16 | 17 | } 18 | 19 | module.exports= {validateToken}; -------------------------------------------------------------------------------- /Backend/src/models/Usermodel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | firstName: { type: String, required: true }, 6 | lastName: { type: String, required: true }, 7 | email: { type: String, required: true, unique: true }, 8 | password: { type: String, required: true }, 9 | isVerified: { type: Boolean, default: false }, 10 | otp: { type: String }, 11 | otpExpires: { type: Date }, 12 | ownedProperties: [{ type: mongoose.Schema.Types.ObjectId, ref: "roomModel" }] 13 | }, 14 | { 15 | timestamps: true, 16 | } 17 | ); 18 | 19 | module.exports = mongoose.model("User", userSchema); 20 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/App.css: -------------------------------------------------------------------------------- 1 | /* #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } */ 43 | -------------------------------------------------------------------------------- /Frontend/Roomzy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roomzy", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/vite": "^4.0.17", 14 | "axios": "^1.9.0", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-icons": "^5.5.0", 18 | "react-router-dom": "^7.6.2", 19 | "tailwindcss": "^4.0.17" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.17.0", 23 | "@types/react": "^18.3.18", 24 | "@types/react-dom": "^18.3.5", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "eslint": "^9.17.0", 27 | "eslint-plugin-react": "^7.37.2", 28 | "eslint-plugin-react-hooks": "^5.0.0", 29 | "eslint-plugin-react-refresh": "^0.4.16", 30 | "globals": "^15.14.0", 31 | "vite": "^6.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "dev": "nodemon src/index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "bcryptjs": "^3.0.0", 15 | "cloudinary": "^1.41.3", 16 | "cookie-parser": "^1.4.7", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.7", 19 | "express": "^4.21.2", 20 | "express-validator": "^7.2.1", 21 | "http-status-codes": "^2.3.0", 22 | "jsonwebtoken": "^9.0.2", 23 | "mongoose": "^8.10.1", 24 | "multer": "^1.4.5-lts.1", 25 | "multer-storage-cloudinary": "^4.0.0", 26 | "node-modules": "^0.0.1", 27 | "nodemailer": "^6.10.0", 28 | "winston": "^3.17.0" 29 | }, 30 | "devDependencies": { 31 | "@types/express": "^5.0.0", 32 | "@types/node": "^22.12.0", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.7.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Backend/src/routes/room-routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const rentalPropertyController = require("../controllers/room-controller"); 4 | const {validateToken} =require("../middleware2/auth"); 5 | const upload=require("../middleware2/cloudinaryStorage"); 6 | 7 | // Create a new room 8 | router.post("/create",validateToken,upload.array("photos", 5), rentalPropertyController.createRoom); 9 | 10 | // Search rooms with filters(like min ,price max price etc); 11 | router.get("/search", rentalPropertyController.getRoomsByFilters); 12 | 13 | // Get rooms by owner ID(all the rooms listed by the owner); 14 | router.get("/roomsByOwner",validateToken, rentalPropertyController.getRoomsByOwner); 15 | 16 | // Get room by ID 17 | router.get("/:roomId", rentalPropertyController.getRoomById); 18 | 19 | 20 | // Update room details 21 | router.patch("/:roomId", rentalPropertyController.updateRoom); 22 | 23 | // Delete a room 24 | router.delete("/:roomId", rentalPropertyController.deleteRoom); 25 | 26 | router.post("/roomreview/:roomId",rentalPropertyController.reviewRoom); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /Frontend/Roomzy/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import Body from "./components/Body/Body"; 3 | import LoginScreen from "./components/loginScreen/LoginScreen"; 4 | import SignupScreen from "./components/SignupScreen/SignupScreen"; 5 | import OtpSender from "./components/OtpSender/OTPSender"; // Make sure this file exists 6 | import Listing from "./components/Listing/listing2"; 7 | import Review from "./components/reviewScreen/Review"; 8 | import Notification from "./components/notificationCenter/NotificationCenter"; 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 | Base Page} /> 15 | } /> 16 | } /> 17 | } /> 18 | } /> {/* ✅ Added OTP route */} 19 | } /> 20 | } /> 21 | } /> 22 | 23 | 24 | ); 25 | } 26 | 27 | export default App; 28 | 29 | -------------------------------------------------------------------------------- /Frontend/Roomzy/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/forgotPassword/ForgotPassword.css: -------------------------------------------------------------------------------- 1 | .forgot-password-container { 2 | max-width: 400px; 3 | padding: 20px; 4 | background: #FFFDEC; 5 | border-radius: 10px; 6 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 7 | text-align: center; 8 | } 9 | 10 | h2 { 11 | color: #333; 12 | margin-bottom: 10px; 13 | } 14 | 15 | p { 16 | color: #666; 17 | margin-bottom: 20px; 18 | } 19 | 20 | input[type="email"] { 21 | width: 90%; 22 | padding: 10px; 23 | margin: 10px 0; 24 | border: 1px solid #ccc; 25 | border-radius: 5px; 26 | font-size: 16px; 27 | transition: border-color 0.3s; 28 | color: #333; 29 | } 30 | 31 | input[type="email"]:focus { 32 | border-color: #007bff; 33 | outline: none; 34 | } 35 | 36 | button { 37 | width: 95%; 38 | padding: 10px; 39 | background-color: #007bff; 40 | color: white; 41 | border: none; 42 | border-radius: 5px; 43 | font-size: 16px; 44 | cursor: pointer; 45 | transition: background-color 0.3s; 46 | } 47 | 48 | button:hover { 49 | background-color: #0056b3; 50 | } 51 | 52 | .success-message { 53 | margin-top: 20px; 54 | color: green; 55 | font-weight: bold; 56 | } 57 | 58 | .close-modal { 59 | margin-top: 10px; 60 | background-color: transparent; 61 | border: none; 62 | color: #007bff; 63 | cursor: pointer; 64 | } 65 | 66 | .close-modal:hover { 67 | text-decoration: underline; 68 | background-color: #FFFDEC; 69 | } -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Listing/listing2.css: -------------------------------------------------------------------------------- 1 | .listing-form { 2 | max-width: 500px; 3 | margin: 40px auto; 4 | padding: 24px 32px; 5 | border-radius: 12px; 6 | box-shadow: 0 2px 16px rgba(0,0,0,0.11); 7 | background: #a7f0f0; 8 | font-family: 'Segoe UI', Arial, sans-serif; 9 | } 10 | 11 | .listing-form h2 { 12 | text-align: center; 13 | margin-bottom: 16px; 14 | } 15 | 16 | .form-row { 17 | margin-bottom: 18px; 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .form-row label { 23 | font-weight: 500; 24 | margin-bottom: 4px; 25 | } 26 | 27 | .form-row input, 28 | .form-row select, 29 | .form-row textarea { 30 | padding: 8px 10px; 31 | border: 1px solid #ccc; 32 | border-radius: 6px; 33 | font-size: 1em; 34 | } 35 | 36 | .form-row textarea { 37 | resize: vertical; 38 | } 39 | 40 | .error { 41 | color: #c00; 42 | font-size: 0.89em; 43 | margin-top: 3px; 44 | } 45 | 46 | .photo-preview { 47 | display: block; 48 | margin-top: 7px; 49 | max-width: 120px; 50 | border-radius: 8px; 51 | box-shadow: 0 1px 5px rgba(0,0,0,0.08); 52 | } 53 | 54 | button[type="submit"] { 55 | display: block; 56 | width: 100%; 57 | padding: 12px; 58 | border: none; 59 | border-radius: 6px; 60 | background: #03031f; 61 | color: #f1f7f8; 62 | font-weight: bold; 63 | font-size: 1.1em; 64 | margin-top: 15px; 65 | cursor: pointer; 66 | transition: 0.2s background; 67 | } 68 | 69 | button[type="submit"]:hover { 70 | background: #070716; 71 | } 72 | 73 | /* export default listing; */ -------------------------------------------------------------------------------- /Backend/src/utils/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | 4 | const dotenv = require("dotenv"); 5 | dotenv.config(); 6 | 7 | const sendEmail = async (to, subject, text) => { 8 | try { 9 | const transporter = nodemailer.createTransport({ 10 | service: "gmail", 11 | auth: { 12 | user: process.env.EMAIL_USER, 13 | pass: process.env.EMAIL_PASS, 14 | }, 15 | }); 16 | 17 | const mailOptions = { from: process.env.EMAIL_USER, to, subject, text }; 18 | await transporter.sendMail(mailOptions); 19 | } catch (error) { 20 | console.error("Error sending email:", error); 21 | throw new Error("Failed to send email"); 22 | } 23 | }; 24 | 25 | module.exports = { sendEmail }; 26 | 27 | // const sendEmail = async ( 28 | // senderEmail: string, 29 | // senderPassword: string, 30 | // recivers: string[], 31 | // message: string 32 | // ): Promise => { 33 | // const transporter = nodemailer.createTransport({ 34 | // service: 'gmail', 35 | // auth: { 36 | // user: senderEmail, 37 | // pass: senderPassword, 38 | // }, 39 | // }); 40 | 41 | // const mailOptions = { 42 | // from: senderEmail, 43 | // to: recivers.join(','), 44 | // subject: 'Elective Notification', 45 | // text: `${message}\n\nWebsite : https://elective.vercel.app`, 46 | // }; 47 | 48 | // const emailResponse = await transporter.sendMail(mailOptions); 49 | // console.log('Email sent:', emailResponse); 50 | // }; -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/OtpSender/OTPSender.css: -------------------------------------------------------------------------------- 1 | /* Fullscreen overlay */ 2 | .otp-modal { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: rgba(0, 0, 0, 0.4); 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | z-index: 1000; 13 | } 14 | 15 | /* OTP card */ 16 | .otp-card { 17 | background: #ffffff; 18 | padding: 35px 30px; 19 | border-radius: 10px; 20 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 21 | text-align: center; 22 | width: 320px; 23 | } 24 | 25 | /* Headings and messages */ 26 | .otp-msg { 27 | font-size: 16px; 28 | color: #222; 29 | margin-bottom: 10px; 30 | } 31 | 32 | /* OTP input layout */ 33 | .otp-box-container { 34 | display: flex; 35 | justify-content: center; 36 | gap: 15px; 37 | margin: 20px 0; 38 | } 39 | 40 | /* Each OTP input box */ 41 | .otp-box { 42 | width: 50px; 43 | height: 50px; 44 | font-size: 24px; 45 | font-weight: bold; 46 | text-align: center; 47 | border: 2px solid #ccc; 48 | border-radius: 8px; 49 | background-color: #f9f9f9; 50 | color: #222; 51 | transition: border-color 0.2s, box-shadow 0.2s; 52 | } 53 | 54 | .otp-box:focus { 55 | border-color: #10a37f; 56 | box-shadow: 0 0 6px #10a37f; 57 | outline: none; 58 | } 59 | 60 | /* Verify button */ 61 | .otp-button { 62 | width: 100%; 63 | padding: 12px; 64 | background-color: #10a37f; 65 | color: #fff; 66 | border: none; 67 | font-size: 16px; 68 | border-radius: 6px; 69 | cursor: pointer; 70 | transition: background-color 0.3s; 71 | } 72 | 73 | .otp-button:hover { 74 | background-color: #0c8564; 75 | } 76 | -------------------------------------------------------------------------------- /Backend/src/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const authService = require("../services/authService"); 2 | 3 | const signup = async (req, res) => { 4 | try { 5 | const response = await authService.signup(req.body); 6 | res.cookie("token",response?.token,{httpOnly:true, maxAge:6000000}); // changed 7 | res.status(201).json(response); 8 | } catch (error) { 9 | res.status(400).json({ error: error.message }); 10 | } 11 | }; 12 | 13 | const verifyOTP = async (req, res) => { 14 | try { 15 | const { email, otp } = req.body; 16 | const response = await authService.verifyOTP(email, otp); 17 | res.status(200).json(response); 18 | } catch (error) { 19 | res.status(400).json({ error: error.message }); 20 | } 21 | }; 22 | 23 | const login = async (req, res) => { 24 | try { 25 | const { email, password } = req.body; 26 | const response = await authService.login(email, password); 27 | res.cookie("token",response?.token,{httpOnly:true, maxAge:6000000}); 28 | res.status(200).json(response); 29 | } catch (error) { 30 | res.status(401).json({ error: error.message }); 31 | } 32 | }; 33 | 34 | const forgotPassword = async (req, res) => { 35 | try { 36 | const response = await authService.forgotPassword(req.body.email); 37 | res.status(200).json(response); 38 | } catch (error) { 39 | res.status(400).json({ error: error.message }); 40 | } 41 | }; 42 | 43 | const resetPassword = async (req, res) => { 44 | try { 45 | const { email, otp, newPassword } = req.body; 46 | const response = await authService.resetPassword(email, otp, newPassword); 47 | res.status(200).json(response); 48 | } catch (error) { 49 | res.status(400).json({ error: error.message }); 50 | } 51 | }; 52 | 53 | module.exports = { signup, verifyOTP, login, forgotPassword, resetPassword }; 54 | -------------------------------------------------------------------------------- /Backend/src/middlewares/validator.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require("http-status-codes"); 2 | const validator = require("validator"); 3 | const Room = require("../models/roommodel"); 4 | 5 | function validateSignup(req, res, next) { 6 | if (!req.body.firstName) { 7 | return res.status(StatusCodes.BAD_REQUEST).json({ 8 | message: "Signup failed", 9 | error: { message: "firstName is required" }, 10 | }); 11 | } 12 | if (!req.body.lastName) { 13 | return res.status(StatusCodes.BAD_REQUEST).json({ 14 | message: "Signup failed", 15 | error: { message: "lastName is required" }, 16 | }); 17 | } 18 | 19 | if (!req.body.email || !validator.isEmail(req.body.email)) { 20 | return res.status(StatusCodes.BAD_REQUEST).json({ 21 | message: "Signup failed", 22 | error: { message: "Invalid email format" }, 23 | }); 24 | } 25 | 26 | if (!req.body.password || req.body.password.length < 6) { 27 | return res.status(StatusCodes.BAD_REQUEST).json({ 28 | message: "Signup failed", 29 | error: { message: "Password must be at least 6 characters long" }, 30 | }); 31 | } 32 | 33 | next(); 34 | } 35 | 36 | const isOwner = (req, res, next) => { 37 | 38 | const { roomId } = req.params; 39 | const userId = req.body.userId; // Assuming you have user info in req.user 40 | 41 | // RentalProperty.findById(roomId) 42 | Room.findById(roomId) 43 | .then(property => { 44 | if (property.owner.toString() === userId.toString()) { 45 | return res.status(403).json({ message: "Owners cannot review their own property." }); 46 | } 47 | next(); 48 | }) 49 | .catch(err => res.status(500).json({ message: "Error finding property." })); 50 | }; 51 | 52 | module.exports = { validateSignup,isOwner }; 53 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/forgotPassword/ForgotPassword.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './ForgotPassword.css'; 3 | import OtpSender from '../OtpSender/OTPSender'; // Import OtpSender component 4 | 5 | const ForgotPassword = ({ onClose }) => { 6 | const [email, setEmail] = useState(''); 7 | const [message, setMessage] = useState(''); 8 | const [showOtp, setShowOtp] = useState(false); // State to show OTP input 9 | 10 | const handleEmailChange = (e) => { 11 | setEmail(e.target.value); 12 | }; 13 | 14 | const handleSubmit = (e) => { 15 | e.preventDefault(); 16 | console.log('Requesting OTP for:', email); 17 | setMessage(`An OTP has been sent to ${email}.`); 18 | setShowOtp(true); // Show OTP input 19 | }; 20 | 21 | return ( 22 |
23 | {!showOtp ? ( // Show this part if OTP is not requested yet 24 | <> 25 |

Forgot Password

26 |

Enter your email to receive an OTP.

27 |
28 | 35 | 36 |
37 | {message &&

{message}

} 38 | 39 | 40 | ) : ( 41 | setShowOtp(false)} />// Show OTP input when button is clicked 42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default ForgotPassword; 48 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/SignupScreen/SignupScreen.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | font-family: 'Arial', sans-serif; 6 | } 7 | 8 | .signup-container { 9 | background-image: url('../../assets/login1.jpg'); 10 | background-size: cover; 11 | background-position: center; 12 | height: 100vh; 13 | width: 100vw; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | font-size: 0.9rem; 18 | } 19 | 20 | .modal { 21 | background: rgba(255, 255, 255, 0.2); 22 | -webkit-backdrop-filter: blur(15px); 23 | backdrop-filter: blur(15px); 24 | border-radius: 16px; 25 | padding: 20px; 26 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); 27 | text-align: center; 28 | width: 500px; 29 | border: 1px solid rgba(255, 255, 255, 0.4); 30 | color: white; 31 | } 32 | 33 | .modal-logo { 34 | width: 20vh; 35 | height: 20vh; 36 | } 37 | 38 | h2 { 39 | color: #fff; 40 | margin-bottom: 10px; 41 | } 42 | 43 | p { 44 | color: white; 45 | font-size: medium; 46 | margin-bottom: 10px; 47 | } 48 | 49 | .error { 50 | color: #ff4d4f; 51 | margin: 10px 0; 52 | font-size: 0.85rem; 53 | } 54 | 55 | .form-container { 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | width: 100%; 60 | } 61 | 62 | input { 63 | width: 90%; 64 | padding: 10px; 65 | margin: 10px 0; 66 | border: none; 67 | border-radius: 5px; 68 | outline: none; 69 | background: rgba(255, 255, 255, 0.3); 70 | color: #fff; 71 | font-size: 1rem; 72 | } 73 | 74 | input::placeholder { 75 | color: #ccc; 76 | } 77 | 78 | button { 79 | width: 40%; 80 | padding: 10px; 81 | margin: 15px 0; 82 | border: none; 83 | border-radius: 5px; 84 | background-color: #007bff; 85 | color: white; 86 | cursor: pointer; 87 | transition: background-color 0.3s; 88 | } 89 | 90 | button:hover { 91 | background-color: #0056b3; 92 | } 93 | 94 | button:disabled { 95 | background-color: #999; 96 | cursor: not-allowed; 97 | } 98 | -------------------------------------------------------------------------------- /Backend/src/services/room-service.js: -------------------------------------------------------------------------------- 1 | const rentalPropertyRepo = require("../repositories/room-repository"); 2 | 3 | // ✅ 1️⃣ Create a new room 4 | const createRoomService = async (roomData) => { 5 | try { 6 | return await rentalPropertyRepo.createRoom(roomData); 7 | } catch (error) { 8 | throw new Error(error.message); 9 | } 10 | }; 11 | 12 | // ✅ 2️⃣ Get all rooms 13 | const getAllRoomsService = async () => { 14 | try { 15 | return await rentalPropertyRepo.getAllRooms(); 16 | } catch (error) { 17 | throw new Error(error.message); 18 | } 19 | }; 20 | 21 | // ✅ 3️⃣ Get rooms by Owner ID 22 | const getRoomsByOwnerService = async (ownerId) => { 23 | try { 24 | return await rentalPropertyRepo.getRoomsByOwner(ownerId); 25 | } catch (error) { 26 | throw new Error(error.message); 27 | } 28 | }; 29 | 30 | // ✅ 4️⃣ Get room by Room ID 31 | const getRoomByIdService = async (roomId) => { 32 | try { 33 | return await rentalPropertyRepo.getRoomById(roomId); 34 | } catch (error) { 35 | throw new Error(error.message); 36 | } 37 | }; 38 | 39 | // ✅ 5️⃣ Update room by Room ID 40 | const updateRoomService = async (roomId, updatedData) => { 41 | try { 42 | return await rentalPropertyRepo.updateRoom(roomId, updatedData); 43 | } catch (error) { 44 | throw new Error(error.message); 45 | } 46 | }; 47 | 48 | // ✅ 6️⃣ Delete room by Room ID 49 | const deleteRoomService = async (roomId, ownerId) => { 50 | try { 51 | return await rentalPropertyRepo.deleteRoom(roomId, ownerId); 52 | } catch (error) { 53 | throw new Error(error.message); 54 | } 55 | }; 56 | 57 | // ✅ 7️⃣ Get rooms by filters (Price, Location, etc.) 58 | const getRoomsByFiltersService = async (filters) => { 59 | try { 60 | return await rentalPropertyRepo.getRoomsByFilters(filters); 61 | } catch (error) { 62 | throw new Error(error.message); 63 | } 64 | }; 65 | 66 | const postRoomReview=async(req,res)=>{ 67 | try{ 68 | return await rentalPropertyRepo.postRoomReview(req,res); 69 | 70 | }catch(err){ 71 | console.log("err2"); 72 | throw new Error(err.mesage); 73 | } 74 | } 75 | 76 | module.exports = { 77 | createRoomService, 78 | getAllRoomsService, 79 | getRoomsByOwnerService, 80 | getRoomByIdService, 81 | updateRoomService, 82 | deleteRoomService, 83 | getRoomsByFiltersService, 84 | postRoomReview 85 | }; 86 | -------------------------------------------------------------------------------- /Backend/src/repositories/user-repository.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/Usermodel"); 2 | 3 | /** 4 | * Find a user by their email address. 5 | * 6 | * @param {string} email - The email of the user to find. 7 | * @returns {Promise} - Returns the user object if found, otherwise null. 8 | */ 9 | const findByEmail = async (email) => { 10 | try { 11 | return await User.findOne({ email }); 12 | } catch (error) { 13 | console.error("Error finding user by email:", error); 14 | throw new Error("Database query failed"); 15 | } 16 | }; 17 | 18 | /** 19 | * Find a user by their email and OTP. 20 | * 21 | * @param {string} email - The email of the user. 22 | * @param {string} otp - The one-time password (OTP) to verify. 23 | * @returns {Promise} - Returns the user object if OTP is valid and not expired, otherwise null. 24 | */ 25 | const findByOTP = async (email, otp) => { 26 | try { 27 | return await User.findOne({ 28 | email, 29 | otp, 30 | otpExpires: { $gt: Date.now() } // Ensures OTP is not expired 31 | }); 32 | } catch (error) { 33 | console.error("Error finding user by OTP:", error); 34 | throw new Error("Database query failed"); 35 | } 36 | }; 37 | 38 | /** 39 | * Create a new user in the database. 40 | * 41 | * @param {Object} userData - The user data containing firstName, lastName, email, password, etc. 42 | * @returns {Promise} - Returns the newly created user object. 43 | */ 44 | const createUser = async (userData) => { 45 | try { 46 | const newUser = new User(userData); 47 | return await newUser.save(); 48 | } catch (error) { 49 | console.error("Error creating user:", error); 50 | throw new Error("Failed to create user"); 51 | } 52 | }; 53 | 54 | /** 55 | * Update a user's details by their email. 56 | * 57 | * @param {string} email - The email of the user to update. 58 | * @param {Object} updateData - The data to update in the user's record. 59 | * @returns {Promise} - Returns the updated user object if found, otherwise null. 60 | */ 61 | const updateUser = async (email, updateData) => { 62 | try { 63 | return await User.findOneAndUpdate( 64 | { email }, 65 | updateData, 66 | { new: true } // Returns the updated document 67 | ); 68 | } catch (error) { 69 | console.error("Error updating user:", error); 70 | throw new Error("Failed to update user"); 71 | } 72 | }; 73 | 74 | module.exports = { findByEmail, findByOTP, createUser, updateUser }; 75 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Search_Section/Search_section.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './Search_section.css'; 3 | 4 | const sections = [ 5 | 6 | { id: 1, name: 'Rent', color: '#28A745' }, 7 | { id: 2, name: 'Furnished', color: '#17A2B8' }, 8 | { id: 3, name: 'ListingType', color: '#DC3545' }, 9 | { id: 4, name: 'OccupancyType', color: '#6F42C1' }, 10 | ]; 11 | 12 | const cities = ['Mumbai', 'Delhi', 'Bangalore', 'Chennai', 'Kolkata', 'Pune', 'Hyderabad']; 13 | 14 | const SearchSection = () => { 15 | const [activeSection, setActiveSection] = useState(null); 16 | const [searchText, setSearchText] = useState(''); 17 | const [filteredCities, setFilteredCities] = useState([]); 18 | 19 | const handleSectionClick = (section) => { 20 | setActiveSection(section); 21 | }; 22 | 23 | const handleSearchChange = (event) => { 24 | const value = event.target.value; 25 | setSearchText(value); 26 | setFilteredCities(cities.filter(city => city.toLowerCase().includes(value.toLowerCase()))); 27 | }; 28 | 29 | // Function to select a city without triggering search or keeping unnecessary columns 30 | const handleCitySelect = (city) => { 31 | setSearchText(city); 32 | setFilteredCities([]); // Hide suggestions after selection 33 | }; 34 | 35 | return ( 36 |
37 |

Find Your Perfect Property

38 |
39 | 46 | 47 |
48 | {filteredCities.length > 0 && ( 49 |
    50 | {filteredCities.map((city, index) => ( 51 |
  • handleCitySelect(city)}>{city}
  • 52 | ))} 53 |
54 | )} 55 |
56 | {sections.map((section) => ( 57 |
handleSectionClick(section)} 61 | style={{ backgroundColor: activeSection?.id === section.id ? section.color : 'white' }} 62 | > 63 | {section.name} 64 |
65 | ))} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default SearchSection; 72 | -------------------------------------------------------------------------------- /Backend/src/models/roommodel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ReviewSchema = new mongoose.Schema({ // changed 4 | user: { type: mongoose.Schema.Types.ObjectId, ref: "User ", required: true }, 5 | rating: { type: Number, min: 1, max: 5}, // Rating between 1 and 5 6 | text: { type: String, maxlength: 1000 }, // Review text 7 | createdAt: { type: Date, default: Date.now } 8 | }); 9 | 10 | const RentalPropertySchema = new mongoose.Schema({ 11 | // Reference to the User (Owner of the Property) 12 | owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, 13 | 14 | // Listing Type (Only Rent or Lease) 15 | listingType: { 16 | type: String, 17 | enum: ["Rent", "Lease"], 18 | required: true 19 | }, 20 | 21 | // Full House or Sharing 22 | occupancyType: { 23 | type: String, 24 | enum: ["Full House", "On Sharing Basis"], 25 | required: true 26 | }, 27 | 28 | // Society/Project Details 29 | societyName: { type: String }, 30 | totalFlatsInSociety: { type: Number }, 31 | 32 | // Location 33 | city: { type: String, required: true }, 34 | locality: { type: String, required: true }, 35 | 36 | // Property Features 37 | bedrooms: { type: Number, required: true }, 38 | balconies: { type: Number, default: 0 }, 39 | floorNo: { type: String }, // Can be "Lower Basement", "Ground", "1", "2", etc. 40 | totalFloors: { type: Number, required: true }, 41 | furnishedStatus: { 42 | type: String, 43 | enum: ["Furnished", "Unfurnished", "Semi-Furnished"], 44 | required: true 45 | }, 46 | bathrooms: { type: Number, required: true }, 47 | // Availability & Transaction Type 48 | availableFrom: { type: Date, required: true }, 49 | ageOfConstruction: { 50 | type: String, 51 | enum: ["New", "0-5 years", "5-10 years", "10+ years"] 52 | }, 53 | 54 | // Rent/Lease Details 55 | monthlyRent: { type: Number, required: true }, 56 | isRentNegotiable: { type: Boolean, default: false }, 57 | securityDeposit: { type: Number }, 58 | maintenanceCharges: { type: Number }, 59 | 60 | // Description 61 | description: { type: String, maxlength: 1000 }, // Optional property description 62 | 63 | // Photos 64 | photos: [{ 65 | type: String, 66 | validate: { 67 | validator: function(url) { 68 | return /^https?:\/\/.+\.(jpg|jpeg|png|gif)$/i.test(url); 69 | }, 70 | message: "Invalid image URL format!" 71 | } 72 | }], 73 | 74 | reviews: [ReviewSchema], // changed 75 | 76 | createdAt: { type: Date, default: Date.now } 77 | }); 78 | 79 | module.exports = mongoose.model("roomModel", RentalPropertySchema); 80 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/OtpSender/OTPSender.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import "./OTPSender.css"; 3 | import { useNavigate } from "react-router-dom"; 4 | import axios from "axios"; 5 | import { BASE_URL } from "../../utils/constants"; // Ensure this points correctly 6 | 7 | const OtpSender = () => { 8 | const [otp, setOtp] = useState(new Array(4).fill("")); 9 | const inputRefs = useRef([]); 10 | const navigate = useNavigate(); 11 | 12 | const handleChange = (index, e) => { 13 | const value = e.target.value.replace(/\D/g, ""); 14 | if (value.length > 1) return; 15 | 16 | const newOtp = [...otp]; 17 | newOtp[index] = value; 18 | setOtp(newOtp); 19 | 20 | if (value && index < 3) { 21 | inputRefs.current[index + 1]?.focus(); 22 | } 23 | }; 24 | 25 | const handleKeyDown = (index, e) => { 26 | if (e.key === "Backspace") { 27 | const newOtp = [...otp]; 28 | newOtp[index] = ""; 29 | setOtp(newOtp); 30 | 31 | if (index > 0 && !otp[index]) { 32 | inputRefs.current[index - 1]?.focus(); 33 | } 34 | } 35 | }; 36 | 37 | const handleVerify = async () => { 38 | const enteredOtp = otp.join(""); 39 | 40 | if (enteredOtp.length !== 4) { 41 | alert("Please enter a valid 4-digit OTP"); 42 | return; 43 | } 44 | 45 | try { 46 | const response = await axios.post(`${BASE_URL}/api/auth/verify-otp`, { 47 | otp: enteredOtp, 48 | }); 49 | 50 | alert("OTP Verified Successfully!"); 51 | navigate("/body"); 52 | } catch (error) { 53 | console.error(error); 54 | alert(error.response?.data?.message || "OTP verification failed"); 55 | } 56 | }; 57 | 58 | return ( 59 |
60 |
61 |

Enter OTP

62 |

We've sent an OTP to your email.

63 |
64 | {otp.map((digit, index) => ( 65 | handleChange(index, e)} 71 | onKeyDown={(e) => handleKeyDown(index, e)} 72 | ref={(el) => (inputRefs.current[index] = el)} 73 | className="otp-box" 74 | autoFocus={index === 0} 75 | /> 76 | ))} 77 |
78 | 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default OtpSender; 87 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/reviewScreen/Review.css: -------------------------------------------------------------------------------- 1 | .review-container { 2 | max-width: 1100px; 3 | margin: auto; 4 | padding: 40px 20px; 5 | font-family: Arial, sans-serif; 6 | } 7 | 8 | .review-top { 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | gap: 40px; 13 | } 14 | 15 | .average-rating, .submit-review { 16 | flex: 1; 17 | min-width: 320px; 18 | background: #f9f9f9; 19 | border-radius: 10px; 20 | padding: 20px; 21 | } 22 | 23 | .average-rating h3, .submit-review h3, .feedback-section h3 { 24 | margin-bottom: 16px; 25 | } 26 | 27 | .score { 28 | font-size: 24px; 29 | font-weight: bold; 30 | } 31 | 32 | .score .stars { 33 | font-size: 20px; 34 | color: #f5c518; 35 | margin-left: 10px; 36 | } 37 | 38 | .bars .bar-row { 39 | display: flex; 40 | align-items: center; 41 | margin: 8px 0; 42 | } 43 | 44 | .bars .bar-row span { 45 | width: 20px; 46 | } 47 | 48 | .bar { 49 | flex: 1; 50 | height: 8px; 51 | background: #ddd; 52 | margin-left: 10px; 53 | border-radius: 4px; 54 | } 55 | 56 | .bar-fill { 57 | height: 100%; 58 | background: #006644; 59 | border-radius: 4px; 60 | } 61 | 62 | .star-5 { width: 90%; } 63 | .star-4 { width: 60%; } 64 | .star-3 { width: 40%; } 65 | .star-2 { width: 30%; } 66 | .star-1 { width: 0%; } 67 | 68 | .submit-review input, 69 | .submit-review textarea { 70 | width: 100%; 71 | padding: 10px; 72 | margin: 10px 0; 73 | border: 1px solid #ccc; 74 | border-radius: 6px; 75 | font-size: 14px; 76 | } 77 | 78 | .rating-input span { 79 | font-size: 24px; 80 | cursor: pointer; 81 | color: #ccc; 82 | } 83 | 84 | .rating-input .filled { 85 | color: #f5c518; 86 | } 87 | 88 | .submit-review button { 89 | background-color: #006644; 90 | color: white; 91 | border: none; 92 | padding: 10px 18px; 93 | border-radius: 5px; 94 | font-size: 14px; 95 | cursor: pointer; 96 | margin-top: 10px; 97 | } 98 | 99 | .feedback-section { 100 | margin-top: 40px; 101 | } 102 | 103 | .feedback-card { 104 | background: #f1f1f1; 105 | padding: 16px; 106 | border-radius: 10px; 107 | margin-bottom: 20px; 108 | } 109 | 110 | .name-rating { 111 | display: flex; 112 | justify-content: space-between; 113 | align-items: center; 114 | } 115 | 116 | .name-rating .stars { 117 | color: #f5c518; 118 | font-size: 16px; 119 | } 120 | 121 | .feedback-card p { 122 | margin: 10px 0; 123 | color: #333; 124 | } 125 | 126 | .date { 127 | font-size: 12px; 128 | color: #888; 129 | } -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/SignupScreen/SignupScreen.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import axios from "axios"; 3 | import "./SignupScreen.css"; 4 | import SignupImage from "../../assets/account.png"; 5 | import { BASE_URL } from "../../utils/constants"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const Signup = () => { 9 | const navigate = useNavigate(); 10 | const [firstName, setFirstName] = useState(""); 11 | const [lastName, setLastName] = useState(""); 12 | const [email, setEmail] = useState(""); 13 | const [password, setPassword] = useState(""); 14 | const [error, setError] = useState(""); 15 | const [loading, setLoading] = useState(false); 16 | 17 | const handleSignup = async (e) => { 18 | e.preventDefault(); 19 | setError(""); 20 | setLoading(true); 21 | 22 | try { 23 | const response = await axios.post(`${BASE_URL}/api/auth/signup`, { 24 | firstName, 25 | lastName, 26 | email, 27 | password, 28 | }); 29 | 30 | // On successful signup, redirect to OTP page 31 | navigate("/otp"); 32 | } catch (err) { 33 | console.log(err); 34 | const message = err.response?.data?.error || "Signup failed"; 35 | setError(message); 36 | } finally { 37 | setLoading(false); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 |
44 | Logo 45 |

Sign Up

46 |

Welcome! Please enter your details to sign up

47 | 48 | {error &&

{error}

} 49 | 50 |
51 | setFirstName(e.target.value)} 56 | required 57 | /> 58 | setLastName(e.target.value)} 63 | required 64 | /> 65 | setEmail(e.target.value)} 70 | required 71 | /> 72 | setPassword(e.target.value)} 77 | required 78 | /> 79 | 80 | 83 |
84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Signup; 90 | -------------------------------------------------------------------------------- /Backend/src/services/authService.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | const jwt = require("jsonwebtoken"); 3 | const UserRepository = require("../repositories/user-repository"); 4 | const { sendEmail } = require("../utils/sendEmail"); 5 | 6 | const generateToken = (user) => { 7 | return jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { 8 | expiresIn: "7d", 9 | }); 10 | }; 11 | const generateOTP = () => Math.floor(1000 + Math.random() * 9000).toString(); 12 | const signup = async (userData) => { 13 | const existingUser = await UserRepository.findByEmail(userData.email); 14 | if (existingUser) throw new Error("User already exists"); 15 | 16 | userData.password = await bcrypt.hash(userData.password, 10); 17 | const otp = generateOTP(); 18 | 19 | const newUser = await UserRepository.createUser({ ...userData, otp, otpExpires: Date.now() + 5 * 60 * 1000 }); 20 | 21 | await sendEmail(newUser.email, "Verify Your Email", `Your OTP: ${otp}`); 22 | 23 | return { message: "User registered successfully. Please verify your email with the OTP sent to your email." }; 24 | }; 25 | 26 | // Verify OTP 27 | const verifyOTP = async (email, otp) => { 28 | const user = await UserRepository.findByOTP(email, otp); 29 | if (!user) throw new Error("Invalid or expired OTP"); 30 | 31 | await UserRepository.updateUser(email, { isVerified: true, otp: undefined, otpExpires: undefined }); 32 | 33 | return { message: "Email verified successfully" }; 34 | }; 35 | 36 | // Login 37 | const login = async (email, password) => { 38 | const user = await UserRepository.findByEmail(email); 39 | if (!user || !(await bcrypt.compare(password, user.password))) { 40 | throw new Error("Invalid credentials"); 41 | } 42 | 43 | if (!user.isVerified) throw new Error("Please verify your email first"); 44 | 45 | return { token: generateToken(user) }; 46 | }; 47 | 48 | // Forgot Password (send OTP) 49 | const forgotPassword = async (email) => { 50 | const user = await UserRepository.findByEmail(email); 51 | if (!user) throw new Error("User not found"); 52 | 53 | const otp = generateOTP(); 54 | await UserRepository.updateUser(email, { otp, otpExpires: Date.now() + 5 * 60 * 1000 }); 55 | 56 | await sendEmail(user.email, "Reset Password OTP", `Your OTP: ${otp}`); 57 | 58 | return { message: "Password reset OTP sent to your email" }; 59 | }; 60 | 61 | // Reset Password (using OTP) 62 | const resetPassword = async (email, otp, newPassword) => { 63 | const user = await UserRepository.findByOTP(email, otp); 64 | if (!user) throw new Error("Invalid or expired OTP"); 65 | 66 | user.password = await bcrypt.hash(newPassword, 10); 67 | user.otp = undefined; 68 | user.otpExpires = undefined; 69 | await user.save(); 70 | 71 | return { message: "Password reset successful" }; 72 | }; 73 | 74 | module.exports = { signup, verifyOTP, login, forgotPassword, resetPassword }; 75 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/loginScreen/LoginScreen.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | font-family: 'Arial', sans-serif; 6 | } 7 | 8 | .login-container { 9 | background-image: url('../../assets/login1.jpg'); 10 | background-size: cover; 11 | background-position: center; 12 | height: 100vh; 13 | width: 100vw; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .modal { 20 | background: rgba(255, 255, 255, 0.2); 21 | -webkit-backdrop-filter: blur(15px); 22 | backdrop-filter: blur(15px); 23 | border-radius: 16px; 24 | padding: 20px; 25 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); 26 | text-align: center; 27 | align-items: center; 28 | width: 300px; 29 | border: 1px solid rgba(255, 255, 255, 0.4); 30 | } 31 | 32 | .modal-logo { 33 | width: 20vh; 34 | height: 20vh; 35 | } 36 | 37 | .form-container { 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | } 42 | 43 | h2 { 44 | color: #fff; 45 | margin-bottom: 10px; 46 | } 47 | 48 | p { 49 | color:gray; 50 | font-size: small; 51 | margin-bottom: 10px; 52 | 53 | } 54 | 55 | input { 56 | width: 88%; 57 | padding: 10px; 58 | margin: 10px 0; 59 | border: none; 60 | border-radius: 5px; 61 | outline: none; 62 | background: rgba(255, 255, 255, 0.3); 63 | color: #fff; 64 | } 65 | 66 | input::placeholder { 67 | color: #ccc; 68 | } 69 | 70 | .checkbox-container { 71 | display: flex; 72 | justify-content: space-between; 73 | align-items: center; 74 | width: 100%; 75 | margin: 8px 0; 76 | 77 | } 78 | 79 | .forgot-password { 80 | color: #007bff; 81 | text-decoration: none; 82 | font-size: 0.7rem; 83 | } 84 | 85 | .checkbox-container label { 86 | display:inline-flex; 87 | align-items: center; 88 | font-size: 0.7rem; 89 | } 90 | 91 | .checkbox-container input { 92 | display: inline-block; 93 | height: 15px; 94 | width:20px; 95 | margin-right: 5px; 96 | } 97 | 98 | .forgot-password:hover { 99 | text-decoration: underline; 100 | } 101 | 102 | button { 103 | width: 95%; 104 | padding: 10px; 105 | margin: 10px 0; 106 | border: none; 107 | border-radius: 5px; 108 | background-color: #007bff; 109 | color: white; 110 | cursor: pointer; 111 | transition: background-color 0.3s; 112 | } 113 | 114 | button:hover { 115 | background-color: #0056b3; 116 | } 117 | 118 | .signup-container { 119 | margin-top: 10px; 120 | font-size: 0.8rem; 121 | color: #fff; 122 | } 123 | 124 | .signup-link { 125 | color: #007bff; 126 | text-decoration: none; 127 | } 128 | 129 | .signup-link:hover { 130 | text-decoration: underline; 131 | } -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/feed/feed.css: -------------------------------------------------------------------------------- 1 | .feed-slider-container { 2 | padding: 20px; 3 | } 4 | 5 | .card-slider { 6 | display: flex; 7 | overflow-x: auto; 8 | gap: 20px; 9 | padding: 10px 0; 10 | scroll-behavior: smooth; 11 | -webkit-overflow-scrolling: touch; 12 | } 13 | 14 | .card-slider::-webkit-scrollbar { 15 | height: 8px; 16 | } 17 | 18 | .card-slider::-webkit-scrollbar-thumb { 19 | background-color: #ccc; 20 | border-radius: 10px; 21 | } 22 | 23 | .card-slider::-webkit-scrollbar-track { 24 | background: transparent; 25 | } 26 | 27 | .card { 28 | min-width: 300px; 29 | max-width: 320px; 30 | background: #fff; 31 | border-radius: 12px; 32 | box-shadow: 0 2px 8px rgba(0,0,0,0.1); 33 | padding: 15px; 34 | text-align: center; 35 | transition: 0.3s; 36 | flex-shrink: 0; 37 | } 38 | 39 | .card img { 40 | width: 100%; 41 | height: 180px; 42 | object-fit: cover; 43 | border-radius: 10px; 44 | } 45 | 46 | .rating { 47 | margin: 8px 0; 48 | } 49 | 50 | .details { 51 | color: black; 52 | margin: 5px 0; 53 | } 54 | 55 | .review-actions { 56 | display: flex; 57 | justify-content: space-between; 58 | gap: 10px; 59 | flex-wrap: wrap; 60 | } 61 | 62 | .review-actions button { 63 | flex: 1; 64 | padding: 8px 10px; 65 | font-size: 14px; 66 | border: none; 67 | border-radius: 6px; 68 | cursor: pointer; 69 | transition: 0.2s; 70 | } 71 | 72 | .review-btn { 73 | background-color: #007bff; 74 | color: white; 75 | } 76 | 77 | .review-btn:hover { 78 | background-color: #0056b3; 79 | } 80 | 81 | button.see-reviews { 82 | background-color: #e0e0e0; 83 | display: flex; 84 | align-items: center; 85 | justify-content: center; 86 | gap: 6px; 87 | } 88 | 89 | button.see-reviews svg { 90 | font-size: 16px; 91 | } 92 | 93 | .review-modal { 94 | position: fixed; 95 | top: 0; 96 | left: 0; 97 | width: 100%; 98 | height: 100%; 99 | background-color: rgba(0, 0, 0, 0.5); 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | z-index: 999; 104 | } 105 | 106 | .review-content { 107 | background: white; 108 | padding: 20px; 109 | width: 90%; 110 | max-width: 500px; 111 | border-radius: 8px; 112 | position: relative; 113 | } 114 | 115 | .review-content textarea { 116 | width: 99%; 117 | height: 100px; 118 | margin-top: 10px; 119 | padding: 10px; 120 | border-radius: 6px; 121 | border: 1px solid #ccc; 122 | resize: none; 123 | } 124 | 125 | .submit-review { 126 | background-color: rgb(16, 158, 16); 127 | color: white; 128 | margin-top: 10px; 129 | padding: 8px 12px; 130 | border: none; 131 | border-radius: 6px; 132 | cursor: pointer; 133 | } 134 | 135 | .close-btn { 136 | position: absolute; 137 | top: 8px; 138 | right: 12px; 139 | font-size: 22px; 140 | cursor: pointer; 141 | } 142 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/notificationCenter/NotificationCenter.css: -------------------------------------------------------------------------------- 1 | .notification-container { 2 | position: relative; 3 | } 4 | 5 | .notification-icon { 6 | background: transparent; 7 | border: none; 8 | cursor: pointer; 9 | position: relative; 10 | color: #333; 11 | } 12 | 13 | .notification-count { 14 | background: red; 15 | color: white; 16 | border-radius: 50%; 17 | font-size: 12px; 18 | width: 18px; 19 | height: 18px; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | position: absolute; 24 | top: -5px; 25 | right: -5px; 26 | } 27 | 28 | .notification-panel { 29 | position: fixed; 30 | top: 0; 31 | right: -350px; 32 | width: 350px; 33 | height: 100%; 34 | background: white; 35 | box-shadow: -2px 0px 10px rgba(0, 0, 0, 0.2); 36 | transition: right 0.3s ease-in-out; 37 | display: flex; 38 | flex-direction: column; 39 | } 40 | 41 | .notification-panel.open { 42 | right: 0; 43 | } 44 | 45 | .panel-header { 46 | display: flex; 47 | justify-content: space-between; 48 | align-items: center; 49 | border-bottom: 1px solid #ddd; 50 | padding: 15px; 51 | } 52 | 53 | .close-btn { 54 | background: none; 55 | border: none; 56 | font-size: 24px; 57 | cursor: pointer; 58 | } 59 | 60 | .notification-tabs { 61 | display: flex; 62 | justify-content: space-around; 63 | background: #f1f1f1; 64 | padding: 10px 0; 65 | } 66 | 67 | .notification-tabs button { 68 | border: none; 69 | background: transparent; 70 | font-size: 14px; 71 | cursor: pointer; 72 | padding: 8px 15px; 73 | } 74 | 75 | .notification-tabs .active { 76 | border-bottom: 3px solid #007bff; 77 | font-weight: bold; 78 | } 79 | 80 | .notification-list { 81 | flex: 1; 82 | overflow-y: auto; 83 | padding: 10px; 84 | max-height: 80vh; /* Makes it scrollable */ 85 | } 86 | 87 | .notification-item { 88 | background: #f9f9f9; 89 | padding: 10px; 90 | border-radius: 5px; 91 | margin-bottom: 10px; 92 | cursor: pointer; 93 | } 94 | 95 | .notification-item.unread { 96 | background:#D8E2DC; 97 | } 98 | 99 | .notification-item.read { 100 | background: #f0f0f0; 101 | } 102 | 103 | .notification-item:hover { 104 | background: #bbdef0; 105 | } 106 | 107 | .user-name { 108 | font-weight: bold; 109 | margin-bottom: 5px; 110 | } 111 | 112 | .notification-message { 113 | font-size: 14px; 114 | color: #555; 115 | } 116 | 117 | .notification-time { 118 | font-size: 12px; 119 | color: #999; 120 | } 121 | 122 | .no-notifications { 123 | text-align: center; 124 | padding: 20px; 125 | color: #777; 126 | } -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Search_Section/Search_section.css: -------------------------------------------------------------------------------- 1 | .search-section { 2 | text-align: center; 3 | padding: 30px; 4 | border-radius: 12px; 5 | color: black; 6 | max-width: 600px; 7 | margin: auto; 8 | background: rgb(140, 164, 243); 9 | box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); 10 | } 11 | 12 | /* Improved heading */ 13 | .search-line { 14 | font-size: 26px; 15 | font-weight: bold; 16 | color: #333; 17 | text-align: center; 18 | margin-bottom: 15px; 19 | } 20 | 21 | /* Centering and making search input more stylish */ 22 | .search-container { 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | gap: 12px; 27 | width: 100%; 28 | } 29 | 30 | /* Modern input field */ 31 | .search-input { 32 | padding: 12px; 33 | width: 280px; 34 | border-radius: 25px; 35 | border: 1px solid #ccc; 36 | font-size: 16px; 37 | transition: all 0.3s ease; 38 | } 39 | 40 | /* Input field hover and focus effects */ 41 | .search-input:hover, 42 | .search-input:focus { 43 | border-color: #007BFF; 44 | outline: none; 45 | box-shadow: 0px 4px 8px rgba(0, 123, 255, 0.2); 46 | } 47 | 48 | /* Stylish search button */ 49 | .search-button { 50 | padding: 12px 18px; 51 | background-color: #007BFF; 52 | color: white; 53 | border: none; 54 | border-radius: 25px; 55 | cursor: pointer; 56 | transition: all 0.3s ease; 57 | font-size: 16px; 58 | font-weight: bold; 59 | } 60 | 61 | .search-button:hover { 62 | background-color: #0056b3; 63 | transform: scale(1.05); 64 | } 65 | 66 | /* Suggestions box */ 67 | .suggestion-box { 68 | background: white; 69 | list-style: none; 70 | padding: 10px; 71 | margin: 8px auto; 72 | border-radius: 8px; 73 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); 74 | max-width: 290px; 75 | font-size: 14px; 76 | } 77 | 78 | /* Styling for each suggestion item */ 79 | .suggestion-box li { 80 | padding: 10px; 81 | cursor: pointer; 82 | transition: background 0.3s ease; 83 | } 84 | 85 | .suggestion-box li:hover { 86 | background: #f0f0f0; 87 | } 88 | 89 | /* Section container for different property types */ 90 | .section-container { 91 | display: flex; 92 | justify-content: center; 93 | gap: 12px; 94 | margin-top: 25px; 95 | } 96 | 97 | /* Section styling */ 98 | .section { 99 | padding: 15px 20px; 100 | border: 1px solid #ddd; 101 | border-radius: 25px; 102 | cursor: pointer; 103 | transition: all 0.3s ease; 104 | display: flex; 105 | align-items: center; 106 | gap: 8px; 107 | font-size: 16px; 108 | font-weight: bold; 109 | background-color: white; 110 | } 111 | 112 | /* Section hover effect */ 113 | .section:hover { 114 | transform: scale(1.05); 115 | background-color: #f8f9fa; 116 | } 117 | 118 | /* Active section */ 119 | .section.active { 120 | color: white; 121 | border: none; 122 | } 123 | 124 | /* Apply colors to sections dynamically */ 125 | .section:nth-child(1).active { background-color: #FFC107; } /* Buy */ 126 | .section:nth-child(2).active { background-color: #28A745; } /* Rent */ 127 | .section:nth-child(3).active { background-color: #17A2B8; } /* Commercial */ 128 | .section:nth-child(4).active { background-color: #DC3545; } /* PG/Co-Living */ 129 | .section:nth-child(5).active { background-color: #6F42C1; } /* Plots */ 130 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/feed/feed.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './Feed.css'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { FaStar, FaComments } from 'react-icons/fa'; 5 | 6 | const Feed = () => { 7 | const navigate = useNavigate(); 8 | 9 | const data = [ 10 | { 11 | id: 1, 12 | title: 'Deluxe Room', 13 | desc: 'Spacious room with ocean view.', 14 | price: "₹25,000", 15 | rating: 4, 16 | img: 'https://housing-images.n7net.in/4f2250e8/857dbfa14c6251d7a79818de5d629400/v0/large/house2.jpeg', 17 | }, 18 | { 19 | id: 2, 20 | title: 'Studio Apartment', 21 | desc: 'Cozy and modern interior.', 22 | price: "₹15,000", 23 | rating: 5, 24 | img: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2', 25 | }, 26 | { 27 | id: 3, 28 | title: 'Single Room', 29 | desc: 'Ideal for students or solo travelers.', 30 | price: "₹35,000", 31 | rating: 3, 32 | img: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c', 33 | }, 34 | { 35 | id: 4, 36 | title: 'Family Suite', 37 | desc: 'Perfect for family vacations.', 38 | price: "₹40,000", 39 | rating: 4, 40 | img: 'https://housing-images.n7net.in/4f2250e8/857dbfa14c6251d7a79818de5d629400/v0/large/house2.jpeg', 41 | }, 42 | { 43 | id: 5, 44 | title: 'Penthouse View', 45 | desc: 'Top-floor luxury suite.', 46 | price: "₹60,000", 47 | rating: 5, 48 | img: 'https://images.unsplash.com/photo-1599423300746-b62533397364', 49 | }, 50 | { 51 | id: 6, 52 | title: 'Modern Flat', 53 | desc: 'Comfortable for working professionals.', 54 | price: "₹30,000", 55 | rating: 4, 56 | img: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2', 57 | }, 58 | ]; 59 | 60 | const [selectedProperty, setSelectedProperty] = useState(null); 61 | 62 | const openReview = (property) => { 63 | setSelectedProperty(property); 64 | }; 65 | 66 | const closeReview = () => { 67 | setSelectedProperty(null); 68 | }; 69 | 70 | const getStars = (rating) => 71 | [...Array(5)].map((_, i) => 72 | i < rating ? : 73 | ); 74 | 75 | return ( 76 |
77 |

Available Rooms

78 |
79 | {data.map((item) => ( 80 |
81 | {item.title} 82 |

{item.title}

83 |

{item.desc}

84 |

{item.price}

85 |
{getStars(item.rating)}
86 |
87 | 90 | 93 |
94 |
95 | ))} 96 |
97 | 98 | {selectedProperty && ( 99 |
100 |
101 | × 102 |

Write a Review for {selectedProperty.title}

103 | 104 | 105 |
106 |
107 | )} 108 |
109 | ); 110 | }; 111 | 112 | export default Feed; 113 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/NavBar/Navbar.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 0.75rem 2rem; 5 | background-color: #1f2937; /* dark slate */ 6 | color: white; 7 | align-items: center; 8 | } 9 | 10 | .btn-custom { 11 | font-size: 1.5rem; 12 | font-weight: bold; 13 | color: white; 14 | text-decoration: none; 15 | } 16 | 17 | .menu-horizontal { 18 | display: flex; 19 | gap: 30px; 20 | } 21 | 22 | .menu-horizontal li { 23 | list-style: none; 24 | cursor: pointer; 25 | transition: transform 0.2s ease, color 0.2s ease; 26 | } 27 | 28 | .menu-horizontal li:hover { 29 | transform: scale(1.2); 30 | color: #facc15; 31 | } 32 | 33 | .menu-horizontal .active { 34 | color: #facc15; 35 | } 36 | 37 | .navbar-end { 38 | display: flex; 39 | align-items: center; 40 | gap: 20px; 41 | } 42 | 43 | .nav-icon { 44 | cursor: pointer; 45 | position: relative; 46 | } 47 | 48 | .dark-mode { 49 | background-color: #121212; 50 | color: #f0f0f0; 51 | } 52 | 53 | .dark-mode .navbar { 54 | background-color: #111827; 55 | } 56 | 57 | .dark-mode .btn-custom { 58 | color: #f0f0f0; 59 | } 60 | 61 | .dark-mode .menu-horizontal li:hover { 62 | color: #facc15; 63 | } 64 | 65 | /* Existing styles remain unchanged */ 66 | 67 | .logout-section { 68 | position: fixed; 69 | bottom: 80; 70 | left: 50; 71 | right: 0; 72 | background-color: #222; 73 | padding: 15px; 74 | text-align: center; 75 | z-index: 10000; 76 | border-top: 1px solid #444; 77 | } 78 | 79 | .logout-btn { 80 | background-color: #4739e6; 81 | color: white; 82 | border: none; 83 | padding: 1px 2px; 84 | border-radius: 2px; 85 | font-size: 16px; 86 | cursor: pointer; 87 | transition: 0.3s; 88 | } 89 | 90 | .logout-btn:hover { 91 | background-color: #d62828; 92 | } 93 | 94 | .notification-count { 95 | position: absolute; 96 | top: -6px; 97 | right: -6px; 98 | background: red; 99 | color: white; 100 | border-radius: 50%; 101 | font-size: 12px; 102 | padding: 2px 6px; 103 | } 104 | 105 | .notification-panel { 106 | position: fixed; 107 | top: 0; 108 | right: -350px; 109 | width: 350px; 110 | height: 100%; 111 | background: white; 112 | box-shadow: -2px 0px 10px rgba(0, 0, 0, 0.2); 113 | transition: right 0.3s ease-in-out; 114 | display: flex; 115 | flex-direction: column; 116 | z-index: 999; 117 | } 118 | 119 | .notification-panel.open { 120 | right: 0; 121 | } 122 | 123 | .panel-header { 124 | display: flex; 125 | justify-content: space-between; 126 | align-items: center; 127 | border-bottom: 1px solid #ddd; 128 | padding: 15px; 129 | } 130 | 131 | .close-btn { 132 | background: none; 133 | border: none; 134 | font-size: 24px; 135 | cursor: pointer; 136 | } 137 | 138 | .notification-tabs { 139 | display: flex; 140 | justify-content: space-around; 141 | background: #f1f1f1; 142 | padding: 10px 0; 143 | } 144 | 145 | .notification-tabs button { 146 | border: none; 147 | background: transparent; 148 | font-size: 14px; 149 | cursor: pointer; 150 | padding: 8px 15px; 151 | } 152 | 153 | .notification-tabs .active { 154 | border-bottom: 3px solid #007bff; 155 | font-weight: bold; 156 | } 157 | 158 | .notification-list { 159 | flex: 1; 160 | overflow-y: auto; 161 | padding: 10px; 162 | max-height: 80vh; 163 | } 164 | 165 | .notification-item { 166 | background: #f9f9f9; 167 | padding: 10px; 168 | border-radius: 5px; 169 | margin-bottom: 10px; 170 | cursor: pointer; 171 | } 172 | 173 | .notification-item.unread { 174 | background: #D8E2DC; 175 | } 176 | 177 | .notification-item.read { 178 | background: #f0f0f0; 179 | } 180 | 181 | .notification-item:hover { 182 | background: #bbdef0; 183 | } 184 | 185 | .user-name { 186 | font-weight: bold; 187 | margin-bottom: 5px; 188 | } 189 | 190 | .notification-message { 191 | font-size: 14px; 192 | color: #555; 193 | } 194 | 195 | .notification-time { 196 | font-size: 12px; 197 | color: #999; 198 | } 199 | 200 | .no-notifications { 201 | text-align: center; 202 | padding: 20px; 203 | color: #777; 204 | } 205 | -------------------------------------------------------------------------------- /Backend/src/controllers/room-controller.js: -------------------------------------------------------------------------------- 1 | const rentalPropertyService = require("../services/room-service"); 2 | 3 | const createRoom = async (req, res) => { 4 | try { 5 | 6 | const roomData = req.body; 7 | const ownerId=req?.user?._id; 8 | roomData.owner=ownerId; 9 | 10 | const imageUrls = req.files?.map((file) => file.path); 11 | if (!imageUrls || imageUrls.length === 0) { 12 | return res.status(400).json({ error: "At least one photo is required." }); 13 | } 14 | roomData.photos = imageUrls; 15 | 16 | const newRoom = await rentalPropertyService.createRoomService(roomData); 17 | res.status(201).json({ message: "Room created successfully!", room: newRoom }); 18 | } catch (error) { 19 | console.error("Error in createRoom: ", error.message); 20 | if (!res.headersSent) { 21 | return res.status(500).json({ error: error.message }); 22 | } 23 | } 24 | }; 25 | 26 | // ✅ 2️⃣ Get all rooms 27 | const getAllRooms = async (req, res) => { 28 | try { 29 | const rooms = await rentalPropertyService.getAllRoomsService(); 30 | res.status(200).json(rooms); 31 | } catch (error) { 32 | res.status(500).json({ error: error.message }); 33 | } 34 | }; 35 | 36 | // ✅ 3️⃣ Get rooms by Owner ID 37 | const getRoomsByOwner = async (req, res) => { 38 | try { 39 | // const { ownerId } = req.params; 40 | 41 | const ownerId=req?.user?._id; 42 | const rooms = await rentalPropertyService.getRoomsByOwnerService(ownerId); 43 | res.status(200).json(rooms); 44 | } catch (error) { 45 | res.status(500).json({ error: error.message }); 46 | } 47 | }; 48 | 49 | // ✅ 4️⃣ Get room by Room ID 50 | const getRoomById = async (req, res) => { 51 | try { 52 | const { roomId } = req.params; 53 | const room = await rentalPropertyService.getRoomByIdService(roomId); 54 | if (!room) { 55 | return res.status(404).json({ message: "Room not found!" }); 56 | } 57 | res.status(200).json(room); 58 | } catch (error) { 59 | res.status(500).json({ error: error.message }); 60 | } 61 | }; 62 | 63 | // ✅ 5️⃣ Update room by Room ID 64 | const updateRoom = async (req, res) => { 65 | try { 66 | const { roomId } = req.params; 67 | const updatedData = req.body; 68 | const updatedRoom = await rentalPropertyService.updateRoomService(roomId, updatedData); 69 | if (!updatedRoom) { 70 | return res.status(404).json({ message: "Room not found or update failed!" }); 71 | } 72 | res.status(200).json({ message: "Room updated successfully!", room: updatedRoom }); 73 | } catch (error) { 74 | res.status(500).json({ error: error.message }); 75 | } 76 | }; 77 | 78 | // ✅ 6️⃣ Delete room by Room ID 79 | const deleteRoom = async (req, res) => { 80 | try { 81 | const { roomId } = req.params; 82 | const { ownerId } = req.body; // Owner ID must be sent in the request body 83 | const deletedRoom = await rentalPropertyService.deleteRoomService(roomId, ownerId); 84 | if (!deletedRoom) { 85 | return res.status(404).json({ message: "Room not found or deletion failed!" }); 86 | } 87 | res.status(200).json({ message: "Room deleted successfully!" }); 88 | } catch (error) { 89 | res.status(500).json({ error: error.message }); 90 | } 91 | }; 92 | 93 | // ✅ 7️⃣ Get rooms by filters (Price, Location, etc.) 94 | const getRoomsByFilters = async (req, res) => { 95 | try { 96 | const filters = req.query; // Filters will be passed via query parameters 97 | const rooms = await rentalPropertyService.getRoomsByFiltersService(filters); 98 | res.status(200).json(rooms); 99 | } catch (error) { 100 | res.status(500).json({ error: error.message }); 101 | } 102 | }; 103 | 104 | const reviewRoom= async (req,res)=>{ 105 | try{ 106 | 107 | const review= await rentalPropertyService.postRoomReview(req,res); 108 | res.status(200).json(review); 109 | 110 | } catch(err){ 111 | 112 | res.status(500).json({error: err.message}); 113 | } 114 | } 115 | 116 | module.exports = { 117 | createRoom, 118 | getAllRooms, 119 | getRoomsByOwner, 120 | getRoomById, 121 | updateRoom, 122 | deleteRoom, 123 | getRoomsByFilters, 124 | reviewRoom 125 | }; 126 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/loginScreen/LoginScreen.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import axios from "axios"; 3 | import { BASE_URL } from "../../utils/constants"; 4 | import './LoginScreen.css'; 5 | import loginImage from '../../assets/account.png'; 6 | import ForgotPasswordModal from '../forgotPassword/ForgotPasswordModal'; 7 | import { useNavigate } from 'react-router-dom'; 8 | 9 | const LoginScreen = () => { 10 | const [isModalOpen, setIsModalOpen] = useState(false); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [error, setError] = useState(""); 14 | const [loading, setLoading] = useState(false); 15 | 16 | const navigate = useNavigate(); 17 | 18 | const handleForgotPasswordClick = () => setIsModalOpen(true); 19 | const handleCloseModal = () => setIsModalOpen(false); 20 | 21 | const handleLogin = async (e) => { 22 | e.preventDefault(); 23 | setError(""); 24 | setLoading(true); 25 | 26 | try { 27 | const response = await axios.post(`${BASE_URL}/api/auth/login`, { 28 | email, 29 | password, 30 | }); 31 | 32 | const { token } = response.data; 33 | localStorage.setItem("authToken", token); 34 | alert("Login successful!"); 35 | navigate("/body"); // ✅ redirect after login 36 | } catch (err) { 37 | console.error(err); 38 | const message = err.response?.data?.error || err.message || "Login failed"; 39 | setError(message); 40 | } finally { 41 | setLoading(false); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 |
48 | Logo 49 |

Sign In

50 |

Welcome back! Please enter your details

51 | 52 | {error &&

{error}

} 53 | 54 |
55 | setEmail(e.target.value)} 60 | required 61 | disabled={loading} 62 | /> 63 | setPassword(e.target.value)} 68 | required 69 | disabled={loading} 70 | /> 71 | 72 |
73 | 77 | Forgot Password? 78 |
79 | 80 | 83 | 84 |
85 |

Don't have an account? 86 | navigate("/signup")} 88 | className="signup-link" 89 | style={{ cursor: "pointer", color: "blue", marginLeft: "5px" }} 90 | > 91 | Sign Up 92 | 93 |

94 |
95 |
96 |
97 | 98 | 99 |
100 | ); 101 | }; 102 | 103 | export default LoginScreen; 104 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/notificationCenter/NotificationCenter.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import "./NotificationCenter.css"; 3 | import { FaBell } from "react-icons/fa"; 4 | 5 | const initialNotifications = [ 6 | { 7 | id: 1, 8 | user: "John Doe", 9 | message: "applied for your apartment at Downtown Heights", 10 | time: "10 min ago", 11 | read: false, 12 | }, 13 | { 14 | id: 2, 15 | user: "Sarah Adams", 16 | message: "submitted documents for verification", 17 | time: "30 min ago", 18 | read: false, 19 | }, 20 | { 21 | id: 3, 22 | user: "Mike Johnson", 23 | message: "requested a property tour", 24 | time: "1 hour ago", 25 | read: true, 26 | }, 27 | { 28 | id: 4, 29 | user: "Reminder", 30 | message: "Your rent for March is due on 5th", 31 | time: "1 day ago", 32 | read: false, 33 | }, 34 | { 35 | id: 5, 36 | user: "Landlord Notification", 37 | message: "Tenant Alex paid rent for March", 38 | time: "2 days ago", 39 | read: true, 40 | }, 41 | ]; 42 | 43 | const NotificationCenter = () => { 44 | const [isOpen, setIsOpen] = useState(false); 45 | const [notifications, setNotifications] = useState(initialNotifications); 46 | const [filter, setFilter] = useState("all"); // all, unread, read 47 | 48 | // Calculate unread notifications count 49 | const unreadCount = notifications.filter(item => !item.read).length; 50 | 51 | // Function to mark notification as read 52 | const markAsRead = (id) => { 53 | setNotifications(prevNotifications => 54 | prevNotifications.map(item => 55 | item.id === id ? { ...item, read: true } : item 56 | ) 57 | ); 58 | }; 59 | 60 | // Simulate receiving a new notification 61 | useEffect(() => { 62 | const interval = setInterval(() => { 63 | setNotifications(prevNotifications => [ 64 | { 65 | id: Date.now(), 66 | user: "New Tenant", 67 | message: "requested a property tour", 68 | time: "Just now", 69 | read: false, 70 | }, 71 | ...prevNotifications, 72 | ]); 73 | }, 15000); // New notification every 15 seconds 74 | 75 | return () => clearInterval(interval); 76 | }, []); 77 | 78 | // Filtered Notifications based on tab selection 79 | const filteredNotifications = notifications.filter(item => { 80 | if (filter === "unread") return !item.read; 81 | if (filter === "read") return item.read; 82 | return true; // all 83 | }); 84 | 85 | return ( 86 |
87 | 91 | 92 |
93 |
94 |

Notifications

95 | 96 |
97 | 98 | {/* Filter Tabs */} 99 |
100 | 101 | 102 | 103 |
104 | 105 | {/* Scrollable Notification List */} 106 |
107 | {filteredNotifications.length === 0 ? ( 108 |

No notifications

109 | ) : ( 110 | filteredNotifications.map(item => ( 111 |
markAsRead(item.id)} 115 | > 116 |

{item.user}

117 |

{item.message}

118 | {item.time} 119 |
120 | )) 121 | )} 122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | export default NotificationCenter; 129 | 130 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/reviewScreen/Review.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './Review.css'; 3 | 4 | const Review = () => { 5 | const [reviews, setReviews] = useState([ 6 | { 7 | name: 'Robert Karmazov', 8 | rating: 5, 9 | feedback: 10 | "I recently had the opportunity to explore RoomZy's UI design system, and it left a lasting impression on my workflow. The system seamlessly blends user-friendly features with a robust set of design components.", 11 | date: '20 days ago', 12 | }, 13 | { 14 | name: 'Nilesh Shah', 15 | rating: 5, 16 | feedback: 17 | "RoomZy seamlessly blends user-friendly features with a robust set of design components, making it a go-to for creating visually stunning and consistent interfaces.", 18 | date: '1 month ago', 19 | }, 20 | { 21 | name: 'Edna Watson', 22 | rating: 4, 23 | feedback: 24 | "I recently had the opportunity to explore RoomZy's UI design system. The system blends user-friendly features and consistent interfaces quite well.", 25 | date: '8 months ago', 26 | }, 27 | ]); 28 | 29 | const [formData, setFormData] = useState({ 30 | name: '', 31 | email: '', 32 | rating: 0, 33 | feedback: '', 34 | }); 35 | 36 | const handleInputChange = (e) => { 37 | setFormData({ ...formData, [e.target.name]: e.target.value }); 38 | }; 39 | 40 | const handleRating = (rate) => { 41 | setFormData({ ...formData, rating: rate }); 42 | }; 43 | 44 | const handleSubmit = () => { 45 | if (formData.name && formData.email && formData.rating && formData.feedback) { 46 | const newReview = { 47 | name: formData.name, 48 | rating: formData.rating, 49 | feedback: formData.feedback, 50 | date: 'just now', 51 | }; 52 | setReviews([newReview, ...reviews]); 53 | setFormData({ name: '', email: '', rating: 0, feedback: '' }); 54 | } 55 | }; 56 | 57 | const getStars = (count) => '★'.repeat(count) + '☆'.repeat(5 - count); 58 | 59 | return ( 60 |
61 |
62 |
63 |
64 |

Average Rating

65 |
4.5 ★★★★☆
66 |
67 | {[5, 4, 3, 2, 1].map((star) => ( 68 |
69 | {star} 70 |
71 |
72 |
73 |
74 | ))} 75 |
76 |
77 | 78 |
79 |

Submit Your Review

80 |
81 | {[1, 2, 3, 4, 5].map((star) => ( 82 | = star ? 'filled' : ''} 85 | onClick={() => handleRating(star)} 86 | > 87 | ★ 88 | 89 | ))} 90 |
91 | 98 | 105 | 111 | 112 |
113 |
114 | 115 |
116 |

Customer Feedbacks

117 | {reviews.map((review, i) => ( 118 |
119 |
120 | {review.name} 121 |
{getStars(review.rating)}
122 |
123 |

{review.feedback}

124 | {review.date} 125 |
126 | ))} 127 |
128 |
129 |
130 | ); 131 | }; 132 | 133 | export default Review; -------------------------------------------------------------------------------- /Backend/src/repositories/room-repository.js: -------------------------------------------------------------------------------- 1 | const Room = require("../models/roommodel"); 2 | const User = require("../models/Usermodel"); 3 | 4 | // ✅ 1️⃣ Create a new room 5 | const createRoom = async (roomData) => { 6 | try { 7 | const { owner } = roomData; 8 | // Check if owner (user) exists 9 | const user = await User.findById(owner); 10 | if (!user) { 11 | throw new Error("User not found!"); 12 | } 13 | 14 | // Create a new room 15 | const newRoom = await Room.create(roomData); 16 | 17 | // Add room ID to user's ownedProperties 18 | user.ownedProperties.push(newRoom._id); 19 | await user.save(); 20 | 21 | return newRoom; 22 | } catch (error) { 23 | throw new Error(error.message); 24 | } 25 | }; 26 | 27 | // ✅ 2️⃣ Get all rooms 28 | const getAllRooms = async () => { 29 | try { 30 | return await Room.find().populate("owner", "firstName lastName email"); 31 | } catch (error) { 32 | throw new Error(error.message); 33 | } 34 | }; 35 | 36 | // ✅ 3️⃣ Get rooms by Owner ID 37 | const getRoomsByOwner = async (ownerId) => { 38 | try { 39 | return await Room.find({ owner: ownerId }).populate("owner", "firstName lastName email"); 40 | } catch (error) { 41 | throw new Error(error.message); 42 | } 43 | }; 44 | 45 | // ✅ 4️⃣ Get room by Room ID 46 | const getRoomById = async (roomId) => { 47 | try { 48 | return await Room.findById(roomId).populate("owner", "firstName lastName email"); 49 | } catch (error) { 50 | throw new Error(error.message); 51 | } 52 | }; 53 | 54 | // ✅ 5️⃣ Update room by Room ID 55 | const updateRoom = async (roomId, updatedData) => { 56 | try { 57 | const updatedRoom = await Room.findByIdAndUpdate(roomId, updatedData, { new: true }); 58 | if (!updatedRoom) throw new Error("Room not found!"); 59 | return updatedRoom; 60 | } catch (error) { 61 | throw new Error(error.message); 62 | } 63 | }; 64 | 65 | // ✅ 6️⃣ Delete room by Room ID 66 | const deleteRoom = async (roomId) => { 67 | try { 68 | // Fetch the room details 69 | const room = await Room.findById(roomId); 70 | if (!room) throw new Error("Room not found!"); 71 | 72 | // Extract owner ID 73 | const ownerId = room.owner; 74 | 75 | // Delete the room 76 | const deletedRoom = await Room.findByIdAndDelete(roomId); 77 | if (!deletedRoom) throw new Error("Room deletion failed!"); 78 | 79 | // Remove room ID from owner's ownedProperties array 80 | await User.findByIdAndUpdate(ownerId, { $pull: { ownedProperties: roomId } }); 81 | 82 | return { message: "Room deleted successfully", deletedRoom }; 83 | } catch (error) { 84 | throw new Error(error.message); 85 | } 86 | }; 87 | 88 | // ✅ 7️⃣ Get rooms by filters (Price, Location, Bedrooms, etc.) 89 | const getRoomsByFilters = async (filters) => { 90 | try { 91 | const query = {}; 92 | 93 | if (filters.city) query.city = filters.city; 94 | if (filters.locality) query.locality = filters.locality; 95 | if (filters.minRent) query.monthlyRent = { $gte: filters.minRent }; 96 | if (filters.maxRent) query.monthlyRent = { ...query.monthlyRent, $lte: filters.maxRent }; 97 | if (filters.bedrooms) query.bedrooms = filters.bedrooms; 98 | if (filters.furnishedStatus) query.furnishedStatus = filters.furnishedStatus; 99 | if (filters.listingType) query.listingType = filters.listingType; 100 | if (filters.occupancyType) query.occupancyType = filters.occupancyType; 101 | 102 | return await Room.find(query).populate("owner", "firstName lastName email"); 103 | } catch (error) { 104 | throw new Error(error.message); 105 | } 106 | }; 107 | 108 | const postRoomReview=async(req,res)=>{ 109 | const { roomId } = req.params; 110 | const { userId, rating, text } = req.body; // Assuming userId is passed in the body 111 | 112 | try { 113 | // Check if the user has already reviewed this property 114 | const property = await Room.findById(roomId); 115 | const existingReview = property.reviews.find(review => review.user.toString() === userId); 116 | 117 | if (existingReview) { 118 | 119 | return { message: "User has already reviewed this property." }; 120 | } 121 | 122 | // Create a new review 123 | const newReview = { user: userId, rating, text }; 124 | property.reviews.push(newReview); 125 | await property.save(); 126 | 127 | return { message: "Review added successfully.", property }; 128 | } catch (error) { 129 | // console.log("err1") 130 | return { message: "Error adding review.", error}; 131 | } 132 | 133 | } 134 | 135 | 136 | module.exports = { 137 | createRoom, 138 | getAllRooms, 139 | getRoomsByOwner, 140 | getRoomById, 141 | updateRoom, 142 | deleteRoom, 143 | getRoomsByFilters, 144 | postRoomReview 145 | }; 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rental Property Management API 2 | 3 | This project is the backend service for a rental property management platform. It provides a robust RESTful API for user authentication (including OTP verification and password reset) and comprehensive CRUD operations for managing rental property listings. The system is built with Node.js, Express, and MongoDB, following a layered architecture for maintainability and scalability. 4 | 5 | ## High-Level Architecture 6 | 7 | The architecture separates concerns into distinct layers: Routing, Controllers, Services, and Repositories (Data Access). This design ensures that business logic is decoupled from the web framework and database implementation. 8 | 9 | mermaid 10 | graph TD 11 | subgraph Client 12 | A[React App / Postman] 13 | end 14 | 15 | subgraph "Backend API (Node.js/Express)" 16 | B[Express Routes] 17 | C[Middleware & Validators] 18 | D[Controllers] 19 | E[Services] 20 | F[Repositories] 21 | end 22 | 23 | subgraph "External Services" 24 | G[MongoDB Database] 25 | H[Email Service] 26 | end 27 | 28 | A -->|API Calls| B 29 | B --> C 30 | C --> D 31 | D -->|Calls Business Logic| E 32 | E -->|Abstracts Data Access| F 33 | E -->|Sends OTP/Notifications| H 34 | F -->|Performs CRUD| G 35 | 36 | 37 | ## Getting Started 38 | 39 | Follow these instructions to get the backend server up and running on your local machine. 40 | 41 | ### Prerequisites 42 | 43 | - [Node.js](https://nodejs.org/) (v18.x or later recommended) 44 | - [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) 45 | - [MongoDB](https://www.mongodb.com/try/download/community) instance (local or cloud-hosted like MongoDB Atlas) 46 | 47 | ### Installation & Setup 48 | 49 | 1. *Clone the repository:* 50 | sh 51 | git clone 52 | cd 53 | 54 | 55 | 2. *Install dependencies:* 56 | sh 57 | npm install 58 | 59 | 60 | 3. *Create an environment file:* 61 | Create a .env file in the root of the project and add the following environment variables. 62 | 63 | env 64 | # MongoDB Connection String 65 | MONGO_URI=mongodb://localhost:27017/rentalDB 66 | 67 | # JSON Web Token Secret 68 | JWT_SECRET=your_super_secret_jwt_key 69 | 70 | # Nodemailer (Gmail) configuration for sending emails 71 | EMAIL_USER=your-email@gmail.com 72 | EMAIL_PASS=your-gmail-app-password 73 | 74 | > *Note:* For EMAIL_PASS, it is highly recommended to use a Gmail "App Password" rather than your regular account password. 75 | 76 | 4. *Start the server:* 77 | sh 78 | node index.js 79 | 80 | The server should now be running on http://localhost:8000. 81 | 82 | ## Module Breakdown 83 | 84 | This project follows a standard layered architecture. Below is a breakdown of the key files and directories. 85 | 86 | ### Root Directory 87 | 88 | - index.js: The main entry point for the application. It initializes the Express server, connects to the database using the configuration from server-config.js, applies middleware, and starts listening for incoming requests. 89 | - vite.config.js / eslint.config.js: Configuration files for the frontend Vite build tool and ESLint for code linting. 90 | - index.html: The main HTML template for the React frontend application. 91 | 92 | ### config/ 93 | 94 | - server-config.js: Manages the connection to the MongoDB database using Mongoose. It reads the MONGO_URI from the environment variables. 95 | - logger-config.js: Configures the winston logger for application-wide logging. It sets up transports for console output and file logging (combined.log) with a custom format. 96 | 97 | ### routes/ 98 | 99 | - index.js: The main router file. It consolidates all feature-specific routers (like authRoutes and room-routes) and exports them as a single module to be used in the main index.js. 100 | - authRoutes.js: Defines all API endpoints related to user authentication, such as /signup, /login, /verify-otp, /forgot-password, and /reset-password. It maps these routes to the corresponding controller functions in authController.js. 101 | - room-routes.js: Defines the RESTful API endpoints for managing rental properties. This includes routes for creating, reading, updating, deleting, and searching for rooms. 102 | 103 | ### controllers/ 104 | 105 | - authController.js: Handles incoming HTTP requests for authentication routes. It receives requests, calls the appropriate methods in authService.js to perform the business logic, and sends back the HTTP response (e.g., success message, JWT token, or error). 106 | - room-controller.js: Manages the request/response cycle for rental property CRUD operations. It delegates the core logic to room-service.js and formats the final response to the client. 107 | - info-controller.js: A simple controller for a health-check or status endpoint, confirming the API is operational. 108 | 109 | ### services/ 110 | 111 | - authService.js: Contains the core business logic for user authentication. It handles user registration, OTP generation and verification, password hashing with bcrypt, JWT generation, and coordinating with the user-repository and sendEmail utility. 112 | - room-service.js: Acts as an intermediary between the room-controller and room-repository. It contains business logic related to managing rental properties, ensuring that data is validated or processed before being sent to the repository layer. 113 | 114 | ### repositories/ 115 | 116 | - user-repository.js: The data access layer for user-related operations. It contains all the Mongoose queries for interacting with the User collection in the database (e.g., findByEmail, createUser, updateUser). 117 | - room-repository.js: The data access layer for rental properties. It encapsulates all database interactions for the roomModel, including creating a room and linking it to an owner, finding rooms by various criteria, and handling updates and deletions. 118 | 119 | ### models/ 120 | 121 | - Usermodel.js: Defines the Mongoose schema for a User. This includes fields like firstName, email, password (hashed), verification status (isVerified), OTP details, and a reference to the properties they own. 122 | - roommodel.js: Defines the Mongoose schema for a rental property (roomModel). It includes detailed fields such as owner, location, property features (bedrooms, bathrooms), rent details, and photos. 123 | 124 | ### middlewares/ 125 | 126 | - validator.js: Contains Express middleware functions for validating incoming request bodies. For example, validateSignup checks if the required fields (firstName, email, password) are present and meet specific criteria before the request reaches the controller. 127 | 128 | ### utils/ 129 | 130 | - sendEmail.js: A utility module that uses nodemailer to send emails. It is configured to use Gmail and is used by the authService to send OTPs for email verification and password resets. 131 | 132 | ### Frontend (src/) 133 | 134 | The repository also contains a basic React + Vite frontend structure. 135 | - App.css, index.css, OTPSender.css, LoginScreen.css, SignupScreen.css, ForgotPassword.css: CSS files that provide styling for the various frontend components and screens of the React application. 136 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/NavBar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { FaHome, FaUser, FaAdjust, FaBell, FaSignOutAlt } from 'react-icons/fa'; 3 | import { BsSun, BsMoon } from 'react-icons/bs'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import './navbar.css'; // You must also include NotificationCenter CSS here or inside navbar.css 6 | 7 | const Navbar = () => { 8 | const [selectedIcon, setSelectedIcon] = useState(null); 9 | const [darkMode, setDarkMode] = useState(false); 10 | const [showLogoutPanel, setShowLogoutPanel] = useState(false); 11 | const [isNotificationOpen, setIsNotificationOpen] = useState(false); 12 | const [notifications, setNotifications] = useState([ 13 | { 14 | id: 1, 15 | user: "John Doe", 16 | message: "applied for your apartment at Downtown Heights", 17 | time: "10 min ago", 18 | read: false, 19 | }, 20 | { 21 | id: 2, 22 | user: "Sarah Adams", 23 | message: "submitted documents for verification", 24 | time: "30 min ago", 25 | read: false, 26 | }, 27 | { 28 | id: 3, 29 | user: "Mike Johnson", 30 | message: "requested a property tour", 31 | time: "1 hour ago", 32 | read: true, 33 | }, 34 | { 35 | id: 4, 36 | user: "Reminder", 37 | message: "Your rent for March is due on 5th", 38 | time: "1 day ago", 39 | read: false, 40 | }, 41 | { 42 | id: 5, 43 | user: "Landlord Notification", 44 | message: "Tenant Alex paid rent for March", 45 | time: "2 days ago", 46 | read: true, 47 | }, 48 | ]); 49 | const [filter, setFilter] = useState("all"); 50 | 51 | const navigate = useNavigate(); 52 | const token = localStorage.getItem('token'); 53 | 54 | const handleIconClick = (iconName) => { 55 | setSelectedIcon(iconName); 56 | if (iconName === 'User') navigate('/listing'); 57 | else if (iconName === 'Home') navigate('/body'); 58 | }; 59 | 60 | const toggleDarkMode = () => { 61 | setDarkMode(prev => !prev); 62 | document.body.classList.toggle('dark-mode'); 63 | }; 64 | 65 | const handleLogout = () => { 66 | localStorage.removeItem('token'); 67 | setShowLogoutPanel(false); 68 | navigate('/login'); 69 | }; 70 | 71 | const markAsRead = (id) => { 72 | setNotifications(prev => 73 | prev.map(item => (item.id === id ? { ...item, read: true } : item)) 74 | ); 75 | }; 76 | 77 | useEffect(() => { 78 | const savedTheme = localStorage.getItem('darkMode') === 'true'; 79 | setDarkMode(savedTheme); 80 | if (savedTheme) document.body.classList.add('dark-mode'); 81 | }, []); 82 | 83 | useEffect(() => { 84 | localStorage.setItem('darkMode', darkMode); 85 | }, [darkMode]); 86 | 87 | useEffect(() => { 88 | const interval = setInterval(() => { 89 | setNotifications(prev => [ 90 | { 91 | id: Date.now(), 92 | user: "New Tenant", 93 | message: "requested a property tour", 94 | time: "Just now", 95 | read: false, 96 | }, 97 | ...prev, 98 | ]); 99 | }, 15000); 100 | 101 | return () => clearInterval(interval); 102 | }, []); 103 | 104 | const unreadCount = notifications.filter(n => !n.read).length; 105 | 106 | const filteredNotifications = notifications.filter(n => 107 | filter === "all" ? true : filter === "unread" ? !n.read : n.read 108 | ); 109 | 110 | return ( 111 |
112 |
113 | RoomZy 114 |
115 | 116 |
117 |
    118 |
  • handleIconClick('User')} style={{ marginRight: '50px', cursor: 'pointer' }}> 119 | 120 |
  • 121 |
  • setIsNotificationOpen(!isNotificationOpen)} style={{ marginRight: '50px', cursor: 'pointer', position: 'relative' }}> 122 | 123 | {unreadCount > 0 && ( 124 | {unreadCount} 125 | )} 126 |
  • 127 |
  • handleIconClick('Payment')} style={{ marginRight: '50px', cursor: 'pointer' }}> 128 | 129 |
  • 130 |
  • handleIconClick('Home')} style={{ marginRight: '50px', cursor: 'pointer' }}> 131 | 132 |
  • 133 |
134 |
135 | 136 |
137 |
138 | {darkMode ? : } 139 |
140 |
setShowLogoutPanel(!showLogoutPanel)} style={{ cursor: 'pointer' }}> 141 | 142 |
143 |
144 | 145 | {showLogoutPanel && ( 146 |
157 |

Are you sure you want to logout?

158 | 168 |
169 | )} 170 | 171 | {/* Notification Slide Panel */} 172 |
173 |
174 |

Notifications

175 | 176 |
177 | 178 |
179 | 180 | 181 | 182 |
183 | 184 |
185 | {filteredNotifications.length === 0 ? ( 186 |

No notifications

187 | ) : ( 188 | filteredNotifications.map(item => ( 189 |
markAsRead(item.id)} 193 | > 194 |

{item.user}

195 |

{item.message}

196 | {item.time} 197 |
198 | )) 199 | )} 200 |
201 |
202 |
203 | ); 204 | }; 205 | 206 | export default Navbar; 207 | -------------------------------------------------------------------------------- /Frontend/Roomzy/src/components/Listing/listing2.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./listing2.css"; // Ensure this path is correct 3 | 4 | const initialState = { 5 | listingType: "", 6 | occupancyType: "", 7 | societyName: "", 8 | city: "", 9 | locality: "", 10 | bedrooms: "", 11 | balcony: "", 12 | floorNumber: "", 13 | totalFloors: "", 14 | furnishedStatus: "", 15 | bathrooms: "", 16 | availableFrom: "", 17 | monthlyRent: "", 18 | securityDeposit: "", 19 | maintenanceCharge: "", 20 | description: "", 21 | photo: null, 22 | }; 23 | 24 | function Listing() { 25 | const [form, setForm] = useState(initialState); 26 | const [errors, setErrors] = useState({}); 27 | const [photoPreview, setPhotoPreview] = useState(null); 28 | 29 | const handleChange = (e) => { 30 | const { name, value, type } = e.target; 31 | if (type === "file") { 32 | const file = e.target.files[0]; 33 | if (file && file.type === "image/jpeg") { 34 | setForm({ ...form, photo: file }); 35 | setPhotoPreview(URL.createObjectURL(file)); 36 | } else { 37 | setForm({ ...form, photo: null }); 38 | setPhotoPreview(null); 39 | } 40 | } else { 41 | setForm({ ...form, [name]: value }); 42 | } 43 | }; 44 | 45 | const validate = () => { 46 | let temp = {}; 47 | if (!form.listingType) temp.listingType = "Required"; 48 | if (!form.occupancyType) temp.occupancyType = "Required"; 49 | if (!form.societyName) temp.societyName = "Required"; 50 | if (!form.city) temp.city = "Required"; 51 | if (!form.locality) temp.locality = "Required"; 52 | if (!form.bedrooms) temp.bedrooms = "Required"; 53 | if (form.balcony === "") temp.balcony = "Required"; 54 | if (!form.floorNumber) temp.floorNumber = "Required"; 55 | if (!form.totalFloors) temp.totalFloors = "Required"; 56 | if (!form.furnishedStatus) temp.furnishedStatus = "Required"; 57 | if (!form.bathrooms) temp.bathrooms = "Required"; 58 | if (!form.availableFrom) temp.availableFrom = "Required"; 59 | if (!form.monthlyRent) temp.monthlyRent = "Required"; 60 | if (!form.securityDeposit) temp.securityDeposit = "Required"; 61 | if (!form.maintenanceCharge) temp.maintenanceCharge = "Required"; 62 | if (!form.description) temp.description = "Required"; 63 | if (!form.photo) temp.photo = "JPG photo required"; 64 | setErrors(temp); 65 | return Object.keys(temp).length === 0; 66 | }; 67 | 68 | const handleSubmit = (e) => { 69 | e.preventDefault(); 70 | if (validate()) { 71 | // Here you can send 'form' data to backend using fetch/axios API call. 72 | alert("Listing submitted successfully!"); 73 | setForm(initialState); 74 | setPhotoPreview(null); 75 | setErrors({}); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 |

Room Listing Form

82 |
83 | 84 | 89 | {errors.listingType && {errors.listingType}} 90 |
91 | 92 |
93 | 94 | 99 | {errors.occupancyType && {errors.occupancyType}} 100 |
101 | 102 |
103 | 104 | 110 | {errors.societyName && {errors.societyName}} 111 |
112 | 113 |
114 | 115 | 121 | {errors.city && {errors.city}} 122 |
123 | 124 |
125 | 126 | 132 | {errors.locality && {errors.locality}} 133 |
134 | 135 |
136 | 137 | 144 | {errors.bedrooms && {errors.bedrooms}} 145 |
146 | 147 |
148 | 149 | 154 | {errors.balcony && {errors.balcony}} 155 |
156 | 157 |
158 | 159 | 166 | {errors.floorNumber && {errors.floorNumber}} 167 |
168 | 169 |
170 | 171 | 178 | {errors.totalFloors && {errors.totalFloors}} 179 |
180 | 181 |
182 | 183 | 188 | {errors.furnishedStatus && {errors.furnishedStatus}} 189 |
190 | 191 |
192 | 193 | 200 | {errors.bathrooms && {errors.bathrooms}} 201 |
202 | 203 |
204 | 205 | 211 | {errors.availableFrom && {errors.availableFrom}} 212 |
213 | 214 |
215 | 216 | 223 | {errors.monthlyRent && {errors.monthlyRent}} 224 |
225 | 226 |
227 | 228 | 235 | {errors.securityDeposit && {errors.securityDeposit}} 236 |
237 | 238 |
239 | 240 | 247 | {errors.maintenanceCharge && {errors.maintenanceCharge}} 248 |
249 | 250 |
251 | 252 |