added map to listings

This commit is contained in:
QkoSad
2022-12-02 22:21:43 +02:00
parent d79737a497
commit 09b6b0ad86
7 changed files with 270 additions and 44 deletions
+49
View File
@@ -12,8 +12,10 @@
"@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",
@@ -3718,6 +3720,16 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz",
@@ -12281,6 +12293,11 @@
"language-subtag-registry": "~0.3.2"
}
},
"node_modules/leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -14981,6 +14998,19 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-leaflet": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.0.tgz",
"integrity": "sha512-9d8T7hzYrQA5GLe3vn0qtRLJzQKgjr080NKa45yArGwuSl1nH/6aK9gp7DeYdktpdO1vKGSUTGW5AsUS064X0A==",
"dependencies": {
"@react-leaflet/core": "^2.1.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -20518,6 +20548,12 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"requires": {}
},
"@remix-run/router": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz",
@@ -26806,6 +26842,11 @@
"language-subtag-registry": "~0.3.2"
}
},
"leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -28585,6 +28626,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-leaflet": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.0.tgz",
"integrity": "sha512-9d8T7hzYrQA5GLe3vn0qtRLJzQKgjr080NKa45yArGwuSl1nH/6aK9gp7DeYdktpdO1vKGSUTGW5AsUS064X0A==",
"requires": {
"@react-leaflet/core": "^2.1.0"
}
},
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
+2
View File
@@ -7,8 +7,10 @@
"@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",
+4 -22
View File
@@ -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>
+7
View File
@@ -11,6 +11,8 @@ 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";
function App() {
return (
@@ -28,6 +30,11 @@ function App() {
<Route path="/sign-up" element={<Signup />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/create-listing" element={<CreateLising />} />
<Route
path="/category/:categoryName/:listingId"
element={<Listing />}
/>
<Route path="/contact/:landlordId" element={<Contact />}/>
</Routes>
<Navbar />
</Router>
+67
View File
@@ -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;
+120
View File
@@ -0,0 +1,120 @@
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";
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>
{/*Slider*/}
<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='&copy; <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;
+21 -22
View File
@@ -1,11 +1,11 @@
import arrowRight from '../assets/svg/keyboardArrowRightIcon.svg'
import homeIcon from '../assets/svg/homeIcon.svg'
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 } from "firebase/firestore";
import { updateDoc, doc } from "firebase/firestore";
function Profile() {
const [changeDetails, setChangeDetails] = useState(false);
@@ -27,13 +27,13 @@ function Profile() {
await updateProfile(auth.currentUser, {
displayName: name,
});
const userRef = doc(db,'users',auth.currentUser.uid)
await updateDoc(userRef,{
name
})
const userRef = doc(db, "users", auth.currentUser.uid);
await updateDoc(userRef, {
name,
});
}
} catch (error) {
toast.error('could no update profile details')
toast.error("could no update profile details");
}
};
const onChange = (e) => {
@@ -46,22 +46,21 @@ function Profile() {
<div className="profile">
<header className="profileHeader">
<p className="pageHeader">My Profile</p>
<button type="button" className="logOut" onclick={onLogout}>
<button type="button" className="logOut" onClick={onLogout}>
Logout
</button>
</header>
<main>
<div className="profileDetailsHeader">
<p className="profileDetailsText">
<p
className="changePersonalDetails"
onClick={() => {
changeDetails && onSubmit();
setChangeDetails((prevState) => !prevState);
}}
>
{changeDetails ? "done" : "change"}
</p>
<p className="profileDetailsText">Personal Details</p>
<p
className="changePersonalDetails"
onClick={() => {
changeDetails && onSubmit();
setChangeDetails((prevState) => !prevState);
}}
>
{changeDetails ? "done" : "change"}
</p>
</div>
<div className="profileCard">
@@ -84,10 +83,10 @@ function Profile() {
/>
</form>
</div>
<Link to='/create-listing' className='createListing'>
<img src={homeIcon} alt='home'/>
<Link to="/create-listing" className="createListing">
<img src={homeIcon} alt="home" />
<p>Sell or rent your home</p>
<img src={arrowRight} alt='arrowRight'/>
<img src={arrowRight} alt="arrowRight" />
</Link>
</main>
</div>