Dynamic Property Listing System with Advanced Filters
This comprehensive guide will walk you through building a property listing system with advanced filtering capabilities using React for the frontend and Node.js/Express for the backend.
System Architecture
Frontend (React) ← API Calls → Backend (Node.js/Express) ← Queries → Database (MongoDB)
Step 1: Backend Setup
1.1 Initialize Node.js Project
mkdir property-listing-system
cd property-listing-system
npm init -y
npm install express mongoose cors dotenv
1.2 Create Server Structure
// server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Routes
app.use('/api/properties', require('./routes/properties'));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
1.3 Property Model
// models/Property.js
const mongoose = require('mongoose');
const propertySchema = new mongoose.Schema({
title: { type: String, required: true },
description: { type: String },
price: { type: Number, required: true },
location: {
city: { type: String, required: true },
state: { type: String, required: true },
coordinates: {
lat: { type: Number },
lng: { type: Number }
}
},
propertyType: {
type: String,
enum: ['House', 'Apartment', 'Villa', 'Commercial'],
required: true
},
bedrooms: { type: Number },
bathrooms: { type: Number },
area: { type: Number }, // in sqft
yearBuilt: { type: Number },
amenities: [{ type: String }],
images: [{ type: String }],
listedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
status: {
type: String,
enum: ['For Sale', 'For Rent', 'Sold'],
default: 'For Sale'
},
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Property', propertySchema);
1.4 Property Routes with Advanced Filtering
// routes/properties.js
const express = require('express');
const router = express.Router();
const Property = require('../models/Property');
// Get all properties with filters
router.get('/', async (req, res) => {
try {
const {
minPrice,
maxPrice,
propertyType,
bedrooms,
location,
amenities,
minArea,
maxArea,
sortBy,
limit = 10,
page = 1
} = req.query;
const filter = {};
// Price range filter
if (minPrice || maxPrice) {
filter.price = {};
if (minPrice) filter.price.$gte = parseInt(minPrice);
if (maxPrice) filter.price.$lte = parseInt(maxPrice);
}
// Property type filter
if (propertyType) {
filter.propertyType = { $in: propertyType.split(',') };
}
// Bedrooms filter
if (bedrooms) {
filter.bedrooms = { $gte: parseInt(bedrooms) };
}
// Location filter
if (location) {
filter['location.city'] = new RegExp(location, 'i');
}
// Amenities filter
if (amenities) {
filter.amenities = { $all: amenities.split(',') };
}
// Area filter
if (minArea || maxArea) {
filter.area = {};
if (minArea) filter.area.$gte = parseInt(minArea);
if (maxArea) filter.area.$lte = parseInt(maxArea);
}
// Sorting
let sortOption = { createdAt: -1 };
if (sortBy === 'price-asc') sortOption = { price: 1 };
if (sortBy === 'price-desc') sortOption = { price: -1 };
if (sortBy === 'newest') sortOption = { createdAt: -1 };
if (sortBy === 'area-asc') sortOption = { area: 1 };
if (sortBy === 'area-desc') sortOption = { area: -1 };
const properties = await Property.find(filter)
.sort(sortOption)
.limit(parseInt(limit))
.skip((parseInt(page) - 1) * parseInt(limit));
const total = await Property.countDocuments(filter);
res.json({
success: true,
count: properties.length,
total,
page: parseInt(page),
pages: Math.ceil(total / parseInt(limit)),
data: properties
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
module.exports = router;
Step 2: Frontend Setup (React)
2.1 Create React App
npx create-react-app client
cd client
npm install axios react-router-dom @material-ui/core @material-ui/icons @material-ui/lab react-leaflet
2.2 Property Listing Component with Filters
// src/components/PropertyListing.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Checkbox,
FormGroup,
FormControlLabel,
Button,
Grid,
Paper,
Typography,
Slider,
Pagination
} from '@material-ui/core';
import { FilterList, Search } from '@material-ui/icons';
const PropertyListing = () => {
const [properties, setProperties] = useState([]);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [limit] = useState(10);
// Filter states
const [filters, setFilters] = useState({
minPrice: '',
maxPrice: '',
propertyType: [],
bedrooms: '',
location: '',
amenities: [],
minArea: '',
maxArea: '',
sortBy: 'newest'
});
const propertyTypes = ['House', 'Apartment', 'Villa', 'Commercial'];
const amenitiesList = ['Parking', 'Gym', 'Pool', 'Security', 'Garden', 'Balcony'];
useEffect(() => {
fetchProperties();
}, [filters, page]);
const fetchProperties = async () => {
try {
setLoading(true);
// Convert filters to query params
const params = new URLSearchParams();
if (filters.minPrice) params.append('minPrice', filters.minPrice);
if (filters.maxPrice) params.append('maxPrice', filters.maxPrice);
if (filters.propertyType.length) params.append('propertyType', filters.propertyType.join(','));
if (filters.bedrooms) params.append('bedrooms', filters.bedrooms);
if (filters.location) params.append('location', filters.location);
if (filters.amenities.length) params.append('amenities', filters.amenities.join(','));
if (filters.minArea) params.append('minArea', filters.minArea);
if (filters.maxArea) params.append('maxArea', filters.maxArea);
if (filters.sortBy) params.append('sortBy', filters.sortBy);
params.append('page', page);
params.append('limit', limit);
const response = await axios.get(`http://localhost:5000/api/properties?${params.toString()}`);
setProperties(response.data.data);
setTotal(response.data.total);
setLoading(false);
} catch (error) {
console.error('Error fetching properties:', error);
setLoading(false);
}
};
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({
...prev,
[name]: value
}));
setPage(1); // Reset to first page when filters change
};
const handleAmenityToggle = (amenity) => {
setFilters(prev => {
const newAmenities = prev.amenities.includes(amenity)
? prev.amenities.filter(a => a !== amenity)
: [...prev.amenities, amenity];
return { ...prev, amenities: newAmenities };
});
setPage(1);
};
const handlePropertyTypeToggle = (type) => {
setFilters(prev => {
const newTypes = prev.propertyType.includes(type)
? prev.propertyType.filter(t => t !== type)
: [...prev.propertyType, type];
return { ...prev, propertyType: newTypes };
});
setPage(1);
};
const handlePageChange = (event, value) => {
setPage(value);
};
return (
<div style={{ padding: '20px' }}>
<Paper elevation={3} style={{ padding: '20px', marginBottom: '20px' }}>
<Typography variant="h6" gutterBottom>
<FilterList /> Filters
</Typography>
<Grid container spacing={3}>
{/* Location Search */}
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Location"
name="location"
value={filters.location}
onChange={handleFilterChange}
variant="outlined"
/>
</Grid>
{/* Price Range */}
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Min Price"
name="minPrice"
type="number"
value={filters.minPrice}
onChange={handleFilterChange}
variant="outlined"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Max Price"
name="maxPrice"
type="number"
value={filters.maxPrice}
onChange={handleFilterChange}
variant="outlined"
/>
</Grid>
{/* Property Type */}
<Grid item xs={12}>
<Typography variant="subtitle1">Property Type</Typography>
<FormGroup row>
{propertyTypes.map(type => (
<FormControlLabel
key={type}
control={
<Checkbox
checked={filters.propertyType.includes(type)}
onChange={() => handlePropertyTypeToggle(type)}
name={type}
/>
}
label={type}
/>
))}
</FormGroup>
</Grid>
{/* Bedrooms */}
<Grid item xs={12} md={4}>
<FormControl fullWidth variant="outlined">
<InputLabel>Bedrooms</InputLabel>
<Select
name="bedrooms"
value={filters.bedrooms}
onChange={handleFilterChange}
label="Bedrooms"
>
<MenuItem value="">Any</MenuItem>
<MenuItem value={1}>1+</MenuItem>
<MenuItem value={2}>2+</MenuItem>
<MenuItem value={3}>3+</MenuItem>
<MenuItem value={4}>4+</MenuItem>
<MenuItem value={5}>5+</MenuItem>
</Select>
</FormControl>
</Grid>
{/* Area Range */}
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Min Area (sqft)"
name="minArea"
type="number"
value={filters.minArea}
onChange={handleFilterChange}
variant="outlined"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Max Area (sqft)"
name="maxArea"
type="number"
value={filters.maxArea}
onChange={handleFilterChange}
variant="outlined"
/>
</Grid>
{/* Amenities */}
<Grid item xs={12}>
<Typography variant="subtitle1">Amenities</Typography>
<FormGroup row>
{amenitiesList.map(amenity => (
<FormControlLabel
key={amenity}
control={
<Checkbox
checked={filters.amenities.includes(amenity)}
onChange={() => handleAmenityToggle(amenity)}
name={amenity}
/>
}
label={amenity}
/>
))}
</FormGroup>
</Grid>
{/* Sorting */}
<Grid item xs={12} md={4}>
<FormControl fullWidth variant="outlined">
<InputLabel>Sort By</InputLabel>
<Select
name="sortBy"
value={filters.sortBy}
onChange={handleFilterChange}
label="Sort By"
>
<MenuItem value="newest">Newest First</MenuItem>
<MenuItem value="price-asc">Price: Low to High</MenuItem>
<MenuItem value="price-desc">Price: High to Low</MenuItem>
<MenuItem value="area-asc">Area: Small to Large</MenuItem>
<MenuItem value="area-desc">Area: Large to Small</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
startIcon={<Search />}
onClick={fetchProperties}
>
Apply Filters
</Button>
</Grid>
</Grid>
</Paper>
{/* Property List */}
{loading ? (
<Typography>Loading properties...</Typography>
) : (
<>
<Typography variant="h6" gutterBottom>
Showing {properties.length} of {total} properties
</Typography>
<Grid container spacing={3}>
{properties.map(property => (
<Grid item xs={12} sm={6} md={4} key={property._id}>
<Paper elevation={2} style={{ padding: '15px', height: '100%' }}>
<Typography variant="h6">{property.title}</Typography>
<Typography color="textSecondary">{property.propertyType} in {property.location.city}</Typography>
<Typography variant="h5" color="primary">
₹{property.price.toLocaleString()}
</Typography>
<Typography>
{property.bedrooms} Beds | {property.bathrooms} Baths | {property.area} sqft
</Typography>
<Typography variant="body2" style={{ marginTop: '10px' }}>
{property.description?.substring(0, 100)}...
</Typography>
</Paper>
</Grid>
))}
</Grid>
{/* Pagination */}
{total > limit && (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
<Pagination
count={Math.ceil(total / limit)}
page={page}
onChange={handlePageChange}
color="primary"
/>
</div>
)}
</>
)}
</div>
);
};
export default PropertyListing;
Step 3: Key Features Implemented
-
Advanced Filtering:
- Price range (min/max)
- Property type (multi-select)
- Bedrooms (minimum)
- Location (city search)
- Amenities (multi-select)
- Area range (min/max sqft)
-
Sorting Options:
- Price (low to high, high to low)
- Area (small to large, large to small)
- Newest first
-
Pagination:
- Server-side pagination for performance
- Configurable page size
-
Responsive Design:
- Works on mobile, tablet, and desktop
- Material-UI components for consistent styling
Step 4: Deployment
4.1 Backend Deployment (Heroku)
# Install Heroku CLI
heroku login
heroku create your-app-name
# Set environment variables
heroku config:set MONGODB_URI=your_mongodb_uri
# Deploy
git add .
git commit -m "Initial commit"
git push heroku master
4.2 Frontend Deployment (Netlify/Vercel)
# Build React app
npm run build
# Deploy to Netlify
netlify deploy --prod
Conclusion
This dynamic property listing system provides:
- Comprehensive filtering capabilities
- Clean, responsive UI
- Scalable backend architecture
- Easy deployment options
The system can be extended with:
- User authentication
- Favorites/saved properties
- Map-based property search
- Image uploads
- Advanced analytics
Would you like me to elaborate on any specific part of this implementation?
Written by think4buysale.in Software and ompropertydealer.com
Top comments (0)