├── src
├── components
│ ├── themeColor.js
│ ├── Loading.jsx
│ ├── PrivateRoute.jsx
│ ├── Theme.jsx
│ ├── Singup.jsx
│ ├── ProfileOption.jsx
│ ├── OAuth.jsx
│ ├── SingIn.jsx
│ ├── SocketConnection.jsx
│ ├── mobileMenu.jsx
│ ├── Contact.jsx
│ ├── ListingCard.jsx
│ ├── PostCard.jsx
│ ├── RentListing.jsx
│ ├── SaleListing.jsx
│ ├── OfferedListing.jsx
│ ├── Conversations.jsx
│ ├── Header.jsx
│ ├── Chat.jsx
│ ├── SkletonLoading.jsx
│ └── Footer.jsx
├── redux
│ ├── search
│ │ └── searchSlice.js
│ ├── saveListing
│ │ └── saveListingSlice.js
│ ├── notifications
│ │ └── notificationSlice.js
│ ├── store.js
│ └── user
│ │ └── userSlice.js
├── main.jsx
├── firebase.js
├── App.jsx
├── index.css
└── pages
│ ├── SaveListing.jsx
│ ├── Login.jsx
│ ├── Home.jsx
│ ├── Message.jsx
│ ├── Profile.jsx
│ ├── Search.jsx
│ └── ListingPage.jsx
├── utils
└── apiHooks.js
├── postcss.config.js
├── dist
├── assets
│ ├── ajax-loader-e7b44c86.gif
│ └── slick-12459f22.svg
└── index.html
├── .gitignore
├── vite.config.js
├── index.html
├── .eslintrc.cjs
├── tailwind.config.js
├── README.md
└── package.json
/src/components/themeColor.js:
--------------------------------------------------------------------------------
1 | export const themeColor = '313A67'
2 |
3 |
--------------------------------------------------------------------------------
/utils/apiHooks.js:
--------------------------------------------------------------------------------
1 | export const baseUrl = "https://property-sale-backend.onrender.com/api";
2 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/dist/assets/ajax-loader-e7b44c86.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emoncr/Property-Sale/HEAD/dist/assets/ajax-loader-e7b44c86.gif
--------------------------------------------------------------------------------
/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Loading = () => {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default Loading
--------------------------------------------------------------------------------
/.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 | *.local
12 |
13 | # Editor directories and files
14 | .vscode/*
15 | !.vscode/extensions.json
16 | .idea
17 | .DS_Store
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 | .env
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | // vite.config.js
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 |
5 | export default defineConfig({
6 | server: {
7 | proxy: {
8 | "/api": {
9 | target: "http://localhost:3000",
10 | changeOrigin: true,
11 | secure: false,
12 | },
13 | },
14 | },
15 | plugins: [react()],
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { Navigate, Outlet } from 'react-router-dom'
4 |
5 | const PrivateRoute = () => {
6 | const { currentUser } = useSelector(state => state.user)
7 |
8 | return (
9 | currentUser ? :
10 | )
11 | }
12 |
13 | export default PrivateRoute
--------------------------------------------------------------------------------
/src/redux/search/searchSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | searchTermState: "",
5 | };
6 |
7 | const searchSlice = createSlice({
8 | name: "search",
9 | initialState,
10 | reducers: {
11 | setSearchTermState: (state, action) => {
12 | state.searchTermState = action.payload;
13 | },
14 | },
15 | });
16 |
17 | export const { setSearchTermState, } = searchSlice.actions;
18 |
19 | export default searchSlice.reducer;
20 |
--------------------------------------------------------------------------------
/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 { Provider } from 'react-redux'
6 | import { persistor, store } from './redux/store.js'
7 | import { PersistGate } from 'redux-persist/integration/react'
8 |
9 | ReactDOM.createRoot(document.getElementById('root')).render(
10 |
11 |
12 |
13 |
14 | ,
15 | )
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 | Property Sale
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/Theme.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { AiOutlineSetting } from 'react-icons/ai';
3 |
4 |
5 |
6 |
7 | const Theme = () => {
8 |
9 | return (
10 |
17 | );
18 | };
19 |
20 | export default Theme;
21 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 | Property Sale
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/redux/saveListing/saveListingSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | saveListings: [],
5 | };
6 |
7 | const saveSlice = createSlice({
8 |
9 | name: "saved_listings",
10 | initialState,
11 |
12 | reducers: {
13 | handleSave: (state, action) => {
14 | state.saveListings.push(action.payload);
15 | },
16 | handleLisingRemove: (state, action) => {
17 | state.saveListings = action.payload;
18 | },
19 | clearSavedListing: (state) => {
20 | state.saveListings = [];
21 | },
22 | },
23 | });
24 |
25 | export const { handleSave, handleLisingRemove,clearSavedListing } = saveSlice.actions;
26 |
27 | export default saveSlice.reducer;
28 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | import { themeColor } from "./src/components/themeColor";
4 |
5 | export default {
6 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | heading: ["KoHo", "sans-serif"],
11 | content: ["Open Sans", "sans-serif"],
12 | oswald: ["Oswald", "sans-serif"],
13 | },
14 | colors: {
15 | "brand-blue": `#${themeColor}`,
16 | "ui-bg": "#f1f5f1",
17 | },
18 | boxShadow: {
19 | brand: "0 2px 5px",
20 | },
21 | },
22 | },
23 | darkMode: "media",
24 | plugins: [require("daisyui"), "prettier-plugin-tailwindcss"],
25 | };
26 |
--------------------------------------------------------------------------------
/src/redux/notifications/notificationSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | notificationsDB: [],
5 | conversationActive: { chatId: "" },
6 | };
7 |
8 | const notificationSlice = createSlice({
9 | name: "notification_slice",
10 | initialState,
11 | reducers: {
12 | setNotification: (state, action) => {
13 | state.notificationsDB = action.payload;
14 | },
15 | deleteNotification: (state, action) => {
16 | state.notificationsDB.pop(action.payload);
17 | },
18 | setSingleNotification: (state, action) => {
19 | state.notificationsDB.push(action.payload);
20 | },
21 | },
22 | });
23 |
24 | export const {
25 | setNotification,
26 | deleteNotification,
27 | setSingleNotification,
28 | setConversationActive,
29 | } = notificationSlice.actions;
30 |
31 | export default notificationSlice.reducer;
32 |
--------------------------------------------------------------------------------
/src/firebase.js:
--------------------------------------------------------------------------------
1 | // Import the functions you need from the SDKs you need
2 | import { initializeApp } from "firebase/app";
3 | import { getAnalytics } from "firebase/analytics";
4 | // TODO: Add SDKs for Firebase products that you want to use
5 | // https://firebase.google.com/docs/web/setup#available-libraries
6 |
7 | // Your web app's Firebase configuration
8 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional
9 | const firebaseConfig = {
10 | apiKey: import.meta.env.VITE_FIRIBASE_API_KEY ,
11 | authDomain: "property-sell-401819.firebaseapp.com",
12 | projectId: "property-sell-401819",
13 | storageBucket: "property-sell-401819.appspot.com",
14 | messagingSenderId: "588716927912",
15 | appId: "1:588716927912:web:a6d64c89172800b05a4b9d",
16 | measurementId: "G-NQ79YSEFBT"
17 | };
18 |
19 | // Initialize Firebase
20 | export const firebaseApp = initializeApp(firebaseConfig);
21 | // const analytics = getAnalytics(firebaseApp);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Hi There !
2 |
3 | This is a real estate application
4 | =======
5 | LIVE_URL: https://property-sell.vercel.app/
6 |
7 | Excited to announce the completion of my latest project, 'Property Sale'! 🏡 Built on MERN stack with real-time chat functionality using Socket.io. A platform where you can buy, sell, or rent properties.
8 |
9 | Here is the overview of the project.
10 |
11 | Technology: 🚀
12 | Backend - Node Js, Express Js, Socket Io, Mongoose, Jwt.
13 |
14 | Database - Mongodb.
15 |
16 | Fronted - React Js, React Router Dom, React Hook Form, Firebase, Tailwind Css, Socket Io Client.
17 |
18 |
19 | Features: 🚀
20 | 1. Real time conversation and local notification.
21 | 2. Search and sort result.
22 | 3. Buyer and saler role management.
23 | 4. Authentication with google or email.
24 | 5. Property Image upload facilities.
25 | 6. Pagination for all posts.
26 |
27 | Ahhh😪...there are many more features here is live url and don't forget to check it out.
28 |
29 | Live URL - https://property-sell.vercel.app/
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import userReducer from "../redux/user/userSlice";
2 | import { combineReducers, configureStore } from "@reduxjs/toolkit";
3 | import { persistReducer } from "redux-persist";
4 | import persistStore from "redux-persist/es/persistStore";
5 | import storage from "redux-persist/lib/storage";
6 | import searchSlice from "./search/searchSlice";
7 | import saveListingSlice from "./saveListing/saveListingSlice";
8 | import notificationSlice from "./notifications/notificationSlice";
9 |
10 | //===== Redux Persist's Code ======//
11 | const rootReducer = combineReducers({
12 | user: userReducer,
13 | search: searchSlice,
14 | notification: notificationSlice,
15 | savedListing: saveListingSlice,
16 | });
17 |
18 | const persistConfig = {
19 | key: "root",
20 | storage,
21 | whitelist: ["user", "savedListing"],
22 | };
23 | const persistedReducer = persistReducer(persistConfig, rootReducer);
24 |
25 | //===== Redux Store ======//
26 | export const store = configureStore({
27 | reducer: persistedReducer,
28 |
29 | //==== Middlware for serializable check =====//
30 | middleware: (getDefaultMiddleware) =>
31 | getDefaultMiddleware({
32 | serializableCheck: false,
33 | }),
34 | });
35 |
36 | export const persistor = persistStore(store);
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "property-sell",
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 | "start": "serve -s dist -l $PORT"
12 | },
13 | "dependencies": {
14 | "@preact/signals-react": "^1.3.7",
15 | "@reduxjs/toolkit": "^1.9.7",
16 | "dotenv": "^16.3.1",
17 | "firebase": "^10.5.0",
18 | "js-cookie": "^3.0.5",
19 | "postcss-plugin": "^1.0.0",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-hook-form": "^7.47.0",
23 | "react-icons": "^4.11.0",
24 | "react-redux": "^8.1.3",
25 | "react-router-dom": "^6.16.0",
26 | "react-slick": "^0.29.0",
27 | "react-toastify": "^9.1.3",
28 | "redux-persist": "^6.0.0",
29 | "serve": "^14.2.4",
30 | "slick-carousel": "^1.8.1",
31 | "socket.io-client": "^4.7.2"
32 | },
33 | "devDependencies": {
34 | "@types/react": "^18.2.15",
35 | "@types/react-dom": "^18.2.7",
36 | "@vitejs/plugin-react": "^4.0.3",
37 | "autoprefixer": "^10.4.16",
38 | "daisyui": "^4.0.3",
39 | "eslint": "^8.45.0",
40 | "eslint-plugin-react": "^7.32.2",
41 | "eslint-plugin-react-hooks": "^4.6.0",
42 | "eslint-plugin-react-refresh": "^0.4.3",
43 | "postcss": "^8.2.9",
44 | "prettier": "^3.1.0",
45 | "prettier-plugin-tailwindcss": "^0.5.7",
46 | "tailwindcss": "^3.3.3",
47 | "vite": "^4.4.5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Route, Routes } from 'react-router-dom'
2 | import Home from './pages/Home'
3 | import Login from './pages/Login'
4 | import Header from './components/Header'
5 | import Theme from './components/Theme'
6 | import Profile from './pages/Profile'
7 | import PrivateRoute from './components/PrivateRoute'
8 | import CreatePost from './pages/CreatePost'
9 | import UpdatePost from './pages/UpdatePost'
10 | import ListingPage from './pages/ListingPage'
11 | import SaveListing from './pages/SaveListing'
12 | import Search from './pages/Search'
13 | import Message from './pages/Message'
14 | import SocketConnection from './components/SocketConnection'
15 |
16 |
17 | function App() {
18 | return (
19 | <>
20 |
21 |
22 |
23 | {/* */}
24 |
25 | } />
26 | } />
27 | } />
28 | } />
29 | } />
30 |
31 | {/* /---------Private Routes-----------/ */}
32 | }>
33 | } />
34 | } />
35 | } />
36 | } />
37 | } />
38 |
39 |
40 |
41 | >
42 | )
43 | }
44 |
45 | export default App
46 |
--------------------------------------------------------------------------------
/dist/assets/slick-12459f22.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Generated by Fontastic.me
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/redux/user/userSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | currentUser: null,
5 | loading: false,
6 | signinError: null,
7 | error: null,
8 | };
9 |
10 | const userSlice = createSlice({
11 | name: "user",
12 | initialState,
13 | reducers: {
14 | loddingStart: (state) => {
15 | state.loading = true;
16 | },
17 | signinSuccess: (state, action) => {
18 | state.currentUser = action.payload;
19 | state.loading = false;
20 | state.signinError = false;
21 | },
22 | signinFailed: (state, action) => {
23 | state.signinError = action.payload;
24 | state.loading = false;
25 | },
26 | userUpdateSuccess: (state, action) => {
27 | state.currentUser = action.payload;
28 | state.loading = false;
29 | state.error = false;
30 | },
31 | userUpdateFailed: (state, action) => {
32 | state.loading = false;
33 | state.error = action.payload;
34 | },
35 |
36 | //=====Handlle User Delete State =====//
37 |
38 | userDeleteSuccess: (state) => {
39 | state.loading = false;
40 | state.currentUser = null;
41 | state.error = false;
42 | },
43 | userDeleteFail: (state, action) => {
44 | state.loading = false;
45 | state.error = action.payload;
46 | },
47 |
48 | // Handle Sign Out
49 | signoutSuccess: (state) => {
50 | state.loading = false;
51 | state.currentUser = null;
52 | state.error = false;
53 | },
54 | signoutFailed: (state, action) => {
55 | state.loading = false;
56 | state.error = action.payload;
57 | },
58 |
59 | // HANDLE lISTINS SAVED ITEMS
60 | handleSave: (state, action) => {
61 | state.savedListing.push(action.payload);
62 | },
63 | handleLisingRemove: (state, action) => {
64 | state.savedListing = action.payload;
65 | },
66 | },
67 | });
68 |
69 | export const {
70 | loddingStart,
71 | signinSuccess,
72 | signinFailed,
73 | userUpdateFailed,
74 | userUpdateSuccess,
75 | userDeleteStart,
76 | userDeleteSuccess,
77 | userDeleteFail,
78 | signoutSuccess,
79 | signoutFailed,
80 | handleSave,
81 | handleLisingRemove,
82 | } = userSlice.actions;
83 |
84 | export default userSlice.reducer;
85 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=KoHo:wght@700&family=Open+Sans:wght@400;500;600;700&display=swap");
2 |
3 | body {
4 | @apply bg-[#f1f5f1] min-h-screen scroll-smooth;
5 | }
6 |
7 | #root {
8 | @apply bg-[#f1f5f1] h-full relative;
9 | }
10 |
11 | .container {
12 | @apply max-w-[1400px] mx-auto px-4 sm:px-10;
13 | }
14 | .search {
15 | @apply input w-full h-10 mx-auto border-2 rounded-md border-brand-blue max-w-full sm:max-w-sm focus:outline-0 text-sm font-semibold bg-transparent text-slate-900 placeholder:text-brand-blue placeholder:font-semibold px-3;
16 | }
17 | .search_btn {
18 | @apply absolute right-0 bg-brand-blue h-full w-10 sm:w-14 sm:max-w-[56px] rounded-e-md max-w-[40px] flex items-center justify-center hover:bg-brand-blue/80 duration-300;
19 | }
20 |
21 | /*=================Form styles start here =========== */
22 | .form_input {
23 | @apply py-2 border-0 border-b-[1px] border-slate-300 placeholder:font-content placeholder:font-medium placeholder:text-sm focus:border-brand-blue focus:outline-none px-2 w-full text-sm text-slate-900 font-content font-semibold bg-white;
24 | }
25 |
26 | /*========tostify design==========*/
27 | .Toastify__toast {
28 | @apply bg-brand-blue;
29 | }
30 | .Toastify__toast-body {
31 | @apply text-white;
32 | }
33 | .Toastify__close-button > svg {
34 | @apply text-white;
35 | }
36 |
37 | .scrollbar-hide::-webkit-scrollbar {
38 | display: none;
39 | }
40 |
41 | .scrollbar-hide {
42 | -ms-overflow-style: none;
43 | scrollbar-width: none;
44 | }
45 | .slick-dots li {
46 | @apply bg-white rounded-full;
47 | }
48 | .post_container .slick-list {
49 | @apply pb-11 sm:pb-16;
50 | }
51 | .post_container .slick-slide {
52 | @apply p-3;
53 | }
54 |
55 | .chats_container {
56 | height: calc(100vh - 75px);
57 | }
58 | .message_container {
59 | height: calc(100vh - 200px);
60 | }
61 |
62 | /* Universal Styles */
63 | h1,
64 | h2,
65 | h3,
66 | h4,
67 | h5,
68 | h6 {
69 | @apply text-[#1f2937];
70 | }
71 | p,
72 | label,
73 | li,
74 | span {
75 | @apply text-[#1f2937];
76 | }
77 | .label-text {
78 | @apply !text-[#1f2937];
79 | }
80 | button > span {
81 | @apply text-white;
82 | }
83 | input {
84 | @apply !text-[#1f2937];
85 | }
86 |
87 | @tailwind base;
88 | @tailwind components;
89 | @tailwind utilities;
90 |
--------------------------------------------------------------------------------
/src/components/Singup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useForm } from 'react-hook-form'
3 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
4 |
5 |
6 |
7 | const Singup = ({ userState }) => {
8 | const { setResponseData, setIsformSubmit } = userState;
9 |
10 | const [loading, setLoading] = useState(false)
11 | const {
12 | register,
13 | handleSubmit,
14 | formState: { errors },
15 | } = useForm();
16 |
17 |
18 |
19 | //======handling form submting function =====//
20 | const onSubmit = async (formData) => {
21 | setLoading(true)
22 | const res = await fetch(`${API_BASE}/api/auth/signup`, {
23 | method: 'POST',
24 | headers: {
25 | 'Content-Type': 'application/json'
26 | },
27 | body: JSON.stringify(formData)
28 | });
29 | const data = await res.json();
30 |
31 | setIsformSubmit(true)
32 | setResponseData(data)
33 | setLoading(false)
34 | };
35 |
36 |
37 | return (
38 | <>
39 |
63 |
64 | >
65 | )
66 | }
67 |
68 | export default Singup
--------------------------------------------------------------------------------
/src/components/ProfileOption.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux';
3 | import { Link } from 'react-router-dom'
4 | import { signoutFailed, signoutSuccess } from '../redux/user/userSlice';
5 | import { ToastContainer, toast } from 'react-toastify';
6 | import { clearSavedListing } from '../redux/saveListing/saveListingSlice';
7 | import { FaBookmark, FaSignOutAlt, FaUser } from 'react-icons/fa';
8 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
9 |
10 | const ProfileOption = ({ user }) => {
11 | const dispatch = useDispatch();
12 |
13 | const handleLogOut = async () => {
14 | try {
15 | const res = await fetch(`${API_BASE}/api/auth/signout`);
16 | const data = await res.json();
17 | if (data.success === false) {
18 | useDispatch(signoutFailed(data.message))
19 | toast.error(data.message, {
20 | autoClose: 2000,
21 | })
22 | }
23 | else {
24 | dispatch(signoutSuccess())
25 | dispatch(clearSavedListing())
26 | }
27 | } catch (error) {
28 | dispatch(signoutFailed(error.message))
29 | toast.error(error.message, {
30 | autoClose: 2000,
31 | })
32 | }
33 | }
34 |
35 |
36 |
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Profile
50 |
51 |
52 |
53 |
54 | Saved Listings
55 |
56 |
57 |
58 |
59 | Logout
60 |
61 |
62 |
63 |
64 |
65 |
66 | )
67 | };
68 |
69 | export default ProfileOption;
--------------------------------------------------------------------------------
/src/components/OAuth.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { GoogleAuthProvider, getAuth, signInWithPopup } from "firebase/auth";
3 | import { firebaseApp } from '../firebase.js';
4 | import { useDispatch } from 'react-redux';
5 | import { signinFailed, signinSuccess } from '../redux/user/userSlice.js';
6 | import { ToastContainer, toast } from 'react-toastify';
7 | import 'react-toastify/dist/ReactToastify.css';
8 | import { useNavigate } from 'react-router-dom';
9 |
10 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
11 |
12 |
13 | const OAuth = () => {
14 | const dispatch = useDispatch();
15 | const navigate = useNavigate();
16 | const handleGoogleSignIn = async () => {
17 | try {
18 | const auth = getAuth(firebaseApp);
19 | const provider = new GoogleAuthProvider();
20 | const result = await signInWithPopup(auth, provider)
21 | const { displayName, email, photoURL } = result.user;
22 |
23 | //=====Fetch The Data To Backend====//
24 | const res = await fetch(`${API_BASE}/api/auth/google`, {
25 | method: "POST",
26 | headers: {
27 | "Content-Type": 'application/json'
28 | },
29 | body: JSON.stringify({
30 | name: displayName,
31 | email,
32 | photo: photoURL,
33 | })
34 | })
35 | const userData = await res.json();
36 | if (userData.success === false) {
37 | dispatch(signinFailed(userData.message));
38 | toast(userData.message);
39 | }
40 | else {
41 | dispatch(signinSuccess(userData));
42 | navigate('/home');
43 | }
44 |
45 | } catch (error) {
46 | console.log(error);
47 | };
48 | };
49 |
50 |
51 |
52 |
53 |
54 |
55 | return (
56 | <>
57 |
58 |
OR
59 |
60 |
63 |
64 |
65 |
66 |
Continue
68 | with Google
69 |
70 |
71 |
72 |
73 |
74 | >
75 |
76 | )
77 | }
78 |
79 | export default OAuth
--------------------------------------------------------------------------------
/src/components/SingIn.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useForm } from 'react-hook-form'
3 | import { useNavigate } from 'react-router-dom';
4 | import { useSelector, useDispatch } from "react-redux";
5 | import { loddingStart, signinSuccess, signinFailed } from '../redux/user/userSlice';
6 | import { ToastContainer, toast } from 'react-toastify';
7 | import 'react-toastify/dist/ReactToastify.css';
8 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
9 |
10 |
11 | const SingIn = () => {
12 | const {
13 | register,
14 | handleSubmit,
15 | formState: { errors },
16 | } = useForm();
17 | const navigate = useNavigate();
18 | const dispatch = useDispatch();
19 |
20 | const { loading } = useSelector((state) => state.user)
21 |
22 |
23 |
24 |
25 | //======handling form submting function =====//
26 | const onSubmit = async (formData) => {
27 | dispatch(loddingStart())
28 | try {
29 | const res = await fetch(`${API_BASE}/api/auth/signin`, {
30 | method: 'POST',
31 | headers: {
32 | 'Content-Type': 'application/json'
33 | },
34 | body: JSON.stringify(formData)
35 | });
36 | const userData = await res.json();
37 |
38 | //===checking reqest success or not ===//
39 | if (userData.success === false) {
40 | dispatch(signinFailed(userData.message))
41 |
42 | //===showing error in tostify====//
43 | toast.error(userData.message, {
44 | autoClose: 2000,
45 | })
46 | }
47 | else {
48 | dispatch(signinSuccess(userData))
49 | navigate('/home')
50 | }
51 | }
52 | catch (error) {
53 | dispatch(signinFailed(error.message))
54 | toast.error(userData.message, {
55 | autoClose: 2000,
56 | })
57 | }
58 | };
59 |
60 |
61 |
62 |
63 |
64 | return (
65 | <>
66 |
86 |
87 | >
88 | )
89 | }
90 |
91 | export default SingIn
--------------------------------------------------------------------------------
/src/components/SocketConnection.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { io } from "socket.io-client";
4 | import {
5 | setNotification,
6 | setSingleNotification,
7 | } from "../redux/notifications/notificationSlice";
8 | import { activeChatId } from "./Conversations";
9 | import { signal } from "@preact/signals-react";
10 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
11 |
12 | //production
13 | // const Node_Env = "local"
14 | export const socket = io("https://thunder-scarlet-wizard.glitch.me/", {
15 | headers: {
16 | "user-agent": "chrome",
17 | },
18 | });
19 |
20 | export const notifySignal = signal({
21 | notifications: [],
22 | });
23 |
24 | const SocketConnection = () => {
25 | const { currentUser } = useSelector((state) => state.user);
26 | const dispatch = useDispatch();
27 |
28 | //======== Get Notification From DB =========//
29 | useEffect(() => {
30 | const loadPrevNotification = async () => {
31 | try {
32 | const unseenNotificaton = await fetch(
33 | `${API_BASE}/api/notification/${currentUser._id}`
34 | );
35 | const res = await unseenNotificaton.json();
36 | if (res.success === false) {
37 | console.log(res);
38 | } else {
39 | notifySignal.value.notifications = res;
40 | dispatch(setNotification(res));
41 | }
42 | } catch (error) {
43 | console.log(error.message);
44 | }
45 | };
46 | currentUser && loadPrevNotification();
47 | }, []);
48 |
49 | //=========== Store notificaions to DB =============//
50 |
51 | const sendNotificationToDB = async (notificationData) => {
52 | try {
53 | const sendNotification = await fetch(
54 | `${API_BASE}/api/notification/create`,
55 | {
56 | method: "POST",
57 | headers: { "Content-Type": "application/json" },
58 | body: JSON.stringify(notificationData),
59 | }
60 | );
61 | const response = await sendNotification.json();
62 | if (response.success === false) {
63 | console.log(response);
64 | }
65 | } catch (error) {
66 | console.log(error);
67 | }
68 | };
69 |
70 | //----- Get Notification from socket and setNotification---------//
71 | useEffect(() => {
72 | socket.on(`${currentUser?._id}`, (socketNotification) => {
73 | const currentPath = window.location.pathname;
74 |
75 | if (currentPath !== "/message") {
76 | activeChatId.value.chatId = "";
77 | }
78 |
79 | if (socketNotification.chatId !== activeChatId.value.chatId) {
80 | const isNotificationExist = notifySignal.value.notifications.some(
81 | (notify) => notify.chatId === socketNotification.chatId
82 | );
83 |
84 | if (!isNotificationExist) {
85 | notifySignal.value.notifications = [
86 | ...notifySignal.value.notifications,
87 | socketNotification,
88 | ];
89 | dispatch(setSingleNotification(socketNotification));
90 | sendNotificationToDB(socketNotification);
91 | }
92 | }
93 | });
94 | });
95 |
96 | return <>>;
97 | };
98 |
99 | export default SocketConnection;
100 |
--------------------------------------------------------------------------------
/src/pages/SaveListing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import ListingCard from '../components/ListingCard'
4 | import { FaArrowRight } from 'react-icons/fa'
5 | import { useNavigate } from 'react-router-dom'
6 | import Footer from '../components/Footer'
7 |
8 | const SaveListing = () => {
9 | const { saveListings } = useSelector(state => state.savedListing)
10 | const navigate = useNavigate()
11 | return (
12 | <>
13 |
14 |
15 |
16 |
Your saved Listing
17 |
navigate('/search')}
20 | >
21 |
22 |
29 |
35 |
36 |
37 |
38 |
39 | Explore More
40 |
41 |
42 |
43 |
44 | {
45 | saveListings.length === 0 ?
46 |
47 |
48 |
49 | Your saved listings are currently empty.
50 |
51 |
52 |
53 | :
54 |
55 | {
56 | saveListings && saveListings.map(listing => )
57 | }
58 |
59 | }
60 |
61 |
62 |
63 |
64 | <>
65 |
66 | >
67 | >
68 | )
69 | }
70 |
71 | export default SaveListing
--------------------------------------------------------------------------------
/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useLayoutEffect, useState } from 'react'
2 | import Singup from '../components/Singup'
3 | import SingIn from '../components/SingIn'
4 | import { ToastContainer, toast } from 'react-toastify';
5 | import 'react-toastify/dist/ReactToastify.css';
6 | import OAuth from '../components/OAuth';
7 | import Footer from '../components/Footer';
8 | import { useSelector } from 'react-redux';
9 | import { useNavigate } from 'react-router-dom';
10 |
11 |
12 |
13 | const Login = () => {
14 | const { currentUser } = useSelector(state => state.user)
15 | const [isNewUser, setIsNewUser] = useState(true)
16 | const [isFormSubmit, setIsformSubmit] = useState(false);
17 | const [responseData, setResponseData] = useState();
18 | const navigate = useNavigate()
19 |
20 | //======handlign Notification for user =====//
21 | const handleTostify = async () => {
22 | await responseData.success && setIsNewUser(!isNewUser)
23 | toast(responseData.message, {
24 | autoClose: 2000,
25 | })
26 | }
27 | useEffect(() => {
28 | isFormSubmit && handleTostify();
29 | setIsformSubmit(false)
30 | }, [responseData])
31 |
32 | useEffect(() => {
33 | if (currentUser && currentUser.email) {
34 | navigate("/profile")
35 | }
36 | }, [])
37 |
38 |
39 |
40 | return (
41 | <>
42 | {
43 | currentUser && currentUser.email
44 | ?
45 |
46 |
47 |
User exist! Redirecting to profile page
48 |
49 |
50 | :
51 |
52 |
53 |
54 |
55 | {
56 | isNewUser ? "Login" : 'Create an account'
57 | }
58 |
59 | {
60 | isNewUser ?
61 |
62 | :
63 |
64 | }
65 |
66 |
67 | {
68 | isNewUser ? "Don’t have an account?" : 'Already have an account?'
69 | }
70 | setIsNewUser(!isNewUser)}
72 | >
73 | {isNewUser ? 'Create an account' : 'Login'}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | }
83 | <>
84 |
85 | >
86 | >
87 | )
88 | }
89 |
90 | export default Login
--------------------------------------------------------------------------------
/src/components/mobileMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | FaBookmark,
4 | FaHome,
5 | FaSignInAlt,
6 | FaSignOutAlt,
7 | FaUser,
8 | FaUsers,
9 | } from "react-icons/fa";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import { useNavigate } from "react-router-dom";
12 | import { signoutFailed, signoutSuccess } from "../redux/user/userSlice";
13 | import { ToastContainer, toast } from "react-toastify";
14 | import { clearSavedListing } from "../redux/saveListing/saveListingSlice";
15 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
16 |
17 | const MobileMenu = ({ menuStatus }) => {
18 | const { currentUser } = useSelector((state) => state.user);
19 | const { setisActiveMoblie, isActiveMoblie } = menuStatus;
20 | const dispatch = useDispatch();
21 | const navigate = useNavigate();
22 | const handleLogOut = async () => {
23 | try {
24 | const res = await fetch(`${API_BASE}/api/auth/signout`);
25 | const data = await res.json();
26 | if (data.success === false) {
27 | useDispatch(signoutFailed(data.message));
28 | toast.error(data.message, {
29 | autoClose: 2000,
30 | });
31 | } else {
32 | dispatch(signoutSuccess());
33 | dispatch(clearSavedListing());
34 | }
35 | } catch (error) {
36 | dispatch(signoutFailed(error.message));
37 | toast.error(error.message, {
38 | autoClose: 2000,
39 | });
40 | }
41 | };
42 |
43 | return (
44 |
45 |
46 | {
48 | navigate("/home"), setisActiveMoblie(!isActiveMoblie);
49 | }}
50 | className="p-2 cursor-pointer rounded-sm mb-3 font-heading hover:bg-brand-blue/40 duration-300"
51 | >
52 |
53 | Home
54 |
55 |
56 | {currentUser && currentUser.email ? (
57 | <>
58 | {
60 | navigate("/profile"), setisActiveMoblie(!isActiveMoblie);
61 | }}
62 | className="p-2 cursor-pointer rounded-sm mb-3 font-heading hover:bg-brand-blue/40 duration-300"
63 | >
64 |
65 | Profile
66 |
67 |
68 | {
70 | navigate("/saved_listing"), setisActiveMoblie(!isActiveMoblie);
71 | }}
72 | className="p-2 cursor-pointer rounded-sm mb-3 font-heading hover:bg-brand-blue/40 duration-300"
73 | >
74 |
75 | Saved Listings
76 |
77 |
78 |
82 |
83 | Log Out
84 |
85 |
86 | >
87 | ) : (
88 | {
90 | navigate("/login"), setisActiveMoblie(!isActiveMoblie);
91 | }}
92 | className="p-2 rounded-sm mb-3 cursor-pointer font-heading hover:bg-brand-blue/40 duration-300"
93 | >
94 |
95 | Login
96 |
97 |
98 | )}
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default MobileMenu;
106 |
--------------------------------------------------------------------------------
/src/components/Contact.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { BsSend } from "react-icons/bs";
3 | import { useSelector } from "react-redux";
4 | import { Link } from "react-router-dom";
5 |
6 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
7 |
8 | const Contact = ({ listing }) => {
9 | const { currentUser } = useSelector((state) => state.user);
10 | const [ownerInfo, setOwnerInfo] = useState({});
11 | const [loading, setLoading] = useState(false);
12 | const [message, setMessage] = useState(null);
13 | const [responseMsg, setResponseMsg] = useState("");
14 | const [sending, setSending] = useState(false);
15 | const [messageSendSuccess, setMessageSendSuccess] = useState(false);
16 |
17 | // =====Load Property Owner Data =====//
18 | useEffect(() => {
19 | (async () => {
20 | setLoading(true);
21 | const res = await fetch(`${API_BASE}/api/users/${listing.userRef}`);
22 | const json = await res.json();
23 | if (json.success === false) {
24 | setLoading(false);
25 | } else {
26 | setOwnerInfo(json);
27 | setLoading(false);
28 | }
29 | })();
30 | }, []);
31 |
32 | const handleChange = (e) => {
33 | setMessage(e.target.value);
34 | };
35 |
36 | const handleSendMsg = async () => {
37 | const conversationApiData = {
38 | creatorId: currentUser._id,
39 | participantId: listing.userRef,
40 | chatPartner: ownerInfo,
41 | chatCreator: currentUser,
42 | };
43 |
44 | try {
45 | setSending(true);
46 | const res = await fetch(`${API_BASE}/api/conversation/create`, {
47 | method: "POST",
48 | headers: { "Content-Type": "application/json" },
49 | body: JSON.stringify(conversationApiData),
50 | });
51 | const json = await res.json();
52 | //===checking reqest success or not ===//
53 | if (json.success === false) {
54 | setResponseMsg("Message sending failed. Try again!");
55 | setSending(false);
56 | } else {
57 | // IF Conversation created successfully
58 | const resMsg = await fetch(`${API_BASE}/api/message/create`, {
59 | method: "POST",
60 | headers: { "Content-Type": "application/json" },
61 | body: JSON.stringify({
62 | sender: currentUser._id,
63 | receiver: listing.userRef,
64 | message: message,
65 | }),
66 | });
67 | const msgJson = await resMsg.json();
68 | //===checking Message request success or not ===//
69 | if (msgJson.success === false) {
70 | setResponseMsg("Message sending failed. Try again!");
71 | setSending(false);
72 | } else {
73 | setResponseMsg(msgJson);
74 | setMessageSendSuccess(true);
75 | setSending(false);
76 | }
77 | }
78 | } catch (error) {
79 | console.log(error);
80 | setSending(false);
81 | }
82 | };
83 |
84 | return (
85 | <>
86 | {loading ? (
87 | "Loading..."
88 | ) : (
89 |
90 |
91 |
92 |
97 |
98 |
99 | {ownerInfo.username}
100 |
101 |
102 | Property Owner
103 |
104 |
105 |
106 |
107 | {sending ? (
108 |
109 |
113 |
114 | Sending...
115 |
116 |
117 | ) : (
118 |
119 | {messageSendSuccess ? (
120 |
121 |
125 | {responseMsg}
126 |
127 |
128 |
129 | Goto Messanger
130 |
131 |
132 |
133 | ) : (
134 | <>
135 |
143 |
148 | Send Messages
149 |
150 |
154 | {responseMsg}
155 |
156 | >
157 | )}
158 |
159 | )}
160 |
161 | )}
162 | >
163 | );
164 | };
165 |
166 | export default Contact;
167 |
--------------------------------------------------------------------------------
/src/components/ListingCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { FaBath, FaBed, FaChartArea, FaBookmark , FaLocationArrow, } from 'react-icons/fa'
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { useNavigate } from 'react-router-dom'
5 | import { clearSavedListing, handleLisingRemove, handleSave } from '../redux/saveListing/saveListingSlice';
6 |
7 | const ListingCard = ({ listing }) => {
8 | const [heart, setHeart] = useState(false);
9 | const { saveListings } = useSelector(state => state.savedListing)
10 | const { currentUser } = useSelector(state => state.user)
11 | const dispatch = useDispatch()
12 | const navigate = useNavigate();
13 | const { title, address, area, bath, bed, discountPrice, imgUrl, offer, price, type, _id } = listing;
14 |
15 | const handleSaveListings = (id) => {
16 | if (currentUser && currentUser.email) {
17 | const isSaved = saveListings.some(saveListing => saveListing._id === id);
18 | if (isSaved) {
19 | const restListings = saveListings.filter(savedListing => savedListing._id !== id);
20 | dispatch(handleLisingRemove(restListings));
21 | setHeart(false);
22 | } else {
23 | const listingToAdd = listing
24 | dispatch(handleSave(listingToAdd));
25 | setHeart(true);
26 | }
27 | }
28 | else {
29 | navigate('/login')
30 | }
31 | };
32 |
33 |
34 |
35 | useEffect(() => {
36 | if (currentUser) {
37 | const isSaved = saveListings.some(saveListing => saveListing._id === _id);
38 | if (isSaved) {
39 | setHeart(true);
40 | } else {
41 | setHeart(false);
42 | }
43 | }
44 | else {
45 | dispatch(clearSavedListing())
46 | }
47 | }, [])
48 |
49 |
50 |
51 | return (
52 |
53 |
54 |
navigate(`/listing/${_id}`)}
57 | >
58 |
62 |
65 | {
66 | offer &&
69 | }
70 |
71 |
72 |
73 |
74 |
75 |
76 |
navigate(`/listing/${_id}`)}
79 | >
80 |
{title}
81 |
83 |
84 | {address}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
93 |
94 | {bed} Bed
95 |
96 |
98 |
99 | {bath} Bath
100 |
101 |
103 |
104 | {area} Area
105 |
106 |
107 |
108 |
109 |
110 |
111 | {/* PRICE CONTAINER SECTION */}
112 |
113 |
114 |
115 | {offer ?
116 |
${discountPrice} ${price}
117 |
118 | :
${price}
119 | }
120 |
121 |
122 | handleSaveListings(_id)}
124 | className={`text-lg drop-shadow-sm duration-300 ${heart ? 'text-brand-blue' : "text-gray-300"} `}>
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | )
134 | }
135 |
136 | export default ListingCard
--------------------------------------------------------------------------------
/src/components/PostCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FaBath, FaBed, FaCamera, FaCheck } from "react-icons/fa"
3 | import { useNavigate } from 'react-router-dom';
4 |
5 |
6 |
7 |
8 |
9 | const PostCard = ({ postInfo }) => {
10 | const {
11 | bed,
12 | address,
13 | bath,
14 | description,
15 | discountPrice,
16 | furnished,
17 | imgUrl,
18 | offer,
19 | parking,
20 | price,
21 | title,
22 | type,
23 | _id } = postInfo.post;
24 | const navigate = useNavigate();
25 |
26 | return (
27 | <>
28 |
31 |
navigate(`/listing/${_id}`)}
33 | className="relative flex items-end overflow-hidden rounded-md h-[200px] cursor-pointer "
34 | >
35 |
36 |
37 |
38 |
39 |
40 |
41 | For {type}
42 |
43 |
44 |
45 |
46 | {imgUrl.length}
47 |
48 |
49 | {
50 | offer &&
51 |
52 | Discount!
53 |
54 |
55 | }
56 |
57 |
58 |
59 |
60 | {/* CARD BODY START HERE */}
61 |
62 |
navigate(`/listing/${_id}`)}
64 | className='cursor-pointer'
65 | >
66 |
68 | {title}
69 |
70 |
{description}
71 |
Address: {address}
72 |
73 |
74 |
75 |
{bed} Bed
76 |
77 |
78 |
79 |
{bath} Bath
80 |
81 |
82 |
parking
83 |
84 |
furnished
85 |
86 |
87 |
88 |
89 |
90 | {offer ?
91 |
92 | ${discountPrice}
93 | {
94 | type === 'rent' && /m
95 | }
96 | ${price}
97 |
98 |
99 | :
100 | ${price}
101 | {
102 | type === 'rent' && /m
103 | }
104 |
}
105 |
106 |
107 |
{postInfo.post.area ? postInfo.post.area : 0} /sqft
108 |
109 |
110 |
111 |
112 |
113 | navigate(`/update_post/${_id}`)}
115 | className='bg-brand-blue rounded-sm py-2 px-7 font-heading text-white hover:opacity-95 text-sm'>
116 | Edit
117 |
118 | postInfo.handlePostDelete(_id)}
120 | className='bg-red-800 py-2 px-5 rounded-sm font-heading text-white hover:opacity-95 text-sm z-10'>
121 | Delete
122 |
123 |
124 |
125 |
126 |
127 | >
128 |
129 | )
130 | }
131 |
132 | export default PostCard
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import Footer from "../components/Footer";
4 | import SaleListing from "../components/SaleListing";
5 | import RentListing from "../components/RentListing";
6 | import OfferedListing from "../components/OfferedListing";
7 |
8 | const Home = () => {
9 | const navigate = useNavigate();
10 |
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 | Let us find your
20 |
21 | Forever Home.
22 |
23 |
24 |
25 |
26 | Welcome to our home listings designed for you. Explore to find
27 | your perfect match. We're here to make finding your dream home a
28 | smooth, enjoyable experience.
29 |
30 |
31 |
32 | navigate("/search")}
34 | className="block w-full rounded bg-brand-blue px-12 py-3 text-sm font-heading uppercase text-white shadow hover:bg-white hover:text-brand-blue duration-300 ease-in-out sm:w-auto"
35 | >
36 | Get Started
37 |
38 |
39 | navigate("/about")}
41 | className="block w-full rounded hover:bg-brand-blue px-12 py-3 text-sm font-heading uppercase hover:text-white shadow bg-white text-brand-blue duration-300 ease-in-out sm:w-auto"
42 | >
43 | Learn More
44 |
45 |
46 |
47 |
48 |
49 |
50 | {/* offer Post Listings */}
51 |
52 |
53 | {/* Anousment Section */}
54 |
55 |
56 |
57 |
58 | Want to sell your property?
59 |
60 |
61 |
navigate("/create_post")}
64 | >
65 |
66 |
73 |
79 |
80 |
81 |
82 |
83 | {"Let's Sell Now!"}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | {/* Sale Post Listings */}
92 |
93 |
94 | {/* Anousment Section */}
95 |
96 |
97 |
98 |
99 | Ready to rent your dream property?
100 |
101 |
102 |
navigate("/search")}
105 | >
106 |
107 |
114 |
120 |
121 |
122 |
123 |
124 | {"Let's Take Rent!"}
125 |
126 |
127 |
128 |
129 |
130 |
131 | {/* Rent Post Listings */}
132 |
133 |
134 | {/* // Footer section code here */}
135 |
136 | >
137 | );
138 | };
139 |
140 | export default Home;
141 |
--------------------------------------------------------------------------------
/src/components/RentListing.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import ListingCard from '../components/ListingCard'
3 | import "slick-carousel/slick/slick.css";
4 | import "slick-carousel/slick/slick-theme.css";
5 | import Slider from "react-slick";
6 | import { BsArrowRight, BsArrowLeft, } from "react-icons/bs";
7 | import SkletonLoading from './SkletonLoading';
8 | import { useNavigate } from 'react-router-dom';
9 |
10 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
11 |
12 |
13 | const RentListing = () => {
14 | const [loading, setLoading] = useState(true)
15 | const [rentListings, setRentlisting] = useState([])
16 | const navigate = useNavigate()
17 |
18 | //===Load Data ===//
19 | useEffect(() => {
20 | (async () => {
21 | try {
22 | setLoading(true)
23 | const res = await fetch(`${API_BASE}/api/posts?type=rent`)
24 | const json = await res.json()
25 | if (json.success === false) {
26 | setLoading(false)
27 | }
28 | else {
29 | setRentlisting(json)
30 | setLoading(false)
31 | }
32 | } catch (error) {
33 | console.log(error);
34 | setLoading(false)
35 | }
36 | })()
37 | }, [])
38 |
39 |
40 | //====SLider Functions=====//
41 | function SamplePrevArrow({ onClick }) {
42 | return (
43 |
48 |
49 |
50 | )
51 | }
52 | function SampleNextArrow({ onClick }) {
53 | return (
54 |
58 |
59 |
60 | )
61 | }
62 |
63 | const settings = {
64 | dots: false,
65 | infinite: true,
66 | lazyLoad: false,
67 | speed: 400,
68 | slidesToShow: 4,
69 | slidesToScroll: 1,
70 | nextArrow: ,
71 | prevArrow: ,
72 | responsive: [
73 | {
74 | breakpoint: 1024,
75 | settings: {
76 | slidesToShow: 3,
77 | }
78 | },
79 | {
80 | breakpoint: 720,
81 | settings: {
82 | slidesToShow: 2,
83 | }
84 | },
85 | {
86 | breakpoint: 480,
87 | settings: {
88 | slidesToShow: 1,
89 | slidesToScroll: 1
90 | }
91 | }
92 | ]
93 | };
94 |
95 | return (
96 | < section >
97 |
100 |
101 |
102 | Explore Our Rental Listings
103 |
104 |
105 | Discover our Rental Showcase! Step into a world of diverse properties waiting for you. Embrace the possibilities of comfortable living spaces and find your perfect rental match. Start exploring your next home now!
106 |
107 |
108 |
109 |
110 | {
111 | loading ?
112 |
113 | :
114 |
115 |
116 | {
117 | rentListings && rentListings.map(listing => )
118 | }
119 |
120 |
121 | }
122 |
123 |
124 |
125 |
navigate('/search')}
128 | >
129 |
130 |
137 |
143 |
144 |
145 |
146 |
147 | Explore More
148 |
149 |
150 |
151 |
152 |
153 |
154 | )
155 | }
156 |
157 | export default RentListing
--------------------------------------------------------------------------------
/src/components/SaleListing.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import ListingCard from '../components/ListingCard'
3 | import "slick-carousel/slick/slick.css";
4 | import "slick-carousel/slick/slick-theme.css";
5 | import Slider from "react-slick";
6 | import { BsArrowRight, BsArrowLeft, } from "react-icons/bs";
7 | import SkletonLoading from './SkletonLoading';
8 | import { useNavigate } from 'react-router-dom';
9 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
10 |
11 |
12 |
13 | const SaleListing = () => {
14 | const [loading, setLoading] = useState(true)
15 | const [saleListings, setSaleListings] = useState([])
16 |
17 | const navigate = useNavigate()
18 |
19 | //===Load Data ===//
20 | useEffect(() => {
21 | (async () => {
22 | try {
23 | setLoading(true)
24 | const res = await fetch(`${API_BASE}/api/posts?type=sale`)
25 | const json = await res.json()
26 | if (json.success === false) {
27 | setLoading(false)
28 | }
29 | else {
30 | setSaleListings(json)
31 | setLoading(false)
32 | }
33 | } catch (error) {
34 | console.log(error);
35 | setLoading(false)
36 | }
37 | })()
38 | }, [])
39 |
40 |
41 | //====SLider Functions=====//
42 | function SamplePrevArrow({ onClick }) {
43 | return (
44 |
49 |
50 |
51 | )
52 | }
53 | function SampleNextArrow({ onClick }) {
54 | return (
55 |
59 |
60 |
61 | )
62 | }
63 |
64 | const settings = {
65 | dots: false,
66 | infinite: true,
67 | lazyLoad: false,
68 | speed: 400,
69 | slidesToShow: 4,
70 | slidesToScroll: 1,
71 | nextArrow: ,
72 | prevArrow: ,
73 | responsive: [
74 | {
75 | breakpoint: 1024,
76 | settings: {
77 | slidesToShow: 3,
78 | }
79 | },
80 | {
81 | breakpoint: 720,
82 | settings: {
83 | slidesToShow: 2,
84 | }
85 | },
86 | {
87 | breakpoint: 480,
88 | settings: {
89 | slidesToShow: 1,
90 | slidesToScroll: 1
91 | }
92 | }
93 | ]
94 | };
95 |
96 | return (
97 | < section >
98 |
101 |
102 |
103 | Explore Our Sale Post
104 |
105 |
106 | Step into our Sale Event and discover an array of incredible offers waiting for you! Unleash your shopping desires with discounts on a wide range of products. Embrace the savings—start shopping now!
107 |
108 |
109 |
110 |
111 | {
112 | loading ?
113 |
114 | :
115 |
116 |
117 | {
118 | saleListings && saleListings.map(listing => )
119 | }
120 |
121 |
122 | }
123 |
124 |
125 |
126 |
127 |
128 |
129 |
navigate('/search')}
132 | >
133 |
134 |
141 |
147 |
148 |
149 |
150 |
151 | Explore More
152 |
153 |
154 |
155 |
156 |
157 |
158 | )
159 | }
160 |
161 | export default SaleListing
--------------------------------------------------------------------------------
/src/components/OfferedListing.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import ListingCard from '../components/ListingCard'
3 | import "slick-carousel/slick/slick.css";
4 | import "slick-carousel/slick/slick-theme.css";
5 | import Slider from "react-slick";
6 | import { BsArrowRight, BsArrowLeft, } from "react-icons/bs";
7 | import SkletonLoading from './SkletonLoading';
8 | import { useNavigate } from 'react-router-dom';
9 |
10 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
11 |
12 |
13 |
14 |
15 | const OfferedListing = () => {
16 | const [loading, setLoading] = useState(true)
17 | const [offerListings, setOfferListings] = useState([])
18 | const navigate = useNavigate()
19 |
20 | //===Load Data ===//
21 | useEffect(() => {
22 | (async () => {
23 | try {
24 | setLoading(true)
25 | const res = await fetch(`${API_BASE}/api/posts?type=all&offer=true`)
26 | const json = await res.json()
27 | if (json.success === false) {
28 | setLoading(false)
29 | }
30 | else {
31 | setOfferListings(json)
32 | setLoading(false)
33 | }
34 | } catch (error) {
35 | console.log(error);
36 | setLoading(false)
37 | }
38 | })()
39 | }, [])
40 |
41 |
42 | //====SLider Functions=====//
43 | function SamplePrevArrow({ onClick }) {
44 | return (
45 |
50 |
51 |
52 | )
53 | }
54 | function SampleNextArrow({ onClick }) {
55 | return (
56 |
60 |
61 |
62 | )
63 | }
64 |
65 | const settings = {
66 | dots: false,
67 | infinite: true,
68 | lazyLoad: false,
69 | speed: 400,
70 | slidesToShow: 4,
71 | slidesToScroll: 1,
72 | nextArrow: ,
73 | prevArrow: ,
74 | responsive: [
75 | {
76 | breakpoint: 1024,
77 | settings: {
78 | slidesToShow: 3,
79 | }
80 | },
81 | {
82 | breakpoint: 720,
83 | settings: {
84 | slidesToShow: 2,
85 | }
86 | },
87 | {
88 | breakpoint: 480,
89 | settings: {
90 | slidesToShow: 1,
91 | slidesToScroll: 1
92 | }
93 | }
94 | ]
95 | };
96 |
97 |
98 |
99 | return (
100 | < section >
101 |
104 |
105 |
106 | Enjoy Our Exciting Discount
107 |
108 |
109 | Find Your Perfect Home! Whether you want to rent or buy, we've got great deals for you. Discover comfortable rentals and awesome properties for sale. Your dream home is just a click away!
110 |
111 |
112 |
113 |
114 | {
115 | loading ?
116 |
117 | :
118 |
119 |
120 | {
121 | offerListings && offerListings.map(listing => )
122 | }
123 |
124 |
125 | }
126 |
127 |
128 |
129 |
navigate('/search')}
132 | >
133 |
134 |
141 |
147 |
148 |
149 |
150 |
151 | Explore More
152 |
153 |
154 |
155 |
156 |
157 |
158 | )
159 | }
160 |
161 | export default OfferedListing
--------------------------------------------------------------------------------
/src/components/Conversations.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from 'react-redux'
2 | import { signal } from '@preact/signals-react'
3 | import { notifySignal } from './SocketConnection'
4 | import { setNotification } from '../redux/notifications/notificationSlice'
5 |
6 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
7 | export const activeChatId = signal({
8 | chatId: ""
9 | })
10 |
11 | const Conversations = ({ conversationInfo }) => {
12 | const { conversation,
13 | trackConversation,
14 | setTrackConversation,
15 | } = conversationInfo
16 | const dispatch = useDispatch();
17 | let notifications = notifySignal.value.notifications;
18 |
19 | const { currentUser } = useSelector(state => state.user)
20 | const { notificationsDB } = useSelector(state => state.notification)
21 |
22 |
23 | const isNotify = notificationsDB.some(notify => notify.chatId === conversation._id);
24 |
25 | const handleNotificationClick = (conversationId) => {
26 | const notificationIndex = notifications.some(notify => notify.chatId === conversationId);
27 |
28 | if (notificationIndex) {
29 | const restNotifications = notifications.filter(notify => notify.chatId !== conversationId)
30 | notifySignal.value.notifications = restNotifications;
31 | dispatch(setNotification(restNotifications));
32 | }
33 | };
34 |
35 |
36 |
37 | //===== Delete Notification From DB========//
38 | const notifyDeleteFromDB = async (notify_from) => {
39 | const isNotifyExistDB = notifications.some(notify => notify.from === notify_from);
40 | if (isNotifyExistDB) {
41 | try {
42 | const dltNotify = await fetch(`${API_BASE}/api/notification/delete/${notify_from}`, {
43 | method: 'DELETE'
44 | })
45 | const res = await dltNotify.json();
46 | if (res.success === false) {
47 | console.log(res.error);
48 | }
49 | } catch (error) {
50 | console.log(error);
51 | }
52 | }
53 | }
54 |
55 |
56 | return (
57 | <>
58 | {
59 | conversation.participantId && conversation.participantId === currentUser._id
60 | ?
61 | // When current user is in participent role
62 | <>
63 | (
65 |
66 | setTrackConversation(
67 |
68 | {
69 | ...trackConversation,
70 | conversationActive: conversation.chatCreator._id,
71 | sender: conversation.chatPartner._id,
72 | receiver: conversation.chatCreator._id,
73 | conversation,
74 | chatId: conversation._id
75 |
76 | }
77 | ),
78 | notifyDeleteFromDB(conversation.chatCreator._id),
79 | handleNotificationClick(conversation._id),
80 | activeChatId.value.chatId = conversation._id
81 |
82 | )
83 | }
84 | className={`chat_user flex items-center justify-center sm:justify-start sm:flex-row sm:gap-4 hover:bg-brand-blue/90 active:bg-brand-blue group w-full p-2 sm:p-3 gap-1 duration-300 cursor-pointer ${trackConversation.conversationActive === conversation.chatCreator._id ? "bg-brand-blue text-white" : "bg-gray-200 text-brand-blue"}`}
85 | >
86 |
87 |
91 |
92 |
93 |
94 |
{conversation.chatCreator.username}
95 | {isNotify &&
new!
}
96 |
97 | {isNotify &&
!
}
98 |
99 | >
100 | :
101 |
102 | // When current user is in creator role
103 | <>
104 | (
106 | setTrackConversation({
107 | ...trackConversation,
108 | conversationActive: conversation.chatPartner._id,
109 | sender: conversation.chatCreator._id,
110 | receiver: conversation.chatPartner._id,
111 | conversation,
112 | chatId: conversation._id
113 | }),
114 | // setSocketMessages([]),
115 | notifyDeleteFromDB(conversation.chatPartner._id),
116 | handleNotificationClick(conversation._id),
117 | activeChatId.value.chatId = conversation._id
118 | )
119 | }
120 | className={`chat_user flex items-center justify-center sm:justify-start sm:flex-row sm:gap-4 hover:bg-brand-blue/90 active:bg-brand-blue group w-full p-2 sm:p-3 gap-1 duration-300 cursor-pointer
121 | ${trackConversation.conversationActive === conversation.chatPartner._id ? "bg-brand-blue text-white" : "bg-gray-200 text-brand-blue"}
122 | `}
123 | >
124 |
125 |
130 |
131 |
132 |
133 |
{conversation.chatPartner.username}
134 | {isNotify &&
new!
}
135 |
136 | {isNotify &&
!
}
137 |
138 |
139 | >
140 | }
141 |
142 | >
143 | )
144 | }
145 |
146 | export default Conversations
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState } from 'react'
2 | import { BsJustifyRight, BsMessenger, BsSearch } from 'react-icons/bs'
3 | import { Link, useNavigate } from 'react-router-dom'
4 | import MobileMenu from './mobileMenu'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import Profile from './ProfileOption'
7 | import { setSearchTermState } from '../redux/search/searchSlice'
8 | import { MdOutlineClose } from "react-icons/md";
9 | import { FaHome } from "react-icons/fa";
10 |
11 |
12 |
13 |
14 | const Header = () => {
15 | const [isActiveMoblie, setisActiveMoblie] = useState(false)
16 | const { currentUser } = useSelector((state) => state.user)
17 | const { notificationsDB } = useSelector(state => state.notification)
18 | const [searchValue, setSearchValue] = useState("")
19 | const navigate = useNavigate()
20 | const dispatch = useDispatch()
21 |
22 |
23 | const handleSubmit = (e) => {
24 | e.preventDefault()
25 | navigate(`/search`)
26 | setSearchValue("")
27 | }
28 |
29 |
30 | return (
31 | <>
32 |
33 |
34 |
35 | {/* Logo container */}
36 |
37 |
38 |
39 |
40 |
41 | Property Sale
42 |
43 |
44 |
45 |
46 |
47 | {/* search Form */}
48 |
67 |
68 | {/*========= when user login ======== */}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {
82 | notificationsDB.length === 0
83 | ?
84 | new
85 | :
86 | {notificationsDB.length}
87 | }
88 |
89 |
90 |
91 |
92 | {
93 | currentUser ?
94 |
95 | :
96 |
97 |
98 | Login
99 |
100 |
101 | }
102 |
103 |
104 |
105 |
106 | {/* User Profile Image */}
107 | {/* {currentUser &&
} */}
108 |
109 |
110 |
111 |
112 | {
113 | notificationsDB.length === 0
114 | ?
115 | new
116 | :
117 | {notificationsDB.length}
118 | }
119 |
120 |
121 |
122 |
setisActiveMoblie(!isActiveMoblie)}
125 | >
126 | {
127 | isActiveMoblie ? :
128 | }
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | {
137 | isActiveMoblie &&
138 | }
139 | >
140 |
141 | )
142 | }
143 |
144 | export default Header
--------------------------------------------------------------------------------
/src/pages/Message.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import Chat from '../components/Chat';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import Conversations, { activeChatId } from '../components/Conversations';
5 | import { ToastContainer, toast } from 'react-toastify';
6 | import { useNavigate } from 'react-router-dom';
7 | import { signoutSuccess } from '../redux/user/userSlice';
8 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
9 |
10 |
11 | const Message = () => {
12 | const { currentUser } = useSelector(state => state.user)
13 | const [conversations, setConversation] = useState([])
14 | const [conversationLoading, setConversationLoading] = useState(true)
15 | const [error, setError] = useState(false)
16 | const [trackConversation, setTrackConversation] = useState({
17 | sender: "",
18 | receiver: "",
19 | conversationActive: null,
20 | })
21 | const navigate = useNavigate()
22 | const dispatch = useDispatch()
23 |
24 |
25 |
26 |
27 | //============ Making null active chat ID For notification ============//
28 | useEffect(() => {
29 | const currentPath = window.location.pathname;
30 | if (currentPath === "/message") {
31 | activeChatId.value.chatId = ""
32 | }
33 | }, [])
34 |
35 |
36 |
37 | // Load Current user Conversations
38 | useEffect(() => {
39 | (async () => {
40 | try {
41 |
42 | setConversationLoading(true)
43 | const res = await fetch(`${API_BASE}/api/conversation/${currentUser._id}`)
44 | const getConversations = await res.json();
45 | if (getConversations.success === false) {
46 | setConversationLoading(false)
47 | toast.error(getConversations.message, {
48 | autoClose: 5000
49 | })
50 | setError(true)
51 | dispatch(signoutSuccess())
52 | }
53 | else {
54 | setConversationLoading(false)
55 | setConversation(getConversations)
56 | }
57 | } catch (error) {
58 | setConversationLoading(false)
59 | toast.error(getConversations.message, {
60 | autoClose: 5000
61 | })
62 | setError(true)
63 | console.log(error);
64 | }
65 | })()
66 | }, [])
67 |
68 |
69 |
70 | return (
71 | <>
72 |
73 |
74 | {
75 | error ?
76 |
77 |
78 | Your session has expired. Please log in again to continue.
79 | navigate('/login')}
82 | >
83 |
84 |
91 |
97 |
98 |
99 |
100 |
101 | Login
102 |
103 |
104 |
105 |
106 |
107 | :
108 |
109 | {
110 | conversations.length === 0 ?
111 |
112 |
113 |
114 | Hey welcome! 😄
115 | This is a blank canvas waiting for your first conversation.
116 |
117 |
118 |
119 | :
120 |
121 |
122 |
123 |
124 | {
125 | conversationLoading
126 | ?
127 |
128 |
Conversation Loading...
129 |
130 | :
131 | <>
132 |
Chats...
133 | {
134 | conversations.length !== 0 &&
135 | conversations.map((conversation) =>
136 |
146 | )
147 |
148 | }
149 | >
150 | }
151 |
152 |
153 | {
154 | trackConversation.conversationActive
155 | ?
156 |
157 |
163 |
164 | :
165 |
166 |
167 |
No Conversation is Selected 🙄
168 |
169 | }
170 |
171 | }
172 |
173 | }
174 |
175 |
176 |
177 | >
178 | )
179 | }
180 |
181 | export default Message
--------------------------------------------------------------------------------
/src/components/Chat.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { BsFillSendFill, BsImage } from "react-icons/bs";
3 | import { useSelector } from "react-redux";
4 | import { socket } from "./SocketConnection";
5 | import { MdDelete } from "react-icons/md";
6 |
7 | import { ToastContainer, toast } from "react-toastify";
8 | import "react-toastify/dist/ReactToastify.css";
9 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
10 |
11 | const Chat = ({ conversationInfo }) => {
12 | const { currentUser } = useSelector((state) => state.user);
13 | const [messageText, setMessageText] = useState([]);
14 | const [typedMessage, setTypedMessage] = useState("");
15 | const [IsSendingError, setSendingError] = useState(false);
16 | const scrollRef = useRef();
17 | const [socketMessages, setSocketMessages] = useState([]);
18 | const [messageLoading, setMessageLoading] = useState(false);
19 |
20 | const {
21 | trackConversation,
22 | setTrackConversation,
23 | conversations,
24 | setConversation,
25 | } = conversationInfo;
26 | const { chatCreator, chatPartner, _id } = trackConversation.conversation;
27 |
28 | //----- Load User Messages
29 | useEffect(() => {
30 | (async () => {
31 | try {
32 | setMessageLoading(true);
33 | const res = await fetch(
34 | `${API_BASE}/api/message?sender=${trackConversation.sender}&receiver=${trackConversation.receiver}`
35 | );
36 | const getMessages = await res.json();
37 |
38 | if (getMessages.success === false) {
39 | setMessageLoading(false);
40 | console.log(getMessages.message);
41 | } else {
42 | setMessageLoading(false);
43 | setSocketMessages([]);
44 | setMessageText(getMessages);
45 | }
46 | } catch (error) {
47 | setMessageLoading(false);
48 | console.log(error);
49 | }
50 | })();
51 | }, [trackConversation]);
52 |
53 | //====== Join Sockets Room Here =======//
54 | useEffect(() => {
55 | socket.emit("join_room", trackConversation.chatId);
56 | }, [trackConversation]);
57 |
58 | //----- Get Message from socket
59 | useEffect(() => {
60 | socket.on("receive_message", (socketMsg) => {
61 | setSocketMessages([
62 | ...socketMessages,
63 | {
64 | message: socketMsg.message,
65 | type: "received",
66 | chatId: socketMsg.chatId,
67 | },
68 | ]);
69 | });
70 | });
71 |
72 | //====== Send Message To Socket ========//
73 | const sendMessageTOSocket = () => {
74 | socket.emit("send_message", {
75 | chatId: trackConversation.chatId,
76 | message: typedMessage,
77 | from: currentUser._id,
78 | to: trackConversation.conversationActive,
79 | });
80 |
81 | setSocketMessages([
82 | ...socketMessages,
83 | {
84 | message: typedMessage,
85 | type: "send",
86 | chatId: trackConversation.chatId,
87 | },
88 | ]);
89 |
90 | setTypedMessage("");
91 | };
92 |
93 | // Handle Message Sending //
94 | const handleSendMsg = async (e) => {
95 | e.preventDefault();
96 | sendMessageTOSocket();
97 | try {
98 | const sendMsgToDB = await fetch(`${API_BASE}/api/message/create`, {
99 | method: "POST",
100 | headers: { "Content-Type": "application/json" },
101 | body: JSON.stringify({
102 | sender: currentUser._id,
103 | receiver: trackConversation.conversationActive,
104 | message: typedMessage,
105 | }),
106 | });
107 | const response = await sendMsgToDB.json();
108 | //===checking Message request success or not ===//
109 | if (response.success === false) {
110 | setSendingError(true);
111 | } else {
112 | setSendingError(false);
113 | }
114 | } catch (error) {
115 | setSendingError(true);
116 | console.log(error);
117 | }
118 | };
119 |
120 | useEffect(() => {
121 | scrollRef.current?.scrollIntoView({ behavior: "smooth" });
122 | }, [socketMessages, messageText]);
123 |
124 | const handleConversationDelete = async () => {
125 | try {
126 | const deleteChat = await fetch(
127 | `${API_BASE}/api/conversation/delete/${_id}`,
128 | {
129 | method: "DELETE",
130 | }
131 | );
132 | if (deleteChat.ok) {
133 | const restConversation = conversations.filter(
134 | (conversation) => conversation._id !== _id
135 | );
136 | setConversation(restConversation);
137 | setTrackConversation({
138 | ...trackConversation,
139 | conversationActive: "",
140 | });
141 | } else {
142 | const errorRes = await deleteChat.json();
143 | toast.error(errorRes.message, {
144 | autoClose: 2000,
145 | });
146 | }
147 | } catch (error) {
148 | toast.error(error.message, {
149 | autoClose: 2000,
150 | });
151 | }
152 | };
153 |
154 | return (
155 | <>
156 | {messageLoading ? (
157 |
158 |
159 | Messages Loading...
160 |
161 |
162 | ) : (
163 |
164 |
165 |
166 |
175 |
176 | {chatPartner._id === currentUser._id
177 | ? chatCreator.username
178 | : chatPartner.username}
179 |
180 |
181 |
182 |
183 |
187 |
188 |
189 | Delete
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | {messageText.map((msg, index) =>
198 | msg.sender === currentUser._id ? (
199 |
207 |
208 |
212 | {msg.message}
213 |
214 |
215 |
216 | ) : (
217 |
225 |
226 |
235 |
239 | {msg.message}
240 |
241 |
242 |
243 | )
244 | )}
245 |
246 | {socketMessages.length !== 0 &&
247 | socketMessages.map(
248 | (msg, index) =>
249 | msg.chatId === trackConversation.chatId && (
250 |
251 | {msg.type === "send" ? (
252 |
256 |
257 |
261 | {msg.message}
262 |
263 |
264 |
265 | ) : (
266 |
270 |
271 |
280 |
281 | {msg.message}
282 |
283 |
284 |
285 | )}
286 |
287 | )
288 | )}
289 | {IsSendingError && (
290 |
291 | Message sending failed!
292 |
293 | )}
294 |
295 |
296 |
320 |
321 |
322 |
323 | )}
324 | >
325 | );
326 | };
327 |
328 | export default Chat;
329 |
--------------------------------------------------------------------------------
/src/components/SkletonLoading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const SkletonLoading = () => {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | >
235 | )
236 | }
237 |
238 | export default SkletonLoading
--------------------------------------------------------------------------------
/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { React, useEffect, useRef, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { AiFillEdit } from "react-icons/ai";
4 | import { BsFillPlusSquareFill } from "react-icons/bs";
5 | import {
6 | getDownloadURL,
7 | getStorage,
8 | ref,
9 | uploadBytesResumable,
10 | } from "firebase/storage";
11 | import { firebaseApp } from "../firebase.js";
12 | import {
13 | loddingStart,
14 | signoutFailed,
15 | signoutSuccess,
16 | userDeleteFail,
17 | userDeleteSuccess,
18 | userUpdateFailed,
19 | userUpdateSuccess,
20 | } from "../redux/user/userSlice.js";
21 | import { ToastContainer, toast } from "react-toastify";
22 | import "react-toastify/dist/ReactToastify.css";
23 | import { useNavigate } from "react-router-dom";
24 | import PostCard from "../components/PostCard.jsx";
25 | import Loading from "../components/Loading.jsx";
26 | import { clearSavedListing } from "../redux/saveListing/saveListingSlice.js";
27 | import Footer from "../components/Footer.jsx";
28 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
29 |
30 | const Profile = () => {
31 | const { currentUser } = useSelector((state) => state.user);
32 | const [file, setFile] = useState(undefined);
33 | const [uploadingPerc, setUploadingPerc] = useState(0);
34 | const [fileUploadError, setFileUploadError] = useState(false);
35 | const [formData, setFormData] = useState({});
36 | const [userPosts, setUserPost] = useState({
37 | isPostExist: false,
38 | posts: [],
39 | });
40 |
41 | const [userPostLoading, setUserPostLoading] = useState(false);
42 |
43 | const fileRef = useRef(null);
44 | const { loading } = useSelector((state) => state.user);
45 | const dispatch = useDispatch();
46 | const navigate = useNavigate();
47 |
48 | const handleFileUpload = (file) => {
49 | if (file) {
50 | const fireBaseStorage = getStorage(firebaseApp);
51 | const fileName = new Date().getTime() + file.name;
52 | const storageRef = ref(fireBaseStorage, fileName);
53 | const uploadTask = uploadBytesResumable(storageRef, file);
54 |
55 | uploadTask.on(
56 | "state_changed",
57 | (snapshot) => {
58 | const progress =
59 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
60 | setUploadingPerc(Math.round(progress));
61 | },
62 | (error) => {
63 | setFileUploadError(true);
64 | },
65 | () => {
66 | getDownloadURL(uploadTask.snapshot.ref).then((downloadUrl) => {
67 | setFormData({ ...formData, avatar: downloadUrl });
68 | });
69 | }
70 | );
71 | }
72 | };
73 |
74 | useEffect(() => {
75 | handleFileUpload(file);
76 | }, [file]);
77 |
78 | const handleChange = (e) => {
79 | setFormData({ ...formData, [e.target.name]: e.target.value });
80 | };
81 |
82 | const handleSubmit = async (e) => {
83 | e.preventDefault();
84 | // setLoading(true)
85 | try {
86 | dispatch(loddingStart());
87 | const res = await fetch(
88 | `${API_BASE}/api/users/update/${currentUser._id}`,
89 | {
90 | method: "POST",
91 | headers: { "Content-Type": "application/json" },
92 | body: JSON.stringify(formData),
93 | }
94 | );
95 | const userData = await res.json();
96 |
97 | //===checking reqest success or not ===//
98 | if (userData.success === false) {
99 | dispatch(userUpdateFailed(userData.message));
100 |
101 | //===showing error in tostify====//
102 | toast.error(userData.message, {
103 | autoClose: 5000,
104 | });
105 | } else {
106 | dispatch(userUpdateSuccess(userData));
107 | toast.success("Profile updated successfully", {
108 | autoClose: 2000,
109 | });
110 | }
111 | } catch (error) {
112 | dispatch(userUpdateFailed(error.message));
113 | toast.error(error.message, {
114 | autoClose: 2000,
115 | });
116 | }
117 | };
118 |
119 | const handleDelete = async () => {
120 | try {
121 | dispatch(loddingStart());
122 | const res = await fetch(
123 | ` ${API_BASE}/api/users/delete/${currentUser._id}`,
124 | {
125 | method: "DELETE",
126 | }
127 | );
128 | const resData = await res.json();
129 | //===checking reqest success or not ===//
130 | if (resData.success === false) {
131 | dispatch(userDeleteFail(resData.message));
132 |
133 | //===showing error in tostify====//
134 | toast.error(resData.message, {
135 | autoClose: 2000,
136 | });
137 | } else {
138 | dispatch(userDeleteSuccess());
139 | }
140 | } catch (error) {
141 | dispatch(userDeleteFail(error.message));
142 | toast.error(error.message, {
143 | autoClose: 2000,
144 | });
145 | }
146 | };
147 |
148 | const handleLogOut = async () => {
149 | try {
150 | const res = await fetch(`${API_BASE}/ api/auth/signout`);
151 | const data = await res.json();
152 | if (data.success === false) {
153 | dispatch(signoutFailed(data.message));
154 | toast.error(data.message, {
155 | autoClose: 2000,
156 | });
157 | } else {
158 | dispatch(signoutSuccess());
159 | dispatch(clearSavedListing());
160 | }
161 | } catch (error) {
162 | dispatch(signoutFailed(error.message));
163 | toast.error(error.message, {
164 | autoClose: 2000,
165 | });
166 | }
167 | };
168 |
169 | // ======Loading User Posts =====//
170 | useEffect(() => {
171 | const loadPost = async () => {
172 | try {
173 | setUserPostLoading(true);
174 | const res = await fetch(
175 | `${API_BASE}/api/users/posts/${currentUser._id}`
176 | );
177 | const data = await res.json();
178 | if (data.success === false) {
179 | toast.error(data.message, {
180 | autoClose: 2000,
181 | });
182 | setUserPostLoading(false);
183 | dispatch(signoutSuccess());
184 | } else {
185 | setUserPost({
186 | ...userPosts,
187 | isPostExist: true,
188 | posts: data,
189 | });
190 | setUserPostLoading(false);
191 | }
192 | } catch (error) {
193 | toast.error(error.message, {
194 | autoClose: 2000,
195 | });
196 | setUserPostLoading(false);
197 | }
198 | };
199 | loadPost();
200 | }, []);
201 |
202 | // ======Handling User Post DELETE =====//
203 | const handlePostDelete = async (postId) => {
204 | try {
205 | const res = await fetch(`${API_BASE}/api/posts/delete/${postId}`, {
206 | method: "DELETE",
207 | });
208 | const data = await res.json();
209 |
210 | //===checking reqest success or not ===//
211 | if (data.success === false) {
212 | //===showing error in tostify====//
213 | toast.error(data.message, {
214 | autoClose: 2000,
215 | });
216 | } else {
217 | const restPost = userPosts.posts.filter((post) => post._id !== postId);
218 | setUserPost({
219 | ...userPosts,
220 | posts: restPost,
221 | });
222 |
223 | toast.success(data, {
224 | autoClose: 2000,
225 | });
226 | }
227 | } catch (error) {
228 | toast.error(error.message, {
229 | autoClose: 2000,
230 | });
231 | }
232 | };
233 |
234 | return (
235 | <>
236 |
237 |
238 |
239 |
322 |
323 |
324 |
328 | Log Out
329 |
330 |
331 |
335 | Delete
336 |
337 |
338 |
339 |
340 |
341 | {/*======== post section start here ========= */}
342 |
343 |
344 | {userPostLoading ? (
345 |
346 |
347 |
348 | Loading your post…
349 |
350 |
351 | ) : (
352 |
353 | {/* ADD NEW POST BUTTON */}
354 |
355 | navigate("/create_post")}
357 | type="submit"
358 | className=" px-5 bg-slate-300 font-heading shadow-lg text-black text-lg rounded-sm hover:opacity-95 w-full h-full flex justify-center items-center flex-col py-10 sm:py-10"
359 | >
360 |
361 | Create New Post
362 |
363 |
364 |
365 | {userPosts.isPostExist &&
366 | userPosts.posts.map((post) => (
367 |
371 | ))}
372 |
373 | )}
374 |
375 |
376 |
377 |
378 |
379 | >
380 | );
381 | };
382 |
383 | export default Profile;
384 |
--------------------------------------------------------------------------------
/src/pages/Search.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { BsSearch } from "react-icons/bs";
3 | import {
4 | FaAngleDoubleLeft,
5 | FaAngleDoubleRight,
6 | FaSearch,
7 | } from "react-icons/fa";
8 | import ListingCard from "../components/ListingCard";
9 | import { useDispatch, useSelector } from "react-redux";
10 | import { setSearchTermState } from "../redux/search/searchSlice";
11 | import Footer from "../components/Footer";
12 | import { LuSearchX } from "react-icons/lu";
13 |
14 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
15 |
16 | const Search = () => {
17 | const [listings, setListings] = useState([]);
18 | const { searchTermState } = useSelector((state) => state.search);
19 | const [loading, setLoading] = useState(false);
20 | const [pageCount, setPageCount] = useState(1);
21 | const scrollRef = useRef();
22 |
23 | const [formState, setFormState] = useState({
24 | searchTerm: "",
25 | parking: false,
26 | type: "all",
27 | furnished: false,
28 | offer: false,
29 | });
30 | const dispatch = useDispatch();
31 | const handleSubmit = (e) => {
32 | e.preventDefault();
33 | dispatch(setSearchTermState(""));
34 | setFormState({
35 | searchTerm: "",
36 | parking: false,
37 | type: "all",
38 | furnished: false,
39 | offer: false,
40 | });
41 | };
42 |
43 | useEffect(() => {
44 | (async () => {
45 | try {
46 | setLoading(true);
47 | const res = await fetch(
48 | `${API_BASE}/api/posts?searchTerm=${searchTermState}&type=${formState.type}&parking=${formState.parking}&furnished=${formState.furnished}&offer=${formState.offer}&page=${pageCount}`
49 | );
50 | const json = await res.json();
51 | if (json.success === false) {
52 | setLoading(false);
53 | } else {
54 | setListings(json);
55 | setLoading(false);
56 | }
57 | } catch (error) {
58 | console.log(error);
59 | setLoading(false);
60 | }
61 | })();
62 | }, [formState, searchTermState, pageCount]);
63 |
64 | const handleChange = (name, value) => {
65 | setPageCount(1);
66 | setFormState({
67 | ...formState,
68 | [name]: value,
69 | });
70 | };
71 |
72 | useEffect(() => {
73 | scrollRef.current.scrollIntoView({ behavior: "smooth" });
74 | }, [listings]);
75 |
76 | return (
77 | <>
78 |
79 |
80 |
81 |
227 |
228 | {loading ? (
229 |
230 |
231 |
232 | Searching...
233 |
234 |
235 | ) : (
236 |
237 | {listings.length !== 0 ? (
238 | <>
239 |
240 | {listings &&
241 | listings.map((listing) => (
242 |
243 | ))}
244 |
245 |
246 |
247 | {/* prev Btn */}
248 | setPageCount(pageCount - 1)}
250 | disabled={pageCount <= 1 || loading}
251 | className="join-item btn bg-brand-blue text-white hover:bg-brand-blue/90
252 | disabled:bg-[#d5d5d5] disabled:text-[#a0a0a0]
253 | "
254 | >
255 |
256 |
257 |
258 |
259 | Page {pageCount}
260 |
261 |
262 | {/* Next Btn */}
263 | setPageCount(pageCount + 1)}
265 | disabled={listings.length < 4 || loading}
266 | className="join-item btn bg-brand-blue text-white hover:bg-brand-blue/90
267 | disabled:bg-[#d5d5d5] disabled:text-[#a0a0a0]"
268 | >
269 |
270 |
271 |
272 |
273 | >
274 | ) : (
275 |
276 |
277 |
278 | Sorry, Listings not found
279 |
280 |
281 | )}
282 |
283 | )}
284 |
285 |
286 |
287 |
288 | <>
289 |
290 | >
291 | >
292 | );
293 | };
294 |
295 | export default Search;
296 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | const Footer = () => {
5 | return (
6 |
249 | )
250 | }
251 |
252 | export default Footer
--------------------------------------------------------------------------------
/src/pages/ListingPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useNavigate, useParams } from "react-router-dom";
3 | import { ToastContainer, toast } from "react-toastify";
4 | import "slick-carousel/slick/slick.css";
5 | import "slick-carousel/slick/slick-theme.css";
6 | import Slider from "react-slick";
7 | import { BsArrowRight, BsArrowLeft } from "react-icons/bs";
8 | import { BiSolidArea } from "react-icons/bi";
9 | import {
10 | FaLocationArrow,
11 | FaBed,
12 | FaBath,
13 | FaAngleUp,
14 | FaAngleDown,
15 | FaShare,
16 | FaLock,
17 | FaBookmark,
18 | } from "react-icons/fa";
19 | import Loading from "../components/Loading";
20 | import { useDispatch, useSelector } from "react-redux";
21 | import Contact from "../components/Contact";
22 | import {
23 | handleLisingRemove,
24 | handleSave,
25 | } from "../redux/saveListing/saveListingSlice";
26 | const API_BASE = import.meta.env.VITE_API_BASE_URL;
27 |
28 | const ListingPage = () => {
29 | const [listings, setListings] = useState({});
30 | const [isFeatureActive, setIsFeatureActive] = useState(false);
31 | const [loading, setLoading] = useState(false);
32 | const [savedListing, setSavedListing] = useState(false);
33 |
34 | const {
35 | area,
36 | address,
37 | bath,
38 | bed,
39 | description,
40 | discountPrice,
41 | furnished,
42 | offer,
43 | parking,
44 | price,
45 | title,
46 | type,
47 | _id,
48 | userRef,
49 | } = listings;
50 |
51 | const navigate = useNavigate();
52 | const params = useParams();
53 | const { currentUser } = useSelector((state) => state.user);
54 |
55 | const { saveListings } = useSelector((state) => state.savedListing);
56 |
57 | const dispatch = useDispatch();
58 |
59 | //====== Loading Post Data Here ======//
60 | useEffect(() => {
61 | (async () => {
62 | setLoading(true);
63 | const res = await fetch(`${API_BASE}/api/posts/${params.id}`);
64 | const json = await res.json();
65 | if (json.success === false) {
66 | toast.error(json.message, {
67 | autoClose: 2000,
68 | });
69 | setLoading(false);
70 | } else {
71 | setListings(json);
72 | setLoading(false);
73 | if (_id) {
74 | const isSaved = saveListings.some(
75 | (saveListing) => saveListing._id === _id
76 | );
77 |
78 | isSaved && setSavedListing(true);
79 | }
80 | }
81 | })();
82 | }, []);
83 |
84 | //====SLider Functions=====//
85 | function SamplePrevArrow({ onClick }) {
86 | return (
87 |
91 |
92 |
93 | );
94 | }
95 | function SampleNextArrow({ onClick }) {
96 | return (
97 |
101 |
102 |
103 | );
104 | }
105 |
106 | const settings = {
107 | dots: true,
108 | infinite: true,
109 | lazyLoad: false,
110 | speed: 500,
111 | slidesToShow: 1,
112 | slidesToScroll: 1,
113 | nextArrow: ,
114 | prevArrow: ,
115 | appendDots: (dots) => (
116 |
119 | ),
120 | };
121 |
122 | // ======Handling User Post DELETE =====//
123 | const handlePostDelete = async (postId) => {
124 | try {
125 | const res = await fetch(`${API_BASE}/api/posts/delete/${postId}`, {
126 | method: "DELETE",
127 | });
128 | const data = await res.json();
129 |
130 | //===checking reqest success or not ===//
131 | if (data.success === false) {
132 | //===showing error in tostify====//
133 | toast.error(data.message, {
134 | autoClose: 2000,
135 | });
136 | } else {
137 | navigate("/profile");
138 | }
139 | } catch (error) {
140 | toast.error(error.message, {
141 | autoClose: 2000,
142 | });
143 | }
144 | };
145 |
146 | const handleUrlShare = async () => {
147 | const url = window.location.href;
148 | try {
149 | await navigator.clipboard.writeText(url);
150 |
151 | toast.success("URL coppied !", {
152 | autoClose: 1000,
153 | });
154 | } catch (error) {
155 | toast.error("URL coppied failed!", {
156 | autoClose: 2000,
157 | });
158 | }
159 | };
160 |
161 | const handleSaveListing = () => {
162 | const isSaved = saveListings.some((saveListing) => saveListing._id === _id);
163 | if (isSaved) {
164 | const restListings = saveListings.filter(
165 | (savedListing) => savedListing._id !== _id
166 | );
167 | dispatch(handleLisingRemove(restListings));
168 | setSavedListing(false);
169 | } else {
170 | const listingToAdd = listings;
171 | dispatch(handleSave(listingToAdd));
172 | setSavedListing(true);
173 | }
174 | };
175 |
176 | return (
177 | <>
178 | {loading ? (
179 | <>
180 |
181 |
182 | Loading your post…
183 |
184 | >
185 | ) : (
186 |
187 |
188 | {listings.imgUrl &&
189 | listings.imgUrl.map((listing, index) => (
190 |
194 |
199 |
200 | ))}
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 | {type}
212 |
213 |
214 |
215 |
216 | {title}
217 |
218 |
219 |
220 | {address}
221 |
222 |
223 |
224 |
225 | Description
226 |
227 |
228 | {description}
229 |
230 |
231 |
232 | {offer ? (
233 |
234 | ${discountPrice}{" "}
235 |
236 | ${price}
237 |
238 |
239 | ) : (
240 |
241 | ${price}
242 |
243 | )}
244 |
245 |
246 |
247 |
248 |
249 | {bed} Beds
250 |
251 |
252 |
253 | {bath} Bath
254 |
255 |
256 |
257 | {area} sqft
258 |
259 |
260 |
261 |
262 | {/* Feature Content Section */}
263 |
264 |
265 |
setIsFeatureActive(!isFeatureActive)}
267 | className="feature_heading flex items-center justify-between cursor-pointer"
268 | >
269 |
270 | Detail & Features
271 |
272 | {isFeatureActive ? (
273 |
274 |
275 |
276 | ) : (
277 |
278 |
279 |
280 | )}
281 |
282 |
287 |
288 |
289 |
290 | Bedrooms
291 |
292 |
293 | {bed}
294 |
295 |
296 |
297 |
298 | BathRoom
299 |
300 |
301 | {bath}
302 |
303 |
304 |
305 |
306 | Parking
307 |
308 |
313 | {parking ? "Yes" : "No"}
314 |
315 |
316 |
317 |
318 | Furnished
319 |
320 |
325 | {furnished ? "Yes" : "No"}
326 |
327 |
328 |
329 |
330 | Area
331 |
332 |
333 | {area} sqft
334 |
335 |
336 | {offer && (
337 |
338 |
339 | Price
340 |
341 |
342 | ${discountPrice}{" "}
343 |
344 |
345 | ${price}
346 |
347 |
348 |
349 |
350 | )}
351 |
352 |
353 |
354 |
355 |
356 |
357 | {currentUser && currentUser._id === userRef ? (
358 |
359 |
360 |
361 |
363 | navigate(`/update_post/${params.id}`)
364 | }
365 | className="bg-brand-blue hover:bg-brand-blue/90 text-white w-full px-2 py-3 text-lg font-heading rounded-sm"
366 | >
367 | Update Post
368 |
369 |
370 |
371 | handlePostDelete(params.id)}
373 | className="bg-red-600 hover:bg-red-600/90 text-white w-full px-2 py-3 text-lg font-heading rounded-sm"
374 | >
375 | Delete Post
376 |
377 |
378 |
379 |
380 | navigate(`/profile`)}
382 | className="bg-amber-700 hover:bg-amber-700/90 uppercase text-white w-full px-2 py-3 text-lg font-heading rounded-sm"
383 | >
384 | My All Posts
385 |
386 |
387 |
388 | ) : (
389 |
390 |
391 |
392 |
396 |
397 |
398 | Share Url
399 |
400 |
401 |
402 |
403 |
407 |
408 |
415 | {savedListing ? "Saved" : "Save"}
416 |
417 |
418 |
419 |
420 |
421 | {currentUser && currentUser.email ? (
422 |
423 |
427 |
428 | ) : (
429 |
navigate("/login")}
431 | className="bg-red-600 hover:bg-red-600/90 text-white w-full px-2 py-3 text-lg font-heading rounded-sm"
432 | >
433 |
434 |
435 | Login to Contact
436 |
437 |
438 | )}
439 |
440 |
441 | )}
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 | )}
450 | >
451 | );
452 | };
453 |
454 | export default ListingPage;
455 |
--------------------------------------------------------------------------------