add listings added
This commit is contained in:
@@ -9,6 +9,8 @@ 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";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -17,12 +19,15 @@ function App() {
|
||||
<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 />} />
|
||||
</Routes>
|
||||
<Navbar />
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { ReactComponent as DeleteIcon } from "../assets/svg/deleteIcon.svg";
|
||||
import bedIcon from "../assets/svg/bedIcon.svg";
|
||||
import bathtubIcon from "../assets/svg/bathtubIcon.svg";
|
||||
|
||||
function ListingItem({ listing, id, onDelete }) {
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
export default ListingItem;
|
||||
@@ -16,7 +16,7 @@ function Navbar() {
|
||||
<footer className="navbar">
|
||||
<nav className="navbarNan">
|
||||
<ul className="navbarListItems">
|
||||
<li className="navbarListItems" onClick={() => navigate("/explore")}>
|
||||
<li className="navbarListItems" onClick={() => navigate("/")}>
|
||||
<ExploreIcon
|
||||
fill={pathMatchRoute("/") ? "#2c2c2c" : "#8f8f8f"}
|
||||
width="36px"
|
||||
|
||||
@@ -8,7 +8,6 @@ const PrivateRoute = () => {
|
||||
if (checkingStatus) {
|
||||
return <Spinner/>
|
||||
}
|
||||
console.log(loggedIn)
|
||||
return loggedIn ? <Outlet /> : <Navigate to="/sign-in" />;
|
||||
};
|
||||
export default PrivateRoute;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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 { async } from "@firebase/util";
|
||||
import Listing from "../components/ListingItem";
|
||||
import ListingItem from "../components/ListingItem";
|
||||
|
||||
function Category() {
|
||||
const [listings, setListings] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
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);
|
||||
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();
|
||||
}, [params.categoryName]);
|
||||
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
<p>No listings for {params.categoryName}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Category;
|
||||
@@ -0,0 +1,317 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { getAuth, onAuthStateChanged } from "firebase/auth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Spinner from "../components/Spinner";
|
||||
|
||||
function CreateLising() {
|
||||
const [geolocationEnabled, setGeolocationEnabled] = useState(true);
|
||||
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 = (e) => {
|
||||
e.prevent.default();
|
||||
};
|
||||
|
||||
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,
|
||||
image: 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">Sell / Rent</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>
|
||||
<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">Discounted Price</label>
|
||||
<p className="imagesInfo">
|
||||
The first image will be the cover (max 6).
|
||||
</p>
|
||||
<input
|
||||
className="formInputFile"
|
||||
type="file"
|
||||
id="image"
|
||||
onChange={onMutate}
|
||||
max="6"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
multiple
|
||||
required
|
||||
/>
|
||||
<button className="primaryButton createListingButton">
|
||||
Create Listing
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default CreateLising;
|
||||
+22
-4
@@ -1,9 +1,27 @@
|
||||
|
||||
import {Link} from 'react-router-dom'
|
||||
import rentCategoryImage from '../assets/jpg/rentCategoryImage.jpg'
|
||||
import sellCategoryImage from '../assets/jpg/sellCategoryImage.jpg'
|
||||
function Explore (){
|
||||
return (
|
||||
<>
|
||||
<h1>My app</h1>
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+76
-3
@@ -1,9 +1,82 @@
|
||||
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 { async } from "@firebase/util";
|
||||
import Listing from "../components/ListingItem";
|
||||
import ListingItem from "../components/ListingItem";
|
||||
|
||||
function Offers() {
|
||||
const [listings, setListings] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
try {
|
||||
const listingsRef = collection(db, "listings");
|
||||
const q = query(
|
||||
listingsRef,
|
||||
where("offer", "==", true),
|
||||
orderBy("timestamp", "desc"),
|
||||
limit(10)
|
||||
);
|
||||
const querySnap = await getDocs(q);
|
||||
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();
|
||||
}, []);
|
||||
|
||||
function Offers (){
|
||||
return (
|
||||
<div className="category">
|
||||
<header>
|
||||
<p className="pageHeader">Offers</p>
|
||||
</header>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : listings && listings.length > 0 ? (
|
||||
<>
|
||||
<h1>My app</h1>
|
||||
<main>
|
||||
<ul className="categoryListings">
|
||||
{listings.map((listing) => (
|
||||
<ListingItem
|
||||
listing={listing.data}
|
||||
id={listing.id}
|
||||
key={listing.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
</>
|
||||
) : (
|
||||
<p>There are no current Offers</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Offers
|
||||
export default Offers;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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";
|
||||
@@ -82,6 +84,11 @@ function Profile() {
|
||||
/>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user