├── client ├── postcss.config.js ├── src │ ├── index.css │ ├── components │ │ ├── PrivateRoute.jsx │ │ ├── OAuth.jsx │ │ ├── Contact.jsx │ │ ├── ListingItem.jsx │ │ └── Header.jsx │ ├── main.jsx │ ├── firebase.js │ ├── redux │ │ ├── store.js │ │ └── user │ │ │ └── userSlice.js │ ├── pages │ │ ├── About.jsx │ │ ├── SignIn.jsx │ │ ├── SignUp.jsx │ │ ├── Home.jsx │ │ ├── Listing.jsx │ │ ├── Search.jsx │ │ ├── Profile.jsx │ │ ├── CreateListing.jsx │ │ └── UpdateListing.jsx │ └── App.jsx ├── tailwind.config.js ├── index.html ├── vite.config.js ├── README.md ├── .eslintrc.cjs └── package.json ├── api ├── utils │ ├── error.js │ └── verifyUser.js ├── routes │ ├── auth.route.js │ ├── user.route.js │ └── listing.route.js ├── models │ ├── user.model.js │ └── listing.model.js ├── index.js └── controllers │ ├── user.controller.js │ ├── auth.controller.js │ └── listing.controller.js ├── .gitignore └── package.json /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body{ 6 | background-color: rgb(241, 245, 241); 7 | } -------------------------------------------------------------------------------- /api/utils/error.js: -------------------------------------------------------------------------------- 1 | export const errorHandler = (statusCode, message) => { 2 | const error = new Error(); 3 | error.statusCode = statusCode; 4 | error.message = message; 5 | return error; 6 | }; 7 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [ 8 | require('@tailwindcss/line-clamp'), 9 | // ... 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /client/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Outlet, Navigate } from 'react-router-dom'; 3 | 4 | export default function PrivateRoute() { 5 | const { currentUser } = useSelector((state) => state.user); 6 | return currentUser ? : ; 7 | } 8 | -------------------------------------------------------------------------------- /api/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { google, signOut, signin, signup } from '../controllers/auth.controller.js'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/signup", signup); 7 | router.post("/signin", signin); 8 | router.post('/google', google); 9 | router.get('/signout', signOut) 10 | 11 | export default router; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sahand Estate 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.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 | .env -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:3000', 10 | secure: false, 11 | }, 12 | }, 13 | }, 14 | 15 | plugins: [react()], 16 | }); 17 | -------------------------------------------------------------------------------- /api/utils/verifyUser.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { errorHandler } from './error.js'; 3 | 4 | export const verifyToken = (req, res, next) => { 5 | const token = req.cookies.access_token; 6 | 7 | if (!token) return next(errorHandler(401, 'Unauthorized')); 8 | 9 | jwt.verify(token, process.env.JWT_SECRET, (err, user) => { 10 | if (err) return next(errorHandler(403, 'Forbidden')); 11 | 12 | req.user = user; 13 | next(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | import { persistor, store } from './redux/store.js'; 6 | import { Provider } from 'react-redux'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /api/routes/user.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { deleteUser, test, updateUser, getUserListings, getUser} from '../controllers/user.controller.js'; 3 | import { verifyToken } from '../utils/verifyUser.js'; 4 | 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/test', test); 9 | router.post('/update/:id', verifyToken, updateUser) 10 | router.delete('/delete/:id', verifyToken, deleteUser) 11 | router.get('/listings/:id', verifyToken, getUserListings) 12 | router.get('/:id', verifyToken, getUser) 13 | 14 | export default router; -------------------------------------------------------------------------------- /api/routes/listing.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { createListing, deleteListing, updateListing, getListing, getListings } from '../controllers/listing.controller.js'; 3 | import { verifyToken } from '../utils/verifyUser.js'; 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/create', verifyToken, createListing); 8 | router.delete('/delete/:id', verifyToken, deleteListing); 9 | router.post('/update/:id', verifyToken, updateListing); 10 | router.get('/get/:id', getListing); 11 | router.get('/get', getListings); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-estate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "nodemon api/index.js", 9 | "start": "node api/index.js", 10 | "build": "npm install && npm install --prefix client && npm run build --prefix client" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cookie-parser": "^1.4.6", 18 | "dotenv": "^16.3.1", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^7.5.0", 22 | "nodemon": "^3.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | avatar:{ 20 | type: String, 21 | default: "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png" 22 | }, 23 | }, 24 | { timestamps: true } 25 | ); 26 | 27 | const User = mongoose.model('User', userSchema); 28 | 29 | export default User; 30 | -------------------------------------------------------------------------------- /client/src/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from 'firebase/app'; 3 | // TODO: Add SDKs for Firebase products that you want to use 4 | // https://firebase.google.com/docs/web/setup#available-libraries 5 | 6 | // Your web app's Firebase configuration 7 | const firebaseConfig = { 8 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 9 | authDomain: 'mern-estate.firebaseapp.com', 10 | projectId: 'mern-estate', 11 | storageBucket: 'mern-estate.appspot.com', 12 | messagingSenderId: '1078482850952', 13 | appId: '1:1078482850952:web:28f19139ab77246602fb3d', 14 | }; 15 | 16 | // Initialize Firebase 17 | export const app = initializeApp(firebaseConfig); 18 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import userReducer from './user/userSlice'; 3 | import { persistReducer, persistStore } from 'redux-persist'; 4 | import storage from 'redux-persist/lib/storage'; 5 | 6 | const rootReducer = combineReducers({ user: userReducer }); 7 | 8 | const persistConfig = { 9 | key: 'root', 10 | storage, 11 | version: 1, 12 | }; 13 | 14 | const persistedReducer = persistReducer(persistConfig, rootReducer); 15 | 16 | export const store = configureStore({ 17 | reducer: persistedReducer, 18 | middleware: (getDefaultMiddleware) => 19 | getDefaultMiddleware({ 20 | serializableCheck: false, 21 | }), 22 | }); 23 | 24 | export const persistor = persistStore(store); 25 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^1.9.5", 14 | "axios": "^1.9.0", 15 | "firebase": "^10.3.1", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-icons": "^4.10.1", 19 | "react-redux": "^8.1.2", 20 | "react-router-dom": "^6.15.0", 21 | "redux-persist": "^6.0.0", 22 | "swiper": "^10.2.0" 23 | }, 24 | "devDependencies": { 25 | "@tailwindcss/line-clamp": "^0.4.4", 26 | "@types/react": "^18.2.15", 27 | "@types/react-dom": "^18.2.7", 28 | "@vitejs/plugin-react-swc": "^3.3.2", 29 | "autoprefixer": "^10.4.15", 30 | "eslint": "^8.45.0", 31 | "eslint-plugin-react": "^7.32.2", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.3", 34 | "postcss": "^8.4.29", 35 | "tailwindcss": "^3.3.3", 36 | "vite": "^4.4.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function About() { 4 | return ( 5 |
6 |

About Sahand Estate

7 |

Sahand Estate is a leading real estate agency that specializes in helping clients buy, sell, and rent properties in the most desirable neighborhoods. Our team of experienced agents is dedicated to providing exceptional service and making the buying and selling process as smooth as possible.

8 |

9 | Our mission is to help our clients achieve their real estate goals by providing expert advice, personalized service, and a deep understanding of the local market. Whether you are looking to buy, sell, or rent a property, we are here to help you every step of the way. 10 |

11 |

Our team of agents has a wealth of experience and knowledge in the real estate industry, and we are committed to providing the highest level of service to our clients. We believe that buying or selling a property should be an exciting and rewarding experience, and we are dedicated to making that a reality for each and every one of our clients.

12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /api/models/listing.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const listingSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | }, 13 | address: { 14 | type: String, 15 | required: true, 16 | }, 17 | regularPrice: { 18 | type: Number, 19 | required: true, 20 | }, 21 | discountPrice: { 22 | type: Number, 23 | required: true, 24 | }, 25 | bathrooms: { 26 | type: Number, 27 | required: true, 28 | }, 29 | bedrooms: { 30 | type: Number, 31 | required: true, 32 | }, 33 | furnished: { 34 | type: Boolean, 35 | required: true, 36 | }, 37 | parking: { 38 | type: Boolean, 39 | required: true, 40 | }, 41 | type: { 42 | type: String, 43 | required: true, 44 | }, 45 | offer: { 46 | type: Boolean, 47 | required: true, 48 | }, 49 | imageUrls: { 50 | type: Array, 51 | required: true, 52 | }, 53 | userRef: { 54 | type: String, 55 | required: true, 56 | }, 57 | }, 58 | { timestamps: true } 59 | ); 60 | 61 | const Listing = mongoose.model('Listing', listingSchema); 62 | 63 | export default Listing; 64 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import mongoose from 'mongoose'; 3 | import dotenv from 'dotenv'; 4 | import userRouter from './routes/user.route.js'; 5 | import authRouter from './routes/auth.route.js'; 6 | import listingRouter from './routes/listing.route.js'; 7 | import cookieParser from 'cookie-parser'; 8 | import path from 'path'; 9 | dotenv.config(); 10 | 11 | mongoose 12 | .connect(process.env.MONGO) 13 | .then(() => { 14 | console.log('Connected to MongoDB!'); 15 | }) 16 | .catch((err) => { 17 | console.log(err); 18 | }); 19 | 20 | const __dirname = path.resolve(); 21 | 22 | const app = express(); 23 | 24 | app.use(express.json()); 25 | 26 | app.use(cookieParser()); 27 | 28 | app.listen(3000, () => { 29 | console.log('Server is running on port 3000!'); 30 | }); 31 | 32 | app.use('/api/user', userRouter); 33 | app.use('/api/auth', authRouter); 34 | app.use('/api/listing', listingRouter); 35 | 36 | 37 | app.use(express.static(path.join(__dirname, '/client/dist'))); 38 | 39 | app.get('*', (req, res) => { 40 | res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')); 41 | }) 42 | 43 | app.use((err, req, res, next) => { 44 | const statusCode = err.statusCode || 500; 45 | const message = err.message || 'Internal Server Error'; 46 | return res.status(statusCode).json({ 47 | success: false, 48 | statusCode, 49 | message, 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import Home from './pages/Home'; 3 | import SignIn from './pages/SignIn'; 4 | import SignUp from './pages/SignUp'; 5 | import About from './pages/About'; 6 | import Profile from './pages/Profile'; 7 | import Header from './components/Header'; 8 | import PrivateRoute from './components/PrivateRoute'; 9 | import CreateListing from './pages/CreateListing'; 10 | import UpdateListing from './pages/UpdateListing'; 11 | import Listing from './pages/Listing'; 12 | import Search from './pages/Search'; 13 | 14 | export default function App() { 15 | return ( 16 | 17 |
18 | 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | 26 | }> 27 | } /> 28 | } /> 29 | } 32 | /> 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/src/components/OAuth.jsx: -------------------------------------------------------------------------------- 1 | import { GoogleAuthProvider, getAuth, signInWithPopup } from 'firebase/auth'; 2 | import { app } from '../firebase'; 3 | import { useDispatch } from 'react-redux'; 4 | import { signInSuccess } from '../redux/user/userSlice'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | export default function OAuth() { 8 | const dispatch = useDispatch(); 9 | const navigate = useNavigate(); 10 | const handleGoogleClick = async () => { 11 | try { 12 | const provider = new GoogleAuthProvider(); 13 | const auth = getAuth(app); 14 | 15 | const result = await signInWithPopup(auth, provider); 16 | 17 | const res = await fetch('/api/auth/google', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | body: JSON.stringify({ 23 | name: result.user.displayName, 24 | email: result.user.email, 25 | photo: result.user.photoURL, 26 | }), 27 | }); 28 | const data = await res.json(); 29 | dispatch(signInSuccess(data)); 30 | navigate('/'); 31 | } catch (error) { 32 | console.log('could not sign in with google', error); 33 | } 34 | }; 35 | return ( 36 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /client/src/components/Contact.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function Contact({ listing }) { 5 | const [landlord, setLandlord] = useState(null); 6 | const [message, setMessage] = useState(''); 7 | const onChange = (e) => { 8 | setMessage(e.target.value); 9 | }; 10 | 11 | useEffect(() => { 12 | const fetchLandlord = async () => { 13 | try { 14 | const res = await fetch(`/api/user/${listing.userRef}`); 15 | const data = await res.json(); 16 | setLandlord(data); 17 | } catch (error) { 18 | console.log(error); 19 | } 20 | }; 21 | fetchLandlord(); 22 | }, [listing.userRef]); 23 | return ( 24 | <> 25 | {landlord && ( 26 |
27 |

28 | Contact {landlord.username}{' '} 29 | for{' '} 30 | {listing.name.toLowerCase()} 31 |

32 | 41 | 42 | 46 | Send Message 47 | 48 |
49 | )} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /client/src/redux/user/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | currentUser: null, 5 | error: null, 6 | loading: false, 7 | }; 8 | 9 | const userSlice = createSlice({ 10 | name: 'user', 11 | initialState, 12 | reducers: { 13 | signInStart: (state) => { 14 | state.loading = true; 15 | }, 16 | signInSuccess: (state, action) => { 17 | state.currentUser = action.payload; 18 | state.loading = false; 19 | state.error = null; 20 | }, 21 | signInFailure: (state, action) => { 22 | state.error = action.payload; 23 | state.loading = false; 24 | }, 25 | updateUserStart: (state) => { 26 | state.loading = true; 27 | }, 28 | updateUserSuccess: (state, action) => { 29 | state.currentUser = action.payload; 30 | state.loading = false; 31 | state.error = null; 32 | }, 33 | updateUserFailure: (state, action) => { 34 | state.error = action.payload; 35 | state.loading = false; 36 | }, 37 | deleteUserStart: (state) => { 38 | state.loading = true; 39 | }, 40 | deleteUserSuccess: (state) => { 41 | state.currentUser = null; 42 | state.loading = false; 43 | state.error = null; 44 | }, 45 | deleteUserFailure: (state, action) => { 46 | state.error = action.payload; 47 | state.loading = false; 48 | }, 49 | signOutUserStart: (state) => { 50 | state.loading = true; 51 | }, 52 | signOutUserSuccess: (state) => { 53 | state.currentUser = null; 54 | state.loading = false; 55 | state.error = null; 56 | }, 57 | signOutUserFailure: (state, action) => { 58 | state.error = action.payload; 59 | state.loading = false; 60 | }, 61 | }, 62 | }); 63 | 64 | export const { 65 | signInStart, 66 | signInSuccess, 67 | signInFailure, 68 | updateUserFailure, 69 | updateUserSuccess, 70 | updateUserStart, 71 | deleteUserFailure, 72 | deleteUserSuccess, 73 | deleteUserStart, 74 | signOutUserFailure, 75 | signOutUserSuccess, 76 | signOutUserStart, 77 | } = userSlice.actions; 78 | 79 | export default userSlice.reducer; 80 | -------------------------------------------------------------------------------- /client/src/components/ListingItem.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { MdLocationOn } from 'react-icons/md'; 3 | 4 | export default function ListingItem({ listing }) { 5 | return ( 6 |
7 | 8 | listing cover 16 |
17 |

18 | {listing.name} 19 |

20 |
21 | 22 |

23 | {listing.address} 24 |

25 |
26 |

27 | {listing.description} 28 |

29 |

30 | $ 31 | {listing.offer 32 | ? listing.discountPrice.toLocaleString('en-US') 33 | : listing.regularPrice.toLocaleString('en-US')} 34 | {listing.type === 'rent' && ' / month'} 35 |

36 |
37 |
38 | {listing.bedrooms > 1 39 | ? `${listing.bedrooms} beds ` 40 | : `${listing.bedrooms} bed `} 41 |
42 |
43 | {listing.bathrooms > 1 44 | ? `${listing.bathrooms} baths ` 45 | : `${listing.bathrooms} bath `} 46 |
47 |
48 |
49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /api/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import bcryptjs from 'bcryptjs'; 2 | import User from '../models/user.model.js'; 3 | import { errorHandler } from '../utils/error.js'; 4 | import Listing from '../models/listing.model.js'; 5 | 6 | export const test = (req, res) => { 7 | res.json({ 8 | message: 'Api route is working!', 9 | }); 10 | }; 11 | 12 | export const updateUser = async (req, res, next) => { 13 | if (req.user.id !== req.params.id) 14 | return next(errorHandler(401, 'You can only update your own account!')); 15 | try { 16 | if (req.body.password) { 17 | req.body.password = bcryptjs.hashSync(req.body.password, 10); 18 | } 19 | 20 | const updatedUser = await User.findByIdAndUpdate( 21 | req.params.id, 22 | { 23 | $set: { 24 | username: req.body.username, 25 | email: req.body.email, 26 | password: req.body.password, 27 | avatar: req.body.avatar, 28 | }, 29 | }, 30 | { new: true } 31 | ); 32 | 33 | const { password, ...rest } = updatedUser._doc; 34 | 35 | res.status(200).json(rest); 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | 41 | export const deleteUser = async (req, res, next) => { 42 | if (req.user.id !== req.params.id) 43 | return next(errorHandler(401, 'You can only delete your own account!')); 44 | try { 45 | await User.findByIdAndDelete(req.params.id); 46 | res.clearCookie('access_token'); 47 | res.status(200).json('User has been deleted!'); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | 53 | export const getUserListings = async (req, res, next) => { 54 | if (req.user.id === req.params.id) { 55 | try { 56 | const listings = await Listing.find({ userRef: req.params.id }); 57 | res.status(200).json(listings); 58 | } catch (error) { 59 | next(error); 60 | } 61 | } else { 62 | return next(errorHandler(401, 'You can only view your own listings!')); 63 | } 64 | }; 65 | 66 | export const getUser = async (req, res, next) => { 67 | try { 68 | 69 | const user = await User.findById(req.params.id); 70 | 71 | if (!user) return next(errorHandler(404, 'User not found!')); 72 | 73 | const { password: pass, ...rest } = user._doc; 74 | 75 | res.status(200).json(rest); 76 | } catch (error) { 77 | next(error); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /client/src/pages/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { 5 | signInStart, 6 | signInSuccess, 7 | signInFailure, 8 | } from '../redux/user/userSlice'; 9 | import OAuth from '../components/OAuth'; 10 | 11 | export default function SignIn() { 12 | const [formData, setFormData] = useState({}); 13 | const { loading, error } = useSelector((state) => state.user); 14 | const navigate = useNavigate(); 15 | const dispatch = useDispatch(); 16 | const handleChange = (e) => { 17 | setFormData({ 18 | ...formData, 19 | [e.target.id]: e.target.value, 20 | }); 21 | }; 22 | const handleSubmit = async (e) => { 23 | e.preventDefault(); 24 | try { 25 | dispatch(signInStart()); 26 | const res = await fetch('/api/auth/signin', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify(formData), 32 | }); 33 | const data = await res.json(); 34 | console.log(data); 35 | if (data.success === false) { 36 | dispatch(signInFailure(data.message)); 37 | return; 38 | } 39 | dispatch(signInSuccess(data)); 40 | navigate('/'); 41 | } catch (error) { 42 | dispatch(signInFailure(error.message)); 43 | } 44 | }; 45 | return ( 46 |
47 |

Sign In

48 |
49 | 56 | 63 | 64 | 70 | 71 | 72 |
73 |

Dont have an account?

74 | 75 | Sign up 76 | 77 |
78 | {error &&

{error}

} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /client/src/pages/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import OAuth from '../components/OAuth'; 4 | 5 | export default function SignUp() { 6 | const [formData, setFormData] = useState({}); 7 | const [error, setError] = useState(null); 8 | const [loading, setLoading] = useState(false); 9 | const navigate = useNavigate(); 10 | const handleChange = (e) => { 11 | setFormData({ 12 | ...formData, 13 | [e.target.id]: e.target.value, 14 | }); 15 | }; 16 | const handleSubmit = async (e) => { 17 | e.preventDefault(); 18 | try { 19 | setLoading(true); 20 | const res = await fetch('/api/auth/signup', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify(formData), 26 | }); 27 | const data = await res.json(); 28 | console.log(data); 29 | if (data.success === false) { 30 | setLoading(false); 31 | setError(data.message); 32 | return; 33 | } 34 | setLoading(false); 35 | setError(null); 36 | navigate('/sign-in'); 37 | } catch (error) { 38 | setLoading(false); 39 | setError(error.message); 40 | } 41 | }; 42 | return ( 43 |
44 |

Sign Up

45 |
46 | 53 | 60 | 67 | 68 | 74 | 75 | 76 |
77 |

Have an account?

78 | 79 | Sign in 80 | 81 |
82 | {error &&

{error}

} 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /client/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { FaSearch } from 'react-icons/fa'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useSelector } from 'react-redux'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | export default function Header() { 7 | const { currentUser } = useSelector((state) => state.user); 8 | const [searchTerm, setSearchTerm] = useState(''); 9 | const navigate = useNavigate(); 10 | const handleSubmit = (e) => { 11 | e.preventDefault(); 12 | const urlParams = new URLSearchParams(window.location.search); 13 | urlParams.set('searchTerm', searchTerm); 14 | const searchQuery = urlParams.toString(); 15 | navigate(`/search?${searchQuery}`); 16 | }; 17 | 18 | useEffect(() => { 19 | const urlParams = new URLSearchParams(location.search); 20 | const searchTermFromUrl = urlParams.get('searchTerm'); 21 | if (searchTermFromUrl) { 22 | setSearchTerm(searchTermFromUrl); 23 | } 24 | }, [location.search]); 25 | return ( 26 |
27 |
28 | 29 |

30 | Sahand 31 | Estate 32 |

33 | 34 |
38 | setSearchTerm(e.target.value)} 44 | /> 45 | 48 |
49 |
    50 | 51 |
  • 52 | Home 53 |
  • 54 | 55 | 56 |
  • 57 | About 58 |
  • 59 | 60 | 61 | {currentUser ? ( 62 | profile 67 | ) : ( 68 |
  • Sign in
  • 69 | )} 70 | 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /api/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model.js'; 2 | import bcryptjs from 'bcryptjs'; 3 | import { errorHandler } from '../utils/error.js'; 4 | import jwt from 'jsonwebtoken'; 5 | 6 | export const signup = async (req, res, next) => { 7 | const { username, email, password } = req.body; 8 | const hashedPassword = bcryptjs.hashSync(password, 10); 9 | const newUser = new User({ username, email, password: hashedPassword }); 10 | try { 11 | await newUser.save(); 12 | res.status(201).json('User created successfully!'); 13 | } catch (error) { 14 | next(error); 15 | } 16 | }; 17 | 18 | export const signin = async (req, res, next) => { 19 | const { email, password } = req.body; 20 | try { 21 | const validUser = await User.findOne({ email }); 22 | if (!validUser) return next(errorHandler(404, 'User not found!')); 23 | const validPassword = bcryptjs.compareSync(password, validUser.password); 24 | if (!validPassword) return next(errorHandler(401, 'Wrong credentials!')); 25 | const token = jwt.sign({ id: validUser._id }, process.env.JWT_SECRET); 26 | const { password: pass, ...rest } = validUser._doc; 27 | res 28 | .cookie('access_token', token, { httpOnly: true }) 29 | .status(200) 30 | .json(rest); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }; 35 | 36 | export const google = async (req, res, next) => { 37 | try { 38 | const user = await User.findOne({ email: req.body.email }); 39 | if (user) { 40 | const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET); 41 | const { password: pass, ...rest } = user._doc; 42 | res 43 | .cookie('access_token', token, { httpOnly: true }) 44 | .status(200) 45 | .json(rest); 46 | } else { 47 | const generatedPassword = 48 | Math.random().toString(36).slice(-8) + 49 | Math.random().toString(36).slice(-8); 50 | const hashedPassword = bcryptjs.hashSync(generatedPassword, 10); 51 | const newUser = new User({ 52 | username: 53 | req.body.name.split(' ').join('').toLowerCase() + 54 | Math.random().toString(36).slice(-4), 55 | email: req.body.email, 56 | password: hashedPassword, 57 | avatar: req.body.photo, 58 | }); 59 | await newUser.save(); 60 | const token = jwt.sign({ id: newUser._id }, process.env.JWT_SECRET); 61 | const { password: pass, ...rest } = newUser._doc; 62 | res 63 | .cookie('access_token', token, { httpOnly: true }) 64 | .status(200) 65 | .json(rest); 66 | } 67 | } catch (error) { 68 | next(error); 69 | } 70 | }; 71 | 72 | export const signOut = async (req, res, next) => { 73 | try { 74 | res.clearCookie('access_token'); 75 | res.status(200).json('User has been logged out!'); 76 | } catch (error) { 77 | next(error); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /api/controllers/listing.controller.js: -------------------------------------------------------------------------------- 1 | import Listing from '../models/listing.model.js'; 2 | import { errorHandler } from '../utils/error.js'; 3 | 4 | export const createListing = async (req, res, next) => { 5 | try { 6 | const listing = await Listing.create(req.body); 7 | return res.status(201).json(listing); 8 | } catch (error) { 9 | next(error); 10 | } 11 | }; 12 | 13 | export const deleteListing = async (req, res, next) => { 14 | const listing = await Listing.findById(req.params.id); 15 | 16 | if (!listing) { 17 | return next(errorHandler(404, 'Listing not found!')); 18 | } 19 | 20 | if (req.user.id !== listing.userRef) { 21 | return next(errorHandler(401, 'You can only delete your own listings!')); 22 | } 23 | 24 | try { 25 | await Listing.findByIdAndDelete(req.params.id); 26 | res.status(200).json('Listing has been deleted!'); 27 | } catch (error) { 28 | next(error); 29 | } 30 | }; 31 | 32 | export const updateListing = async (req, res, next) => { 33 | const listing = await Listing.findById(req.params.id); 34 | if (!listing) { 35 | return next(errorHandler(404, 'Listing not found!')); 36 | } 37 | if (req.user.id !== listing.userRef) { 38 | return next(errorHandler(401, 'You can only update your own listings!')); 39 | } 40 | 41 | try { 42 | const updatedListing = await Listing.findByIdAndUpdate( 43 | req.params.id, 44 | req.body, 45 | { new: true } 46 | ); 47 | res.status(200).json(updatedListing); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | 53 | export const getListing = async (req, res, next) => { 54 | try { 55 | const listing = await Listing.findById(req.params.id); 56 | if (!listing) { 57 | return next(errorHandler(404, 'Listing not found!')); 58 | } 59 | res.status(200).json(listing); 60 | } catch (error) { 61 | next(error); 62 | } 63 | }; 64 | 65 | export const getListings = async (req, res, next) => { 66 | try { 67 | const limit = parseInt(req.query.limit) || 9; 68 | const startIndex = parseInt(req.query.startIndex) || 0; 69 | let offer = req.query.offer; 70 | 71 | if (offer === undefined || offer === 'false') { 72 | offer = { $in: [false, true] }; 73 | } 74 | 75 | let furnished = req.query.furnished; 76 | 77 | if (furnished === undefined || furnished === 'false') { 78 | furnished = { $in: [false, true] }; 79 | } 80 | 81 | let parking = req.query.parking; 82 | 83 | if (parking === undefined || parking === 'false') { 84 | parking = { $in: [false, true] }; 85 | } 86 | 87 | let type = req.query.type; 88 | 89 | if (type === undefined || type === 'all') { 90 | type = { $in: ['sale', 'rent'] }; 91 | } 92 | 93 | const searchTerm = req.query.searchTerm || ''; 94 | 95 | const sort = req.query.sort || 'createdAt'; 96 | 97 | const order = req.query.order || 'desc'; 98 | 99 | const listings = await Listing.find({ 100 | name: { $regex: searchTerm, $options: 'i' }, 101 | offer, 102 | furnished, 103 | parking, 104 | type, 105 | }) 106 | .sort({ [sort]: order }) 107 | .limit(limit) 108 | .skip(startIndex); 109 | 110 | return res.status(200).json(listings); 111 | } catch (error) { 112 | next(error); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /client/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Swiper, SwiperSlide } from 'swiper/react'; 4 | import { Navigation } from 'swiper/modules'; 5 | import SwiperCore from 'swiper'; 6 | import 'swiper/css/bundle'; 7 | import ListingItem from '../components/ListingItem'; 8 | 9 | export default function Home() { 10 | const [offerListings, setOfferListings] = useState([]); 11 | const [saleListings, setSaleListings] = useState([]); 12 | const [rentListings, setRentListings] = useState([]); 13 | SwiperCore.use([Navigation]); 14 | console.log(offerListings); 15 | useEffect(() => { 16 | const fetchOfferListings = async () => { 17 | try { 18 | const res = await fetch('/api/listing/get?offer=true&limit=4'); 19 | const data = await res.json(); 20 | setOfferListings(data); 21 | fetchRentListings(); 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | }; 26 | const fetchRentListings = async () => { 27 | try { 28 | const res = await fetch('/api/listing/get?type=rent&limit=4'); 29 | const data = await res.json(); 30 | setRentListings(data); 31 | fetchSaleListings(); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | }; 36 | 37 | const fetchSaleListings = async () => { 38 | try { 39 | const res = await fetch('/api/listing/get?type=sale&limit=4'); 40 | const data = await res.json(); 41 | setSaleListings(data); 42 | } catch (error) { 43 | log(error); 44 | } 45 | }; 46 | fetchOfferListings(); 47 | }, []); 48 | return ( 49 |
50 | {/* top */} 51 |
52 |

53 | Find your next perfect 54 |
55 | place with ease 56 |

57 |
58 | Sahand Estate is the best place to find your next perfect place to 59 | live. 60 |
61 | We have a wide range of properties for you to choose from. 62 |
63 | 67 | Let's get started... 68 | 69 |
70 | 71 | {/* swiper */} 72 | 73 | {offerListings && 74 | offerListings.length > 0 && 75 | offerListings.map((listing) => ( 76 | 77 |
85 |
86 | ))} 87 |
88 | 89 | {/* listing results for offer, sale and rent */} 90 | 91 |
92 | {offerListings && offerListings.length > 0 && ( 93 |
94 |
95 |

Recent offers

96 | Show more offers 97 |
98 |
99 | {offerListings.map((listing) => ( 100 | 101 | ))} 102 |
103 |
104 | )} 105 | {rentListings && rentListings.length > 0 && ( 106 |
107 |
108 |

Recent places for rent

109 | Show more places for rent 110 |
111 |
112 | {rentListings.map((listing) => ( 113 | 114 | ))} 115 |
116 |
117 | )} 118 | {saleListings && saleListings.length > 0 && ( 119 |
120 |
121 |

Recent places for sale

122 | Show more places for sale 123 |
124 |
125 | {saleListings.map((listing) => ( 126 | 127 | ))} 128 |
129 |
130 | )} 131 |
132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /client/src/pages/Listing.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { Swiper, SwiperSlide } from 'swiper/react'; 4 | import SwiperCore from 'swiper'; 5 | import { useSelector } from 'react-redux'; 6 | import { Navigation } from 'swiper/modules'; 7 | import 'swiper/css/bundle'; 8 | import { 9 | FaBath, 10 | FaBed, 11 | FaChair, 12 | FaMapMarkedAlt, 13 | FaMapMarkerAlt, 14 | FaParking, 15 | FaShare, 16 | } from 'react-icons/fa'; 17 | import Contact from '../components/Contact'; 18 | 19 | // https://sabe.io/blog/javascript-format-numbers-commas#:~:text=The%20best%20way%20to%20format,format%20the%20number%20with%20commas. 20 | 21 | export default function Listing() { 22 | SwiperCore.use([Navigation]); 23 | const [listing, setListing] = useState(null); 24 | const [loading, setLoading] = useState(false); 25 | const [error, setError] = useState(false); 26 | const [copied, setCopied] = useState(false); 27 | const [contact, setContact] = useState(false); 28 | const params = useParams(); 29 | const { currentUser } = useSelector((state) => state.user); 30 | 31 | useEffect(() => { 32 | const fetchListing = async () => { 33 | try { 34 | setLoading(true); 35 | const res = await fetch(`/api/listing/get/${params.listingId}`); 36 | const data = await res.json(); 37 | if (data.success === false) { 38 | setError(true); 39 | setLoading(false); 40 | return; 41 | } 42 | setListing(data); 43 | setLoading(false); 44 | setError(false); 45 | } catch (error) { 46 | setError(true); 47 | setLoading(false); 48 | } 49 | }; 50 | fetchListing(); 51 | }, [params.listingId]); 52 | 53 | return ( 54 |
55 | {loading &&

Loading...

} 56 | {error && ( 57 |

Something went wrong!

58 | )} 59 | {listing && !loading && !error && ( 60 |
61 | 62 | {listing.imageUrls.map((url) => ( 63 | 64 |
71 |
72 | ))} 73 |
74 |
75 | { 78 | navigator.clipboard.writeText(window.location.href); 79 | setCopied(true); 80 | setTimeout(() => { 81 | setCopied(false); 82 | }, 2000); 83 | }} 84 | /> 85 |
86 | {copied && ( 87 |

88 | Link copied! 89 |

90 | )} 91 |
92 |

93 | {listing.name} - ${' '} 94 | {listing.offer 95 | ? listing.discountPrice.toLocaleString('en-US') 96 | : listing.regularPrice.toLocaleString('en-US')} 97 | {listing.type === 'rent' && ' / month'} 98 |

99 |

100 | 101 | {listing.address} 102 |

103 |
104 |

105 | {listing.type === 'rent' ? 'For Rent' : 'For Sale'} 106 |

107 | {listing.offer && ( 108 |

109 | ${+listing.regularPrice - +listing.discountPrice} OFF 110 |

111 | )} 112 |
113 |

114 | Description - 115 | {listing.description} 116 |

117 |
    118 |
  • 119 | 120 | {listing.bedrooms > 1 121 | ? `${listing.bedrooms} beds ` 122 | : `${listing.bedrooms} bed `} 123 |
  • 124 |
  • 125 | 126 | {listing.bathrooms > 1 127 | ? `${listing.bathrooms} baths ` 128 | : `${listing.bathrooms} bath `} 129 |
  • 130 |
  • 131 | 132 | {listing.parking ? 'Parking spot' : 'No Parking'} 133 |
  • 134 |
  • 135 | 136 | {listing.furnished ? 'Furnished' : 'Unfurnished'} 137 |
  • 138 |
139 | {currentUser && listing.userRef !== currentUser._id && !contact && ( 140 | 146 | )} 147 | {contact && } 148 |
149 |
150 | )} 151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /client/src/pages/Search.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import ListingItem from '../components/ListingItem'; 4 | 5 | export default function Search() { 6 | const navigate = useNavigate(); 7 | const [sidebardata, setSidebardata] = useState({ 8 | searchTerm: '', 9 | type: 'all', 10 | parking: false, 11 | furnished: false, 12 | offer: false, 13 | sort: 'created_at', 14 | order: 'desc', 15 | }); 16 | 17 | const [loading, setLoading] = useState(false); 18 | const [listings, setListings] = useState([]); 19 | const [showMore, setShowMore] = useState(false); 20 | 21 | useEffect(() => { 22 | const urlParams = new URLSearchParams(location.search); 23 | const searchTermFromUrl = urlParams.get('searchTerm'); 24 | const typeFromUrl = urlParams.get('type'); 25 | const parkingFromUrl = urlParams.get('parking'); 26 | const furnishedFromUrl = urlParams.get('furnished'); 27 | const offerFromUrl = urlParams.get('offer'); 28 | const sortFromUrl = urlParams.get('sort'); 29 | const orderFromUrl = urlParams.get('order'); 30 | 31 | if ( 32 | searchTermFromUrl || 33 | typeFromUrl || 34 | parkingFromUrl || 35 | furnishedFromUrl || 36 | offerFromUrl || 37 | sortFromUrl || 38 | orderFromUrl 39 | ) { 40 | setSidebardata({ 41 | searchTerm: searchTermFromUrl || '', 42 | type: typeFromUrl || 'all', 43 | parking: parkingFromUrl === 'true' ? true : false, 44 | furnished: furnishedFromUrl === 'true' ? true : false, 45 | offer: offerFromUrl === 'true' ? true : false, 46 | sort: sortFromUrl || 'created_at', 47 | order: orderFromUrl || 'desc', 48 | }); 49 | } 50 | 51 | const fetchListings = async () => { 52 | setLoading(true); 53 | setShowMore(false); 54 | const searchQuery = urlParams.toString(); 55 | const res = await fetch(`/api/listing/get?${searchQuery}`); 56 | const data = await res.json(); 57 | if (data.length > 8) { 58 | setShowMore(true); 59 | } else { 60 | setShowMore(false); 61 | } 62 | setListings(data); 63 | setLoading(false); 64 | }; 65 | 66 | fetchListings(); 67 | }, [location.search]); 68 | 69 | const handleChange = (e) => { 70 | if ( 71 | e.target.id === 'all' || 72 | e.target.id === 'rent' || 73 | e.target.id === 'sale' 74 | ) { 75 | setSidebardata({ ...sidebardata, type: e.target.id }); 76 | } 77 | 78 | if (e.target.id === 'searchTerm') { 79 | setSidebardata({ ...sidebardata, searchTerm: e.target.value }); 80 | } 81 | 82 | if ( 83 | e.target.id === 'parking' || 84 | e.target.id === 'furnished' || 85 | e.target.id === 'offer' 86 | ) { 87 | setSidebardata({ 88 | ...sidebardata, 89 | [e.target.id]: 90 | e.target.checked || e.target.checked === 'true' ? true : false, 91 | }); 92 | } 93 | 94 | if (e.target.id === 'sort_order') { 95 | const sort = e.target.value.split('_')[0] || 'created_at'; 96 | 97 | const order = e.target.value.split('_')[1] || 'desc'; 98 | 99 | setSidebardata({ ...sidebardata, sort, order }); 100 | } 101 | }; 102 | 103 | const handleSubmit = (e) => { 104 | e.preventDefault(); 105 | const urlParams = new URLSearchParams(); 106 | urlParams.set('searchTerm', sidebardata.searchTerm); 107 | urlParams.set('type', sidebardata.type); 108 | urlParams.set('parking', sidebardata.parking); 109 | urlParams.set('furnished', sidebardata.furnished); 110 | urlParams.set('offer', sidebardata.offer); 111 | urlParams.set('sort', sidebardata.sort); 112 | urlParams.set('order', sidebardata.order); 113 | const searchQuery = urlParams.toString(); 114 | navigate(`/search?${searchQuery}`); 115 | }; 116 | 117 | const onShowMoreClick = async () => { 118 | const numberOfListings = listings.length; 119 | const startIndex = numberOfListings; 120 | const urlParams = new URLSearchParams(location.search); 121 | urlParams.set('startIndex', startIndex); 122 | const searchQuery = urlParams.toString(); 123 | const res = await fetch(`/api/listing/get?${searchQuery}`); 124 | const data = await res.json(); 125 | if (data.length < 9) { 126 | setShowMore(false); 127 | } 128 | setListings([...listings, ...data]); 129 | }; 130 | return ( 131 |
132 |
133 |
134 |
135 | 138 | 146 |
147 |
148 | 149 |
150 | 157 | Rent & Sale 158 |
159 |
160 | 167 | Rent 168 |
169 |
170 | 177 | Sale 178 |
179 |
180 | 187 | Offer 188 |
189 |
190 |
191 | 192 |
193 | 200 | Parking 201 |
202 |
203 | 210 | Furnished 211 |
212 |
213 |
214 | 215 | 226 |
227 | 230 |
231 |
232 |
233 |

234 | Listing results: 235 |

236 |
237 | {!loading && listings.length === 0 && ( 238 |

No listing found!

239 | )} 240 | {loading && ( 241 |

242 | Loading... 243 |

244 | )} 245 | 246 | {!loading && 247 | listings && 248 | listings.map((listing) => ( 249 | 250 | ))} 251 | 252 | {showMore && ( 253 | 259 | )} 260 |
261 |
262 |
263 | ); 264 | } 265 | -------------------------------------------------------------------------------- /client/src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { useRef, useState, useEffect } from 'react'; 3 | import { 4 | getDownloadURL, 5 | getStorage, 6 | ref, 7 | uploadBytesResumable, 8 | } from 'firebase/storage'; 9 | import { app } from '../firebase'; 10 | import { 11 | updateUserStart, 12 | updateUserSuccess, 13 | updateUserFailure, 14 | deleteUserFailure, 15 | deleteUserStart, 16 | deleteUserSuccess, 17 | signOutUserStart, 18 | } from '../redux/user/userSlice'; 19 | import { useDispatch } from 'react-redux'; 20 | import { Link } from 'react-router-dom'; 21 | export default function Profile() { 22 | const fileRef = useRef(null); 23 | const { currentUser, loading, error } = useSelector((state) => state.user); 24 | const [file, setFile] = useState(undefined); 25 | const [filePerc, setFilePerc] = useState(0); 26 | const [fileUploadError, setFileUploadError] = useState(false); 27 | const [formData, setFormData] = useState({}); 28 | const [updateSuccess, setUpdateSuccess] = useState(false); 29 | const [showListingsError, setShowListingsError] = useState(false); 30 | const [userListings, setUserListings] = useState([]); 31 | const dispatch = useDispatch(); 32 | 33 | // firebase storage 34 | // allow read; 35 | // allow write: if 36 | // request.resource.size < 2 * 1024 * 1024 && 37 | // request.resource.contentType.matches('image/.*') 38 | 39 | useEffect(() => { 40 | if (file) { 41 | handleFileUpload(file); 42 | } 43 | }, [file]); 44 | 45 | const handleFileUpload = (file) => { 46 | const storage = getStorage(app); 47 | const fileName = new Date().getTime() + file.name; 48 | const storageRef = ref(storage, fileName); 49 | const uploadTask = uploadBytesResumable(storageRef, file); 50 | 51 | uploadTask.on( 52 | 'state_changed', 53 | (snapshot) => { 54 | const progress = 55 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 56 | setFilePerc(Math.round(progress)); 57 | }, 58 | (error) => { 59 | setFileUploadError(true); 60 | }, 61 | () => { 62 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => 63 | setFormData({ ...formData, avatar: downloadURL }) 64 | ); 65 | } 66 | ); 67 | }; 68 | 69 | const handleChange = (e) => { 70 | setFormData({ ...formData, [e.target.id]: e.target.value }); 71 | }; 72 | 73 | const handleSubmit = async (e) => { 74 | e.preventDefault(); 75 | try { 76 | dispatch(updateUserStart()); 77 | const res = await fetch(`/api/user/update/${currentUser._id}`, { 78 | method: 'POST', 79 | headers: { 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify(formData), 83 | }); 84 | const data = await res.json(); 85 | if (data.success === false) { 86 | dispatch(updateUserFailure(data.message)); 87 | return; 88 | } 89 | 90 | dispatch(updateUserSuccess(data)); 91 | setUpdateSuccess(true); 92 | } catch (error) { 93 | dispatch(updateUserFailure(error.message)); 94 | } 95 | }; 96 | 97 | const handleDeleteUser = async () => { 98 | try { 99 | dispatch(deleteUserStart()); 100 | const res = await fetch(`/api/user/delete/${currentUser._id}`, { 101 | method: 'DELETE', 102 | }); 103 | const data = await res.json(); 104 | if (data.success === false) { 105 | dispatch(deleteUserFailure(data.message)); 106 | return; 107 | } 108 | dispatch(deleteUserSuccess(data)); 109 | } catch (error) { 110 | dispatch(deleteUserFailure(error.message)); 111 | } 112 | }; 113 | 114 | const handleSignOut = async () => { 115 | try { 116 | dispatch(signOutUserStart()); 117 | const res = await fetch('/api/auth/signout'); 118 | const data = await res.json(); 119 | if (data.success === false) { 120 | dispatch(deleteUserFailure(data.message)); 121 | return; 122 | } 123 | dispatch(deleteUserSuccess(data)); 124 | } catch (error) { 125 | dispatch(deleteUserFailure(data.message)); 126 | } 127 | }; 128 | 129 | const handleShowListings = async () => { 130 | try { 131 | setShowListingsError(false); 132 | const res = await fetch(`/api/user/listings/${currentUser._id}`); 133 | const data = await res.json(); 134 | if (data.success === false) { 135 | setShowListingsError(true); 136 | return; 137 | } 138 | 139 | setUserListings(data); 140 | } catch (error) { 141 | setShowListingsError(true); 142 | } 143 | }; 144 | 145 | const handleListingDelete = async (listingId) => { 146 | try { 147 | const res = await fetch(`/api/listing/delete/${listingId}`, { 148 | method: 'DELETE', 149 | }); 150 | const data = await res.json(); 151 | if (data.success === false) { 152 | console.log(data.message); 153 | return; 154 | } 155 | 156 | setUserListings((prev) => 157 | prev.filter((listing) => listing._id !== listingId) 158 | ); 159 | } catch (error) { 160 | console.log(error.message); 161 | } 162 | }; 163 | return ( 164 |
165 |

Profile

166 |
167 | setFile(e.target.files[0])} 169 | type='file' 170 | ref={fileRef} 171 | hidden 172 | accept='image/*' 173 | /> 174 | fileRef.current.click()} 176 | src={formData.avatar || currentUser.avatar} 177 | alt='profile' 178 | className='rounded-full h-24 w-24 object-cover cursor-pointer self-center mt-2' 179 | /> 180 |

181 | {fileUploadError ? ( 182 | 183 | Error Image upload (image must be less than 2 mb) 184 | 185 | ) : filePerc > 0 && filePerc < 100 ? ( 186 | {`Uploading ${filePerc}%`} 187 | ) : filePerc === 100 ? ( 188 | Image successfully uploaded! 189 | ) : ( 190 | '' 191 | )} 192 |

193 | 201 | 209 | 216 | 222 | 226 | Create Listing 227 | 228 |
229 |
230 | 234 | Delete account 235 | 236 | 237 | Sign out 238 | 239 |
240 | 241 |

{error ? error : ''}

242 |

243 | {updateSuccess ? 'User is updated successfully!' : ''} 244 |

245 | 248 |

249 | {showListingsError ? 'Error showing listings' : ''} 250 |

251 | 252 | {userListings && userListings.length > 0 && ( 253 |
254 |

255 | Your Listings 256 |

257 | {userListings.map((listing) => ( 258 |
262 | 263 | listing cover 268 | 269 | 273 |

{listing.name}

274 | 275 | 276 |
277 | 283 | 284 | 285 | 286 |
287 |
288 | ))} 289 |
290 | )} 291 |
292 | ); 293 | } 294 | -------------------------------------------------------------------------------- /client/src/pages/CreateListing.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { app } from '../firebase'; 3 | import { useSelector } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import axios from 'axios'; 6 | 7 | export default function CreateListing() { 8 | const { currentUser } = useSelector((state) => state.user); 9 | const navigate = useNavigate(); 10 | const [files, setFiles] = useState([]); 11 | const [formData, setFormData] = useState({ 12 | imageUrls: [], 13 | name: '', 14 | description: '', 15 | address: '', 16 | type: 'rent', 17 | bedrooms: 1, 18 | bathrooms: 1, 19 | regularPrice: 50, 20 | discountPrice: 0, 21 | offer: false, 22 | parking: false, 23 | furnished: false, 24 | }); 25 | const [imageUploadError, setImageUploadError] = useState(false); 26 | const [uploading, setUploading] = useState(false); 27 | const [error, setError] = useState(false); 28 | const [loading, setLoading] = useState(false); 29 | console.log(formData); 30 | const handleImageSubmit = (e) => { 31 | if (files.length > 0 && files.length + formData.imageUrls.length < 7) { 32 | setUploading(true); 33 | setImageUploadError(false); 34 | const promises = []; 35 | 36 | for (let i = 0; i < files.length; i++) { 37 | promises.push(storeImage(files[i])); 38 | } 39 | Promise.all(promises) 40 | .then((urls) => { 41 | setFormData({ 42 | ...formData, 43 | imageUrls: formData.imageUrls.concat(urls), 44 | }); 45 | setImageUploadError(false); 46 | setUploading(false); 47 | }) 48 | .catch((err) => { 49 | setImageUploadError('Image upload failed (2 mb max per image)'); 50 | setUploading(false); 51 | }); 52 | } else { 53 | setImageUploadError('You can only upload 6 images per listing'); 54 | setUploading(false); 55 | } 56 | }; 57 | 58 | 59 | const storeImage = async (file) => { 60 | return new Promise(async (resolve, reject) => { 61 | const formData = new FormData(); 62 | formData.append('file', file); 63 | formData.append('upload_preset', 'UPLOAD_PRESET'); // Replace this 64 | formData.append('cloud_name', 'CLOUD_NAME'); // Replace this 65 | 66 | try { 67 | const res = await axios.post( 68 | `https://api.cloudinary.com/v1_1/dflansvri/image/upload`, 69 | formData 70 | ); 71 | resolve(res.data.secure_url); // Cloudinary returns secure_url 72 | } catch (error) { 73 | reject(error); 74 | } 75 | }); 76 | }; 77 | 78 | 79 | const handleRemoveImage = (index) => { 80 | setFormData({ 81 | ...formData, 82 | imageUrls: formData.imageUrls.filter((_, i) => i !== index), 83 | }); 84 | }; 85 | 86 | const handleChange = (e) => { 87 | if (e.target.id === 'sale' || e.target.id === 'rent') { 88 | setFormData({ 89 | ...formData, 90 | type: e.target.id, 91 | }); 92 | } 93 | 94 | if ( 95 | e.target.id === 'parking' || 96 | e.target.id === 'furnished' || 97 | e.target.id === 'offer' 98 | ) { 99 | setFormData({ 100 | ...formData, 101 | [e.target.id]: e.target.checked, 102 | }); 103 | } 104 | 105 | if ( 106 | e.target.type === 'number' || 107 | e.target.type === 'text' || 108 | e.target.type === 'textarea' 109 | ) { 110 | setFormData({ 111 | ...formData, 112 | [e.target.id]: e.target.value, 113 | }); 114 | } 115 | }; 116 | 117 | const handleSubmit = async (e) => { 118 | e.preventDefault(); 119 | try { 120 | if (formData.imageUrls.length < 1) 121 | return setError('You must upload at least one image'); 122 | if (+formData.regularPrice < +formData.discountPrice) 123 | return setError('Discount price must be lower than regular price'); 124 | setLoading(true); 125 | setError(false); 126 | const res = await fetch('/api/listing/create', { 127 | method: 'POST', 128 | headers: { 129 | 'Content-Type': 'application/json', 130 | }, 131 | body: JSON.stringify({ 132 | ...formData, 133 | userRef: currentUser._id, 134 | }), 135 | }); 136 | const data = await res.json(); 137 | setLoading(false); 138 | if (data.success === false) { 139 | setError(data.message); 140 | } 141 | navigate(`/listing/${data._id}`); 142 | } catch (error) { 143 | setError(error.message); 144 | setLoading(false); 145 | } 146 | }; 147 | return ( 148 |
149 |

150 | Create a Listing 151 |

152 |
153 |
154 | 165 |