completed search funtionality
This commit is contained in:
parent
917b01c521
commit
4b0a86b0dc
5 changed files with 215 additions and 5 deletions
|
@ -9,6 +9,7 @@ import PrivateRoute from './components/PrivateRoute'
|
||||||
import CreatListing from './pages/CreatListing'
|
import CreatListing from './pages/CreatListing'
|
||||||
import UpdateListing from './pages/UpdateListing'
|
import UpdateListing from './pages/UpdateListing'
|
||||||
import Listing from './pages/Listing'
|
import Listing from './pages/Listing'
|
||||||
|
import SearchProperties from './pages/SearchProperties'
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
@ -23,6 +24,7 @@ const App = () => {
|
||||||
<Route path='/updateListing/:listingId' element={<UpdateListing/>}/>
|
<Route path='/updateListing/:listingId' element={<UpdateListing/>}/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='/listing/:listingId' element={<Listing/>}/>
|
<Route path='/listing/:listingId' element={<Listing/>}/>
|
||||||
|
<Route path='/search' element={<SearchProperties/>}/>
|
||||||
<Route path='/login' element={<Login/>}/>
|
<Route path='/login' element={<Login/>}/>
|
||||||
<Route path='/signup' element={<SignUp/>}/>
|
<Route path='/signup' element={<SignUp/>}/>
|
||||||
<Route path='/contact' element={<Contact/>}/>
|
<Route path='/contact' element={<Contact/>}/>
|
||||||
|
|
26
real_estate/client/src/components/ListingCard.jsx
Normal file
26
real_estate/client/src/components/ListingCard.jsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {Link} from 'react-router-dom'
|
||||||
|
|
||||||
|
const ListingCard = ({listing}) => {
|
||||||
|
return (
|
||||||
|
<div className='bg-white shadow-md hover:shadow-lg transition-shadow overflow-hidden rounded-md w-full sm:w-[230px]'>
|
||||||
|
<Link to={`/listing/${listing._id}`}>
|
||||||
|
<img src={listing.imageUrls[0]} alt="" className='h-[300px] sm:h-[150px] w-full object-cover hover:scale-105 transition-scale duration-300'/>
|
||||||
|
<div className="p-2">
|
||||||
|
<p className='font-semibold text-gray-800 text-xl truncate mt-2 mb-1'>
|
||||||
|
${listing.offer ? listing.discountPrice.toLocaleString('en-Us') : listing.currentPrice.toLocaleString('en-Us')}
|
||||||
|
{listing.type === 'rent' && '/month'}
|
||||||
|
</p>
|
||||||
|
<div className="flex mb-1 gap-2">
|
||||||
|
<p className='text-sm'><span className='font-bold'>{listing.bed}</span> bds |</p>
|
||||||
|
<p className='text-sm'><span className='font-bold'>{listing.bath}</span> ba </p>
|
||||||
|
</div>
|
||||||
|
<div className='mb-4'>
|
||||||
|
<p className="text-sm truncate">{listing.address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListingCard
|
32
real_estate/client/src/components/Search.jsx
Normal file
32
real_estate/client/src/components/Search.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { IoIosSearch } from "react-icons/io";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set('searchTerm', searchTerm);
|
||||||
|
const searchQuery = urlParams.toString();
|
||||||
|
navigate(`/search?${searchQuery}`);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const searchTermFormUrl = urlParams.get('searchTerm');
|
||||||
|
if(searchTermFormUrl){
|
||||||
|
setSearchTerm(searchTermFormUrl);
|
||||||
|
}
|
||||||
|
}, [location.search])
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="text-black mt-5 bg-white rounded-lg flex items-center">
|
||||||
|
<input type="text" placeholder="Search your dream property" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="px-4 py-3 focus:outline-none bg-transparent text-sm w-60 sm:w-80"/>
|
||||||
|
<button>
|
||||||
|
<IoIosSearch className="text-slate-600 text-lg m-2"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search
|
|
@ -1,4 +1,4 @@
|
||||||
import { IoIosSearch } from "react-icons/io";
|
import Search from "../components/Search"
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -8,10 +8,7 @@ const Home = () => {
|
||||||
<div className="relative z-30 text-white text-center flex flex-col justify-center items-center">
|
<div className="relative z-30 text-white text-center flex flex-col justify-center items-center">
|
||||||
<h1 className="uppercase tracking-tight text-5xl font-serif mb-4 text-shadow-lg shadow-black">Chicago properties for sale</h1>
|
<h1 className="uppercase tracking-tight text-5xl font-serif mb-4 text-shadow-lg shadow-black">Chicago properties for sale</h1>
|
||||||
<p className="text-shadow-DEFAULT shadow-black">Lorem ipsum dolor sit, amet consectetur adipisicing elit. <br />Alias dolorem error atque aut, asperiores et tempore iste quaerat pariatur maxime?</p>
|
<p className="text-shadow-DEFAULT shadow-black">Lorem ipsum dolor sit, amet consectetur adipisicing elit. <br />Alias dolorem error atque aut, asperiores et tempore iste quaerat pariatur maxime?</p>
|
||||||
<form className="text-black mt-5 bg-white rounded-lg flex items-center">
|
<Search/>
|
||||||
<input type="text" placeholder="Search your dream property" className="px-4 py-3 focus:outline-none bg-transparent text-sm w-60 sm:w-80"/>
|
|
||||||
<IoIosSearch className="text-slate-600 text-lg m-2"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
153
real_estate/client/src/pages/SearchProperties.jsx
Normal file
153
real_estate/client/src/pages/SearchProperties.jsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ListingCard from "../components/ListingCard";
|
||||||
|
|
||||||
|
|
||||||
|
const SearchProperties = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [listings, setListings] = useState([]);
|
||||||
|
const [sidebarSearch, setSidebarSearch] = useState({
|
||||||
|
searchTerm: '',
|
||||||
|
type: 'all',
|
||||||
|
parking: false,
|
||||||
|
offer: false,
|
||||||
|
furnished: false,
|
||||||
|
sort: 'created_at',
|
||||||
|
order: 'desc',
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const searchTermFromUrl = urlParams.get('searchTerm');
|
||||||
|
const typeFromUrl = urlParams.get('type');
|
||||||
|
const parkingFromUrl = urlParams.get('parking');
|
||||||
|
const furnishedFromUrl = urlParams.get('furnished');
|
||||||
|
const offerFromUrl = urlParams.get('offer');
|
||||||
|
const sortFromUrl = urlParams.get('sort');
|
||||||
|
const orderFromUrl = urlParams.get('order');
|
||||||
|
if(searchTermFromUrl || typeFromUrl || parkingFromUrl || furnishedFromUrl || offerFromUrl || sortFromUrl || orderFromUrl){
|
||||||
|
setSidebarSearch({
|
||||||
|
searchTerm: searchTermFromUrl || '',
|
||||||
|
type: typeFromUrl || 'all',
|
||||||
|
parking: parkingFromUrl === 'true' ? true : false,
|
||||||
|
furnished: furnishedFromUrl === 'true' ? true : false,
|
||||||
|
offer: offerFromUrl === 'true' ? true : false,
|
||||||
|
sort: sortFromUrl || 'created_at',
|
||||||
|
order: orderFromUrl || 'desc',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const fetchListings = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const searchQuery = urlParams.toString();
|
||||||
|
const res = await fetch(`/server/listing/get?${searchQuery}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setListings(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
fetchListings();
|
||||||
|
|
||||||
|
}, [location.search])
|
||||||
|
|
||||||
|
//Search by...
|
||||||
|
const handleChange = (e) => {
|
||||||
|
if(e.target.id === 'all' || e.target.id === 'rent' || e.target.id === 'sale'){
|
||||||
|
setSidebarSearch({...sidebarSearch, type: e.target.id});
|
||||||
|
}
|
||||||
|
if(e.target.id === 'searchTerm'){
|
||||||
|
setSidebarSearch({...sidebarSearch, searchTerm: e.target.value});
|
||||||
|
}
|
||||||
|
if(e.target.id === 'parking' || e.target.id === 'furnished' || e.target.id === 'offer'){
|
||||||
|
setSidebarSearch({...sidebarSearch, [e.target.id]: e.target.checked || e.target.checked === 'true' ? true : false,});
|
||||||
|
}
|
||||||
|
if(e.target.id === 'sort_order') {
|
||||||
|
const sort = e.target.value.split('_')[0] || 'created_at';
|
||||||
|
const order = e.target.value.split('_')[1] || 'desc';
|
||||||
|
setSidebarSearch({ ...sidebarSearch, sort, order });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Submit form
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.set('searchTerm', sidebarSearch.searchTerm);
|
||||||
|
urlParams.set('type', sidebarSearch.type);
|
||||||
|
urlParams.set('parking', sidebarSearch.parking);
|
||||||
|
urlParams.set('furnished', sidebarSearch.furnished);
|
||||||
|
urlParams.set('offer', sidebarSearch.offer);
|
||||||
|
urlParams.set('sort', sidebarSearch.sort);
|
||||||
|
urlParams.set('order', sidebarSearch.order);
|
||||||
|
const searchQuery = urlParams.toString();
|
||||||
|
navigate(`/search?${searchQuery}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row">
|
||||||
|
<div className="p-10 border-b-2 md:border-r-2 md:min-h-screen">
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="whitespace-nowrap font-semibold text-blue-900">Search: </label>
|
||||||
|
<input type="text" id="searchTerm" placeholder="Search your dream property" value={sidebarSearch.searchTerm} onChange={handleChange} className="border rounded-md p-2 w-full text-sm"/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
|
<label className="whitespace-nowrap font-semibold text-blue-900">Type: </label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.type === 'all'} id="all" className="w-4"/>
|
||||||
|
<span className="text-sm">Rent & Sale</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.type === 'rent'} id="rent" className="w-4"/>
|
||||||
|
<span className="text-sm">Rent</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.type === 'sale'} id="sale" className="w-4"/>
|
||||||
|
<span className="text-sm">Sale</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.offer} id="offer" className="w-4"/>
|
||||||
|
<span className="text-sm">Offer</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
|
<label className="whitespace-nowrap font-semibold text-blue-900">Amenities: </label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.parking} id="parking" className="w-4"/>
|
||||||
|
<span className="text-sm">Garage</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input type='checkbox' onChange={handleChange} checked={sidebarSearch.furnished} id="furnished" className="w-4"/>
|
||||||
|
<span className="text-sm">Furnished</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="whitespace-nowrap font-semibold text-blue-900">Sort: </label>
|
||||||
|
<select onChange={handleChange} defaultValue={'created_at_desc'} id="sort_order" className="border rounded-md p-1 text-sm">
|
||||||
|
<option value='currentPrice_desc'>Price hight to low</option>
|
||||||
|
<option value='currentPrice_asc'>Price low to hight</option>
|
||||||
|
<option value='createdAt_desc'>Latest</option>
|
||||||
|
<option value='createdAt_asc'>Oldest</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button className="bg-blue-950 text-white font-semibold p-1 rounded-md hover:opacity-95">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-2xl font-semibold border-b p-10 text-blue-900 capitalize">Listing results</h1>
|
||||||
|
<div className="flex flex-wrap p-10 gap-4">
|
||||||
|
{!loading && listings.length === 0 &&(
|
||||||
|
<p className="text-red-700 text-md font-semibold tracking-wider capitalize ">No listing found</p>
|
||||||
|
)}
|
||||||
|
{loading && (
|
||||||
|
<p className="text-blue-900 text-md font-semibold tracking-wider capitalize text-center w-full">loading...</p>
|
||||||
|
)}
|
||||||
|
{!loading && listings && listings.map((listing) => (<ListingCard key={listing._id} listing={listing}/>))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchProperties
|
Loading…
Add table
Reference in a new issue