DEV Community

Rahul Kapoor
Rahul Kapoor

Posted on

Dynamic Property Listing System with Advanced Filters

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Step 3: Key Features Implemented

  1. Advanced Filtering:

    • Price range (min/max)
    • Property type (multi-select)
    • Bedrooms (minimum)
    • Location (city search)
    • Amenities (multi-select)
    • Area range (min/max sqft)
  2. Sorting Options:

    • Price (low to high, high to low)
    • Area (small to large, large to small)
    • Newest first
  3. Pagination:

    • Server-side pagination for performance
    • Configurable page size
  4. 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
Enter fullscreen mode Exit fullscreen mode

4.2 Frontend Deployment (Netlify/Vercel)

# Build React app
npm run build

# Deploy to Netlify
netlify deploy --prod
Enter fullscreen mode Exit fullscreen mode

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)