finished
This commit is contained in:
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ function Slider() {
|
||||
fetchListings();
|
||||
}, []);
|
||||
if (loading) return <Spinner />;
|
||||
console.log(listings[0].data.imgUrls);
|
||||
if (listings.length === 0) return <></>
|
||||
return (
|
||||
listings && (
|
||||
<>
|
||||
|
||||
@@ -11,5 +11,5 @@ const firebaseConfig = {
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
initializeApp(firebaseConfig);
|
||||
export const db = getFirestore()
|
||||
|
||||
+44
-1
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user