Compare commits
10 Commits
7d4e2d1b69
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 199899d371 | |||
| 85f56e2f80 | |||
| 7841afe8de | |||
| 09b6b0ad86 | |||
| d79737a497 | |||
| 5c96af8b6c | |||
| 8cc369bfe6 | |||
| e31b90d9ab | |||
| 19fb5256cf | |||
| 98d2d372e8 |
@@ -1,70 +1,6 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
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)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
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)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
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)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
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)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
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)
|
||||
# House marketplace
|
||||
---
|
||||
>React app for buying and selling houses. It uses firebase as a backend.
|
||||
---
|
||||
## Usage:
|
||||
`npm start`
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"firebase": "^9.14.0",
|
||||
"leaflet": "^1.9.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-leaflet": "^4.2.0",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^9.1.1",
|
||||
"swiper": "^8.4.5",
|
||||
"uuid": "^9.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -10,34 +10,16 @@
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
|
||||
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
||||
crossorigin=""/>
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,47 @@
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import PrivateRoute from "./components/PrivateRoute";
|
||||
import Navbar from "./components/Navbar";
|
||||
import Explore from "./pages/Explore";
|
||||
import Profile from "./pages/Profile";
|
||||
import ForgotPassword from "./pages/ForgotPassword";
|
||||
import Offers from "./pages/Offers";
|
||||
import Signin from "./pages/Signin";
|
||||
import Signup from "./pages/Signup";
|
||||
import Category from "./pages/Category";
|
||||
import CreateLising from "./pages/CreateListing";
|
||||
import Listing from "./pages/Listing";
|
||||
import Contact from "./pages/Contact";
|
||||
import EditListing from "./pages/EditListing";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
<>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Explore />} />
|
||||
<Route path="/offers" element={<Offers />} />
|
||||
<Route path="/category/:categoryName" element={<Category />} />
|
||||
|
||||
<Route path="/profile" element={<PrivateRoute />}>
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Route>
|
||||
<Route path="/sign-in" element={<Signin />} />
|
||||
<Route path="/sign-up" element={<Signup />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/create-listing" element={<CreateLising />} />
|
||||
<Route path="/edit-listing/:listingId" element={<EditListing />} />
|
||||
<Route
|
||||
path="/category/:categoryName/:listingId"
|
||||
element={<Listing />}
|
||||
/>
|
||||
<Route path="/contact/:landlordId" element={<Contact />}/>
|
||||
</Routes>
|
||||
<Navbar />
|
||||
</Router>
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M20,7h-5V4c0-1.1-0.9-2-2-2h-2C9.9,2,9,2.9,9,4v3H4C2.9,7,2,7.9,2,9v11c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V9 C22,7.9,21.1,7,20,7z M9,12c0.83,0,1.5,0.67,1.5,1.5S9.83,15,9,15s-1.5-0.67-1.5-1.5S8.17,12,9,12z M12,18H6v-0.75c0-1,2-1.5,3-1.5 s3,0.5,3,1.5V18z M13,9h-2V4h2V9z M18,16.5h-4V15h4V16.5z M18,13.5h-4V12h4V13.5z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><g><circle cx="7" cy="7" r="2"/></g><g><path d="M20,13V4.83C20,3.27,18.73,2,17.17,2c-0.75,0-1.47,0.3-2,0.83l-1.25,1.25C13.76,4.03,13.59,4,13.41,4 c-0.4,0-0.77,0.12-1.08,0.32l2.76,2.76c0.2-0.31,0.32-0.68,0.32-1.08c0-0.18-0.03-0.34-0.07-0.51l1.25-1.25 C16.74,4.09,16.95,4,17.17,4C17.63,4,18,4.37,18,4.83V13h-6.85c-0.3-0.21-0.57-0.45-0.82-0.72l-1.4-1.55 c-0.19-0.21-0.43-0.38-0.69-0.5C7.93,10.08,7.59,10,7.24,10C6,10.01,5,11.01,5,12.25V13H2v6c0,1.1,0.9,2,2,2c0,0.55,0.45,1,1,1 h14c0.55,0,1-0.45,1-1c1.1,0,2-0.9,2-2v-6H20z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><rect fill="none" height="3" width="5" x="6" y="7"/><rect fill="none" height="3" width="5" x="13" y="7"/><path d="M20,10V7c0-1.1-0.9-2-2-2H6C4.9,5,4,5.9,4,7v3c-1.1,0-2,0.9-2,2v5h1.33L4,19h1l0.67-2h12.67L19,19h1l0.67-2H22v-5 C22,10.9,21.1,10,20,10z M11,10H6V7h5V10z M18,10h-5V7h5V10z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 490 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||
|
After Width: | Height: | Size: 206 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||
|
After Width: | Height: | Size: 234 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z"/></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1,15 @@
|
||||
<svg width="124" height="124" viewBox="0 0 124 124" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M119.081 51.0933L69.0114 51.0909C66.8004 51.0909 65.0083 52.8828 65.0083 55.0938V71.0887C65.0083 73.2992 66.8004 75.0916 69.0112 75.0916H97.2073C94.1198 83.1043 88.3572 89.8147 81.005 94.0785L93.0277 114.891C112.314 103.737 123.716 84.1664 123.716 62.2585C123.716 59.1391 123.486 56.9092 123.026 54.3982C122.677 52.4905 121.02 51.0933 119.081 51.0933Z" fill="#167EE6"/>
|
||||
<path d="M62.3391 99.1246C48.5404 99.1246 36.4944 91.5853 30.0247 80.429L9.21289 92.4247C19.8039 110.781 39.6442 123.141 62.3391 123.141C73.4724 123.141 83.9775 120.144 93.0272 114.92V114.891L81.0044 94.0785C75.505 97.2682 69.141 99.1246 62.3391 99.1246Z" fill="#12B347"/>
|
||||
<path d="M93.0275 114.919V114.891L81.0047 94.0781C75.5053 97.2675 69.1418 99.1242 62.3394 99.1242V123.141C73.4727 123.141 83.9783 120.143 93.0275 114.919Z" fill="#0F993E"/>
|
||||
<path d="M24.9802 61.7646C24.9802 54.9631 26.8363 48.5999 30.0253 43.1007L9.21345 31.105C3.96074 40.1262 0.963379 50.6028 0.963379 61.7646C0.963379 72.9265 3.96074 83.4031 9.21345 92.4242L30.0253 80.4285C26.8363 74.9294 24.9802 68.5661 24.9802 61.7646Z" fill="#FFD500"/>
|
||||
<path d="M62.3391 24.4057C71.3372 24.4057 79.6023 27.603 86.0581 32.9214C87.6508 34.2334 89.9656 34.1387 91.4244 32.6798L102.757 21.3467C104.413 19.6915 104.295 16.9821 102.527 15.4482C91.7102 6.06454 77.6368 0.388916 62.3391 0.388916C39.6442 0.388916 19.8039 12.7498 9.21289 31.1056L30.0247 43.1013C36.4944 31.9449 48.5404 24.4057 62.3391 24.4057Z" fill="#FF4B26"/>
|
||||
<path d="M86.0584 32.9214C87.6511 34.2334 89.9661 34.1387 91.4248 32.6798L102.758 21.3467C104.413 19.6915 104.295 16.9821 102.527 15.4482C91.7106 6.0643 77.6372 0.388916 62.3394 0.388916V24.4057C71.3372 24.4057 79.6026 27.603 86.0584 32.9214Z" fill="#D93F21"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="122.752" height="122.752" fill="white" transform="translate(0.963379 0.388794)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
|
||||
|
After Width: | Height: | Size: 192 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM13 20.01L4 11V4h7v-.01l9 9-7 7.02z"/><circle cx="6.5" cy="6.5" r="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
|
After Width: | Height: | Size: 266 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>
|
||||
|
After Width: | Height: | Size: 516 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
|
||||
|
After Width: | Height: | Size: 194 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,60 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { ReactComponent as DeleteIcon } from "../assets/svg/deleteIcon.svg";
|
||||
import { ReactComponent as EditIcon } from "../assets/svg/editIcon.svg";
|
||||
import bedIcon from "../assets/svg/bedIcon.svg";
|
||||
import bathtubIcon from "../assets/svg/bathtubIcon.svg";
|
||||
|
||||
function ListingItem({ listing, id, onDelete, onEdit }) {
|
||||
return (
|
||||
<li className="categoryListing">
|
||||
<Link
|
||||
to={`/category/${listing.type}/${id}`}
|
||||
className="categoryListingLink"
|
||||
>
|
||||
<img
|
||||
src={listing.imgUrls[0]}
|
||||
alt={listing.name}
|
||||
className="categoryListingImg"
|
||||
/>
|
||||
<div className="categoryListingDetails">
|
||||
<p className="categoryListingLocation">{listing.location}</p>
|
||||
<p className="categoryListingName">{listing.name}</p>
|
||||
<p className="categoryListingPrice">
|
||||
$
|
||||
{listing.offer
|
||||
? listing.discountedPrice
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
: listing.regularPrice
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
{listing.type === "rent" && " / Month"}
|
||||
</p>
|
||||
<div className="categoryListingInfoDiv">
|
||||
<img src={bedIcon} alt="bed" />
|
||||
<p className="categoryListingInfoText">
|
||||
{listing.bedrooms > 1
|
||||
? `${listing.bedrooms} Bedrooms`
|
||||
: "1 Bedrooms"}
|
||||
</p>
|
||||
<img src={bathtubIcon} alt="bath" />
|
||||
<p className="categoryListingInfoText">
|
||||
{listing.bathrooms > 1
|
||||
? `${listing.bathrooms} Bedrooms`
|
||||
: "1 Bathrooms"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{onDelete && (
|
||||
<DeleteIcon
|
||||
className="removeIcon"
|
||||
fill="rgb(231,76,60)"
|
||||
onClick={() => onDelete(listing.id, listing.name)}
|
||||
/>
|
||||
)}
|
||||
{onEdit && <EditIcon className="editIcon" onClick={() => onEdit(id)} />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
export default ListingItem;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { ReactComponent as OfferIcon } from "../assets/svg/localOfferIcon.svg";
|
||||
import { ReactComponent as ExploreIcon } from "../assets/svg/exploreIcon.svg";
|
||||
import { ReactComponent as PersonIcon } from "../assets/svg/personOutlineIcon.svg";
|
||||
|
||||
function Navbar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const pathMatchRoute = (route) => {
|
||||
if (route === location.pathname) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<footer className="navbar">
|
||||
<nav className="navbarNan">
|
||||
<ul className="navbarListItems">
|
||||
<li className="navbarListItems" onClick={() => navigate("/")}>
|
||||
<ExploreIcon
|
||||
fill={pathMatchRoute("/") ? "#2c2c2c" : "#8f8f8f"}
|
||||
width="36px"
|
||||
height="36px"
|
||||
/>
|
||||
<p
|
||||
className={
|
||||
pathMatchRoute("/")
|
||||
? "navbarListItemNameActive"
|
||||
: "navbarListItemName"
|
||||
}
|
||||
>
|
||||
Explore
|
||||
</p>
|
||||
</li>
|
||||
<li className="navbarListItems" onClick={() => navigate("/offers")}>
|
||||
<OfferIcon
|
||||
fill={pathMatchRoute("/offers") ? "#2c2c2c" : "#8f8f8f"}
|
||||
width="36px"
|
||||
height="36px"
|
||||
/>
|
||||
<p
|
||||
className={
|
||||
pathMatchRoute("/offers")
|
||||
? "navbarListItemNameActive"
|
||||
: "navbarListItemName"
|
||||
}
|
||||
>
|
||||
Offers
|
||||
</p>
|
||||
</li>
|
||||
<li className="navbarListItems" onClick={() => navigate("/profile")}>
|
||||
<PersonIcon
|
||||
fill={pathMatchRoute("/profile") ? "#2c2c2c" : "#8f8f8f"}
|
||||
width="36px"
|
||||
height="36px"
|
||||
/>
|
||||
<p
|
||||
className={
|
||||
pathMatchRoute("/profile")
|
||||
? "navbarListItemNameActive"
|
||||
: "navbarListItemName"
|
||||
}
|
||||
>
|
||||
Profile
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
export default Navbar
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
|
||||
import { doc, setDoc, getDoc, serverTimestamp } from "firebase/firestore";
|
||||
import { db } from "../firebase.config";
|
||||
import { toast } from "react-toastify";
|
||||
import googleIcon from "../assets/svg/googleIcon.svg";
|
||||
|
||||
function OAuth() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const onGoogleClick = async () => {
|
||||
try {
|
||||
const auth = getAuth();
|
||||
const provider = new GoogleAuthProvider();
|
||||
const result = await signInWithPopup(auth, provider);
|
||||
const user = result.user;
|
||||
|
||||
const docRef = doc(db, "users", user.uid);
|
||||
const docSnap = await getDoc(docRef);
|
||||
|
||||
if (!docSnap.exists()) {
|
||||
await setDoc(doc(db, "users", user.uid), {
|
||||
name: user.displayName,
|
||||
email: user.email,
|
||||
timestamp: serverTimestamp(),
|
||||
});
|
||||
}
|
||||
navigate('/')
|
||||
} catch (error) {
|
||||
toast.error('Could not authorize with google')
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="socialLogin">
|
||||
<p>Sign {location.pathname === "/sign-up" ? "up" : "in"} with</p>
|
||||
<button className="socialIconDiv" onClick={onGoogleClick}>
|
||||
<img className="socialIconImg" src={googleIcon} alt="google" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default OAuth;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useAuthStatus } from "../hooks/useAuthStatus";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const { loggedIn, checkingStatus } = useAuthStatus();
|
||||
|
||||
if (checkingStatus) {
|
||||
return <Spinner/>
|
||||
}
|
||||
return loggedIn ? <Outlet /> : <Navigate to="/sign-in" />;
|
||||
};
|
||||
export default PrivateRoute;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getDocs, query, orderBy, limit, collection } from "firebase/firestore";
|
||||
import { db } from "../firebase.config";
|
||||
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from "swiper";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
|
||||
|
||||
function Slider() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [listings, setListings] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
const listingsRef = collection(db, "listings");
|
||||
const q = query(listingsRef, orderBy("timestamp", "desc"), limit(5));
|
||||
const querySnap = await getDocs(q);
|
||||
|
||||
let listings = [];
|
||||
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({
|
||||
id: doc.id,
|
||||
data: doc.data(),
|
||||
});
|
||||
});
|
||||
|
||||
setListings(listings);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchListings();
|
||||
}, []);
|
||||
if (loading) return <Spinner />;
|
||||
if (listings.length === 0) return <></>
|
||||
return (
|
||||
listings && (
|
||||
<>
|
||||
<p className="exploreHeading">Recommended</p>
|
||||
<Swiper slidesPerView={1} pagination={{ clickable: true }}>
|
||||
{listings.map(({ data, id }) => (
|
||||
<SwiperSlide
|
||||
key={id}
|
||||
onClick={() => navigate(`/category/${data.type}/${id}`)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: `url(${data.imgUrls[0]}) center no-repeat`,
|
||||
backgroundSize: "cover",
|
||||
minHeight: "20rem",
|
||||
}}
|
||||
className="swiperSlideDiv"
|
||||
>
|
||||
<p className="swiperSlideText">{data.name}</p>
|
||||
<p className="swiperSlidePrice">
|
||||
${data.discountedPrice ?? data.regularPrice}{" "}
|
||||
{data.type === "rent" && "/ month"}
|
||||
</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
export default Slider;
|
||||
@@ -0,0 +1,7 @@
|
||||
function Spinner(){
|
||||
return (
|
||||
<div className="loadingSpinerContainer">
|
||||
<div className="loadingSpinner"></div>
|
||||
</div>)
|
||||
}
|
||||
export default Spinner
|
||||
@@ -0,0 +1,15 @@
|
||||
import { initializeApp } from "firebase/app";
|
||||
import {getFirestore} from 'firebase/firestore'
|
||||
// Your web app's Firebase configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDkQS2GoBx-IR_BR4dD_4bPUNGaPyMQugI",
|
||||
authDomain: "house-marketplace-app-7e89f.firebaseapp.com",
|
||||
projectId: "house-marketplace-app-7e89f",
|
||||
storageBucket: "house-marketplace-app-7e89f.appspot.com",
|
||||
messagingSenderId: "66680163160",
|
||||
appId: "1:66680163160:web:c1874be8bb6d9e273e732d"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
initializeApp(firebaseConfig);
|
||||
export const db = getFirestore()
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { getAuth, onAuthStateChanged } from "firebase/auth";
|
||||
|
||||
export const useAuthStatus = () => {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [checkingStatus, setCheckingStatus] = useState(true);
|
||||
const isMouted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMouted) {
|
||||
const auth = getAuth();
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
setLoggedIn(true);
|
||||
}
|
||||
setCheckingStatus(false);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
isMouted.current = false;
|
||||
};
|
||||
},[isMouted]);
|
||||
return { loggedIn, checkingStatus };
|
||||
};
|
||||
@@ -1,13 +1,821 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
html::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
background-color: #f2f4f8;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.input,
|
||||
.passwordInput,
|
||||
.emailInput,
|
||||
.nameInput,
|
||||
.textarea {
|
||||
box-shadow: rgba(0, 0, 0, 0.11);
|
||||
border: none;
|
||||
background: #ffffff;
|
||||
border-radius: 3rem;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
padding: 0 3rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
.input,
|
||||
.passwordInput,
|
||||
.emailInput,
|
||||
.nameInput,
|
||||
.textarea {
|
||||
padding: 0 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
padding: 1rem 1.5rem;
|
||||
height: 300px;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
cursor: pointer;
|
||||
background: #00cc66;
|
||||
border-radius: 1rem;
|
||||
padding: 0.85rem 2rem;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.removeIcon {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -3%;
|
||||
right: -2%;
|
||||
}
|
||||
.editIcon {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -3.4%;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.pageContainer,
|
||||
.offers,
|
||||
.profile,
|
||||
.listingDetails,
|
||||
.category,
|
||||
.explore {
|
||||
margin: 1rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.pageContainer,
|
||||
.offers,
|
||||
.profile,
|
||||
.listingDetails,
|
||||
.category,
|
||||
.explore {
|
||||
margin: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingSpinnerContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 8px solid;
|
||||
border-color: #00cc66 transparent #00cc66 transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.2s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 85px;
|
||||
background-color: #ffffff;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbarNav {
|
||||
width: 100%;
|
||||
margin-top: 0.75rem;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.navbarListItems {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbarListItem {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbarListItemName,
|
||||
.navbarListItemNameActive {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #8f8f8f;
|
||||
}
|
||||
.navbarListItemNameActive {
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.nameInput {
|
||||
margin-bottom: 2rem;
|
||||
background: url('./assets/svg/badgeIcon.svg') #ffffff 2.5% center no-repeat;
|
||||
}
|
||||
|
||||
.emailInput {
|
||||
margin-bottom: 2rem;
|
||||
background: url('./assets/svg/personIcon.svg') #ffffff 2.5% center no-repeat;
|
||||
}
|
||||
|
||||
.passwordInputDiv {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.passwordInput {
|
||||
margin-bottom: 2rem;
|
||||
background: url('./assets/svg/lockIcon.svg') #ffffff 2.5% center no-repeat;
|
||||
}
|
||||
|
||||
.showPassword {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -4%;
|
||||
right: 1%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.forgotPasswordLink {
|
||||
cursor: pointer;
|
||||
color: #00cc66;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signInBar,
|
||||
.signUpBar {
|
||||
margin-top: 3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
.signInButton,
|
||||
.signUpButton,
|
||||
.signInText,
|
||||
.signUpText {
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.signInBar,
|
||||
.signUpBar {
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
.signInText,
|
||||
.signUpText {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.signInButton,
|
||||
.signUpButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-color: #00cc66;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.signInButton,
|
||||
.signUpButton {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.socialLogin {
|
||||
margin-top: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.socialIconDiv {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
margin: 1.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.socialIconImg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.registerLink {
|
||||
margin-top: 4rem;
|
||||
color: #00cc66;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1217px) {
|
||||
.explore {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
}
|
||||
@media (max-height: 536) {
|
||||
.explore {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.exploreHeading,
|
||||
.exploreCategoryHeading {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.exploreCategoryHeading {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.swiper-container {
|
||||
min-height: 225px;
|
||||
height: 23vw;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet-active {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.swiperSlideDiv {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiperSlideImg {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.swiperSlideText {
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
font-weight: 600;
|
||||
max-width: 90%;
|
||||
font-size: 1.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.swiperSlideText {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.swiperSlidePrice {
|
||||
color: #000000;
|
||||
position: absolute;
|
||||
top: 143px;
|
||||
left: 11px;
|
||||
font-weight: 600;
|
||||
max-width: 90%;
|
||||
background-color: #ffffff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.swiperSlidePrice {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.exploreCategories {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.exploreCategories a {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.exploreCategoryImg {
|
||||
min-height: 115px;
|
||||
height: 15vw;
|
||||
width: 100%;
|
||||
border-radius: 1.5rem;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.exploreCategoryName {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.category {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
|
||||
.categoryListings {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.categoryListing {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.categoryListingLink {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.categoryListingImg {
|
||||
width: 30%;
|
||||
height: 100px;
|
||||
border-radius: 1.5rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.categoryListingImg {
|
||||
width: 19%;
|
||||
height: 217px;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryListingDetails {
|
||||
width: 65%;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.categoryListingDetails {
|
||||
width: 79%;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryListingLocation {
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.categoryListingName {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.categoryListingPrice {
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: #00cc66;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoryListingInfoDiv {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 275px;
|
||||
}
|
||||
|
||||
.categoryListingInfoText {
|
||||
font-weight: 500;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
cursor: pointer;
|
||||
width: 8rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
border-radius: 1rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.listingDetails {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
|
||||
.shareIconDiv {
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
top: 3%;
|
||||
right: 5%;
|
||||
z-index: 2;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.listingName {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.listingLocation {
|
||||
margin-top: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.discountPrice {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.listingType {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: #00cc66;
|
||||
color: #ffffff;
|
||||
border-radius: 2rem;
|
||||
display: inline;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.listingDetailsList {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
.listingDetailsList li {
|
||||
margin: 0.3rem 0;
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.listingLocationTitle {
|
||||
margin-top: 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.leafletContainer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.leafletContainer {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.linkCopied {
|
||||
position: fixed;
|
||||
top: 9%;
|
||||
right: 5%;
|
||||
z-index: 2;
|
||||
background-color: #ffffff;
|
||||
border-radius: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contactListingName {
|
||||
margin-top: -1rem;
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contactListingLocation {
|
||||
margin-top: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contactLandlord {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.landlordName {
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.messageForm {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.messageDiv {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.messageLabel {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profile {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
|
||||
.profileHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logOut {
|
||||
cursor: pointer;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background-color: #00cc66;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.profileDetailsHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.personalDetailsText {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.changePersonalDetails {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #00cc66;
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
background-color: #ffffff;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.2);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.profileDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profileName,
|
||||
.profileEmail,
|
||||
.profileAddress,
|
||||
.profileAddressActive,
|
||||
.profileEmailActive,
|
||||
.profileNameActive {
|
||||
all: unset;
|
||||
margin: 0.3rem 0;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
}
|
||||
.profileNameActive {
|
||||
background-color: rgba(44, 44, 44, 0.1);
|
||||
}
|
||||
|
||||
.profileEmail,
|
||||
.profileAddress,
|
||||
.profileAddressActive,
|
||||
.profileEmailActive {
|
||||
font-weight: 500;
|
||||
}
|
||||
.profileEmailActive {
|
||||
background-color: rgba(44, 44, 44, 0.1);
|
||||
}
|
||||
|
||||
.profileAddressActive {
|
||||
background-color: rgba(44, 44, 44, 0.1);
|
||||
}
|
||||
|
||||
.createListing {
|
||||
background-color: #ffffff;
|
||||
border-radius: 1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.2);
|
||||
margin-top: 2rem;
|
||||
font-weight: 600;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.listingText {
|
||||
margin-top: 3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lisitingsList {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formButtons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.formButton,
|
||||
.formInput,
|
||||
.formInputAddress,
|
||||
.formInputName,
|
||||
.formInputSmall,
|
||||
.formInputFile,
|
||||
.formButtonActive {
|
||||
padding: 0.9rem 3rem;
|
||||
background-color: #ffffff;
|
||||
font-weight: 600;
|
||||
border-radius: 1rem;
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem 0.5rem 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.formButtonActive {
|
||||
background-color: #00cc66;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.formInput,
|
||||
.formInputAddress,
|
||||
.formInputName,
|
||||
.formInputSmall,
|
||||
.formInputFile {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
.formInputSmall,
|
||||
.formInputFile {
|
||||
margin-right: 3rem;
|
||||
padding: 0.9rem 0.7rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formInputName {
|
||||
padding: 0.9rem 1rem;
|
||||
width: 90%;
|
||||
max-width: 326px;
|
||||
}
|
||||
|
||||
.formInputAddress {
|
||||
padding: 0.9rem 1rem;
|
||||
width: 90%;
|
||||
max-width: 326px;
|
||||
}
|
||||
|
||||
.formPriceDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formPriceText {
|
||||
margin-left: -1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.imagesInfo {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.formInputFile {
|
||||
width: 100%;
|
||||
}
|
||||
.formInputFile::-webkit-file-upload-button {
|
||||
background-color: #00cc66;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.createListingButton {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
.offers {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
|
||||
.offerBadge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
border-radius: 1rem;
|
||||
margin-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
@@ -11,7 +10,3 @@ root.render(
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,126 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
collection,
|
||||
getDocs,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
limit,
|
||||
startAfter,
|
||||
} from "firebase/firestore";
|
||||
import { db } from "../firebase.config";
|
||||
import { toast } from "react-toastify";
|
||||
import Spinner from "../components/Spinner";
|
||||
import ListingItem from "../components/ListingItem";
|
||||
|
||||
function Category() {
|
||||
const [listings, setListings] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastFetchedlisting, setLastFetchedlisting] = useState(null);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
try {
|
||||
const listingsRef = collection(db, "listings");
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("type", "==", params.categoryName),
|
||||
orderBy("timestamp", "desc"),
|
||||
limit(10)
|
||||
);
|
||||
|
||||
const querySnap = await getDocs(q);
|
||||
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
|
||||
setLastFetchedlisting(lastVisible);
|
||||
|
||||
let listings = [];
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({
|
||||
id: doc.id,
|
||||
data: doc.data(),
|
||||
});
|
||||
});
|
||||
|
||||
setListings(listings);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
toast.error("could not fetch listings");
|
||||
}
|
||||
};
|
||||
|
||||
fetchListings();
|
||||
}, [params.categoryName]);
|
||||
const onFetchMoreListings = async () => {
|
||||
try {
|
||||
const listingsRef = collection(db, "listings");
|
||||
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("type", "==", params.categoryName),
|
||||
orderBy("timestamp", "desc"),
|
||||
startAfter(lastFetchedlisting),
|
||||
limit(10)
|
||||
);
|
||||
|
||||
const querySnap = await getDocs(q);
|
||||
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
|
||||
setLastFetchedlisting(lastVisible);
|
||||
|
||||
let listings = [];
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({
|
||||
id: doc.id,
|
||||
data: doc.data(),
|
||||
});
|
||||
});
|
||||
|
||||
setListings((prevState) => [...prevState, ...listings]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("could not fetch listings");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="category">
|
||||
<header>
|
||||
<p className="pageHeader">
|
||||
{params.categoryName === "rent"
|
||||
? "Places for rent"
|
||||
: "Places for sale"}
|
||||
</p>
|
||||
</header>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : listings && listings.length > 0 ? (
|
||||
<>
|
||||
<main>
|
||||
<ul className="categoryListings">
|
||||
{listings.map((listing) => (
|
||||
<ListingItem
|
||||
listing={listing.data}
|
||||
id={listing.id}
|
||||
key={listing.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
<br />
|
||||
<br />
|
||||
{lastFetchedlisting && (
|
||||
<p className="loadMore" onClick={onFetchMoreListings}>
|
||||
Load More
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>No listings for {params.categoryName}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Category;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { doc, getDoc } from "firebase/firestore";
|
||||
import { db } from "../firebase.config";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
function Contact() {
|
||||
const [message, setMassage] = useState("");
|
||||
const [landlord, setLandlord] = useState(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const getLandlord = async () => {
|
||||
const docRef = doc(db, "users", params.landlordId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
|
||||
if (docSnap.exists()) {
|
||||
setLandlord(docSnap.data());
|
||||
} else {
|
||||
toast.error("Landlord does not exist");
|
||||
}
|
||||
};
|
||||
getLandlord();
|
||||
}, [params.landlordId]);
|
||||
const onChange = (e) => setMassage(e.target.value);
|
||||
|
||||
return (
|
||||
<div className="pageContainer">
|
||||
<header>
|
||||
<p className="pageHeader">Contact Landlord</p>
|
||||
</header>
|
||||
{landlord !== null && (
|
||||
<main>
|
||||
<div className="contactLandlord">
|
||||
<p className="landlordName">Contact {landlord?.name}</p>
|
||||
</div>
|
||||
<form className="messageForm">
|
||||
<div className="messageDiv">
|
||||
<label htmlFor="message" className="messageLable">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
onChange={onChange}
|
||||
name="message"
|
||||
id="message"
|
||||
className="textarea"
|
||||
value={message}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href={`mailto:${landlord.email}?Subject=${searchParams.get(
|
||||
"listingName"
|
||||
)}&body=${message}`}
|
||||
>
|
||||
<button className="primaryButton" type="button">
|
||||
Send Message
|
||||
</button>
|
||||
</a>
|
||||
</form>
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Contact;
|
||||
@@ -0,0 +1,418 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { getAuth, onAuthStateChanged } from "firebase/auth";
|
||||
import {
|
||||
getStorage,
|
||||
ref,
|
||||
uploadBytesResumable,
|
||||
getDownloadURL,
|
||||
} from "firebase/storage";
|
||||
import { db } from "../firebase.config";
|
||||
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Spinner from "../components/Spinner";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
function CreateLising() {
|
||||
const [geolocationEnabled, _] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
type: "rent",
|
||||
name: "",
|
||||
bedrooms: 1,
|
||||
bathrooms: 1,
|
||||
parking: false,
|
||||
furnished: false,
|
||||
address: "",
|
||||
offer: false,
|
||||
regularPrice: 0,
|
||||
discountedPrice: 0,
|
||||
images: {},
|
||||
latittude: 0,
|
||||
longitude: 0,
|
||||
});
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
bedrooms,
|
||||
bathrooms,
|
||||
parking,
|
||||
furnished,
|
||||
address,
|
||||
offer,
|
||||
regularPrice,
|
||||
discountedPrice,
|
||||
images,
|
||||
latittude,
|
||||
longitude,
|
||||
} = formData;
|
||||
|
||||
const auth = getAuth();
|
||||
const navigate = useNavigate();
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
setFormData({ ...formData, userRef: user.uid });
|
||||
} else {
|
||||
navigate("/sign-in");
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [isMounted]);
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(false);
|
||||
|
||||
if (discountedPrice >= regularPrice) {
|
||||
setLoading(false);
|
||||
toast.error("Discounted price needs to be less than regular price");
|
||||
return;
|
||||
}
|
||||
if (images.length > 6) {
|
||||
setLoading(false);
|
||||
toast.error("Max 6 images");
|
||||
return;
|
||||
}
|
||||
let geolocation = {};
|
||||
let location;
|
||||
|
||||
if (geolocationEnabled) {
|
||||
//Don't want to register for a geolocation
|
||||
/* const response = await fetch(
|
||||
`http://api.positionstack.com/v1/forward?access_key=${APIKEY}&query=${address}`
|
||||
);
|
||||
const data = await response.json();
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
latitude: data.data[0].latitude,
|
||||
longitude: data.data[0].longitude,
|
||||
}));
|
||||
*/
|
||||
} else {
|
||||
geolocation.lat = latittude;
|
||||
geolocation.lng = longitude;
|
||||
location = address;
|
||||
}
|
||||
const storeImage = async (image) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storage = getStorage();
|
||||
const fileName = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`;
|
||||
const storageRef = ref(storage, "images/" + fileName);
|
||||
const uploadTask = uploadBytesResumable(storageRef, image);
|
||||
uploadTask.on(
|
||||
"state_changed",
|
||||
(snapshot) => {
|
||||
const progress =
|
||||
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
console.log("Upload is " + progress + "% done");
|
||||
switch (snapshot.state) {
|
||||
case "paused":
|
||||
console.log("Upload is paused");
|
||||
break;
|
||||
case "running":
|
||||
console.log("Upload is running");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
},
|
||||
() => {
|
||||
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||
resolve(downloadURL);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
const imgUrls = await Promise.all(
|
||||
[...images].map((image) => storeImage(image))
|
||||
).catch(() => {
|
||||
setLoading(false);
|
||||
toast.error("Image is not uploaded");
|
||||
return;
|
||||
});
|
||||
|
||||
const formDataCopy = {
|
||||
...formData,
|
||||
imgUrls,
|
||||
geolocation,
|
||||
timestamp: serverTimestamp(),
|
||||
};
|
||||
formDataCopy.location = address;
|
||||
delete formDataCopy.images;
|
||||
delete formDataCopy.address;
|
||||
location && (formDataCopy.location = location);
|
||||
!formDataCopy.offer && delete formDataCopy.discountedPrice;
|
||||
|
||||
const docRef = await addDoc(collection(db, "listings"), formDataCopy);
|
||||
setLoading(false);
|
||||
toast.success("Listing saved");
|
||||
navigate(`/category/${formDataCopy.type}/${docRef.id}`);
|
||||
};
|
||||
|
||||
const onMutate = (e) => {
|
||||
let boolean = null;
|
||||
if (e.target.value === "false") {
|
||||
boolean = false;
|
||||
}
|
||||
if (e.target.value === "true") {
|
||||
boolean = true;
|
||||
}
|
||||
|
||||
if (e.target.files) {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
images: e.target.files,
|
||||
}));
|
||||
}
|
||||
if (!e.target.files) {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id]: boolean ?? e.target.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="profile">
|
||||
<header>
|
||||
<p className="pageHeader">Create a listing</p>
|
||||
</header>
|
||||
<main>
|
||||
<form onSubmit={onSubmit}>
|
||||
<label className="formLabel">Sell / Rent</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
type="button"
|
||||
className={type === "sale" ? "formButtonActive" : "formButton"}
|
||||
id="type"
|
||||
value="sale"
|
||||
onClick={onMutate}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={type === "rent" ? "formButtonActive" : "formButton"}
|
||||
id="type"
|
||||
value="rent"
|
||||
onClick={onMutate}
|
||||
>
|
||||
Rent
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel">Name</label>
|
||||
<input
|
||||
className="formInputName"
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={onMutate}
|
||||
maxLength="32"
|
||||
minLength="10"
|
||||
required
|
||||
/>
|
||||
<div className="formRooms fles">
|
||||
<div>
|
||||
<label className="formLabel">Bedrooms</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="bedrooms"
|
||||
value={bedrooms}
|
||||
onChange={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="formLabel">Bathrooms</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="bathrooms"
|
||||
value={bathrooms}
|
||||
onChange={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="formLabel">Parking spot</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={parking ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="parking"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!parking && parking !== null ? "formButtonActive" : "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="parking"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="formLabel">Furnished</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={furnished ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="furnished"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!furnished && furnished !== null
|
||||
? "formButtonActive"
|
||||
: "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="furnished"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel">Address</label>
|
||||
<textarea
|
||||
className="formInputAddress"
|
||||
type="text"
|
||||
id="address"
|
||||
value={address}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
{!geolocationEnabled && (
|
||||
<div className="formLatLng fles">
|
||||
<div>
|
||||
<label className="formLabel">Latitude</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="latittude"
|
||||
value={latittude}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="formLabel">Longitude</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="longitude"
|
||||
value={longitude}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<label className="formLabel">Offer</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={offer ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="offer"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!offer && offer !== null ? "formButtonActive" : "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="offer"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel"> Regular Price</label>
|
||||
<div className="formPriceDiv">
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="regularPrice"
|
||||
value={regularPrice}
|
||||
onChange={onMutate}
|
||||
min="50"
|
||||
max="750000000"
|
||||
required
|
||||
/>
|
||||
{formData.type === "rent" && (
|
||||
<p className="formPriceText">$ / Month</p>
|
||||
)}
|
||||
</div>
|
||||
{offer && (
|
||||
<>
|
||||
<label className="formLabel">Discounted Price</label>
|
||||
<input
|
||||
className="formInputName"
|
||||
type="number"
|
||||
id="discountedPrice"
|
||||
value={discountedPrice}
|
||||
onChange={onMutate}
|
||||
min="50"
|
||||
max="750000000"
|
||||
required={offer}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label className="formLabel">Images</label>
|
||||
<p className="imagesInfo">
|
||||
The first image will be the cover (max 6).
|
||||
</p>
|
||||
<input
|
||||
className="formInputFile"
|
||||
type="file"
|
||||
id="images"
|
||||
onChange={onMutate}
|
||||
max="6"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
multiple
|
||||
required
|
||||
/>
|
||||
<button className="primaryButton createListingButton">
|
||||
Create Listing
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default CreateLising;
|
||||
@@ -0,0 +1,447 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { getAuth, onAuthStateChanged } from "firebase/auth";
|
||||
import {
|
||||
getStorage,
|
||||
ref,
|
||||
uploadBytesResumable,
|
||||
getDownloadURL,
|
||||
} from "firebase/storage";
|
||||
import { db } from "../firebase.config";
|
||||
import { doc, updateDoc, getDoc, serverTimestamp } from "firebase/firestore";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import Spinner from "../components/Spinner";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
function EditListing() {
|
||||
const [geolocationEnabled, setGeolocationEnabled] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [listing, setListing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
type: "rent",
|
||||
name: "",
|
||||
bedrooms: 1,
|
||||
bathrooms: 1,
|
||||
parking: false,
|
||||
furnished: false,
|
||||
address: "",
|
||||
offer: false,
|
||||
regularPrice: 0,
|
||||
discountedPrice: 0,
|
||||
images: {},
|
||||
latittude: 0,
|
||||
longitude: 0,
|
||||
});
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
bedrooms,
|
||||
bathrooms,
|
||||
parking,
|
||||
furnished,
|
||||
address,
|
||||
offer,
|
||||
regularPrice,
|
||||
discountedPrice,
|
||||
images,
|
||||
latittude,
|
||||
longitude,
|
||||
} = formData;
|
||||
|
||||
const auth = getAuth();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (listing && listing.userRef !== auth.currentUser.uid) {
|
||||
toast.error("You cannot edit that listing");
|
||||
navigate("/");
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
setFormData({ ...formData, userRef: user.uid });
|
||||
} else {
|
||||
navigate("/sign-in");
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [isMounted]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const fetchListing = async () => {
|
||||
const docRef = doc(db, "listings", params.listingId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
setListing(docSnap.data());
|
||||
setFormData({ ...docSnap.data(), address: docSnap.data().location });
|
||||
setListing(false);
|
||||
} else {
|
||||
navigate("/");
|
||||
toast.error("Listing does not exist");
|
||||
}
|
||||
};
|
||||
fetchListing();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(false);
|
||||
|
||||
if (discountedPrice >= regularPrice) {
|
||||
setLoading(false);
|
||||
toast.error("Discounted price needs to be less than regular price");
|
||||
return;
|
||||
}
|
||||
if (images.length > 6) {
|
||||
setLoading(false);
|
||||
toast.error("Max 6 images");
|
||||
return;
|
||||
}
|
||||
let geolocation = {};
|
||||
let location;
|
||||
|
||||
if (geolocationEnabled) {
|
||||
//Don't want to register for a geolocation
|
||||
/* const response = await fetch(
|
||||
`http://api.positionstack.com/v1/forward?access_key=${APIKEY}&query=${address}`
|
||||
);
|
||||
const data = await response.json();
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
latitude: data.data[0].latitude,
|
||||
longitude: data.data[0].longitude,
|
||||
}));
|
||||
*/
|
||||
} else {
|
||||
geolocation.lat = latittude;
|
||||
geolocation.lng = longitude;
|
||||
location = address;
|
||||
}
|
||||
const storeImage = async (image) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storage = getStorage();
|
||||
const fileName = `${auth.currentUser.uid}-${image.name}-${uuidv4()}`;
|
||||
const storageRef = ref(storage, "images/" + fileName);
|
||||
const uploadTask = uploadBytesResumable(storageRef, image);
|
||||
uploadTask.on(
|
||||
"state_changed",
|
||||
(snapshot) => {
|
||||
const progress =
|
||||
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
console.log("Upload is " + progress + "% done");
|
||||
switch (snapshot.state) {
|
||||
case "paused":
|
||||
console.log("Upload is paused");
|
||||
break;
|
||||
case "running":
|
||||
console.log("Upload is running");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
},
|
||||
() => {
|
||||
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||
resolve(downloadURL);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
const imgUrls = await Promise.all(
|
||||
[...images].map((image) => storeImage(image))
|
||||
).catch(() => {
|
||||
setLoading(false);
|
||||
toast.error("Image is not uploaded");
|
||||
return;
|
||||
});
|
||||
|
||||
const formDataCopy = {
|
||||
...formData,
|
||||
imgUrls,
|
||||
geolocation,
|
||||
timestamp: serverTimestamp(),
|
||||
};
|
||||
formDataCopy.location = address;
|
||||
delete formDataCopy.images;
|
||||
delete formDataCopy.address;
|
||||
location && (formDataCopy.location = location);
|
||||
!formDataCopy.offer && delete formDataCopy.discountedPrice;
|
||||
|
||||
const docRef = doc(db, "listings", params.listingId);
|
||||
await updateDoc(docRef, formDataCopy);
|
||||
|
||||
setLoading(false);
|
||||
toast.success("Listing saved");
|
||||
navigate(`/category/${formDataCopy.type}/${docRef.id}`);
|
||||
};
|
||||
|
||||
const onMutate = (e) => {
|
||||
let boolean = null;
|
||||
if (e.target.value === "false") {
|
||||
boolean = false;
|
||||
}
|
||||
if (e.target.value === "true") {
|
||||
boolean = true;
|
||||
}
|
||||
|
||||
if (e.target.files) {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
images: e.target.files,
|
||||
}));
|
||||
}
|
||||
if (!e.target.files) {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id]: boolean ?? e.target.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
<header>
|
||||
<p className="pageHeader">Edit a listing</p>
|
||||
</header>
|
||||
<main>
|
||||
<form onSubmit={onSubmit}>
|
||||
<label className="formLabel">Sell / Rent</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
type="button"
|
||||
className={type === "sale" ? "formButtonActive" : "formButton"}
|
||||
id="type"
|
||||
value="sale"
|
||||
onClick={onMutate}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={type === "rent" ? "formButtonActive" : "formButton"}
|
||||
id="type"
|
||||
value="rent"
|
||||
onClick={onMutate}
|
||||
>
|
||||
Rent
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel">Name</label>
|
||||
<input
|
||||
className="formInputName"
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={onMutate}
|
||||
maxLength="32"
|
||||
minLength="10"
|
||||
required
|
||||
/>
|
||||
<div className="formRooms fles">
|
||||
<div>
|
||||
<label className="formLabel">Bedrooms</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="bedrooms"
|
||||
value={bedrooms}
|
||||
onChange={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="formLabel">Bathrooms</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="bathrooms"
|
||||
value={bathrooms}
|
||||
onChange={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="formLabel">Parking spot</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={parking ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="parking"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
min="1"
|
||||
max="50"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!parking && parking !== null ? "formButtonActive" : "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="parking"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="formLabel">Furnished</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={furnished ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="furnished"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!furnished && furnished !== null
|
||||
? "formButtonActive"
|
||||
: "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="furnished"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel">Address</label>
|
||||
<textarea
|
||||
className="formInputAddress"
|
||||
type="text"
|
||||
id="address"
|
||||
value={address}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
{!geolocationEnabled && (
|
||||
<div className="formLatLng fles">
|
||||
<div>
|
||||
<label className="formLabel">Latitude</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="latittude"
|
||||
value={latittude}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="formLabel">Longitude</label>
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="longitude"
|
||||
value={longitude}
|
||||
onChange={onMutate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<label className="formLabel">Offer</label>
|
||||
<div className="formButtons">
|
||||
<button
|
||||
className={offer ? "formButtonActive" : "formButton"}
|
||||
type="button"
|
||||
id="offer"
|
||||
value={true}
|
||||
onClick={onMutate}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
!offer && offer !== null ? "formButtonActive" : "formButton"
|
||||
}
|
||||
type="button"
|
||||
id="offer"
|
||||
value={false}
|
||||
onClick={onMutate}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
<label className="formLabel"> Regular Price</label>
|
||||
<div className="formPriceDiv">
|
||||
<input
|
||||
className="formInputSmall"
|
||||
type="number"
|
||||
id="regularPrice"
|
||||
value={regularPrice}
|
||||
onChange={onMutate}
|
||||
min="50"
|
||||
max="750000000"
|
||||
required
|
||||
/>
|
||||
{formData.type === "rent" && (
|
||||
<p className="formPriceText">$ / Month</p>
|
||||
)}
|
||||
</div>
|
||||
{offer && (
|
||||
<>
|
||||
<label className="formLabel">Discounted Price</label>
|
||||
<input
|
||||
className="formInputName"
|
||||
type="number"
|
||||
id="discountedPrice"
|
||||
value={discountedPrice}
|
||||
onChange={onMutate}
|
||||
min="50"
|
||||
max="750000000"
|
||||
required={offer}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label className="formLabel">Images</label>
|
||||
<p className="imagesInfo">
|
||||
The first image will be the cover (max 6).
|
||||
</p>
|
||||
<input
|
||||
className="formInputFile"
|
||||
type="file"
|
||||
id="images"
|
||||
onChange={onMutate}
|
||||
max="6"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
multiple
|
||||
required
|
||||
/>
|
||||
<button className="primaryButton createListingButton">
|
||||
Edit Listing
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default EditListing;
|
||||
@@ -0,0 +1,30 @@
|
||||
import Slider from '../components/Slider';
|
||||
import {Link} from 'react-router-dom'
|
||||
import rentCategoryImage from '../assets/jpg/rentCategoryImage.jpg'
|
||||
import sellCategoryImage from '../assets/jpg/sellCategoryImage.jpg'
|
||||
function Explore (){
|
||||
return (
|
||||
<div className='export'>
|
||||
<header>
|
||||
<p className='pageHeader'>Explore</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<Slider/>
|
||||
<p className='exploreCategoryHeading'>Categories</p>
|
||||
<div className='exploreCategories'>
|
||||
<Link to='/category/rent'>
|
||||
<img src={rentCategoryImage} alt='rent' className='exploreCategoryImg'/>
|
||||
</Link>
|
||||
<p className='exploreCategoryName'>Places for rent</p>
|
||||
<Link to='/category/sale'>
|
||||
<img src={sellCategoryImage} alt='sell' className='exploreCategoryImg'/>
|
||||
</Link>
|
||||
<p className='exploreCategoryName'>Places for sale</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Explore
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getAuth,sendPasswordResetEmail } from "firebase/auth";
|
||||
import { toast } from "react-toastify";
|
||||
import {ReactComponent as ArrowRightIcon} from '../assets/svg/keyboardArrowRightIcon.svg'
|
||||
|
||||
function ForgotPassword (){
|
||||
const [email, setEmail] = useState('')
|
||||
const onChange = e =>{setEmail(e.target.value)}
|
||||
const onSubmit =async (e) =>{
|
||||
|
||||
e.preventDefault()
|
||||
try{
|
||||
const auth=getAuth()
|
||||
await sendPasswordResetEmail(auth, email)
|
||||
toast.success('Email was sent')
|
||||
}
|
||||
catch(error){
|
||||
toast.error("Could not send reset email")
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="pageContainer">
|
||||
<header>
|
||||
<p className="pageHeader">Forgot Password</p>
|
||||
|
||||
</header>
|
||||
<main>
|
||||
<form onSubmit={onSubmit}
|
||||
>
|
||||
<input type='email' className='emailInput'placeholder="Email" id='email' value={email} onChange={onChange} />
|
||||
<Link className="forgotPasswordLink" to='/sign-in'>Sign In</Link>
|
||||
<div className="signInBar">
|
||||
<div className="signInText">Send Reset Link</div>
|
||||
<button className="signInButton"><ArrowRightIcon fill='#ffffff' width='34px' height='34px'/></button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ForgotPassword
|
||||
@@ -0,0 +1,138 @@
|
||||
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from "swiper";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { getDoc, doc } from "firebase/firestore";
|
||||
import { getAuth } from "firebase/auth";
|
||||
import { db } from "../firebase.config";
|
||||
import Spinner from "../components/Spinner";
|
||||
import shareIcon from "../assets/svg/shareIcon.svg";
|
||||
|
||||
SwiperCore.use([Navigation, Pagination,Scrollbar,A11y])
|
||||
|
||||
function Listing() {
|
||||
const [listing, setListing] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [shareLinkCopied, setShareLinkCopied] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const auth = getAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
const docRef = doc(db, "listings", params.listingId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
|
||||
if (docSnap.exists()) {
|
||||
setListing(docSnap.data());
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchListings();
|
||||
}, [navigate, params.listingId]);
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
return (
|
||||
<main>
|
||||
<Swiper slidesPerView={1} pagination={{ clickable: true }}>
|
||||
{listing.imgUrls.map((url, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div
|
||||
className="swiperSlideDiv"
|
||||
style={{
|
||||
background: `url(${listing.imgUrls[index]}) center no-repeat`,
|
||||
backgroundSize: "cover",
|
||||
minHeight:'20rem'
|
||||
}}
|
||||
></div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
<div
|
||||
className="shareIconDiv"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
setShareLinkCopied(true);
|
||||
setTimeout(() => {
|
||||
setShareLinkCopied(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<img src={shareIcon} alt="" />
|
||||
</div>
|
||||
|
||||
{shareLinkCopied && <p className="linkCopied">Link Copied!</p>}
|
||||
|
||||
<div className="listingDetails">
|
||||
<p className="listingName">
|
||||
{listing.name} -{" "}
|
||||
{listing.offer
|
||||
? listing.discountedPrice
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
: listing.regularPrice
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
</p>
|
||||
<p className="listingLocation">{listing.location}</p>
|
||||
<p className="listingType">
|
||||
For {listing.type === "rent" ? "Rent" : "Sale"}
|
||||
</p>
|
||||
{listing.offer && (
|
||||
<p className="discountPrice">
|
||||
${listing.regularPrice - listing.discountedPrice} discount
|
||||
</p>
|
||||
)}
|
||||
<ul className="listingDetailsList">
|
||||
<li>
|
||||
{listing.bedrooms > 1
|
||||
? `${listing.bedrooms} Bedrooms`
|
||||
: "1 Bedroom"}
|
||||
</li>
|
||||
<li>
|
||||
{listing.bathrooms > 1
|
||||
? `${listing.bathrooms} Bathrooms`
|
||||
: "1 Bathroom"}
|
||||
</li>
|
||||
<li>{listing.parking && "Parking Spot"}</li>
|
||||
<li>{listing.furnished && "Furnished"}</li>
|
||||
</ul>
|
||||
<p className="listingLocationTitle">Location</p>
|
||||
|
||||
<div className="leafletContainer">
|
||||
<MapContainer
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
center={[listing.geolocation.lat, listing.geolocation.lng]}
|
||||
zoom={13}
|
||||
scrollWheelZoom={false}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png"
|
||||
/>
|
||||
|
||||
<Marker
|
||||
position={[listing.geolocation.lat, listing.geolocation.lng]}
|
||||
>
|
||||
<Popup>{listing.location}</Popup>
|
||||
</Marker>
|
||||
</MapContainer>
|
||||
</div>
|
||||
|
||||
{auth.currentUser?.uid !== listing.userRef && (
|
||||
<Link
|
||||
to={`/contact/${listing.userRef}?listingName=${listing.name}`}
|
||||
className="primaryButton"
|
||||
>
|
||||
Contact Landlord
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
export default Listing;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
collection,
|
||||
getDocs,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
limit,
|
||||
startAfter,
|
||||
} from "firebase/firestore";
|
||||
import { db } from "../firebase.config";
|
||||
import { toast } from "react-toastify";
|
||||
import Spinner from "../components/Spinner";
|
||||
import ListingItem from "../components/ListingItem";
|
||||
|
||||
function Offers() {
|
||||
const [listings, setListings] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastFetchedlisting, setLastFetchedlisting] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
try {
|
||||
const listingsRef = collection(db, "listings");
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("offer", "==", true),
|
||||
orderBy("timestamp", "desc"),
|
||||
limit(2)
|
||||
);
|
||||
|
||||
const querySnap = await getDocs(q);
|
||||
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
|
||||
setLastFetchedlisting(lastVisible);
|
||||
|
||||
let listings = [];
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({
|
||||
id: doc.id,
|
||||
data: doc.data(),
|
||||
});
|
||||
});
|
||||
|
||||
setListings(listings);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("could not fetch listings");
|
||||
}
|
||||
};
|
||||
|
||||
fetchListings();
|
||||
}, []);
|
||||
|
||||
const onFetchMoreListings = async () => {
|
||||
try {
|
||||
const listingsRef = collection(db, "listings");
|
||||
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("offer", "==", true),
|
||||
orderBy("timestamp", "desc"),
|
||||
startAfter(lastFetchedlisting),
|
||||
limit(10)
|
||||
);
|
||||
|
||||
const querySnap = await getDocs(q);
|
||||
const lastVisible = querySnap.docs[querySnap.docs.length - 1];
|
||||
setLastFetchedlisting(lastVisible);
|
||||
|
||||
let listings = [];
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({
|
||||
id: doc.id,
|
||||
data: doc.data(),
|
||||
});
|
||||
});
|
||||
|
||||
setListings((prevState) => [...prevState, ...listings]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("could not fetch listings");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="category">
|
||||
<header>
|
||||
<p className="pageHeader">Offers</p>
|
||||
</header>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : listings && listings.length > 0 ? (
|
||||
<>
|
||||
<main>
|
||||
<ul className="categoryListings">
|
||||
{listings.map((listing) => (
|
||||
<ListingItem
|
||||
listing={listing.data}
|
||||
id={listing.id}
|
||||
key={listing.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
<br />
|
||||
<br />
|
||||
{lastFetchedlisting && (
|
||||
<p className="loadMore" onClick={onFetchMoreListings}>
|
||||
Load More
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>There are no current Offers</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Offers;
|
||||
@@ -0,0 +1,159 @@
|
||||
import ListingItem from "../components/ListingItem";
|
||||
import arrowRight from "../assets/svg/keyboardArrowRightIcon.svg";
|
||||
import homeIcon from "../assets/svg/homeIcon.svg";
|
||||
import { toast } from "react-toastify";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAuth, updateProfile } from "firebase/auth";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { db } from "../firebase.config";
|
||||
import {
|
||||
updateDoc,
|
||||
doc,
|
||||
collection,
|
||||
getDocs,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
deleteDoc,
|
||||
} from "firebase/firestore";
|
||||
|
||||
function Profile() {
|
||||
const [changeDetails, setChangeDetails] = useState(false);
|
||||
const auth = getAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [listings, setListings] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: auth.currentUser.displayName,
|
||||
email: auth.currentUser.email,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { name, email } = formData;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserListingts = async () => {
|
||||
const listingsRef = collection(db, "listings");
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("userRef", "==", auth.currentUser.uid),
|
||||
orderBy("timestamp", "desc")
|
||||
);
|
||||
const querySnap = await getDocs(q);
|
||||
const listings = [];
|
||||
querySnap.forEach((doc) => {
|
||||
return listings.push({ id: doc.id, data: doc.data() });
|
||||
});
|
||||
setListings(listings);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchUserListingts();
|
||||
}, [auth.currentUser.uid]);
|
||||
|
||||
const onLogout = () => {
|
||||
auth.signOut();
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (auth.currentUser.displayName !== name) {
|
||||
await updateProfile(auth.currentUser, {
|
||||
displayName: name,
|
||||
});
|
||||
const userRef = doc(db, "users", auth.currentUser.uid);
|
||||
await updateDoc(userRef, {
|
||||
name,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("could no update profile details");
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onDelete = async (listingId) => {
|
||||
if (window.confirm("Are you sure you want to delte?")) {
|
||||
await deleteDoc(doc(db, "listings", listingId));
|
||||
const updatedListings = listings.filter(
|
||||
(listing) => listing.id !== listingId
|
||||
);
|
||||
setListings(updatedListings);
|
||||
toast.success("Successfully deleted listing ");
|
||||
}
|
||||
};
|
||||
|
||||
const onEdit = (listingId)=> navigate(`/edit-listing/${listingId}`)
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
<header className="profileHeader">
|
||||
<p className="pageHeader">My Profile</p>
|
||||
<button type="button" className="logOut" onClick={onLogout}>
|
||||
Logout
|
||||
</button>
|
||||
</header>
|
||||
<main>
|
||||
<div className="profileDetailsHeader">
|
||||
<p className="profileDetailsText">Personal Details</p>
|
||||
<p
|
||||
className="changePersonalDetails"
|
||||
onClick={() => {
|
||||
changeDetails && onSubmit();
|
||||
setChangeDetails((prevState) => !prevState);
|
||||
}}
|
||||
>
|
||||
{changeDetails ? "done" : "change"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="profileCard">
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={!changeDetails ? "profileEmail" : "profileEmailActive"}
|
||||
disabled={!changeDetails}
|
||||
value={email}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
className={!changeDetails ? "profileName" : "profileNameActive"}
|
||||
disabled={!changeDetails}
|
||||
value={name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<Link to="/create-listing" className="createListing">
|
||||
<img src={homeIcon} alt="home" />
|
||||
<p>Sell or rent your home</p>
|
||||
<img src={arrowRight} alt="arrowRight" />
|
||||
</Link>
|
||||
{!loading && listings?.length > 0 && (
|
||||
<>
|
||||
<p className="listingText">Your Listings</p>
|
||||
<ul className="listingsList">
|
||||
{listings.map((listing) => (
|
||||
<ListingItem
|
||||
key={listing.id}
|
||||
listing={listing.data}
|
||||
id={listing.id}
|
||||
onDelete={() => onDelete(listing.id)}
|
||||
onEdit={() => onEdit(listing.id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Profile;
|
||||
@@ -0,0 +1,85 @@
|
||||
import OAuth from "../components/OAuth";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
|
||||
import { ReactComponent as ArrowRightIcon } from "../assets/svg/keyboardArrowRightIcon.svg";
|
||||
import visibilityIcon from "../assets/svg/visibilityIcon.svg";
|
||||
|
||||
function Signin() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [formData, setFormData] = useState({ email: "", password: "" });
|
||||
const { email, password } = formData;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id]: e.target.value,
|
||||
}));
|
||||
};
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const auth = getAuth();
|
||||
try {
|
||||
const userCredential = await signInWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
if (userCredential.user) {
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Bad user credentials")
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="pageContainer">
|
||||
<header>
|
||||
<p className="pageHeader">Welcome Back!</p>
|
||||
</header>
|
||||
<form onSubmit={onSubmit}>
|
||||
<input
|
||||
className="emailInput"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="passwordInputDiv">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className="passwordInput"
|
||||
placeholder="Password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<img
|
||||
className="showPassword"
|
||||
src={visibilityIcon}
|
||||
onClick={() => setShowPassword((prevState) => !prevState)}
|
||||
/>
|
||||
</div>
|
||||
<Link to="/forgot-password" className="forgotPasswordLink">
|
||||
Forgot Password
|
||||
</Link>
|
||||
<div className="signInBar">
|
||||
<p className="sigInText">Sign In</p>
|
||||
<button className="signInButton">
|
||||
<ArrowRightIcon fill="#ffffff" width="34px" height="34px" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<OAuth />
|
||||
<Link to="/sign-up" className="registerLink">
|
||||
Sign Up Instead
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Signin;
|
||||
@@ -0,0 +1,113 @@
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { toast } from "react-toastify";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { ReactComponent as ArrowRightIcon } from "../assets/svg/keyboardArrowRightIcon.svg";
|
||||
import visibilityIcon from "../assets/svg/visibilityIcon.svg";
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
updateProfile,
|
||||
} from "firebase/auth";
|
||||
import { db } from "../firebase.config";
|
||||
import { setDoc, doc, serverTimestamp } from "firebase/firestore";
|
||||
import OAuth from "../components/OAuth";
|
||||
|
||||
function Signup() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const { name, email, password } = formData;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const auth = getAuth();
|
||||
const userCredential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
const user = userCredential.user;
|
||||
updateProfile(auth.currentUser, { displayName: name });
|
||||
|
||||
const fromDataCopy = { ...formData };
|
||||
delete fromDataCopy.password;
|
||||
fromDataCopy.timestamp = serverTimestamp();
|
||||
|
||||
await setDoc(doc(db, "users", user.uid), fromDataCopy);
|
||||
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("something went wrong with the registration");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="pageContainer">
|
||||
<header>
|
||||
<p className="pageHeader">Welcome Back!</p>
|
||||
</header>
|
||||
<form onSubmit={onSubmit}>
|
||||
<input
|
||||
className="nameInput"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<input
|
||||
className="emailInput"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="passwordInputDiv">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className="passwordInput"
|
||||
placeholder="Password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<img
|
||||
className="showPassword"
|
||||
src={visibilityIcon}
|
||||
onClick={() => setShowPassword((prevState) => !prevState)}
|
||||
/>
|
||||
</div>
|
||||
<Link to="/forgot-password" className="forgotPasswordLink">
|
||||
Forgot Password
|
||||
</Link>
|
||||
<div className="signUpBar">
|
||||
<p className="sigUpText">Sign In</p>
|
||||
<button className="signUpButton">
|
||||
<ArrowRightIcon fill="#ffffff" width="34px" height="34px" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<OAuth />
|
||||
<Link to="/sign-in" className="registerLink">
|
||||
Sign In Instead
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Signup;
|
||||
@@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||