This commit is contained in:
QkoSad
2022-12-04 16:36:39 +02:00
parent 7841afe8de
commit 85f56e2f80
9 changed files with 551 additions and 9 deletions
+2
View File
@@ -13,6 +13,7 @@ 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 (
@@ -30,6 +31,7 @@ function App() {
<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 />}
+3 -1
View File
@@ -1,9 +1,10 @@
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 }) {
function ListingItem({ listing, id, onDelete, onEdit }) {
return (
<li className="categoryListing">
<Link
@@ -52,6 +53,7 @@ function ListingItem({ listing, id, onDelete }) {
onClick={() => onDelete(listing.id, listing.name)}
/>
)}
{onEdit && <EditIcon className="editIcon" onClick={() => onEdit(id)} />}
</li>
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ function Slider() {
fetchListings();
}, []);
if (loading) return <Spinner />;
console.log(listings[0].data.imgUrls);
if (listings.length === 0) return <></>
return (
listings && (
<>
+1 -1
View File
@@ -11,5 +11,5 @@ const firebaseConfig = {
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
initializeApp(firebaseConfig);
export const db = getFirestore()
+44 -1
View File
@@ -17,6 +17,7 @@ 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();
@@ -30,7 +31,11 @@ function Category() {
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({
@@ -42,13 +47,44 @@ function Category() {
setListings(listings);
setLoading(false);
} catch (error) {
console.log(error)
console.log(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">
@@ -74,6 +110,13 @@ function Category() {
))}
</ul>
</main>
<br />
<br />
{lastFetchedlisting && (
<p className="loadMore" onClick={onFetchMoreListings}>
Load More
</p>
)}
</>
) : (
<p>No listings for {params.categoryName}</p>
+4 -2
View File
@@ -14,7 +14,7 @@ import Spinner from "../components/Spinner";
import { toast } from "react-toastify";
function CreateLising() {
const [geolocationEnabled, setGeolocationEnabled] = useState(false);
const [geolocationEnabled, _] = useState(false);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
type: "rent",
@@ -119,6 +119,8 @@ function CreateLising() {
case "running":
console.log("Upload is running");
break;
default:
break;
}
},
(error) => {
@@ -146,7 +148,7 @@ function CreateLising() {
geolocation,
timestamp: serverTimestamp(),
};
formDataCopy.location=address
formDataCopy.location = address;
delete formDataCopy.images;
delete formDataCopy.address;
location && (formDataCopy.location = location);
+447
View File
@@ -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;
+45 -3
View File
@@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import {
collection,
getDocs,
@@ -17,8 +16,8 @@ import ListingItem from "../components/ListingItem";
function Offers() {
const [listings, setListings] = useState(null);
const [loading, setLoading] = useState(true);
const [lastFetchedlisting, setLastFetchedlisting] = useState(null);
const params = useParams();
useEffect(() => {
const fetchListings = async () => {
@@ -28,9 +27,13 @@ function Offers() {
listingsRef,
where("offer", "==", true),
orderBy("timestamp", "desc"),
limit(10)
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({
@@ -50,6 +53,38 @@ function Offers() {
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>
@@ -70,6 +105,13 @@ function Offers() {
))}
</ul>
</main>
<br />
<br />
{lastFetchedlisting && (
<p className="loadMore" onClick={onFetchMoreListings}>
Load More
</p>
)}
</>
) : (
<p>There are no current Offers</p>
+4
View File
@@ -87,6 +87,9 @@ function Profile() {
toast.success("Successfully deleted listing ");
}
};
const onEdit = (listingId)=> navigate(`/edit-listing/${listingId}`)
return (
<div className="profile">
<header className="profileHeader">
@@ -143,6 +146,7 @@ function Profile() {
listing={listing.data}
id={listing.id}
onDelete={() => onDelete(listing.id)}
onEdit={() => onEdit(listing.id)}
/>
))}
</ul>