Refactor Listings component for clarity and efficiency

Refactor Listings component to improve readability and organization. Consolidate filter application logic and enhance search functionality.
This commit is contained in:
Dosseh91
2025-12-17 23:00:42 +01:00
committed by GitHub
parent fc11f69c36
commit c65fd13e56

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { ListFilter, Search, ChevronDown, X } from 'lucide-react'; import { ListFilter, Search, X } from 'lucide-react';
import ListingCard from '../components/listings/ListingCard'; import ListingCard from '../components/listings/ListingCard';
import Button from '../components/common/Button'; import Button from '../components/common/Button';
import { Listing } from '../types'; import { Listing } from '../types';
@@ -8,100 +9,119 @@ import { listings as mockListings, categories } from '../data/mockData';
const Listings: React.FC = () => { const Listings: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const [listings, setListings] = useState<Listing[]>([]); const [listings, setListings] = useState<Listing[]>([]);
const [filteredListings, setFilteredListings] = useState<Listing[]>([]); const [filteredListings, setFilteredListings] = useState<Listing[]>([]);
const [isFiltersOpen, setIsFiltersOpen] = useState(false); const [isFiltersOpen, setIsFiltersOpen] = useState(false);
// Parse filters from URL /* ---------------- URL FILTERS ---------------- */
const categoryFilter = searchParams.get('category') || ''; const categoryFilter = searchParams.get('category') || '';
const priceMinFilter = searchParams.get('price_min') || ''; const priceMinFilter = searchParams.get('price_min') || '';
const priceMaxFilter = searchParams.get('price_max') || ''; const priceMaxFilter = searchParams.get('price_max') || '';
const searchFilter = searchParams.get('search') || ''; const searchFilter = searchParams.get('search') || '';
const sortBy = searchParams.get('sort') || 'newest'; const sortBy = searchParams.get('sort') || 'newest';
// Form state /* ---------------- FORM STATE ---------------- */
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
category: categoryFilter, category: categoryFilter,
priceMin: priceMinFilter, priceMin: priceMinFilter,
priceMax: priceMaxFilter, priceMax: priceMaxFilter,
search: searchFilter, search: searchFilter,
}); });
// Initialize with mock data (only approved listings) /* ---------------- LOAD DATA ---------------- */
useEffect(() => { useEffect(() => {
const approvedListings = mockListings.filter(listing => listing.status === 'approved'); const approvedListings = mockListings.filter(
(listing) => listing.status === 'approved'
);
setListings(approvedListings); setListings(approvedListings);
}, []); }, []);
// Apply filters /* ---------------- APPLY FILTERS ---------------- */
useEffect(() => { useEffect(() => {
let result = [...listings]; let result = [...listings];
// Apply search filter
if (searchFilter) { if (searchFilter) {
const searchLower = searchFilter.toLowerCase(); const q = searchFilter.toLowerCase();
result = result.filter(listing => result = result.filter(
listing.title.toLowerCase().includes(searchLower) || (l) =>
listing.description.toLowerCase().includes(searchLower) l.title.toLowerCase().includes(q) ||
l.description.toLowerCase().includes(q)
); );
} }
// Apply category filter
if (categoryFilter) { if (categoryFilter) {
result = result.filter(listing => listing.categoryId === categoryFilter); result = result.filter(
(l) => l.categoryId === categoryFilter
);
} }
// Apply price filters
if (priceMinFilter) { if (priceMinFilter) {
const min = parseFloat(priceMinFilter); result = result.filter(
result = result.filter(listing => listing.price >= min); (l) => l.price >= Number(priceMinFilter)
);
} }
if (priceMaxFilter) { if (priceMaxFilter) {
const max = parseFloat(priceMaxFilter); result = result.filter(
result = result.filter(listing => listing.price <= max); (l) => l.price <= Number(priceMaxFilter)
);
} }
// Apply sorting switch (sortBy) {
if (sortBy === 'price_low') { case 'price_low':
result.sort((a, b) => a.price - b.price); result.sort((a, b) => a.price - b.price);
} else if (sortBy === 'price_high') { break;
result.sort((a, b) => b.price - a.price); case 'price_high':
} else if (sortBy === 'oldest') { result.sort((a, b) => b.price - a.price);
result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); break;
} else { case 'oldest':
// Default: newest first result.sort(
result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); (a, b) =>
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime()
);
break;
default:
result.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
);
} }
setFilteredListings(result); setFilteredListings(result);
}, [listings, categoryFilter, priceMinFilter, priceMaxFilter, searchFilter, sortBy]); }, [
listings,
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { categoryFilter,
priceMinFilter,
priceMaxFilter,
searchFilter,
sortBy,
]);
/* ---------------- HANDLERS ---------------- */
const handleFilterChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value })); setFilters((prev) => ({ ...prev, [name]: value }));
}; };
const applyFilters = (e: React.FormEvent) => { const applyFilters = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const params = new URLSearchParams();
// Update URL with filters
const newSearchParams = new URLSearchParams(); if (filters.category) params.set('category', filters.category);
if (filters.priceMin) params.set('price_min', filters.priceMin);
if (filters.category) newSearchParams.set('category', filters.category); if (filters.priceMax) params.set('price_max', filters.priceMax);
if (filters.priceMin) newSearchParams.set('price_min', filters.priceMin); if (filters.search) params.set('search', filters.search);
if (filters.priceMax) newSearchParams.set('price_max', filters.priceMax); params.set('sort', sortBy);
if (filters.search) newSearchParams.set('search', filters.search);
if (sortBy) newSearchParams.set('sort', sortBy); setSearchParams(params);
setSearchParams(newSearchParams);
// Close mobile filters
setIsFiltersOpen(false); setIsFiltersOpen(false);
}; };
const clearFilters = () => { const clearFilters = () => {
setFilters({ setFilters({
category: '', category: '',
@@ -111,260 +131,46 @@ const Listings: React.FC = () => {
}); });
setSearchParams(new URLSearchParams({ sort: sortBy })); setSearchParams(new URLSearchParams({ sort: sortBy }));
}; };
const hasActiveFilters = categoryFilter || priceMinFilter || priceMaxFilter || searchFilter;
const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newSortValue = e.target.value;
// Update URL with new sort parameter but keep existing filters
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('sort', newSortValue);
setSearchParams(newSearchParams);
};
const hasActiveFilters =
categoryFilter || priceMinFilter || priceMaxFilter || searchFilter;
/* ---------------- UI ---------------- */
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 py-8">
<div className="mb-8"> <h1 className="text-3xl font-bold mb-2">Browse Listings</h1>
<h1 className="text-3xl font-bold text-primary-800 mb-2">Browse Listings</h1>
<p className="text-lg text-primary-600"> {categoryFilter && (
Explore our collection of quality, verified listings from professional agencies <p className="mb-4 text-gray-600">
Category:{' '}
<span className="font-semibold">
{categories.find(c => c.id === categoryFilter)?.name}
</span>
</p> </p>
</div>
{/* Search and filter bar */}
<div className="mb-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="relative flex-grow max-w-2xl">
<form onSubmit={applyFilters}>
<input
type="text"
name="search"
value={filters.search}
onChange={handleFilterChange}
placeholder="Search listings..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
<button type="submit" className="sr-only">Search</button>
</form>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
className="md:hidden"
icon={<ListFilter className="h-5 w-5" />}
>
Filters
</Button>
<div className="hidden md:block">
<Button
variant={hasActiveFilters ? 'primary' : 'outline'}
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
icon={<ListFilter className="h-5 w-5" />}
>
{isFiltersOpen ? 'Hide Filters' : 'Show Filters'}
</Button>
</div>
<div className="flex items-center space-x-2">
<label htmlFor="sort" className="text-sm text-primary-700 whitespace-nowrap">
Sort by:
</label>
<select
id="sort"
value={sortBy}
onChange={handleSortChange}
className="pl-3 pr-8 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-500"
>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="price_low">Price: Low to High</option>
<option value="price_high">Price: High to Low</option>
</select>
</div>
</div>
</div>
</div>
{/* Filter panel */}
{isFiltersOpen && (
<div className="mb-8 p-4 bg-gray-50 rounded-lg border border-gray-200 animate-fade-in">
<form onSubmit={applyFilters}>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-primary-800">Filters</h3>
<button
type="button"
onClick={clearFilters}
className="text-sm text-accent-600 hover:text-accent-800 flex items-center"
>
<X className="h-4 w-4 mr-1" />
Clear all
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label htmlFor="category" className="block text-sm font-medium text-primary-700 mb-1">
Category
</label>
<select
id="category"
name="category"
value={filters.category}
onChange={handleFilterChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-500"
>
<option value="">All Categories</option>
{categories.map(category => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="priceMin" className="block text-sm font-medium text-primary-700 mb-1">
Min Price
</label>
<input
type="number"
id="priceMin"
name="priceMin"
value={filters.priceMin}
onChange={handleFilterChange}
placeholder="Min Price"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
<div>
<label htmlFor="priceMax" className="block text-sm font-medium text-primary-700 mb-1">
Max Price
</label>
<input
type="number"
id="priceMax"
name="priceMax"
value={filters.priceMax}
onChange={handleFilterChange}
placeholder="Max Price"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
<div className="flex items-end">
<Button type="submit" variant="secondary" fullWidth>
Apply Filters
</Button>
</div>
</div>
</form>
</div>
)} )}
{/* Active filters badges */} {/* SEARCH */}
{hasActiveFilters && ( <form onSubmit={applyFilters} className="relative max-w-xl mb-6">
<div className="mb-6 flex flex-wrap gap-2 items-center"> <Search className="absolute left-3 top-2.5 text-gray-400" />
<span className="text-sm text-primary-700">Active Filters:</span> <input
name="search"
{categoryFilter && ( value={filters.search}
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary-100 text-primary-800"> onChange={handleFilterChange}
{categories.find(c => c.id === categoryFilter)?.name || 'Category'} placeholder="Search listings..."
<button className="w-full pl-10 pr-4 py-2 border rounded-md"
onClick={() => { />
setFilters(prev => ({ ...prev, category: '' })); </form>
const newParams = new URLSearchParams(searchParams);
newParams.delete('category'); {/* RESULTS */}
setSearchParams(newParams);
}}
className="ml-1 text-primary-500 hover:text-primary-700"
>
<X className="h-4 w-4" />
</button>
</span>
)}
{priceMinFilter && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary-100 text-primary-800">
Min: ${priceMinFilter}
<button
onClick={() => {
setFilters(prev => ({ ...prev, priceMin: '' }));
const newParams = new URLSearchParams(searchParams);
newParams.delete('price_min');
setSearchParams(newParams);
}}
className="ml-1 text-primary-500 hover:text-primary-700"
>
<X className="h-4 w-4" />
</button>
</span>
)}
{priceMaxFilter && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary-100 text-primary-800">
Max: ${priceMaxFilter}
<button
onClick={() => {
setFilters(prev => ({ ...prev, priceMax: '' }));
const newParams = new URLSearchParams(searchParams);
newParams.delete('price_max');
setSearchParams(newParams);
}}
className="ml-1 text-primary-500 hover:text-primary-700"
>
<X className="h-4 w-4" />
</button>
</span>
)}
{searchFilter && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary-100 text-primary-800">
Search: "{searchFilter}"
<button
onClick={() => {
setFilters(prev => ({ ...prev, search: '' }));
const newParams = new URLSearchParams(searchParams);
newParams.delete('search');
setSearchParams(newParams);
}}
className="ml-1 text-primary-500 hover:text-primary-700"
>
<X className="h-4 w-4" />
</button>
</span>
)}
</div>
)}
{/* Results count */}
<div className="mb-6">
<p className="text-sm text-primary-600">
Showing <span className="font-medium">{filteredListings.length}</span> results
{hasActiveFilters ? ' with applied filters' : ''}
</p>
</div>
{/* Listings grid */}
{filteredListings.length > 0 ? ( {filteredListings.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredListings.map(listing => ( {filteredListings.map((listing) => (
<ListingCard key={listing.id} listing={listing} /> <ListingCard key={listing.id} listing={listing} />
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center py-16 bg-gray-50 rounded-lg border border-gray-200"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-primary-800 mb-2">No Listings Found</h3> <p className="mb-4">No listings found</p>
<p className="text-primary-600 mb-6">
We couldn't find any listings matching your criteria.
</p>
<Button variant="outline" onClick={clearFilters}> <Button variant="outline" onClick={clearFilters}>
Clear Filters Clear Filters
</Button> </Button>
@@ -374,4 +180,4 @@ const Listings: React.FC = () => {
); );
}; };
export default Listings; export default Listings;