├── public ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── postcss.config.js ├── src ├── setupTests.js ├── reportWebVitals.js ├── components │ ├── Spinner.jsx │ ├── PrivateRoute.jsx │ ├── OAuth.jsx │ ├── Contact.jsx │ ├── Header.jsx │ ├── Slider.jsx │ └── ListingItem.jsx ├── index.css ├── index.js ├── firebase.js ├── assets │ └── svg │ │ └── spinner.svg ├── hooks │ └── useAuthStatus.jsx ├── App.js ├── pages │ ├── ForgotPassword.jsx │ ├── Offers.jsx │ ├── Category.jsx │ ├── SignIn.jsx │ ├── Home.jsx │ ├── SignUp.jsx │ ├── Listing.jsx │ ├── Profile.jsx │ ├── CreateListing.jsx │ └── EditListing.jsx └── img │ └── realtor-logo.svg ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mardoqueu/realtor-clone-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [ 10 | require('@tailwindcss/forms'), 11 | ], 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import spinner from '../assets/svg/spinner.svg' 3 | const Spinner = () => { 4 | return ( 5 |
6 |
7 | Loading... 8 |
9 |
10 | ); 11 | } 12 | 13 | export default Spinner; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background-color: rgb(240, 253, 244); 7 | } 8 | 9 | .swiper-button-next:after, 10 | .swiper-button-prev:after{ 11 | color: #a8dadc; 12 | } 13 | 14 | .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{ 15 | background: #a8dadc !important 16 | } 17 | 18 | html::-webkit-scrollbar{ 19 | display: none; 20 | } 21 | 22 | html{ 23 | scrollbar-width: none; 24 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | 19 | -------------------------------------------------------------------------------- /src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet, Navigate } from 'react-router-dom'; 3 | import {useAuthStatus} from '../hooks/useAuthStatus'; 4 | import Spinner from './Spinner'; 5 | 6 | /* Import the outlets for adding the Children inside this and navigate the person to sing in page 7 | */ 8 | const PrivateRoute = () => { 9 | const {loggedIn, checkingStatus} = useAuthStatus(); 10 | /* if checkingStatus is true, it means we were getting the information 11 | so just show the loading */ 12 | if(checkingStatus){ 13 | return ; 14 | } 15 | /* Otherwise, if the logging is true, we get the outlie order and if 16 | it's false, we are going to be redirect to the sign in page */ 17 | return loggedIn ? : 18 | } 19 | 20 | export default PrivateRoute; 21 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import {getFirestore} from "firebase/firestore" 4 | 5 | // TODO: Add SDKs for Firebase products that you want to use 6 | // https://firebase.google.com/docs/web/setup#available-libraries 7 | 8 | // Your web app's Firebase configuration 9 | const firebaseConfig = { 10 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 11 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 12 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 13 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 14 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 15 | appId: process.env.REACT_APP_FIREBASE_MESSAGING_APP_ID, 16 | }; 17 | 18 | // Initialize Firebase 19 | initializeApp(firebaseConfig); 20 | export const db = getFirestore(); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | Realtor Clone React 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/svg/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/hooks/useAuthStatus.jsx: -------------------------------------------------------------------------------- 1 | import { getAuth, onAuthStateChanged } from 'firebase/auth'; 2 | import {useEffect, useState } from 'react'; 3 | 4 | /* This hook is getting information from the firebase, including the getAuth 5 | and onAuthStateChanged */ 6 | export const useAuthStatus = () => { 7 | /* loggedIn false by default */ 8 | const [loggedIn, setLoggedIn] = useState(false) 9 | /* checkingStatus is true by default */ 10 | const [checkingStatus, setCheckingStatus] = useState(true) 11 | 12 | /* After using useEffect one time rendering and getting information from the Auth 13 | it gets true or false of the person to be authenticated or not */ 14 | useEffect(() => { 15 | const auth = getAuth() 16 | onAuthStateChanged(auth, (user) => { 17 | /* if the user exists it means the person is logged 18 | */ if(user){ 19 | setLoggedIn(true) 20 | } 21 | /* put the setCheckingStatus to false 22 | */ setCheckingStatus(false) 23 | }) 24 | }, []); 25 | /* export the return, the loggedIn and the checkingStatus */ 26 | return {loggedIn, checkingStatus} 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtor-clone-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "firebase": "^9.13.0", 10 | "leaflet": "^1.9.2", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-icons": "^4.6.0", 14 | "react-leaflet": "^4.1.0", 15 | "react-moment": "^1.1.2", 16 | "react-router": "^6.4.2", 17 | "react-router-dom": "^6.4.2", 18 | "react-scripts": "5.0.1", 19 | "react-toastify": "^9.0.8", 20 | "swiper": "^8.4.4", 21 | "uuid": "^9.0.0", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@tailwindcss/forms": "^0.5.3", 50 | "autoprefixer": "^10.4.12", 51 | "postcss": "^8.4.18", 52 | "tailwindcss": "^3.2.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/OAuth.jsx: -------------------------------------------------------------------------------- 1 | import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; 2 | import { doc, getDoc, serverTimestamp, setDoc } from 'firebase/firestore'; 3 | import React from 'react'; 4 | import {FcGoogle} from 'react-icons/fc' 5 | import { toast } from 'react-toastify'; 6 | import { db } from "../firebase"; 7 | import { useNavigate } from "react-router-dom"; 8 | 9 | const OAuth = () => { 10 | const navigate = useNavigate(); 11 | 12 | async function onGoogleClick(){ 13 | try { 14 | /* Firstly sing up the person with pop up 15 | */ const auth = getAuth(); 16 | const provider = new GoogleAuthProvider(); 17 | const result = await signInWithPopup(auth, provider); 18 | /* Got the user using result which is coming as a promise from signInWithPopup */ 19 | const user = result.user; 20 | 21 | /* check if the user already exists 22 | 23 | */ 24 | const docRef = doc(db, "users", user.uid); 25 | const docSnap = await getDoc(docRef); 26 | 27 | if (!docSnap.exists()) { 28 | await setDoc(docRef, { 29 | name: user.displayName, 30 | email: user.email, 31 | timestamp: serverTimestamp(), 32 | }); 33 | } 34 | 35 | /* redirect the user to home page */ 36 | navigate("/"); 37 | 38 | /* catching error */ 39 | } catch (error) { 40 | toast.error("Could not authorize with Google") 41 | } 42 | } 43 | return ( 44 | 56 | ); 57 | } 58 | 59 | export default OAuth; 60 | -------------------------------------------------------------------------------- /src/components/Contact.jsx: -------------------------------------------------------------------------------- 1 | import { doc, getDoc } from "firebase/firestore"; 2 | import { useState } from "react"; 3 | import { useEffect } from "react"; 4 | import { toast } from "react-toastify"; 5 | import { db } from "../firebase"; 6 | 7 | export default function Contact({ userRef, listing }) { 8 | const [landlord, setLandlord] = useState(null); 9 | const [message, setMessage] = useState(""); 10 | useEffect(() => { 11 | async function getLandlord() { 12 | const docRef = doc(db, "users", userRef); 13 | const docSnap = await getDoc(docRef); 14 | if (docSnap.exists()) { 15 | setLandlord(docSnap.data()); 16 | } else { 17 | toast.error("Could not get landlord data"); 18 | } 19 | } 20 | getLandlord(); 21 | }, [userRef]); 22 | function onChange(e) { 23 | setMessage(e.target.value); 24 | } 25 | return ( 26 | <> 27 | {/* if the landlord exists adn it's not equal to null, we are going to have the contact information for the listing, the text area and the button. */} 28 | {landlord !== null && ( 29 |
30 |

31 | Contact {landlord.name} for the {listing.name.toLowerCase()} 32 |

33 |
34 | 42 |
43 | 46 | {/* The button is inside an anchor tag, which is going to redirect the person to the mail software with the email and Subject. */} 47 | 50 | 51 |
52 | )} 53 | 54 | ); 55 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import {BrowserRouter as Router, Routes, Route} from "react-router-dom" 2 | import Home from "./pages/Home" 3 | import Profile from "./pages/Profile" 4 | import SignIn from "./pages/SignIn" 5 | import SignUp from "./pages/SignUp" 6 | import PrivateRoute from "./components/PrivateRoute" 7 | import ForgotPassword from "./pages/ForgotPassword" 8 | import Offers from "./pages/Offers" 9 | import Header from "./components/Header" 10 | import { ToastContainer } from "react-toastify" 11 | import 'react-toastify/dist/ReactToastify.css'; 12 | import CreateListing from "./pages/CreateListing" 13 | import EditListing from "./pages/EditListing" 14 | import Listing from "./pages/Listing"; 15 | import Category from "./pages/Category" 16 | 17 | 18 | function App() { 19 | return ( 20 | <> 21 | 22 |
23 | 24 | }/> 25 | {/* Protected the profile page by putting it inside another router 26 | and with the path of profile */} 27 | }> 28 | }/> 29 | 30 | }/> 31 | }/> 32 | }/> 33 | } 34 | /> 35 | }/> 36 | }/> 37 | {/* Protected the profile page by putting it inside another router 38 | and with the path of profile */} 39 | }> 40 | }/> 41 | 42 | {/* Protected the profile page by putting it inside another router 43 | and with the path of profile */} 44 | }> 45 | }/> 46 | 47 | 48 | 49 | 61 | 62 | ); 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | import IMG from "../img/realtor-logo.svg" 4 | import {getAuth, onAuthStateChanged} from "firebase/auth" 5 | const Header = () => { 6 | const [pageState, setPageState] = useState("Sign in") 7 | /*useLocation returns the current location object, which represents the current URL in web browsers. */ 8 | const location = useLocation(); 9 | 10 | /*useNavigate returns an imperative method for changing the location. Used by s, but may also be used by other elements to change the location. */ 11 | const navigate = useNavigate() 12 | const auth = getAuth(); 13 | /* added useEffect to track the changes in auth */ 14 | useEffect(() => { 15 | onAuthStateChanged(auth, (user) =>{ 16 | if(user){ 17 | setPageState('Profile') 18 | }else{ 19 | setPageState('Sign in') 20 | } 21 | } ) 22 | }, [auth]); 23 | 24 | function pathMatchRoute(route){ 25 | if(route === location.pathname){ 26 | return true; 27 | } 28 | } 29 | 30 | return ( 31 |
32 |
33 |
34 | logo navigate("/")}> 37 |
38 |
39 | {/* Items of menu where I used useLocation to returns the current location of each element on menu and the useNavigation to returns the changing of each element on menu */} 40 |
    41 |
  • navigate("/")}>Home
  • 42 |
  • navigate("/offers")}>Offers
  • 43 | 44 |
  • navigate("/profile")}> 47 | {pageState} 48 |
  • 49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | export default Header; 57 | -------------------------------------------------------------------------------- /src/components/Slider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useEffect, useState } from 'react'; 3 | import {collection, orderBy, query, limit, getDocs} from 'firebase/firestore' 4 | import {db} from '../firebase'; 5 | import Spinner from '../components/Spinner'; 6 | import {Swiper, SwiperSlide} from 'swiper/react'; 7 | import SwiperCore, { 8 | EffectFade, 9 | Autoplay, 10 | Navigation, 11 | Pagination 12 | } from "swiper" 13 | import "swiper/css/bundle" 14 | import { useNavigate } from 'react-router'; 15 | 16 | export default function Slider() { 17 | const [listings, setListings] = useState(null); 18 | const [loading, setLoading] = useState(true); 19 | const navigate = useNavigate() 20 | 21 | SwiperCore.use([Autoplay, Navigation, Pagination]); 22 | useEffect(()=>{ 23 | async function fetchListings(){ 24 | const listingsRef = collection(db, "listings"); 25 | const q = query(listingsRef, orderBy("timestamp", "desc"), limit(5)) 26 | const querySnap = await getDocs(q) 27 | let listings = []; 28 | querySnap.forEach((doc)=>{ 29 | return listings.push({ 30 | id: doc.id, 31 | data: doc.data(), 32 | }); 33 | }) 34 | setListings(listings); 35 | setLoading(false); 36 | } 37 | fetchListings() 38 | }, []) 39 | 40 | if(loading){ 41 | return 42 | } 43 | if(listings.length === 0){ 44 | return <>; 45 | } 46 | return ( 47 | listings && ( 48 | <> 49 | 56 | {listings.map(({ data, id }) => ( 57 | navigate(`/category/${data.type}/${id}`)} 60 | > 61 |
69 |

70 | {data.name} 71 |

72 |

73 | ${data.discountedPrice ?? data.regularPrice} 74 | {data.type === "rent" && " / month"} 75 |

76 |
77 | ))} 78 |
79 | 80 | ) 81 | ); 82 | } -------------------------------------------------------------------------------- /src/components/ListingItem.jsx: -------------------------------------------------------------------------------- 1 | import Moment from "react-moment"; 2 | import { Link } from "react-router-dom"; 3 | import {MdLocationOn} from 'react-icons/md'; 4 | import {FaTrash} from 'react-icons/fa'; 5 | import {MdEdit} from 'react-icons/md'; 6 | 7 | export default function ListingItem({ listing, id, onEdit, onDelete }) { 8 | /* Listing items we have created the listing */ 9 | return
  • 10 | 11 | 14 | {/* Adding moment to get the correct time */} 15 | {listing.timestamp?.toDate()} 16 |
    17 |
    18 | 19 |

    {listing.address}

    20 |
    21 |

    {listing.name}

    22 |

    ${listing.offer 23 | ? listing.discountedPrice 24 | .toString() 25 | .replace(/\B(?=(\d{3})+(!\d))/g, ",") 26 | : listing.regularPrice 27 | .toString() 28 | .replace(/\B(?=(\d{3})+(!\d))/g, ",")} 29 | {listing.type === "rent" && " / month"} 30 |

    31 |
    32 |
    33 |

    {listing.bedrooms > 1 ? `${listing.bedrooms} Beds` : "1 Bed"}

    34 |
    35 |
    36 |

    37 | {listing.bathrooms > 1 ? `${listing.bathrooms} Baths` : "1 Bath"}

    38 |
    39 |
    40 |
    41 | 42 | {/* if delete exists, it is going to have the trash icon, and this is going to call the function */} 43 | {onDelete && ( 44 | onDelete(listing.id)}/> 46 | 47 | )} 48 | {onEdit && ( 49 | onEdit(listing.id)}/> 51 | 52 | )} 53 |
  • ; 54 | } -------------------------------------------------------------------------------- /src/pages/ForgotPassword.jsx: -------------------------------------------------------------------------------- 1 | import { getAuth, sendPasswordResetEmail } from 'firebase/auth'; 2 | import React from 'react'; 3 | import {useState} from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | import { toast } from 'react-toastify'; 6 | import OAuth from '../components/OAuth'; 7 | 8 | 9 | const ForgotPassword = () => { 10 | const [email, setEmail] = useState(""); 11 | function onChange(e){ 12 | setEmail(e.target.value); 13 | }; 14 | 15 | async function onSubmit(e){ 16 | /* Prevent to refresh the page */ 17 | e.preventDefault(); 18 | try { 19 | /* Send the password */ 20 | const auth = getAuth() 21 | await sendPasswordResetEmail(auth, email) 22 | /* if it was successful */ 23 | toast.success("Email was sent") 24 | } catch (error) { 25 | toast.error("Could not resend password") 26 | } 27 | } 28 | return ( 29 |
    30 |

    Forgot Password?

    31 |
    32 |
    33 | key 37 |
    38 |
    39 |
    40 | 48 |
    49 |

    Don't have a account? 50 | Register 53 |

    54 |

    55 | Sign in instead 59 |

    60 |
    61 | 64 |
    67 |

    OR

    68 |
    69 | 70 | 71 | 72 |
    73 |
    74 | 75 |
    76 | ); 77 | } 78 | 79 | export default ForgotPassword; 80 | -------------------------------------------------------------------------------- /src/pages/Offers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useEffect } from 'react'; 3 | import { useState } from 'react' 4 | import { toast } from 'react-toastify'; 5 | import {collection, getDocs, limit, orderBy, query, startAfter, where} from 'firebase/firestore' 6 | import { db } from '../firebase'; 7 | import Spinner from '../components/Spinner'; 8 | import ListingItem from '../components/ListingItem'; 9 | 10 | export default function Offers() { 11 | const [listings, setListings] = useState(null); 12 | const [loading, setLoading] = useState(true); 13 | const [lastFetchedListing, setLastFetchListing] = useState(null); 14 | /* useEffect for fetch the listing by putting the reference, the address and the query */ 15 | useEffect(() => { 16 | async function fetchListings() { 17 | try { 18 | const listingRef = collection(db, "listings"); 19 | const q = query( 20 | listingRef, 21 | where("offer", "==", true), 22 | orderBy("timestamp", "desc"), 23 | limit(8) 24 | ); 25 | const querySnap = await getDocs(q); 26 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]; 27 | setLastFetchListing(lastVisible); 28 | const listings = []; 29 | querySnap.forEach((doc) => { 30 | return listings.push({ 31 | id: doc.id, 32 | data: doc.data(), 33 | }); 34 | }); 35 | setListings(listings); 36 | setLoading(false); 37 | } catch (error) { 38 | toast.error("Could not fetch listing"); 39 | } 40 | } 41 | 42 | fetchListings(); 43 | }, []); 44 | 45 | async function onFetchMoreListings() { 46 | try { 47 | const listingRef = collection(db, "listings"); 48 | const q = query( 49 | listingRef, 50 | where("offer", "==", true), 51 | orderBy("timestamp", "desc"), 52 | startAfter(lastFetchedListing), 53 | limit(4) 54 | ); 55 | const querySnap = await getDocs(q); 56 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]; 57 | setLastFetchListing(lastVisible); 58 | const listings = []; 59 | querySnap.forEach((doc) => { 60 | return listings.push({ 61 | id: doc.id, 62 | data: doc.data(), 63 | }); 64 | }); 65 | setListings((prevState)=>[...prevState, ...listings]); 66 | setLoading(false); 67 | } catch (error) { 68 | toast.error("Could not fetch listing"); 69 | } 70 | } 71 | 72 | return ( 73 |
    74 |

    Offers

    75 | {loading ? ( 76 | 77 | ) : listings && listings.length > 0 ? ( 78 | <> 79 |
    80 |
      81 | {listings.map((listing)=>( 82 | 87 | ))} 88 |
    89 |
    90 | {lastFetchedListing && ( 91 |
    92 | 98 |
    99 | )} 100 | 101 | ) : ( 102 |

    There are no current offers

    103 | )} 104 |
    105 | ); 106 | } -------------------------------------------------------------------------------- /src/pages/Category.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useEffect } from 'react'; 3 | import { useState } from 'react' 4 | import { toast } from 'react-toastify'; 5 | import {collection, getDocs, limit, orderBy, query, startAfter, where} from 'firebase/firestore' 6 | import { db } from '../firebase'; 7 | import Spinner from '../components/Spinner'; 8 | import ListingItem from '../components/ListingItem'; 9 | import { useParams } from 'react-router-dom'; 10 | 11 | 12 | export default function Category() { 13 | const [listings, setListings] = useState(null); 14 | const [loading, setLoading] = useState(true); 15 | const [lastFetchedListing, setLastFetchListing] = useState(null); 16 | const params = useParams() 17 | /* useEffect for fetch the listing by putting the reference, the address and the query */ 18 | useEffect(() => { 19 | async function fetchListings() { 20 | try { 21 | const listingRef = collection(db, "listings"); 22 | const q = query( 23 | listingRef, 24 | where("type", "==", params.categoryName), 25 | orderBy("timestamp", "desc"), 26 | limit(8) 27 | ); 28 | const querySnap = await getDocs(q); 29 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]; 30 | setLastFetchListing(lastVisible); 31 | const listings = []; 32 | querySnap.forEach((doc) => { 33 | return listings.push({ 34 | id: doc.id, 35 | data: doc.data(), 36 | }); 37 | }); 38 | setListings(listings); 39 | setLoading(false); 40 | } catch (error) { 41 | toast.error("Could not fetch listing"); 42 | } 43 | } 44 | 45 | fetchListings(); 46 | }, [params.categoryName]); 47 | 48 | async function onFetchMoreListings() { 49 | try { 50 | const listingRef = collection(db, "listings"); 51 | const q = query( 52 | listingRef, 53 | where("type", "==", params.categoryName), 54 | orderBy("timestamp", "desc"), 55 | startAfter(lastFetchedListing), 56 | limit(4) 57 | ); 58 | const querySnap = await getDocs(q); 59 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]; 60 | setLastFetchListing(lastVisible); 61 | const listings = []; 62 | querySnap.forEach((doc) => { 63 | return listings.push({ 64 | id: doc.id, 65 | data: doc.data(), 66 | }); 67 | }); 68 | setListings((prevState)=>[...prevState, ...listings]); 69 | setLoading(false); 70 | } catch (error) { 71 | toast.error("Could not fetch listing"); 72 | } 73 | } 74 | 75 | return ( 76 |
    77 |

    78 | {params.categoryName === "rent" ? "Places for rent" : "Places for sell"} 79 |

    80 | {loading ? ( 81 | 82 | ) : listings && listings.length > 0 ? ( 83 | <> 84 |
    85 |
      86 | {listings.map((listing)=>( 87 | 92 | ))} 93 |
    94 |
    95 | {lastFetchedListing && ( 96 |
    97 | 103 |
    104 | )} 105 | 106 | ) : ( 107 |

    There are no current {" "} {params.categoryName === "rent" ? "Places for rent" : "Places for sell"}

    108 | )} 109 |
    110 | ); 111 | } -------------------------------------------------------------------------------- /src/pages/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useState} from 'react'; 3 | import {AiFillEyeInvisible, AiFillEye} from 'react-icons/ai' 4 | import { Link, useNavigate } from 'react-router-dom'; 5 | import OAuth from '../components/OAuth'; 6 | import { signInWithEmailAndPassword, getAuth } from 'firebase/auth'; 7 | import {toast} from "react-toastify" 8 | 9 | 10 | const SignIn = () => { 11 | /* Created a hook for show password */ 12 | const [showPassword, setShowPassword] = useState(false); 13 | 14 | const navigate = useNavigate(); 15 | /* created a hook for the formdata */ 16 | const [formData, setFormData] = useState({ 17 | /* Initial value was an empty string */ 18 | email: "", 19 | password: "", 20 | }); 21 | const {email, password} = formData; 22 | /* function for any change happens to the forms we are going to get it gets and puts it inside of the formdata state */ 23 | function onChange(e){ 24 | setFormData((prevState)=> ({ 25 | ...prevState, 26 | [e.target.id]: e.target.value, 27 | })); 28 | } 29 | 30 | async function onSubmit(e){ 31 | /* Prevent to refresh the page */ 32 | e.preventDefault() 33 | try { 34 | const auth = getAuth() 35 | const userCredential = await signInWithEmailAndPassword(auth, email, password) 36 | if(userCredential.user){ 37 | navigate("/") 38 | } 39 | } catch (error) { 40 | toast.error("Bad user credentials") 41 | } 42 | } 43 | return ( 44 |
    45 | {/* Just added h1 for the title sign up */} 46 |

    Sign In

    47 |
    48 |
    49 | {/* Adding image */} 50 | key 54 |
    55 | {/* Adding a form with two inputs 56 | 1. For the email 57 | 2. For the password 58 | So each input has a value onChange for tracking the changes inside our input */} 59 |
    60 |
    61 | 69 | {/* For the password field added the condition if the show password is true, we want to see the icon AiFillEyeInvisible and it it's false it shows AiFillEye */} 70 |
    71 | 79 | {showPassword ? setShowPassword((prevState) => ! prevState)}/> 81 | : 82 | setShowPassword((prevState) => ! prevState)} 84 | />} 85 |
    86 |
    87 |

    Don't have a account? 88 | Register 91 |

    92 |

    93 | Forgot password? 97 |

    98 |
    99 | 102 |
    105 |

    OR

    106 |
    107 | 108 | 109 | 110 |
    111 |
    112 | 113 |
    114 | ); 115 | } 116 | 117 | export default SignIn; 118 | -------------------------------------------------------------------------------- /src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { collection, doc, getDoc, getDocs,limit, orderBy, query, where } from 'firebase/firestore'; 2 | import { useEffect, useState } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import ListingItem from '../components/ListingItem'; 5 | import Slider from '../components/Slider'; 6 | import { db } from '../firebase'; 7 | 8 | const Home = () => { 9 | // Offers 10 | const [offerListings, setOfferListings] = useState(null) 11 | 12 | useEffect(()=>{ 13 | async function fetchListings() { 14 | try { 15 | // get reference 16 | const listingsRef = collection(db, "listings"); 17 | // create the query 18 | const q = query( 19 | listingsRef, 20 | where("offer", "==", true), 21 | orderBy("timestamp", "desc"), 22 | limit(4) 23 | ); 24 | // execute the query 25 | const querySnap = await getDocs(q); 26 | const listings = []; 27 | querySnap.forEach((doc) => { 28 | return listings.push({ 29 | id: doc.id, 30 | data: doc.data(), 31 | }); 32 | }); 33 | setOfferListings(listings); 34 | } catch (error) { 35 | console.log(error); 36 | } 37 | } 38 | fetchListings() 39 | }, []) 40 | // Places for rent 41 | const [rentListings, setRentListings] = useState(null) 42 | 43 | useEffect(()=>{ 44 | async function fetchListings() { 45 | try { 46 | // get reference 47 | const listingsRef = collection(db, "listings"); 48 | // create the query 49 | const q = query( 50 | listingsRef, 51 | where("type", "==", "rent"), 52 | orderBy("timestamp", "desc"), 53 | limit(4) 54 | ); 55 | // execute the query 56 | const querySnap = await getDocs(q); 57 | const listings = []; 58 | querySnap.forEach((doc) => { 59 | return listings.push({ 60 | id: doc.id, 61 | data: doc.data(), 62 | }); 63 | }); 64 | setRentListings(listings); 65 | } catch (error) { 66 | console.log(error); 67 | } 68 | } 69 | fetchListings() 70 | }, []) 71 | // Places for rent 72 | const [saleListings, setSaleListings] = useState(null) 73 | 74 | useEffect(()=>{ 75 | async function fetchListings() { 76 | try { 77 | // get reference 78 | const listingsRef = collection(db, "listings"); 79 | // create the query 80 | const q = query( 81 | listingsRef, 82 | where("type", "==", "sale"), 83 | orderBy("timestamp", "desc"), 84 | limit(4) 85 | ); 86 | // execute the query 87 | const querySnap = await getDocs(q); 88 | const listings = []; 89 | querySnap.forEach((doc) => { 90 | return listings.push({ 91 | id: doc.id, 92 | data: doc.data(), 93 | }); 94 | }); 95 | setSaleListings(listings); 96 | } catch (error) { 97 | console.log(error); 98 | } 99 | } 100 | fetchListings() 101 | }, []) 102 | return ( 103 |
    104 | 105 |
    106 | {offerListings && offerListings.length > 0 && ( 107 |
    108 |

    Recent offers

    109 | 110 |

    Show more offers

    111 | 112 |
      113 | {offerListings.map((listing) => ( 114 | 119 | ))} 120 |
    121 |
    122 | )} 123 | {rentListings && rentListings.length > 0 && ( 124 |
    125 |

    Places for rent

    126 | 127 |

    Show more places

    128 | 129 |
      130 | {rentListings.map((listing) => ( 131 | 136 | ))} 137 |
    138 |
    139 | )} 140 | {saleListings && saleListings.length > 0 && ( 141 |
    142 |

    Places for sale

    143 | 144 |

    Show more places for sale

    145 | 146 |
      147 | {saleListings.map((listing) => ( 148 | 153 | ))} 154 |
    155 |
    156 | )} 157 |
    158 |
    159 | ); 160 | } 161 | 162 | export default Home; 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React.js & Firebase Project - ReactJS 18, Firebase 9 Project 2 | 3 | 4 | React.js and Firebase portfolio project. Build Realtor (Real estate) clone using React js 18, Firebase 9, Tailwind CSS 3 5 | 6 | ![screencapture-realtor-clone-react-blond-vercel-app-2022-11-15-12_21_34](https://user-images.githubusercontent.com/11077068/201957419-742ac966-a3c7-431b-93bd-f89b60f65fb6.png) 7 | 8 | ### Project Description 9 | - Realtor.com is a real estate listings website operated by the News Corp subsidiary Move, Inc. and based in Santa Clara, California. It is the second most visited real estate listings website in the United States as of 2021, with over 100 million monthly active users. This project focus on build a realtor clone application. 10 | 11 | ### Stacks 12 | - Build Realtor (Real estate) clone using React js 18, Firebase 9, Tailwind CSS 3, and React router 6. 13 | - Create a React js project from scratch 14 | - Use Firebase auth for complete authentication 15 | - Use Firebase Firestore to store and fetch data 16 | - Sign up/in the users using username/password and Google oAuth using Firebase auth 17 | - Add forgot password functionality using Firebase auth 18 | - Work with latest versions like React js 18, Firebase 9 and Tailwind CSS 3 19 | - CRUD operations including create, read, update and delete using Firebase Firestore 20 | - React router version 6 (latest version) to create routes, get the params and redirect 21 | - Create pages and routes in a react project 22 | - React toastify to create nice notifications 23 | - Create private route and custom hook for protecting the user profile page 24 | - Spinner and loader 25 | - React event listeners like onChange and onSubmit 26 | - Reusable component such as listing cards 27 | - Create an image slider using Swiper js latest version 28 | - Add map to the page using leaflet and react leaflet packages 29 | - Deploy to vercel 30 | - Google geolocation api and how to convert address to latitude and longitude 31 | - Tailwind CSS 3 to style a react project 32 | - useEffect and useState react hooks 33 | 34 | 35 | ### Screens 36 | 37 | 38 | ![screencapture-realtor-clone-react-blond-vercel-app-2022-11-15-12_21_34](https://user-images.githubusercontent.com/11077068/201959452-01580f92-613f-4f42-9cd7-c800e7099031.png) 39 | 40 | ![screencapture-realtor-clone-react-blond-vercel-app-profile-2022-11-15-12_30_08](https://user-images.githubusercontent.com/11077068/201959465-9e779363-463b-46b9-93f2-05ef7c372b55.png) 41 | 42 | ![screencapture-realtor-clone-react-blond-vercel-app-create-listing-2022-11-15-12_30_20](https://user-images.githubusercontent.com/11077068/201959490-76ea9147-aef8-45e2-bf49-3ba03f1280e3.png) 43 | 44 | ### Mobile 45 | 46 | 47 | ![screencapture-realtor-clone-react-blond-vercel-app-2022-11-15-12_28_52](https://user-images.githubusercontent.com/11077068/201960237-4e24456c-8857-45e8-8531-ea3f69773181.png) 48 | 49 | ![screencapture-realtor-clone-react-blond-vercel-app-category-sale-J184UWtV8lm724yWXgCK-2022-11-15-12_29_21](https://user-images.githubusercontent.com/11077068/201960243-9d2ba727-0f36-4418-bb53-6106829906c6.png) 50 | 51 | 52 | # Getting Started with Create React App 53 | 54 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 55 | 56 | ## Available Scripts 57 | 58 | In the project directory, you can run: 59 | 60 | ### `npm start` 61 | 62 | Runs the app in the development mode.\ 63 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 64 | 65 | The page will reload when you make changes.\ 66 | You may also see any lint errors in the console. 67 | 68 | ### `npm test` 69 | 70 | Launches the test runner in the interactive watch mode.\ 71 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 72 | 73 | ### `npm run build` 74 | 75 | Builds the app for production to the `build` folder.\ 76 | It correctly bundles React in production mode and optimizes the build for the best performance. 77 | 78 | The build is minified and the filenames include the hashes.\ 79 | Your app is ready to be deployed! 80 | 81 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 82 | 83 | ### `npm run eject` 84 | 85 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 86 | 87 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 88 | 89 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 90 | 91 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 92 | 93 | ## Learn More 94 | 95 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 96 | 97 | To learn React, check out the [React documentation](https://reactjs.org/). 98 | 99 | ### Code Splitting 100 | 101 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 102 | 103 | ### Analyzing the Bundle Size 104 | 105 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 106 | 107 | ### Making a Progressive Web App 108 | 109 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 110 | 111 | ### Advanced Configuration 112 | 113 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 114 | 115 | ### Deployment 116 | 117 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 118 | 119 | ### `npm run build` fails to minify 120 | 121 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 122 | -------------------------------------------------------------------------------- /src/pages/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useState} from 'react'; 3 | import {AiFillEyeInvisible, AiFillEye} from 'react-icons/ai' 4 | import { Link } from 'react-router-dom'; 5 | import OAuth from '../components/OAuth'; 6 | import {getAuth, createUserWithEmailAndPassword, updateProfile} from "firebase/auth"; 7 | import {db} from "../firebase" 8 | import { doc, serverTimestamp, setDoc } from 'firebase/firestore'; 9 | import { useNavigate } from 'react-router-dom'; 10 | import { toast } from 'react-toastify'; 11 | 12 | const SignUp = () => { 13 | /* Created a hook for show password */ 14 | const [showPassword, setShowPassword] = useState(false); 15 | /* created a hook for the formdata */ 16 | const [formData, setFormData] = useState({ 17 | /* Initial value was an empty string */ 18 | name:"", 19 | email: "", 20 | password: "", 21 | }); 22 | const {name, email, password} = formData; 23 | /* function for any change happens to the forms we are going to get it gets and puts it inside of the formdata state */ 24 | const navigate = useNavigate() 25 | function onChange(e){ 26 | setFormData((prevState)=> ({ 27 | ...prevState, 28 | [e.target.id]: e.target.value, 29 | })); 30 | } 31 | async function onSubmit(e){ 32 | e.preventDefault(); 33 | 34 | try { 35 | const auth = getAuth() 36 | const userCredential = await createUserWithEmailAndPassword(auth, 37 | email, 38 | password); 39 | 40 | updateProfile(auth.currentUser, { 41 | displayName : name, 42 | }) 43 | const user = userCredential.user 44 | const formDataCopy = {...formData} 45 | delete formDataCopy.password 46 | formDataCopy.timestamp = serverTimestamp(); 47 | 48 | await setDoc(doc(db, "users", user.uid), formDataCopy) 49 | toast.success("Sign up was successful") 50 | navigate("/"); 51 | } catch (error) { 52 | toast.error("Something went wrong with the registration") 53 | console.log(error) 54 | } 55 | } 56 | return ( 57 |
    58 | {/* Just added h1 for the title sign up */} 59 |

    Sign Up

    60 |
    61 |
    62 | {/* Adding image */} 63 | key 67 |
    68 |
    69 | {/* Adding a form with three inputs 70 | 1. For the name 71 | 2. For the email 72 | 3. For the password 73 | So each input has a value onChange for tracking the changes inside our input */} 74 |
    75 | 83 | 91 | {/* For the password field added the condition if the show password is true, we want to see the icon AiFillEyeInvisible and it it's false it shows AiFillEye */} 92 |
    93 | 101 | {showPassword ? setShowPassword((prevState) => ! prevState)}/> 103 | : 104 | setShowPassword((prevState) => ! prevState)} 106 | />} 107 |
    108 |
    109 |

    Have a account? 110 | Sing In 113 |

    114 |

    115 | Forgot password? 119 |

    120 |
    121 | 124 |
    127 |

    OR

    128 |
    129 | 130 | 131 | 132 |
    133 |
    134 | 135 |
    136 | ); 137 | } 138 | 139 | export default SignUp; 140 | -------------------------------------------------------------------------------- /src/pages/Listing.jsx: -------------------------------------------------------------------------------- 1 | import { doc, getDoc } from "firebase/firestore"; 2 | import { useState } from "react"; 3 | import { useEffect } from "react"; 4 | import { useParams } from "react-router-dom"; 5 | import Spinner from "../components/Spinner"; 6 | import { db } from "../firebase"; 7 | import { Swiper, SwiperSlide } from "swiper/react"; 8 | import SwiperCore, { 9 | EffectFade, 10 | Autoplay, 11 | Navigation, 12 | Pagination, 13 | } from "swiper"; 14 | import "swiper/css/bundle"; 15 | import {FaShare, FaMapMarkerAlt, FaBed, FaBath, FaParking, FaChair} from 'react-icons/fa'; 16 | import {getAuth} from "firebase/auth"; 17 | import Contact from "../components/Contact"; 18 | import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet"; 19 | export default function Listing() { 20 | const auth = getAuth(); 21 | const position = [51.505, -0.09]; 22 | const params = useParams(); 23 | const [listing, setListing] = useState(null); 24 | const [loading, setLoading] = useState(true); 25 | const [shareLinkCopied, setShareLinkCopied] = useState(false); 26 | const [contactLandlord, setContactLandlord] = useState(false); 27 | SwiperCore.use([Autoplay, Navigation, Pagination]); 28 | useEffect(() => { 29 | async function fetchListing() { 30 | const docRef = doc(db, "listings", params.listingId); 31 | const docSnap = await getDoc(docRef); 32 | if (docSnap.exists()) { 33 | setListing(docSnap.data()); 34 | setLoading(false); 35 | 36 | } 37 | } 38 | fetchListing(); 39 | }, [params.listingId]); 40 | 41 | if (loading) { 42 | return ; 43 | } 44 | return ( 45 |
    46 | 54 | {listing.imgUrls.map((url, index) => ( 55 | 56 |
    63 |
    64 | ))} 65 |
    66 |
    { 69 | navigator.clipboard.writeText(window.location.href); 70 | setShareLinkCopied(true); 71 | setTimeout(() => { 72 | setShareLinkCopied(false); 73 | }, 2000); 74 | }} 75 | > 76 | 77 |
    78 | {shareLinkCopied && ( 79 |

    80 | Link Copied 81 |

    82 | )} 83 | 84 |
    85 |
    86 |

    87 | {listing.name} - ${" "} 88 | {listing.regularPrice.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} 89 | {listing.type === "rent" ? " / month" : ""} 90 |

    91 |

    92 | 93 | {listing.address} 94 |

    95 |
    96 |

    97 | {listing.type === "rent" ? "Rent" : "Sale"} 98 |

    99 | {listing.offer && ( 100 |

    101 | ${listing.discountedPrice.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} discount 102 |

    103 | )} 104 |
    105 |

    106 | Description - 107 | {listing.description} 108 |

    109 |
      110 |
    • 111 | 112 | {+listing.bedrooms > 1 ? `${listing.bedrooms} Beds` : "1 Bed"} 113 |
    • 114 |
    • 115 | 116 | {+listing.bathrooms > 1 ? `${listing.bathrooms} Baths` : "1 Bath"} 117 |
    • 118 |
    • 119 | 120 | {listing.parking ? "Parking spot" : "No parking"} 121 |
    • 122 |
    • 123 | 124 | {listing.furnished ? "Furnished" : "Not furnished"} 125 |
    • 126 |
    127 | {/* the condition if the person is visiting this page should not be the owner of the listing page */} 128 | {listing.userRef !== auth.currentUser?.uid && !contactLandlord && ( 129 |
    130 | 136 |
    137 | )} 138 | {contactLandlord && ( 139 | 140 | )} 141 |
    142 |
    143 | 150 | 154 | 155 | 156 | A pretty CSS3 popup.
    Easily customizable. 157 |
    158 |
    159 |
    160 |
    161 |
    162 |
    163 | ); 164 | } -------------------------------------------------------------------------------- /src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { getAuth, updateProfile } from 'firebase/auth'; 2 | import { collection, deleteDoc, doc, getDocs, orderBy, query, updateDoc, where } from "firebase/firestore"; 3 | import { useEffect, useState } from 'react'; 4 | import { useNavigate } from 'react-router'; 5 | import { toast } from 'react-toastify'; 6 | import { db } from "../firebase"; 7 | import {FcHome} from "react-icons/fc" 8 | import { Link } from 'react-router-dom'; 9 | import ListingItem from '../components/ListingItem'; 10 | import { info } from 'autoprefixer'; 11 | 12 | const Profile = () => { 13 | /* So first we got the information from the auth and put it inside formData, but we can not get it directly because we would get an error, so it needs to wait until the information is coming from the auth, that's why I have to added the middleware */ 14 | const auth = getAuth(); 15 | const navigate = useNavigate(); 16 | const [changeDetail, setChangeDetail] = useState(false); 17 | const [listings, setListings] = useState(null); 18 | const [loading, setLoading] = useState(true); 19 | const [formData, setFormData] = useState({ 20 | name: auth.currentUser.displayName, 21 | email: auth.currentUser.email, 22 | }); 23 | /* structure and name and email from formData 24 | */ 25 | const {name, email} = formData; 26 | /* function for logging out the person. It uses auth that sing out to log out the person and navigates the person to home page */ 27 | function onLogout(){ 28 | auth.signOut(); 29 | navigate("/"); 30 | } 31 | function onChange(e) { 32 | setFormData((prevState) => ({ 33 | ...prevState, 34 | [e.target.id]: e.target.value, 35 | })); 36 | } 37 | async function onSubmit() { 38 | try { 39 | if (auth.currentUser.displayName !== name) { 40 | //update display name in firebase auth 41 | await updateProfile(auth.currentUser, { 42 | displayName: name, 43 | }); 44 | 45 | // update name in the firestore 46 | 47 | const docRef = doc(db, "users", auth.currentUser.uid); 48 | await updateDoc(docRef, { 49 | name, 50 | }); 51 | } 52 | toast.success("Profile details updated"); 53 | } catch (error) { 54 | toast.error("Could not update the profile details"); 55 | } 56 | } 57 | useEffect(() => { 58 | async function fetchUserListing(){ 59 | const listingRef = collection(db, "listings"); 60 | const q = query(listingRef, where("userRef", "==", auth.currentUser.uid), orderBy("timestamp", "desc") 61 | ); 62 | const querySnap = await getDocs(q); 63 | let listings = []; 64 | querySnap.forEach((doc)=>{ 65 | return listings.push({ 66 | id : doc.id, 67 | data: doc.data(), 68 | }) 69 | }); 70 | setListings(listings) 71 | setLoading(false); 72 | } 73 | fetchUserListing(); 74 | }, [auth.currentUser.uid]); 75 | 76 | /* Using deleteDoc for deleting the listing */ 77 | async function onDelete(listingID) { 78 | if (window.confirm("Are you sure you want to delete?")) { 79 | await deleteDoc(doc(db, "listings", listingID)); 80 | const updatedListings = listings.filter( 81 | (listing) => listing.id !== listingID 82 | ); 83 | setListings(updatedListings); 84 | toast.success("Successfully deleted the listing"); 85 | } 86 | } 87 | function onEdit(listingID) { 88 | navigate(`/edit-listing/${listingID}`); 89 | } 90 | return ( 91 | <> 92 |
    93 |

    94 | My profile 95 |

    96 |
    97 |
    98 | {/* Name input*/} 99 | 100 | 109 | 110 | {/* Email input*/} 111 | 112 | 113 |
    114 |

    Do you want to change your name? 115 | { 117 | changeDetail && onSubmit(); 118 | setChangeDetail((prevState) => !prevState); 119 | }} 120 | className="text-red-600 hover:text-red-700 transition ease-in-out duration-200 ml-1 cursor-pointer" 121 | > 122 | {changeDetail ? "Apply change" : "Edit"} 123 | 124 | 125 |

    126 |

    Sign out

    127 |
    128 | 129 |
    130 | 137 |
    138 |
    139 |
    140 | {!loading && listings.length > 0 && ( 141 | <> 142 |

    My Listings

    143 |
      144 | {listings.map((listing) => ( 145 | onDelete(listing.id)} 150 | onEdit={()=>onEdit(listing.id)} 151 | /> 152 | ))} 153 |
    154 | 155 | )} 156 |
    157 | 158 | ); 159 | } 160 | 161 | export default Profile; 162 | -------------------------------------------------------------------------------- /src/img/realtor-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/pages/CreateListing.jsx: -------------------------------------------------------------------------------- 1 | import { info } from 'autoprefixer'; 2 | import React, { useState } from 'react'; 3 | import Spinner from '../components/Spinner'; 4 | import {toast} from 'react-toastify'; 5 | import { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; 6 | import { getAuth } from 'firebase/auth'; 7 | import {v4 as uuidv4} from 'uuid'; 8 | import {addDoc, collection, serverTimestamp} from 'firebase/firestore'; 9 | import { db } from '../firebase'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | const CreateListing = () => { 13 | const navigate = useNavigate(); 14 | const auth = getAuth(); 15 | const [geolocationEnabled, setGeolocationEnabled] = useState(true); 16 | const [loading, setLoading] = useState(false); 17 | const [formData, setFormData] = useState ({ 18 | type: 'rent', 19 | name: "", 20 | bedrooms: 1, 21 | bathrooms: 1, 22 | parking: false, 23 | furnished: false, 24 | address: "", 25 | description: "", 26 | offer: false, 27 | regularPrice: 0, 28 | discountedPrice: 0, 29 | latitude: 0, 30 | longitude: 0, 31 | images: {}, 32 | }); 33 | const { 34 | type, 35 | name, 36 | bedrooms, 37 | bathrooms, 38 | parking, 39 | furnished, 40 | address, 41 | description, 42 | offer, 43 | regularPrice, 44 | discountedPrice, 45 | latitude, 46 | longitude, 47 | images 48 | } = formData; 49 | function onChange(e){ 50 | let boolean = null; 51 | if(e.target.value === "true"){ 52 | boolean = true; 53 | } 54 | if(e.target.value === "false"){ 55 | boolean = false; 56 | } 57 | //Files 58 | if(e.target.files){ 59 | setFormData((prevState)=> ({ 60 | ...prevState, 61 | images: e.target.files 62 | })) 63 | } 64 | // Text/Boolean/ Number 65 | if(!e.target.files){ 66 | setFormData((prevState)=>({ 67 | ...prevState, 68 | [e.target.id]: boolean ?? e.target.value 69 | })) 70 | } 71 | } 72 | async function onSubmit(e){ 73 | e.preventDefault(); 74 | setLoading(true); 75 | if(+discountedPrice >= regularPrice){ 76 | setLoading(false) 77 | toast.error("Discounted price needs to be less than regular price") 78 | return; 79 | } 80 | if(images.length > 6){ 81 | setLoading(false) 82 | toast.error("maximum 6 images are allowed"); 83 | return; 84 | } 85 | let geolocation = {} 86 | let location 87 | if(geolocationEnabled){ 88 | const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${process.env.REACT_APP_GEOCODE_API_KEY}`); 89 | const data = await response.json(); 90 | console.log(data); 91 | geolocation.lat = data.results[0]?.geometry.location.lat ?? 0; 92 | geolocation.lng = data.results[0]?.geometry.location.lng ?? 0; 93 | 94 | location = data.status === "ZERO_RESULTS" && undefined; 95 | 96 | if(location === undefined){ 97 | setLoading(false); 98 | toast.error("please enter a correct address") 99 | return; 100 | } 101 | } else{ 102 | geolocation.lat = latitude; 103 | geolocation.lng = longitude; 104 | } 105 | 106 | async function storeImage(image) { 107 | return new Promise((resolve, reject) => { 108 | const storage = getStorage(); 109 | const filename = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`; 110 | const storageRef = ref(storage, filename); 111 | const uploadTask = uploadBytesResumable(storageRef, image); 112 | uploadTask.on( 113 | "state_changed", 114 | (snapshot) => { 115 | // Observe state change events such as progress, pause, and resume 116 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 117 | const progress = 118 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 119 | console.log("Upload is " + progress + "% done"); 120 | switch (snapshot.state) { 121 | case "paused": 122 | console.log("Upload is paused"); 123 | break; 124 | case "running": 125 | console.log("Upload is running"); 126 | break; 127 | } 128 | }, 129 | (error) => { 130 | // Handle unsuccessful uploads 131 | reject(error); 132 | }, 133 | () => { 134 | // Handle successful uploads on complete 135 | // For instance, get the download URL: https://firebasestorage.googleapis.com/... 136 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { 137 | resolve(downloadURL); 138 | }); 139 | } 140 | ); 141 | }); 142 | } 143 | 144 | const imgUrls = await Promise.all( 145 | [...images].map((image) => storeImage(image)) 146 | ).catch((error) => { 147 | setLoading(false); 148 | toast.error("Images not uploaded"); 149 | return; 150 | }); 151 | 152 | const formDataCopy = { 153 | ...formData, 154 | imgUrls, 155 | geolocation, 156 | timestamp: serverTimestamp(), 157 | userRef: auth.currentUser.uid, 158 | }; 159 | delete formDataCopy.images; 160 | !formDataCopy.offer && delete formDataCopy.discountedPrice; 161 | delete formDataCopy.latitude; 162 | delete formDataCopy.longitude; 163 | const dofRef = await addDoc(collection(db, "listings"), formDataCopy); 164 | setLoading(false); 165 | toast.success("Listing created") 166 | navigate(`/category/${formDataCopy.type}/${dofRef.id}`) 167 | } 168 | 169 | 170 | 171 | if(loading){ 172 | return 173 | } 174 | return ( 175 |
    176 |

    Create a Listing

    177 |
    178 |

    Sell / Rent

    179 |
    180 | 190 | 191 | 201 |
    202 |

    Name

    203 | 213 |
    214 |
    215 |

    Beds

    216 | 225 |
    226 | 227 |
    228 |

    Baths

    229 | 238 |
    239 |
    240 |

    Parking Spot

    241 |
    242 | 252 | 253 | 263 |
    264 |

    Furnished

    265 |
    266 | 276 | 277 | 287 |
    288 |

    Address

    289 | 297 | 298 | {!geolocationEnabled && ( 299 |
    300 |
    301 |

    Latitude

    303 | 312 |
    313 |
    314 |

    Longitude

    316 | 325 |
    326 |
    327 | )} 328 |

    Description

    329 | 337 | 338 |

    Offer

    339 |
    340 | 351 | 362 |
    363 |
    364 |
    365 |

    Regular price

    366 |
    367 | 375 | {type === 'rent' && ( 376 |
    377 |

    $ / Month

    378 |
    379 | )} 380 |
    381 |
    382 |
    383 | {offer && ( 384 |
    385 |
    386 |

    Discounted Price

    387 |
    388 | 397 | {type === 'rent' && ( 398 |
    399 |

    $ / Month

    400 |
    401 | )} 402 |
    403 |
    404 |
    405 | )} 406 |
    407 |

    Images

    408 |

    The first image will be the cover(max 6)

    409 | 418 |
    419 | 422 |
    423 |
    424 | ); 425 | } 426 | 427 | export default CreateListing; 428 | -------------------------------------------------------------------------------- /src/pages/EditListing.jsx: -------------------------------------------------------------------------------- 1 | import { info } from 'autoprefixer'; 2 | import React, { useState } from 'react'; 3 | import Spinner from '../components/Spinner'; 4 | import {toast} from 'react-toastify'; 5 | import { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; 6 | import { getAuth } from 'firebase/auth'; 7 | import {v4 as uuidv4} from 'uuid'; 8 | import {addDoc, collection, doc, getDoc, serverTimestamp, updateDoc} from 'firebase/firestore'; 9 | import { db } from '../firebase'; 10 | import { useNavigate, useParams } from 'react-router-dom'; 11 | import { useEffect } from 'react'; 12 | import { async } from '@firebase/util'; 13 | 14 | const EditListing = () => { 15 | const navigate = useNavigate(); 16 | const auth = getAuth(); 17 | const [geolocationEnabled, setGeolocationEnabled] = useState(true); 18 | const [loading, setLoading] = useState(false); 19 | const [listing, setListing] = useState(null); 20 | const [formData, setFormData] = useState ({ 21 | type: 'rent', 22 | name: "", 23 | bedrooms: 1, 24 | bathrooms: 1, 25 | parking: false, 26 | furnished: false, 27 | address: "", 28 | description: "", 29 | offer: false, 30 | regularPrice: 0, 31 | discountedPrice: 0, 32 | latitude: 0, 33 | longitude: 0, 34 | images: {}, 35 | }); 36 | const { 37 | type, 38 | name, 39 | bedrooms, 40 | bathrooms, 41 | parking, 42 | furnished, 43 | address, 44 | description, 45 | offer, 46 | regularPrice, 47 | discountedPrice, 48 | latitude, 49 | longitude, 50 | images 51 | } = formData; 52 | 53 | const params = useParams(); 54 | 55 | /* protect this page from theperson that is not the owner of the listing.by just checking if the use of ref is not equal to the authorization user id */ 56 | useEffect(()=>{ 57 | if(listing && listing.userRef !== auth.currentUser.uid){ 58 | toast.error("You can not edit this listing") 59 | navigate("/") 60 | } 61 | }, [auth.currentUser.uid, listing, navigate]) 62 | 63 | useEffect(()=>{ 64 | setLoading(true); 65 | async function fetchListing(){ 66 | const docRef = doc(db, "listings", params.listingId); 67 | const docSnap = await getDoc(docRef); 68 | if(docSnap.exists()){ 69 | setListing(docSnap.data()); 70 | setFormData({...docSnap.data()}) 71 | setLoading(false); 72 | }else{ 73 | navigate("/") 74 | toast.error("Listing not found") 75 | } 76 | } 77 | fetchListing(); 78 | }, [navigate, params.listingId]) 79 | 80 | 81 | 82 | function onChange(e){ 83 | let boolean = null; 84 | if(e.target.value === "true"){ 85 | boolean = true; 86 | } 87 | if(e.target.value === "false"){ 88 | boolean = false; 89 | } 90 | //Files 91 | if(e.target.files){ 92 | setFormData((prevState)=> ({ 93 | ...prevState, 94 | images: e.target.files 95 | })) 96 | } 97 | // Text/Boolean/ Number 98 | if(!e.target.files){ 99 | setFormData((prevState)=>({ 100 | ...prevState, 101 | [e.target.id]: boolean ?? e.target.value 102 | })) 103 | } 104 | } 105 | async function onSubmit(e){ 106 | e.preventDefault(); 107 | setLoading(true); 108 | if(+discountedPrice >= regularPrice){ 109 | setLoading(false) 110 | toast.error("Discounted price needs to be less than regular price") 111 | return; 112 | } 113 | if(images.length > 6){ 114 | setLoading(false) 115 | toast.error("maximum 6 images are allowed"); 116 | return; 117 | } 118 | let geolocation = {} 119 | let location 120 | if(geolocationEnabled){ 121 | const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${process.env.REACT_APP_GEOCODE_API_KEY}`); 122 | const data = await response.json(); 123 | console.log(data); 124 | geolocation.lat = data.results[0]?.geometry.location.lat ?? 0; 125 | geolocation.lng = data.results[0]?.geometry.location.lng ?? 0; 126 | 127 | location = data.status === "ZERO_RESULTS" && undefined; 128 | 129 | if(location === undefined){ 130 | setLoading(false); 131 | toast.error("please enter a correct address") 132 | return; 133 | } 134 | } else{ 135 | geolocation.lat = latitude; 136 | geolocation.lng = longitude; 137 | } 138 | 139 | async function storeImage(image) { 140 | return new Promise((resolve, reject) => { 141 | const storage = getStorage(); 142 | const filename = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`; 143 | const storageRef = ref(storage, filename); 144 | const uploadTask = uploadBytesResumable(storageRef, image); 145 | uploadTask.on( 146 | "state_changed", 147 | (snapshot) => { 148 | // Observe state change events such as progress, pause, and resume 149 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 150 | const progress = 151 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 152 | console.log("Upload is " + progress + "% done"); 153 | switch (snapshot.state) { 154 | case "paused": 155 | console.log("Upload is paused"); 156 | break; 157 | case "running": 158 | console.log("Upload is running"); 159 | break; 160 | } 161 | }, 162 | (error) => { 163 | // Handle unsuccessful uploads 164 | reject(error); 165 | }, 166 | () => { 167 | // Handle successful uploads on complete 168 | // For instance, get the download URL: https://firebasestorage.googleapis.com/... 169 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { 170 | resolve(downloadURL); 171 | }); 172 | } 173 | ); 174 | }); 175 | } 176 | 177 | const imgUrls = await Promise.all( 178 | [...images].map((image) => storeImage(image)) 179 | ).catch((error) => { 180 | setLoading(false); 181 | toast.error("Images not uploaded"); 182 | return; 183 | }); 184 | 185 | const formDataCopy = { 186 | ...formData, 187 | imgUrls, 188 | geolocation, 189 | timestamp: serverTimestamp(), 190 | userRef: auth.currentUser.uid, 191 | }; 192 | delete formDataCopy.images; 193 | !formDataCopy.offer && delete formDataCopy.discountedPrice; 194 | delete formDataCopy.latitude; 195 | delete formDataCopy.longitude; 196 | const dofRef = doc(db, "listings", params.listingId) 197 | 198 | await updateDoc(dofRef, formDataCopy); 199 | setLoading(false); 200 | toast.success("Listing edited successfully"); 201 | navigate(`/category/${formDataCopy.type}/${dofRef.id}`) 202 | } 203 | 204 | 205 | 206 | if(loading){ 207 | return 208 | } 209 | return ( 210 |
    211 |

    Edit Listing

    212 |
    213 |

    Sell / Rent

    214 |
    215 | 225 | 226 | 236 |
    237 |

    Name

    238 | 248 |
    249 |
    250 |

    Beds

    251 | 260 |
    261 | 262 |
    263 |

    Baths

    264 | 273 |
    274 |
    275 |

    Parking Spot

    276 |
    277 | 287 | 288 | 298 |
    299 |

    Furnished

    300 |
    301 | 311 | 312 | 322 |
    323 |

    Address

    324 | 332 | 333 | {!geolocationEnabled && ( 334 |
    335 |
    336 |

    Latitude

    338 | 347 |
    348 |
    349 |

    Longitude

    351 | 360 |
    361 |
    362 | )} 363 |

    Description

    364 | 372 | 373 |

    Offer

    374 |
    375 | 385 | 386 | 396 |
    397 |
    398 |
    399 |

    Regular price

    400 |
    401 | 409 | {type === 'rent' && ( 410 |
    411 |

    $ / Month

    412 |
    413 | )} 414 |
    415 |
    416 |
    417 | {offer && ( 418 |
    419 |
    420 |

    Discounted Price

    421 |
    422 | 431 | {type === 'rent' && ( 432 |
    433 |

    $ / Month

    434 |
    435 | )} 436 |
    437 |
    438 |
    439 | )} 440 |
    441 |

    Images

    442 |

    The first image will be the cover(max 6)

    443 | 452 |
    453 | 456 |
    457 |
    458 | ); 459 | } 460 | 461 | export default EditListing; 462 | --------------------------------------------------------------------------------