├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── assets
├── jpg
│ ├── rentCategoryImage.jpg
│ └── sellCategoryImage.jpg
└── svg
│ ├── badgeIcon.svg
│ ├── bathtubIcon.svg
│ ├── bedIcon.svg
│ ├── checkIcon.svg
│ ├── deleteIcon.svg
│ ├── editIcon.svg
│ ├── exploreIcon.svg
│ ├── googleIcon.svg
│ ├── homeIcon.svg
│ ├── keyboardArrowRightIcon.svg
│ ├── localOfferIcon.svg
│ ├── lockIcon.svg
│ ├── personIcon.svg
│ ├── personOutlineIcon.svg
│ ├── shareIcon.svg
│ ├── uploadIcon.svg
│ └── visibilityIcon.svg
├── components
├── ListingItem.jsx
├── Navbar.jsx
├── OAuth.jsx
├── PrivateRoute.jsx
├── Slider.jsx
└── Spinner.jsx
├── firebase.config.js
├── hooks
└── useAuthStatus.js
├── index.css
├── index.js
├── pages
├── Category.jsx
├── Contact.jsx
├── CreateListing.jsx
├── EditListing.jsx
├── Explore.jsx
├── ForgotPassword.jsx
├── Listing.jsx
├── Offers.jsx
├── Profile.jsx
├── SignIn.jsx
└── SignUp.jsx
└── reportWebVitals.js
/.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
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # House Marketplace
2 |
3 | Find and list houses for sale or for rent. This is a React / Firebase v9 project from the React Front To Back 2022 course.
4 |
5 | ## Usage
6 |
7 | ### Geolocation
8 |
9 | The listings use Google geocoding to get the coords from the address field. You need to either rename .env.example to .env and add your Google Geocode API key OR in the **CreateListing.jsx** file you can set **geolocationEnabled** to "false" and it will add a lat/lng field to the form.
10 |
11 | ### Run
12 |
13 | ```bash
14 | npm start
15 | ```
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "house-marketplace",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.15.0",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "firebase": "^9.4.1",
10 | "leaflet": "^1.7.1",
11 | "react": "^17.0.2",
12 | "react-dom": "^17.0.2",
13 | "react-helmet": "^6.1.0",
14 | "react-leaflet": "^3.2.2",
15 | "react-router-dom": "^6.0.2",
16 | "react-scripts": "4.0.3",
17 | "react-toastify": "^8.1.0",
18 | "swiper": "^6.8.1",
19 | "uuid": "^8.3.2",
20 | "web-vitals": "^1.1.2"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/house-marketplace/3c01914ed909d26a01ef034c81d62199b97498d2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
33 | House Marketplace
34 |
35 |
36 |
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/house-marketplace/3c01914ed909d26a01ef034c81d62199b97498d2/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/house-marketplace/3c01914ed909d26a01ef034c81d62199b97498d2/public/logo512.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
2 | import { ToastContainer } from 'react-toastify'
3 | import 'react-toastify/dist/ReactToastify.css'
4 | import Navbar from './components/Navbar'
5 | import PrivateRoute from './components/PrivateRoute'
6 | import Explore from './pages/Explore'
7 | import Offers from './pages/Offers'
8 | import Category from './pages/Category'
9 | import Profile from './pages/Profile'
10 | import SignIn from './pages/SignIn'
11 | import SignUp from './pages/SignUp'
12 | import ForgotPassword from './pages/ForgotPassword'
13 | import CreateListing from './pages/CreateListing'
14 | import EditListing from './pages/EditListing'
15 | import Listing from './pages/Listing'
16 | import Contact from './pages/Contact'
17 |
18 | function App() {
19 | return (
20 | <>
21 |
22 |
23 | } />
24 | } />
25 | } />
26 | }>
27 | } />
28 |
29 | } />
30 | } />
31 | } />
32 | } />
33 | } />
34 | }
37 | />
38 | } />
39 |
40 |
41 |
42 |
43 |
44 | >
45 | )
46 | }
47 |
48 | export default App
49 |
--------------------------------------------------------------------------------
/src/assets/jpg/rentCategoryImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/house-marketplace/3c01914ed909d26a01ef034c81d62199b97498d2/src/assets/jpg/rentCategoryImage.jpg
--------------------------------------------------------------------------------
/src/assets/jpg/sellCategoryImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/house-marketplace/3c01914ed909d26a01ef034c81d62199b97498d2/src/assets/jpg/sellCategoryImage.jpg
--------------------------------------------------------------------------------
/src/assets/svg/badgeIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/bathtubIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/bedIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/checkIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/deleteIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/editIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/exploreIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/googleIcon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/assets/svg/homeIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/keyboardArrowRightIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/localOfferIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/lockIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/personIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/personOutlineIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/shareIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/uploadIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/visibilityIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ListingItem.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import { ReactComponent as DeleteIcon } from '../assets/svg/deleteIcon.svg'
3 | import { ReactComponent as EditIcon } from '../assets/svg/editIcon.svg'
4 | import bedIcon from '../assets/svg/bedIcon.svg'
5 | import bathtubIcon from '../assets/svg/bathtubIcon.svg'
6 |
7 | function ListingItem({ listing, id, onEdit, onDelete }) {
8 | return (
9 |
10 |
14 |
19 |
20 |
{listing.location}
21 |
{listing.name}
22 |
23 |
24 | $
25 | {listing.offer
26 | ? listing.discountedPrice
27 | .toString()
28 | .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
29 | : listing.regularPrice
30 | .toString()
31 | .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
32 | {listing.type === 'rent' && ' / Month'}
33 |
34 |
35 |

36 |
37 | {listing.bedrooms > 1
38 | ? `${listing.bedrooms} Bedrooms`
39 | : '1 Bedroom'}
40 |
41 |

42 |
43 | {listing.bathrooms > 1
44 | ? `${listing.bathrooms} Bathrooms`
45 | : '1 Bathroom'}
46 |
47 |
48 |
49 |
50 |
51 | {onDelete && (
52 | onDelete(listing.id, listing.name)}
56 | />
57 | )}
58 |
59 | {onEdit && onEdit(id)} />}
60 |
61 | )
62 | }
63 |
64 | export default ListingItem
65 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate, useLocation } from 'react-router-dom'
2 | import { ReactComponent as OfferIcon } from '../assets/svg/localOfferIcon.svg'
3 | import { ReactComponent as ExploreIcon } from '../assets/svg/exploreIcon.svg'
4 | import { ReactComponent as PersonOutlineIcon } from '../assets/svg/personOutlineIcon.svg'
5 |
6 | function Navbar() {
7 | const navigate = useNavigate()
8 | const location = useLocation()
9 |
10 | const pathMatchRoute = (route) => {
11 | if (route === location.pathname) {
12 | return true
13 | }
14 | }
15 |
16 | return (
17 |
71 | )
72 | }
73 |
74 | export default Navbar
75 |
--------------------------------------------------------------------------------
/src/components/OAuth.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation, useNavigate } from 'react-router-dom'
2 | import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth'
3 | import { doc, setDoc, getDoc, serverTimestamp } from 'firebase/firestore'
4 | import { db } from '../firebase.config'
5 | import { toast } from 'react-toastify'
6 | import googleIcon from '../assets/svg/googleIcon.svg'
7 |
8 | function OAuth() {
9 | const navigate = useNavigate()
10 | const location = useLocation()
11 |
12 | const onGoogleClick = async () => {
13 | try {
14 | const auth = getAuth()
15 | const provider = new GoogleAuthProvider()
16 | const result = await signInWithPopup(auth, provider)
17 | const user = result.user
18 |
19 | // Check for user
20 | const docRef = doc(db, 'users', user.uid)
21 | const docSnap = await getDoc(docRef)
22 |
23 | // If user, doesn't exist, create user
24 | if (!docSnap.exists()) {
25 | await setDoc(doc(db, 'users', user.uid), {
26 | name: user.displayName,
27 | email: user.email,
28 | timestamp: serverTimestamp(),
29 | })
30 | }
31 | navigate('/')
32 | } catch (error) {
33 | toast.error('Could not authorize with Google')
34 | }
35 | }
36 |
37 | return (
38 |
39 |
Sign {location.pathname === '/sign-up' ? 'up' : 'in'} with
40 |
43 |
44 | )
45 | }
46 |
47 | export default OAuth
48 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Outlet } from 'react-router-dom'
2 | import { useAuthStatus } from '../hooks/useAuthStatus'
3 | import Spinner from './Spinner'
4 |
5 | const PrivateRoute = () => {
6 | const { loggedIn, checkingStatus } = useAuthStatus()
7 |
8 | if (checkingStatus) {
9 | return
10 | }
11 |
12 | return loggedIn ? :
13 | }
14 |
15 | export default PrivateRoute
16 |
--------------------------------------------------------------------------------
/src/components/Slider.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { collection, getDocs, query, orderBy, limit } from 'firebase/firestore'
4 | import { db } from '../firebase.config'
5 | import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper'
6 | import { Swiper, SwiperSlide } from 'swiper/react'
7 | import 'swiper/swiper-bundle.css'
8 | import Spinner from './Spinner'
9 | SwiperCore.use([Navigation, Pagination, Scrollbar, A11y])
10 |
11 | function Slider() {
12 | const [loading, setLoading] = useState(true)
13 | const [listings, setListings] = useState(null)
14 |
15 | const navigate = useNavigate()
16 |
17 | useEffect(() => {
18 | const fetchListings = async () => {
19 | const listingsRef = collection(db, 'listings')
20 | const q = query(listingsRef, orderBy('timestamp', 'desc'), limit(5))
21 | const querySnap = await getDocs(q)
22 |
23 | let listings = []
24 |
25 | querySnap.forEach((doc) => {
26 | return listings.push({
27 | id: doc.id,
28 | data: doc.data(),
29 | })
30 | })
31 |
32 | setListings(listings)
33 | setLoading(false)
34 | }
35 |
36 | fetchListings()
37 | }, [])
38 |
39 | if (loading) {
40 | return
41 | }
42 |
43 | if (listings.length === 0) {
44 | return <>>
45 | }
46 |
47 | return (
48 | listings && (
49 | <>
50 | Recommended
51 |
52 |
53 | {listings.map(({ data, id }) => (
54 | navigate(`/category/${data.type}/${id}`)}
57 | >
58 |
65 |
{data.name}
66 |
67 | ${data.discountedPrice ?? data.regularPrice}{' '}
68 | {data.type === 'rent' && '/ month'}
69 |
70 |
71 |
72 | ))}
73 |
74 | >
75 | )
76 | )
77 | }
78 |
79 | export default Slider
80 |
--------------------------------------------------------------------------------
/src/components/Spinner.jsx:
--------------------------------------------------------------------------------
1 | function Spinner() {
2 | return (
3 |
6 | )
7 | }
8 |
9 | export default Spinner
10 |
--------------------------------------------------------------------------------
/src/firebase.config.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from 'firebase/app'
2 | import { getFirestore } from 'firebase/firestore'
3 |
4 | // Your web app's Firebase configuration
5 | const firebaseConfig = {
6 | apiKey: 'AIzaSyDA8LVcBB6ZuFMGtZZLEh_veJ44WGrNRdE',
7 | authDomain: 'house-marketplace-app-fb1d0.firebaseapp.com',
8 | projectId: 'house-marketplace-app-fb1d0',
9 | storageBucket: 'house-marketplace-app-fb1d0.appspot.com',
10 | messagingSenderId: '832068369979',
11 | appId: '1:832068369979:web:dce177da9bfc60a4b4e61e',
12 | }
13 |
14 | // Initialize Firebase
15 | initializeApp(firebaseConfig)
16 | export const db = getFirestore()
17 |
--------------------------------------------------------------------------------
/src/hooks/useAuthStatus.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from 'react'
2 | import { getAuth, onAuthStateChanged } from 'firebase/auth'
3 |
4 | export const useAuthStatus = () => {
5 | const [loggedIn, setLoggedIn] = useState(false)
6 | const [checkingStatus, setCheckingStatus] = useState(true)
7 | const isMounted = useRef(true)
8 |
9 | useEffect(() => {
10 | if (isMounted) {
11 | const auth = getAuth()
12 | onAuthStateChanged(auth, (user) => {
13 | if (user) {
14 | setLoggedIn(true)
15 | }
16 | setCheckingStatus(false)
17 | })
18 | }
19 |
20 | return () => {
21 | isMounted.current = false
22 | }
23 | }, [isMounted])
24 |
25 | return { loggedIn, checkingStatus }
26 | }
27 |
28 | // Protected routes in v6
29 | // https://stackoverflow.com/questions/65505665/protected-route-with-firebase
30 |
31 | // Fix memory leak warning
32 | // https://stackoverflow.com/questions/59780268/cleanup-memory-leaks-on-an-unmounted-component-in-react-hooks
33 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@200;300;400;500;600;700;800;900&display=swap');
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | html::-webkit-scrollbar {
7 | display: none;
8 | }
9 |
10 | body {
11 | font-family: 'Montserrat', sans-serif;
12 | background-color: #f2f4f8;
13 | margin: 0;
14 | box-sizing: border-box;
15 | }
16 |
17 | a {
18 | text-decoration: none;
19 | display: block;
20 | color: #000000;
21 | }
22 |
23 | button {
24 | outline: none;
25 | border: none;
26 | }
27 |
28 | .input,
29 | .passwordInput,
30 | .emailInput,
31 | .nameInput,
32 | .textarea {
33 | box-shadow: rgba(0, 0, 0, 0.11);
34 | border: none;
35 | background: #ffffff;
36 | border-radius: 3rem;
37 | height: 3rem;
38 | width: 100%;
39 | outline: none;
40 | font-family: 'Montserrat', sans-serif;
41 | padding: 0 3rem;
42 | font-size: 1rem;
43 | }
44 | @media (min-width: 1100px) {
45 | .input,
46 | .passwordInput,
47 | .emailInput,
48 | .nameInput,
49 | .textarea {
50 | padding: 0 5rem;
51 | }
52 | }
53 |
54 | .textarea {
55 | padding: 1rem 1.5rem;
56 | height: 300px;
57 | border-radius: 1rem;
58 | }
59 |
60 | .primaryButton {
61 | cursor: pointer;
62 | background: #00cc66;
63 | border-radius: 1rem;
64 | padding: 0.85rem 2rem;
65 | color: #ffffff;
66 | font-weight: 600;
67 | font-size: 1.25rem;
68 | width: 80%;
69 | margin: 0 auto;
70 | display: flex;
71 | align-items: center;
72 | justify-content: center;
73 | }
74 |
75 | .removeIcon {
76 | cursor: pointer;
77 | position: absolute;
78 | top: -3%;
79 | right: -2%;
80 | }
81 |
82 | .editIcon {
83 | cursor: pointer;
84 | position: absolute;
85 | top: -3.4%;
86 | right: 20px;
87 | }
88 |
89 | .pageContainer,
90 | .offers,
91 | .profile,
92 | .listingDetails,
93 | .category,
94 | .explore {
95 | margin: 1rem;
96 | }
97 | @media (min-width: 1024px) {
98 | .pageContainer,
99 | .offers,
100 | .profile,
101 | .listingDetails,
102 | .category,
103 | .explore {
104 | margin: 3rem;
105 | }
106 | }
107 |
108 | .loadingSpinnerContainer {
109 | position: fixed;
110 | top: 0;
111 | right: 0;
112 | bottom: 0;
113 | left: 0;
114 | background-color: rgba(0, 0, 0, 0.5);
115 | z-index: 5000;
116 | display: flex;
117 | justify-content: center;
118 | align-items: center;
119 | }
120 |
121 | .pageContainer{
122 | margin-bottom: 10rem;
123 | }
124 |
125 | .loadingSpinner {
126 | width: 64px;
127 | height: 64px;
128 | border: 8px solid;
129 | border-color: #00cc66 transparent #00cc66 transparent;
130 | border-radius: 50%;
131 | animation: spin 1.2s linear infinite;
132 | }
133 | @keyframes spin {
134 | 0% {
135 | transform: rotate(0deg);
136 | }
137 | 100% {
138 | transform: rotate(360deg);
139 | }
140 | }
141 |
142 | .pageHeader {
143 | font-size: 2rem;
144 | font-weight: 800;
145 | }
146 |
147 | .navbar {
148 | position: fixed;
149 | left: 0;
150 | bottom: 0;
151 | right: 0;
152 | height: 85px;
153 | background-color: #ffffff;
154 | z-index: 1000;
155 | display: flex;
156 | justify-content: center;
157 | align-items: center;
158 | }
159 |
160 | .navbarNav {
161 | width: 100%;
162 | margin-top: 0.75rem;
163 | overflow-y: hidden;
164 | }
165 |
166 | .navbarListItems {
167 | margin: 0;
168 | padding: 0;
169 | display: flex;
170 | justify-content: space-evenly;
171 | align-items: center;
172 | }
173 |
174 | .navbarListItem {
175 | cursor: pointer;
176 | display: flex;
177 | flex-direction: column;
178 | align-items: center;
179 | }
180 |
181 | .navbarListItemName,
182 | .navbarListItemNameActive {
183 | margin-top: 0.25rem;
184 | font-size: 14px;
185 | font-weight: 600;
186 | color: #8f8f8f;
187 | }
188 | .navbarListItemNameActive {
189 | color: #2c2c2c;
190 | }
191 |
192 | .nameInput {
193 | margin-bottom: 2rem;
194 | background: url('./assets/svg/badgeIcon.svg') #ffffff 2.5% center no-repeat;
195 | }
196 |
197 | .emailInput {
198 | margin-bottom: 2rem;
199 | background: url('./assets/svg/personIcon.svg') #ffffff 2.5% center no-repeat;
200 | }
201 |
202 | .passwordInputDiv {
203 | position: relative;
204 | }
205 |
206 | .passwordInput {
207 | margin-bottom: 2rem;
208 | background: url('./assets/svg/lockIcon.svg') #ffffff 2.5% center no-repeat;
209 | }
210 |
211 | .showPassword {
212 | cursor: pointer;
213 | position: absolute;
214 | top: -4%;
215 | right: 1%;
216 | padding: 1rem;
217 | }
218 |
219 | .forgotPasswordLink {
220 | cursor: pointer;
221 | color: #00cc66;
222 | font-weight: 600;
223 | text-align: right;
224 | }
225 |
226 | .signInBar,
227 | .signUpBar {
228 | margin-top: 3rem;
229 | display: flex;
230 | justify-content: space-between;
231 | align-items: center;
232 | position: inherit;
233 | }
234 |
235 | .signInButton,
236 | .signUpButton,
237 | .signInText,
238 | .signUpText {
239 | cursor: pointer;
240 | }
241 | @media (min-width: 1024px) {
242 | .signInBar,
243 | .signUpBar {
244 | justify-content: start;
245 | }
246 | }
247 |
248 | .signInText,
249 | .signUpText {
250 | font-size: 1.5rem;
251 | font-weight: 700;
252 | }
253 |
254 | .signInButton,
255 | .signUpButton {
256 | display: flex;
257 | justify-content: center;
258 | align-items: center;
259 | width: 3rem;
260 | height: 3rem;
261 | background-color: #00cc66;
262 | border-radius: 50%;
263 | }
264 | @media (min-width: 1024px) {
265 | .signInButton,
266 | .signUpButton {
267 | margin-left: 3rem;
268 | }
269 | }
270 |
271 | .socialLogin {
272 | margin-top: 4rem;
273 | display: flex;
274 | flex-direction: column;
275 | align-items: center;
276 | }
277 |
278 | .socialIconDiv {
279 | cursor: pointer;
280 | display: flex;
281 | justify-content: center;
282 | align-items: center;
283 | padding: 0.75rem;
284 | margin: 1.5rem;
285 | width: 3rem;
286 | height: 3rem;
287 | background-color: #ffffff;
288 | border-radius: 50%;
289 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
290 | }
291 |
292 | .socialIconImg {
293 | width: 100%;
294 | }
295 |
296 | .registerLink {
297 | margin-top: 4rem;
298 | color: #00cc66;
299 | font-weight: 600;
300 | text-align: center;
301 | margin-bottom: 3rem;
302 | }
303 |
304 | @media (min-width: 1217px) {
305 | .explore {
306 | margin-bottom: 10rem;
307 | }
308 | }
309 | @media (max-height: 536px) {
310 | .explore {
311 | margin-bottom: 10rem;
312 | }
313 | }
314 |
315 | .exploreHeading,
316 | .exploreCategoryHeading {
317 | font-weight: 700;
318 | }
319 |
320 | .exploreCategoryHeading {
321 | margin-top: 3rem;
322 | }
323 |
324 | .swiper-container {
325 | min-height: 225px;
326 | height: 23vw;
327 | }
328 |
329 | .swiper-pagination-bullet-active {
330 | background-color: #ffffff !important;
331 | }
332 |
333 | .swiperSlideDiv {
334 | position: relative;
335 | width: 100%;
336 | height: 100%;
337 | }
338 |
339 | .swiperSlideImg {
340 | width: 100%;
341 | object-fit: cover;
342 | }
343 |
344 | .swiperSlideText {
345 | color: #ffffff;
346 | position: absolute;
347 | top: 70px;
348 | left: 0;
349 | font-weight: 600;
350 | max-width: 90%;
351 | font-size: 1.25rem;
352 | background-color: rgba(0, 0, 0, 0.8);
353 | padding: 0.5rem;
354 | }
355 | @media (min-width: 1024px) {
356 | .swiperSlideText {
357 | font-size: 1.75rem;
358 | }
359 | }
360 |
361 | .swiperSlidePrice {
362 | color: #000000;
363 | position: absolute;
364 | top: 143px;
365 | left: 11px;
366 | font-weight: 600;
367 | max-width: 90%;
368 | background-color: #ffffff;
369 | padding: 0.25rem 0.5rem;
370 | border-radius: 1rem;
371 | }
372 | @media (min-width: 1024px) {
373 | .swiperSlidePrice {
374 | font-size: 1.25rem;
375 | }
376 | }
377 |
378 | .exploreCategories {
379 | display: flex;
380 | justify-content: space-between;
381 | }
382 | .exploreCategories a {
383 | width: 48%;
384 | }
385 |
386 | .exploreCategoryImg {
387 | min-height: 115px;
388 | height: 15vw;
389 | width: 100%;
390 | border-radius: 1.5rem;
391 | object-fit: cover;
392 | margin: 0 auto;
393 | }
394 |
395 | .exploreCategoryName {
396 | font-weight: 500;
397 | text-align: left;
398 | }
399 |
400 | .category {
401 | margin-bottom: 10rem;
402 | }
403 |
404 | .categoryListings {
405 | padding: 0;
406 | }
407 |
408 | .categoryListing {
409 | display: flex;
410 | justify-content: space-between;
411 | align-items: center;
412 | margin-bottom: 1rem;
413 | position: relative;
414 | }
415 |
416 | .categoryListingLink {
417 | display: contents;
418 | }
419 |
420 | .categoryListingImg {
421 | width: 30%;
422 | height: 100px;
423 | border-radius: 1.5rem;
424 | object-fit: cover;
425 | }
426 | @media (min-width: 1024px) {
427 | .categoryListingImg {
428 | width: 19%;
429 | height: 217px;
430 | }
431 | }
432 |
433 | .categoryListingDetails {
434 | width: 65%;
435 | }
436 | @media (min-width: 1024px) {
437 | .categoryListingDetails {
438 | width: 79%;
439 | }
440 | }
441 |
442 | .categoryListingLocation {
443 | font-weight: 600;
444 | font-size: 0.7rem;
445 | opacity: 0.8;
446 | margin-bottom: 0;
447 | }
448 |
449 | .categoryListingName {
450 | font-weight: 600;
451 | font-size: 1.25rem;
452 | margin: 0;
453 | }
454 |
455 | .categoryListingPrice {
456 | margin-top: 0.5rem;
457 | font-weight: 600;
458 | font-size: 1.1rem;
459 | color: #00cc66;
460 | margin-bottom: 0;
461 | display: flex;
462 | align-items: center;
463 | }
464 |
465 | .categoryListingInfoDiv {
466 | display: flex;
467 | justify-content: space-between;
468 | max-width: 275px;
469 | }
470 |
471 | .categoryListingInfoText {
472 | font-weight: 500;
473 | font-size: 0.7rem;
474 | }
475 |
476 | .loadMore {
477 | cursor: pointer;
478 | width: 8rem;
479 | margin: 0 auto;
480 | text-align: center;
481 | padding: 0.25rem 0.5rem;
482 | background-color: #000000;
483 | color: #ffffff;
484 | font-weight: 600;
485 | border-radius: 1rem;
486 | opacity: 0.7;
487 | margin-top: 2rem;
488 | }
489 |
490 | .listingDetails {
491 | margin-bottom: 10rem;
492 | }
493 |
494 | .shareIconDiv {
495 | cursor: pointer;
496 | position: fixed;
497 | top: 3%;
498 | right: 5%;
499 | z-index: 2;
500 | background-color: #ffffff;
501 | border-radius: 50%;
502 | width: 3rem;
503 | height: 3rem;
504 | display: flex;
505 | justify-content: center;
506 | align-items: center;
507 | }
508 |
509 | .listingName {
510 | font-weight: 600;
511 | font-size: 1.5rem;
512 | margin-bottom: 0.5rem;
513 | }
514 |
515 | .listingLocation {
516 | margin-top: 0;
517 | font-weight: 600;
518 | }
519 |
520 | .discountPrice {
521 | padding: 0.25rem 0.5rem;
522 | background-color: #000000;
523 | color: #ffffff;
524 | border-radius: 1rem;
525 | font-size: 0.8rem;
526 | font-weight: 600;
527 | display: inline;
528 | }
529 |
530 | .listingType {
531 | padding: 0.25rem 0.5rem;
532 | background-color: #00cc66;
533 | color: #ffffff;
534 | border-radius: 2rem;
535 | display: inline;
536 | font-weight: 600;
537 | font-size: 0.8rem;
538 | margin-right: 1rem;
539 | }
540 |
541 | .listingDetailsList {
542 | padding: 0;
543 | list-style-type: none;
544 | }
545 | .listingDetailsList li {
546 | margin: 0.3rem 0;
547 | font-weight: 500;
548 | opacity: 0.8;
549 | }
550 |
551 | .listingLocationTitle {
552 | margin-top: 2rem;
553 | font-weight: 600;
554 | font-size: 1.25rem;
555 | }
556 |
557 | .leafletContainer {
558 | width: 100%;
559 | height: 200px;
560 | overflow-x: hidden;
561 | margin-bottom: 3rem;
562 | }
563 | @media (min-width: 1024px) {
564 | .leafletContainer {
565 | height: 400px;
566 | }
567 | }
568 |
569 | .linkCopied {
570 | position: fixed;
571 | top: 9%;
572 | right: 5%;
573 | z-index: 2;
574 | background-color: #ffffff;
575 | border-radius: 1rem;
576 | padding: 0.5rem 1rem;
577 | font-weight: 600;
578 | }
579 |
580 | .contactListingName {
581 | margin-top: -1rem;
582 | margin-bottom: 0;
583 | font-weight: 600;
584 | }
585 |
586 | .contactListingLocation {
587 | margin-top: 0.25rem;
588 | font-weight: 600;
589 | }
590 |
591 | .contactLandlord {
592 | margin-top: 2rem;
593 | display: flex;
594 | align-items: center;
595 | }
596 |
597 | .landlordName {
598 | font-weight: 600;
599 | font-size: 1.2rem;
600 | }
601 |
602 | .messageForm {
603 | margin-top: 0.5rem;
604 | }
605 |
606 | .messageDiv {
607 | margin-top: 2rem;
608 | display: flex;
609 | flex-direction: column;
610 | margin-bottom: 4rem;
611 | }
612 |
613 | .messageLabel {
614 | margin-bottom: 0.5rem;
615 | }
616 |
617 | .profile {
618 | margin-bottom: 10rem;
619 | }
620 |
621 | .profileHeader {
622 | display: flex;
623 | justify-content: space-between;
624 | align-items: center;
625 | }
626 |
627 | .logOut {
628 | cursor: pointer;
629 | font-family: 'Montserrat', sans-serif;
630 | font-size: 1rem;
631 | font-weight: 600;
632 | color: #ffffff;
633 | background-color: #00cc66;
634 | padding: 0.25rem 0.75rem;
635 | border-radius: 1rem;
636 | }
637 |
638 | .profileDetailsHeader {
639 | display: flex;
640 | justify-content: space-between;
641 | max-width: 500px;
642 | }
643 |
644 | .personalDetailsText {
645 | font-weight: 600;
646 | }
647 |
648 | .changePersonalDetails {
649 | cursor: pointer;
650 | font-weight: 600;
651 | color: #00cc66;
652 | }
653 |
654 | .profileCard {
655 | background-color: #ffffff;
656 | border-radius: 1rem;
657 | padding: 1rem;
658 | box-shadow: rgba(0, 0, 0, 0.2);
659 | max-width: 500px;
660 | }
661 |
662 | .profileDetails {
663 | display: flex;
664 | flex-direction: column;
665 | }
666 |
667 | .profileName,
668 | .profileEmail,
669 | .profileAddress,
670 | .profileAddressActive,
671 | .profileEmailActive,
672 | .profileNameActive {
673 | all: unset;
674 | margin: 0.3rem 0;
675 | font-weight: 600;
676 | width: 100%;
677 | }
678 | .profileNameActive {
679 | background-color: rgba(44, 44, 44, 0.1);
680 | }
681 |
682 | .profileEmail,
683 | .profileAddress,
684 | .profileAddressActive,
685 | .profileEmailActive {
686 | font-weight: 500;
687 | }
688 | .profileEmailActive {
689 | background-color: rgba(44, 44, 44, 0.1);
690 | }
691 |
692 | .profileAddressActive {
693 | background-color: rgba(44, 44, 44, 0.1);
694 | }
695 |
696 | .createListing {
697 | background-color: #ffffff;
698 | border-radius: 1rem;
699 | padding: 0.25rem 1rem;
700 | box-shadow: rgba(0, 0, 0, 0.2);
701 | margin-top: 2rem;
702 | font-weight: 600;
703 | max-width: 500px;
704 | display: flex;
705 | justify-content: space-between;
706 | align-items: center;
707 | }
708 |
709 | .listingText {
710 | margin-top: 3rem;
711 | font-weight: 600;
712 | }
713 |
714 | .lisitingsList {
715 | padding: 0;
716 | }
717 |
718 | .formLabel {
719 | font-weight: 600;
720 | margin-top: 1rem;
721 | display: block;
722 | }
723 |
724 | .formButtons {
725 | display: flex;
726 | }
727 |
728 | .formButton,
729 | .formInput,
730 | .formInputAddress,
731 | .formInputName,
732 | .formInputSmall,
733 | .formInputFile,
734 | .formButtonActive {
735 | padding: 0.9rem 3rem;
736 | background-color: #ffffff;
737 | font-weight: 600;
738 | border-radius: 1rem;
739 | font-size: 1rem;
740 | margin: 0.5rem 0.5rem 0 0;
741 | display: flex;
742 | justify-content: center;
743 | align-items: center;
744 | }
745 | .formButtonActive {
746 | background-color: #00cc66;
747 | color: #ffffff;
748 | }
749 |
750 | .flex {
751 | display: flex;
752 | }
753 |
754 | .formInput,
755 | .formInputAddress,
756 | .formInputName,
757 | .formInputSmall,
758 | .formInputFile {
759 | border: none;
760 | outline: none;
761 | font-family: 'Montserrat', sans-serif;
762 | }
763 | .formInputSmall,
764 | .formInputFile {
765 | margin-right: 3rem;
766 | padding: 0.9rem 0.7rem;
767 | text-align: center;
768 | }
769 |
770 | .formInputName {
771 | padding: 0.9rem 1rem;
772 | width: 90%;
773 | max-width: 326px;
774 | }
775 |
776 | .formInputAddress {
777 | padding: 0.9rem 1rem;
778 | width: 90%;
779 | max-width: 326px;
780 | }
781 |
782 | .formPriceDiv {
783 | display: flex;
784 | align-items: center;
785 | }
786 |
787 | .formPriceText {
788 | margin-left: -1.5rem;
789 | font-weight: 600;
790 | }
791 |
792 | .imagesInfo {
793 | font-size: 0.9rem;
794 | opacity: 0.75;
795 | }
796 |
797 | .formInputFile {
798 | width: 100%;
799 | }
800 | .formInputFile::-webkit-file-upload-button {
801 | background-color: #00cc66;
802 | border: none;
803 | color: #ffffff;
804 | font-weight: 600;
805 | padding: 0.5rem 0.75rem;
806 | border-radius: 1rem;
807 | margin-right: 1rem;
808 | }
809 |
810 | .createListingButton {
811 | margin-top: 5rem;
812 | }
813 |
814 | .offers {
815 | margin-bottom: 10rem;
816 | }
817 |
818 | .offerBadge {
819 | padding: 0.25rem 0.5rem;
820 | background-color: #000000;
821 | color: #ffffff;
822 | border-radius: 1rem;
823 | margin-left: 1rem;
824 | font-size: 0.8rem;
825 | opacity: 0.75;
826 | }
827 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
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 |
--------------------------------------------------------------------------------
/src/pages/Category.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useParams } from 'react-router-dom'
3 | import {
4 | collection,
5 | getDocs,
6 | query,
7 | where,
8 | orderBy,
9 | limit,
10 | startAfter,
11 | } from 'firebase/firestore'
12 | import { db } from '../firebase.config'
13 | import { toast } from 'react-toastify'
14 | import Spinner from '../components/Spinner'
15 | import ListingItem from '../components/ListingItem'
16 |
17 | function Category() {
18 | const [listings, setListings] = useState(null)
19 | const [loading, setLoading] = useState(true)
20 | const [lastFetchedListing, setLastFetchedListing] = useState(null)
21 |
22 | const params = useParams()
23 |
24 | useEffect(() => {
25 | const fetchListings = async () => {
26 | try {
27 | // Get reference
28 | const listingsRef = collection(db, 'listings')
29 |
30 | // Create a query
31 | const q = query(
32 | listingsRef,
33 | where('type', '==', params.categoryName),
34 | orderBy('timestamp', 'desc'),
35 | limit(10)
36 | )
37 |
38 | // Execute query
39 | const querySnap = await getDocs(q)
40 |
41 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]
42 | setLastFetchedListing(lastVisible)
43 |
44 | const listings = []
45 |
46 | querySnap.forEach((doc) => {
47 | return listings.push({
48 | id: doc.id,
49 | data: doc.data(),
50 | })
51 | })
52 |
53 | setListings(listings)
54 | setLoading(false)
55 | } catch (error) {
56 | toast.error('Could not fetch listings')
57 | }
58 | }
59 |
60 | fetchListings()
61 | }, [params.categoryName])
62 |
63 | // Pagination / Load More
64 | const onFetchMoreListings = async () => {
65 | try {
66 | // Get reference
67 | const listingsRef = collection(db, 'listings')
68 |
69 | // Create a query
70 | const q = query(
71 | listingsRef,
72 | where('type', '==', params.categoryName),
73 | orderBy('timestamp', 'desc'),
74 | startAfter(lastFetchedListing),
75 | limit(10)
76 | )
77 |
78 | // Execute query
79 | const querySnap = await getDocs(q)
80 |
81 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]
82 | setLastFetchedListing(lastVisible)
83 |
84 | const listings = []
85 |
86 | querySnap.forEach((doc) => {
87 | return listings.push({
88 | id: doc.id,
89 | data: doc.data(),
90 | })
91 | })
92 |
93 | setListings((prevState) => [...prevState, ...listings])
94 | setLoading(false)
95 | } catch (error) {
96 | toast.error('Could not fetch listings')
97 | }
98 | }
99 |
100 | return (
101 |
102 |
109 |
110 | {loading ? (
111 |
112 | ) : listings && listings.length > 0 ? (
113 | <>
114 |
115 |
116 | {listings.map((listing) => (
117 |
122 | ))}
123 |
124 |
125 |
126 |
127 |
128 | {lastFetchedListing && (
129 |
130 | Load More
131 |
132 | )}
133 | >
134 | ) : (
135 |
No listings for {params.categoryName}
136 | )}
137 |
138 | )
139 | }
140 |
141 | export default Category
142 |
--------------------------------------------------------------------------------
/src/pages/Contact.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { useParams, useSearchParams } from 'react-router-dom'
3 | import { doc, getDoc } from 'firebase/firestore'
4 | import { db } from '../firebase.config'
5 | import { toast } from 'react-toastify'
6 |
7 | function Contact() {
8 | const [message, setMessage] = useState('')
9 | const [landlord, setLandlord] = useState(null)
10 | // eslint-disable-next-line
11 | const [searchParams, setSearchParams] = useSearchParams()
12 |
13 | const params = useParams()
14 |
15 | useEffect(() => {
16 | const getLandlord = async () => {
17 | const docRef = doc(db, 'users', params.landlordId)
18 | const docSnap = await getDoc(docRef)
19 |
20 | if (docSnap.exists()) {
21 | setLandlord(docSnap.data())
22 | } else {
23 | toast.error('Could not get landlord data')
24 | }
25 | }
26 |
27 | getLandlord()
28 | }, [params.landlordId])
29 |
30 | const onChange = (e) => setMessage(e.target.value)
31 |
32 | return (
33 |
34 |
35 | Contact Landlord
36 |
37 |
38 | {landlord !== null && (
39 |
40 |
41 |
Contact {landlord?.name}
42 |
43 |
44 |
68 |
69 | )}
70 |
71 | )
72 | }
73 |
74 | export default Contact
75 |
--------------------------------------------------------------------------------
/src/pages/CreateListing.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react'
2 | import { getAuth, onAuthStateChanged } from 'firebase/auth'
3 | import {
4 | getStorage,
5 | ref,
6 | uploadBytesResumable,
7 | getDownloadURL,
8 | } from 'firebase/storage'
9 | import { addDoc, collection, serverTimestamp } from 'firebase/firestore'
10 | import { db } from '../firebase.config'
11 | import { useNavigate } from 'react-router-dom'
12 | import { toast } from 'react-toastify'
13 | import { v4 as uuidv4 } from 'uuid'
14 | import Spinner from '../components/Spinner'
15 |
16 | function CreateListing() {
17 | // eslint-disable-next-line
18 | const [geolocationEnabled, setGeolocationEnabled] = useState(true)
19 | const [loading, setLoading] = useState(false)
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 | offer: false,
29 | regularPrice: 0,
30 | discountedPrice: 0,
31 | images: {},
32 | latitude: 0,
33 | longitude: 0,
34 | })
35 |
36 | const {
37 | type,
38 | name,
39 | bedrooms,
40 | bathrooms,
41 | parking,
42 | furnished,
43 | address,
44 | offer,
45 | regularPrice,
46 | discountedPrice,
47 | images,
48 | latitude,
49 | longitude,
50 | } = formData
51 |
52 | const auth = getAuth()
53 | const navigate = useNavigate()
54 | const isMounted = useRef(true)
55 |
56 | useEffect(() => {
57 | if (isMounted) {
58 | onAuthStateChanged(auth, (user) => {
59 | if (user) {
60 | setFormData({ ...formData, userRef: user.uid })
61 | } else {
62 | navigate('/sign-in')
63 | }
64 | })
65 | }
66 |
67 | return () => {
68 | isMounted.current = false
69 | }
70 | // eslint-disable-next-line react-hooks/exhaustive-deps
71 | }, [isMounted])
72 |
73 | const onSubmit = async (e) => {
74 | e.preventDefault()
75 |
76 | setLoading(true)
77 |
78 | if (discountedPrice >= regularPrice) {
79 | setLoading(false)
80 | toast.error('Discounted price needs to be less than regular price')
81 | return
82 | }
83 |
84 | if (images.length > 6) {
85 | setLoading(false)
86 | toast.error('Max 6 images')
87 | return
88 | }
89 |
90 | let geolocation = {}
91 | let location
92 |
93 | if (geolocationEnabled) {
94 | const response = await fetch(
95 | `https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${process.env.REACT_APP_GEOCODE_API_KEY}`
96 | )
97 |
98 | const data = await response.json()
99 |
100 | geolocation.lat = data.results[0]?.geometry.location.lat ?? 0
101 | geolocation.lng = data.results[0]?.geometry.location.lng ?? 0
102 |
103 | location =
104 | data.status === 'ZERO_RESULTS'
105 | ? undefined
106 | : data.results[0]?.formatted_address
107 |
108 | if (location === undefined || location.includes('undefined')) {
109 | setLoading(false)
110 | toast.error('Please enter a correct address')
111 | return
112 | }
113 | } else {
114 | geolocation.lat = latitude
115 | geolocation.lng = longitude
116 | }
117 |
118 | // Store image in firebase
119 | const storeImage = async (image) => {
120 | return new Promise((resolve, reject) => {
121 | const storage = getStorage()
122 | const fileName = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`
123 |
124 | const storageRef = ref(storage, 'images/' + fileName)
125 |
126 | const uploadTask = uploadBytesResumable(storageRef, image)
127 |
128 | uploadTask.on(
129 | 'state_changed',
130 | (snapshot) => {
131 | const progress =
132 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100
133 | console.log('Upload is ' + progress + '% done')
134 | switch (snapshot.state) {
135 | case 'paused':
136 | console.log('Upload is paused')
137 | break
138 | case 'running':
139 | console.log('Upload is running')
140 | break
141 | default:
142 | break
143 | }
144 | },
145 | (error) => {
146 | reject(error)
147 | },
148 | () => {
149 | // Handle successful uploads on complete
150 | // For instance, get the download URL: https://firebasestorage.googleapis.com/...
151 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
152 | resolve(downloadURL)
153 | })
154 | }
155 | )
156 | })
157 | }
158 |
159 | const imgUrls = await Promise.all(
160 | [...images].map((image) => storeImage(image))
161 | ).catch(() => {
162 | setLoading(false)
163 | toast.error('Images not uploaded')
164 | return
165 | })
166 |
167 | const formDataCopy = {
168 | ...formData,
169 | imgUrls,
170 | geolocation,
171 | timestamp: serverTimestamp(),
172 | }
173 |
174 | formDataCopy.location = address
175 | delete formDataCopy.images
176 | delete formDataCopy.address
177 | !formDataCopy.offer && delete formDataCopy.discountedPrice
178 |
179 | const docRef = await addDoc(collection(db, 'listings'), formDataCopy)
180 | setLoading(false)
181 | toast.success('Listing saved')
182 | navigate(`/category/${formDataCopy.type}/${docRef.id}`)
183 | }
184 |
185 | const onMutate = (e) => {
186 | let boolean = null
187 |
188 | if (e.target.value === 'true') {
189 | boolean = true
190 | }
191 | if (e.target.value === 'false') {
192 | boolean = false
193 | }
194 |
195 | // Files
196 | if (e.target.files) {
197 | setFormData((prevState) => ({
198 | ...prevState,
199 | images: e.target.files,
200 | }))
201 | }
202 |
203 | // Text/Booleans/Numbers
204 | if (!e.target.files) {
205 | setFormData((prevState) => ({
206 | ...prevState,
207 | [e.target.id]: boolean ?? e.target.value,
208 | }))
209 | }
210 | }
211 |
212 | if (loading) {
213 | return
214 | }
215 |
216 | return (
217 |
218 |
219 | Create a Listing
220 |
221 |
222 |
223 |
449 |
450 |
451 | )
452 | }
453 |
454 | export default CreateListing
455 |
--------------------------------------------------------------------------------
/src/pages/EditListing.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react'
2 | import { getAuth, onAuthStateChanged } from 'firebase/auth'
3 | import {
4 | getStorage,
5 | ref,
6 | uploadBytesResumable,
7 | getDownloadURL,
8 | } from 'firebase/storage'
9 | import { doc, updateDoc, getDoc, serverTimestamp } from 'firebase/firestore'
10 | import { db } from '../firebase.config'
11 | import { useNavigate, useParams } from 'react-router-dom'
12 | import { toast } from 'react-toastify'
13 | import { v4 as uuidv4 } from 'uuid'
14 | import Spinner from '../components/Spinner'
15 |
16 | function EditListing() {
17 | // eslint-disable-next-line
18 | const [geolocationEnabled, setGeolocationEnabled] = useState(true)
19 | const [loading, setLoading] = useState(false)
20 | const [listing, setListing] = useState(false)
21 | const [formData, setFormData] = useState({
22 | type: 'rent',
23 | name: '',
24 | bedrooms: 1,
25 | bathrooms: 1,
26 | parking: false,
27 | furnished: false,
28 | address: '',
29 | offer: false,
30 | regularPrice: 0,
31 | discountedPrice: 0,
32 | images: {},
33 | latitude: 0,
34 | longitude: 0,
35 | })
36 |
37 | const {
38 | type,
39 | name,
40 | bedrooms,
41 | bathrooms,
42 | parking,
43 | furnished,
44 | address,
45 | offer,
46 | regularPrice,
47 | discountedPrice,
48 | images,
49 | latitude,
50 | longitude,
51 | } = formData
52 |
53 | const auth = getAuth()
54 | const navigate = useNavigate()
55 | const params = useParams()
56 | const isMounted = useRef(true)
57 |
58 | // Redirect if listing is not user's
59 | useEffect(() => {
60 | if (listing && listing.userRef !== auth.currentUser.uid) {
61 | toast.error('You can not edit that listing')
62 | navigate('/')
63 | }
64 | })
65 |
66 | // Fetch listing to edit
67 | useEffect(() => {
68 | setLoading(true)
69 | const fetchListing = async () => {
70 | const docRef = doc(db, 'listings', params.listingId)
71 | const docSnap = await getDoc(docRef)
72 | if (docSnap.exists()) {
73 | setListing(docSnap.data())
74 | setFormData({ ...docSnap.data(), address: docSnap.data().location })
75 | setLoading(false)
76 | } else {
77 | navigate('/')
78 | toast.error('Listing does not exist')
79 | }
80 | }
81 |
82 | fetchListing()
83 | }, [params.listingId, navigate])
84 |
85 | // Sets userRef to logged in user
86 | useEffect(() => {
87 | if (isMounted) {
88 | onAuthStateChanged(auth, (user) => {
89 | if (user) {
90 | setFormData({ ...formData, userRef: user.uid })
91 | } else {
92 | navigate('/sign-in')
93 | }
94 | })
95 | }
96 |
97 | return () => {
98 | isMounted.current = false
99 | }
100 | // eslint-disable-next-line react-hooks/exhaustive-deps
101 | }, [isMounted])
102 |
103 | const onSubmit = async (e) => {
104 | e.preventDefault()
105 |
106 | setLoading(true)
107 |
108 | if (discountedPrice >= regularPrice) {
109 | setLoading(false)
110 | toast.error('Discounted price needs to be less than regular price')
111 | return
112 | }
113 |
114 | if (images.length > 6) {
115 | setLoading(false)
116 | toast.error('Max 6 images')
117 | return
118 | }
119 |
120 | let geolocation = {}
121 | let location
122 |
123 | if (geolocationEnabled) {
124 | const response = await fetch(
125 | `https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${process.env.REACT_APP_GEOCODE_API_KEY}`
126 | )
127 |
128 | const data = await response.json()
129 |
130 | geolocation.lat = data.results[0]?.geometry.location.lat ?? 0
131 | geolocation.lng = data.results[0]?.geometry.location.lng ?? 0
132 |
133 | location =
134 | data.status === 'ZERO_RESULTS'
135 | ? undefined
136 | : data.results[0]?.formatted_address
137 |
138 | if (location === undefined || location.includes('undefined')) {
139 | setLoading(false)
140 | toast.error('Please enter a correct address')
141 | return
142 | }
143 | } else {
144 | geolocation.lat = latitude
145 | geolocation.lng = longitude
146 | }
147 |
148 | // Store image in firebase
149 | const storeImage = async (image) => {
150 | return new Promise((resolve, reject) => {
151 | const storage = getStorage()
152 | const fileName = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`
153 |
154 | const storageRef = ref(storage, 'images/' + fileName)
155 |
156 | const uploadTask = uploadBytesResumable(storageRef, image)
157 |
158 | uploadTask.on(
159 | 'state_changed',
160 | (snapshot) => {
161 | const progress =
162 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100
163 | console.log('Upload is ' + progress + '% done')
164 | switch (snapshot.state) {
165 | case 'paused':
166 | console.log('Upload is paused')
167 | break
168 | case 'running':
169 | console.log('Upload is running')
170 | break
171 | default:
172 | break
173 | }
174 | },
175 | (error) => {
176 | reject(error)
177 | },
178 | () => {
179 | // Handle successful uploads on complete
180 | // For instance, get the download URL: https://firebasestorage.googleapis.com/...
181 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
182 | resolve(downloadURL)
183 | })
184 | }
185 | )
186 | })
187 | }
188 |
189 | const imgUrls = await Promise.all(
190 | [...images].map((image) => storeImage(image))
191 | ).catch(() => {
192 | setLoading(false)
193 | toast.error('Images not uploaded')
194 | return
195 | })
196 |
197 | const formDataCopy = {
198 | ...formData,
199 | imgUrls,
200 | geolocation,
201 | timestamp: serverTimestamp(),
202 | }
203 |
204 | formDataCopy.location = address
205 | delete formDataCopy.images
206 | delete formDataCopy.address
207 | !formDataCopy.offer && delete formDataCopy.discountedPrice
208 |
209 | // Update listing
210 | const docRef = doc(db, 'listings', params.listingId)
211 | await updateDoc(docRef, formDataCopy)
212 | setLoading(false)
213 | toast.success('Listing saved')
214 | navigate(`/category/${formDataCopy.type}/${docRef.id}`)
215 | }
216 |
217 | const onMutate = (e) => {
218 | let boolean = null
219 |
220 | if (e.target.value === 'true') {
221 | boolean = true
222 | }
223 | if (e.target.value === 'false') {
224 | boolean = false
225 | }
226 |
227 | // Files
228 | if (e.target.files) {
229 | setFormData((prevState) => ({
230 | ...prevState,
231 | images: e.target.files,
232 | }))
233 | }
234 |
235 | // Text/Booleans/Numbers
236 | if (!e.target.files) {
237 | setFormData((prevState) => ({
238 | ...prevState,
239 | [e.target.id]: boolean ?? e.target.value,
240 | }))
241 | }
242 | }
243 |
244 | if (loading) {
245 | return
246 | }
247 |
248 | return (
249 |
250 |
253 |
254 |
255 |
481 |
482 |
483 | )
484 | }
485 |
486 | export default EditListing
487 |
--------------------------------------------------------------------------------
/src/pages/Explore.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import Slider from '../components/Slider'
3 | import rentCategoryImage from '../assets/jpg/rentCategoryImage.jpg'
4 | import sellCategoryImage from '../assets/jpg/sellCategoryImage.jpg'
5 |
6 | function Explore() {
7 | return (
8 |
9 |
12 |
13 |
14 |
15 |
16 | Categories
17 |
18 |
19 |

24 |
Places for rent
25 |
26 |
27 |

32 |
Places for sale
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default Explore
41 |
--------------------------------------------------------------------------------
/src/pages/ForgotPassword.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { getAuth, sendPasswordResetEmail } from 'firebase/auth'
4 | import { toast } from 'react-toastify'
5 | import { ReactComponent as ArrowRightIcon } from '../assets/svg/keyboardArrowRightIcon.svg'
6 |
7 | function ForgotPassword() {
8 | const [email, setEmail] = useState('')
9 |
10 | const onChange = (e) => setEmail(e.target.value)
11 |
12 | const onSubmit = async (e) => {
13 | e.preventDefault()
14 | try {
15 | const auth = getAuth()
16 | await sendPasswordResetEmail(auth, email)
17 | toast.success('Email was sent')
18 | } catch (error) {
19 | toast.error('Could not send reset email')
20 | }
21 | }
22 |
23 | return (
24 |
25 |
26 | Forgot Password
27 |
28 |
29 |
30 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default ForgotPassword
56 |
--------------------------------------------------------------------------------
/src/pages/Listing.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { Link, useNavigate, useParams } from 'react-router-dom'
3 | import { Helmet } from 'react-helmet'
4 | import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
5 | import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper'
6 | import { Swiper, SwiperSlide } from 'swiper/react'
7 | import 'swiper/swiper-bundle.css'
8 | import { getDoc, doc } from 'firebase/firestore'
9 | import { getAuth } from 'firebase/auth'
10 | import { db } from '../firebase.config'
11 | import Spinner from '../components/Spinner'
12 | import shareIcon from '../assets/svg/shareIcon.svg'
13 | SwiperCore.use([Navigation, Pagination, Scrollbar, A11y])
14 |
15 | function Listing() {
16 | const [listing, setListing] = useState(null)
17 | const [loading, setLoading] = useState(true)
18 | const [shareLinkCopied, setShareLinkCopied] = useState(false)
19 |
20 | const navigate = useNavigate()
21 | const params = useParams()
22 | const auth = getAuth()
23 |
24 | useEffect(() => {
25 | const fetchListing = async () => {
26 | const docRef = doc(db, 'listings', params.listingId)
27 | const docSnap = await getDoc(docRef)
28 |
29 | if (docSnap.exists()) {
30 | setListing(docSnap.data())
31 | setLoading(false)
32 | }
33 | }
34 |
35 | fetchListing()
36 | }, [navigate, params.listingId])
37 |
38 | if (loading) {
39 | return
40 | }
41 |
42 | return (
43 |
44 |
45 | {listing.name}
46 |
47 |
48 | {listing.imgUrls.map((url, index) => (
49 |
50 |
57 |
58 | ))}
59 |
60 |
61 | {
64 | navigator.clipboard.writeText(window.location.href)
65 | setShareLinkCopied(true)
66 | setTimeout(() => {
67 | setShareLinkCopied(false)
68 | }, 2000)
69 | }}
70 | >
71 |

72 |
73 |
74 | {shareLinkCopied && Link Copied!
}
75 |
76 |
77 |
78 | {listing.name} - $
79 | {listing.offer
80 | ? listing.discountedPrice
81 | .toString()
82 | .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
83 | : listing.regularPrice
84 | .toString()
85 | .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
86 |
87 |
{listing.location}
88 |
89 | For {listing.type === 'rent' ? 'Rent' : 'Sale'}
90 |
91 | {listing.offer && (
92 |
93 | ${listing.regularPrice - listing.discountedPrice} discount
94 |
95 | )}
96 |
97 |
98 | -
99 | {listing.bedrooms > 1
100 | ? `${listing.bedrooms} Bedrooms`
101 | : '1 Bedroom'}
102 |
103 | -
104 | {listing.bathrooms > 1
105 | ? `${listing.bathrooms} Bathrooms`
106 | : '1 Bathroom'}
107 |
108 | - {listing.parking && 'Parking Spot'}
109 | - {listing.furnished && 'Furnished'}
110 |
111 |
112 |
Location
113 |
114 |
115 |
121 |
125 |
126 |
129 | {listing.location}
130 |
131 |
132 |
133 |
134 | {auth.currentUser?.uid !== listing.userRef && (
135 |
139 | Contact Landlord
140 |
141 | )}
142 |
143 |
144 | )
145 | }
146 |
147 | export default Listing
148 |
149 | // https://stackoverflow.com/questions/67552020/how-to-fix-error-failed-to-compile-node-modules-react-leaflet-core-esm-pat
150 |
--------------------------------------------------------------------------------
/src/pages/Offers.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import {
3 | collection,
4 | getDocs,
5 | query,
6 | where,
7 | orderBy,
8 | limit,
9 | startAfter,
10 | } from 'firebase/firestore'
11 | import { db } from '../firebase.config'
12 | import { toast } from 'react-toastify'
13 | import Spinner from '../components/Spinner'
14 | import ListingItem from '../components/ListingItem'
15 |
16 | function Offers() {
17 | const [listings, setListings] = useState(null)
18 | const [loading, setLoading] = useState(true)
19 | const [lastFetchedListing, setLastFetchedListing] = useState(null)
20 |
21 | useEffect(() => {
22 | const fetchListings = async () => {
23 | try {
24 | // Get reference
25 | const listingsRef = collection(db, 'listings')
26 |
27 | // Create a query
28 | const q = query(
29 | listingsRef,
30 | where('offer', '==', true),
31 | orderBy('timestamp', 'desc'),
32 | limit(10)
33 | )
34 |
35 | // Execute query
36 | const querySnap = await getDocs(q)
37 |
38 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]
39 | setLastFetchedListing(lastVisible)
40 |
41 | const listings = []
42 |
43 | querySnap.forEach((doc) => {
44 | return listings.push({
45 | id: doc.id,
46 | data: doc.data(),
47 | })
48 | })
49 |
50 | setListings(listings)
51 | setLoading(false)
52 | } catch (error) {
53 | toast.error('Could not fetch listings')
54 | }
55 | }
56 |
57 | fetchListings()
58 | }, [])
59 |
60 | // Pagination / Load More
61 | const onFetchMoreListings = async () => {
62 | try {
63 | // Get reference
64 | const listingsRef = collection(db, 'listings')
65 |
66 | // Create a query
67 | const q = query(
68 | listingsRef,
69 | where('offer', '==', true),
70 | orderBy('timestamp', 'desc'),
71 | startAfter(lastFetchedListing),
72 | limit(10)
73 | )
74 |
75 | // Execute query
76 | const querySnap = await getDocs(q)
77 |
78 | const lastVisible = querySnap.docs[querySnap.docs.length - 1]
79 | setLastFetchedListing(lastVisible)
80 |
81 | const listings = []
82 |
83 | querySnap.forEach((doc) => {
84 | return listings.push({
85 | id: doc.id,
86 | data: doc.data(),
87 | })
88 | })
89 |
90 | setListings((prevState) => [...prevState, ...listings])
91 | setLoading(false)
92 | } catch (error) {
93 | toast.error('Could not fetch listings')
94 | }
95 | }
96 |
97 | return (
98 |
99 |
102 |
103 | {loading ? (
104 |
105 | ) : listings && listings.length > 0 ? (
106 | <>
107 |
108 |
109 | {listings.map((listing) => (
110 |
115 | ))}
116 |
117 |
118 |
119 |
120 |
121 | {lastFetchedListing && (
122 |
123 | Load More
124 |
125 | )}
126 | >
127 | ) : (
128 |
There are no current offers
129 | )}
130 |
131 | )
132 | }
133 |
134 | export default Offers
135 |
--------------------------------------------------------------------------------
/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { getAuth, updateProfile } from 'firebase/auth'
4 | import {
5 | updateDoc,
6 | doc,
7 | collection,
8 | getDocs,
9 | query,
10 | where,
11 | orderBy,
12 | deleteDoc,
13 | } from 'firebase/firestore'
14 | import { db } from '../firebase.config'
15 | import { useNavigate } from 'react-router-dom'
16 | import { toast } from 'react-toastify'
17 | import ListingItem from '../components/ListingItem'
18 | import arrowRight from '../assets/svg/keyboardArrowRightIcon.svg'
19 | import homeIcon from '../assets/svg/homeIcon.svg'
20 |
21 | function Profile() {
22 | const auth = getAuth()
23 | const [loading, setLoading] = useState(true)
24 | const [listings, setListings] = useState(null)
25 | const [changeDetails, setChangeDetails] = useState(false)
26 | const [formData, setFormData] = useState({
27 | name: auth.currentUser.displayName,
28 | email: auth.currentUser.email,
29 | })
30 |
31 | const { name, email } = formData
32 |
33 | const navigate = useNavigate()
34 |
35 | useEffect(() => {
36 | const fetchUserListings = async () => {
37 | const listingsRef = collection(db, 'listings')
38 |
39 | const q = query(
40 | listingsRef,
41 | where('userRef', '==', auth.currentUser.uid),
42 | orderBy('timestamp', 'desc')
43 | )
44 |
45 | const querySnap = await getDocs(q)
46 |
47 | let listings = []
48 |
49 | querySnap.forEach((doc) => {
50 | return listings.push({
51 | id: doc.id,
52 | data: doc.data(),
53 | })
54 | })
55 |
56 | setListings(listings)
57 | setLoading(false)
58 | }
59 |
60 | fetchUserListings()
61 | }, [auth.currentUser.uid])
62 |
63 | const onLogout = () => {
64 | auth.signOut()
65 | navigate('/')
66 | }
67 |
68 | const onSubmit = async () => {
69 | try {
70 | if (auth.currentUser.displayName !== name) {
71 | // Update display name in fb
72 | await updateProfile(auth.currentUser, {
73 | displayName: name,
74 | })
75 |
76 | // Update in firestore
77 | const userRef = doc(db, 'users', auth.currentUser.uid)
78 | await updateDoc(userRef, {
79 | name,
80 | })
81 | }
82 | } catch (error) {
83 | console.log(error)
84 | toast.error('Could not update profile details')
85 | }
86 | }
87 |
88 | const onChange = (e) => {
89 | setFormData((prevState) => ({
90 | ...prevState,
91 | [e.target.id]: e.target.value,
92 | }))
93 | }
94 |
95 | const onDelete = async (listingId) => {
96 | if (window.confirm('Are you sure you want to delete?')) {
97 | await deleteDoc(doc(db, 'listings', listingId))
98 | const updatedListings = listings.filter(
99 | (listing) => listing.id !== listingId
100 | )
101 | setListings(updatedListings)
102 | toast.success('Successfully deleted listing')
103 | }
104 | }
105 |
106 | const onEdit = (listingId) => navigate(`/edit-listing/${listingId}`)
107 |
108 | return (
109 |
110 |
111 | My Profile
112 |
115 |
116 |
117 |
118 |
119 |
Personal Details
120 |
{
123 | changeDetails && onSubmit()
124 | setChangeDetails((prevState) => !prevState)
125 | }}
126 | >
127 | {changeDetails ? 'done' : 'change'}
128 |
129 |
130 |
131 |
132 |
150 |
151 |
152 |
153 |
154 | Sell or rent your home
155 |
156 |
157 |
158 | {!loading && listings?.length > 0 && (
159 | <>
160 | Your Listings
161 |
162 | {listings.map((listing) => (
163 | onDelete(listing.id)}
168 | onEdit={() => onEdit(listing.id)}
169 | />
170 | ))}
171 |
172 | >
173 | )}
174 |
175 |
176 | )
177 | }
178 |
179 | export default Profile
180 |
--------------------------------------------------------------------------------
/src/pages/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { toast } from 'react-toastify'
3 | import { Link, useNavigate } from 'react-router-dom'
4 | import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
5 | import OAuth from '../components/OAuth'
6 | import { ReactComponent as ArrowRightIcon } from '../assets/svg/keyboardArrowRightIcon.svg'
7 | import visibilityIcon from '../assets/svg/visibilityIcon.svg'
8 |
9 | function SignIn() {
10 | const [showPassword, setShowPassword] = useState(false)
11 | const [formData, setFormData] = useState({
12 | email: '',
13 | password: '',
14 | })
15 | const { email, password } = formData
16 |
17 | const navigate = useNavigate()
18 |
19 | const onChange = (e) => {
20 | setFormData((prevState) => ({
21 | ...prevState,
22 | [e.target.id]: e.target.value,
23 | }))
24 | }
25 |
26 | const onSubmit = async (e) => {
27 | e.preventDefault()
28 |
29 | try {
30 | const auth = getAuth()
31 |
32 | const userCredential = await signInWithEmailAndPassword(
33 | auth,
34 | email,
35 | password
36 | )
37 |
38 | if (userCredential.user) {
39 | navigate('/')
40 | }
41 | } catch (error) {
42 | toast.error('Bad User Credentials')
43 | }
44 | }
45 |
46 | return (
47 | <>
48 |
49 |
52 |
53 |
92 |
93 |
94 |
95 |
96 | Sign Up Instead
97 |
98 |
99 | >
100 | )
101 | }
102 |
103 | export default SignIn
104 |
--------------------------------------------------------------------------------
/src/pages/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { Link, useNavigate } from 'react-router-dom'
3 | import { toast } from 'react-toastify'
4 | import {
5 | getAuth,
6 | createUserWithEmailAndPassword,
7 | updateProfile,
8 | } from 'firebase/auth'
9 | import { setDoc, doc, serverTimestamp } from 'firebase/firestore'
10 | import { db } from '../firebase.config'
11 | import OAuth from '../components/OAuth'
12 | import { ReactComponent as ArrowRightIcon } from '../assets/svg/keyboardArrowRightIcon.svg'
13 | import visibilityIcon from '../assets/svg/visibilityIcon.svg'
14 |
15 | function SignUp() {
16 | const [showPassword, setShowPassword] = useState(false)
17 | const [formData, setFormData] = useState({
18 | name: '',
19 | email: '',
20 | password: '',
21 | })
22 | const { name, email, password } = formData
23 |
24 | const navigate = useNavigate()
25 |
26 | const onChange = (e) => {
27 | setFormData((prevState) => ({
28 | ...prevState,
29 | [e.target.id]: e.target.value,
30 | }))
31 | }
32 |
33 | const onSubmit = async (e) => {
34 | e.preventDefault()
35 |
36 | try {
37 | const auth = getAuth()
38 |
39 | const userCredential = await createUserWithEmailAndPassword(
40 | auth,
41 | email,
42 | password
43 | )
44 |
45 | const user = userCredential.user
46 |
47 | updateProfile(auth.currentUser, {
48 | displayName: name,
49 | })
50 |
51 | const formDataCopy = { ...formData }
52 | delete formDataCopy.password
53 | formDataCopy.timestamp = serverTimestamp()
54 |
55 | await setDoc(doc(db, 'users', user.uid), formDataCopy)
56 |
57 | navigate('/')
58 | } catch (error) {
59 | toast.error('Something went wrong with registration')
60 | }
61 | }
62 |
63 | return (
64 | <>
65 |
66 |
69 |
70 |
117 |
118 |
119 |
120 |
121 | Sign In Instead
122 |
123 |
124 | >
125 | )
126 | }
127 |
128 | export default SignUp
129 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------