Add sortable columns to list view
- Click column headers to sort by that field - Click again to toggle ascending/descending - Arrow indicators show current sort direction - Dropdown filter stays in sync with column sorting
This commit is contained in:
121
src/App.jsx
121
src/App.jsx
@ -19,7 +19,8 @@ const ApartmentDashboard = () => {
|
|||||||
// Filters
|
// Filters
|
||||||
const [priceFilter, setPriceFilter] = useState({ min: 0, max: 10000 });
|
const [priceFilter, setPriceFilter] = useState({ min: 0, max: 10000 });
|
||||||
const [bedroomFilter, setBedroomFilter] = useState('all');
|
const [bedroomFilter, setBedroomFilter] = useState('all');
|
||||||
const [sortBy, setSortBy] = useState('alphabetical');
|
const [sortBy, setSortBy] = useState('unitCode');
|
||||||
|
const [sortDirection, setSortDirection] = useState('asc');
|
||||||
const [watchedUnits, setWatchedUnits] = useState(new Set());
|
const [watchedUnits, setWatchedUnits] = useState(new Set());
|
||||||
const [unitsViewMode, setUnitsViewMode] = useState('list');
|
const [unitsViewMode, setUnitsViewMode] = useState('list');
|
||||||
|
|
||||||
@ -371,19 +372,31 @@ const ApartmentDashboard = () => {
|
|||||||
|
|
||||||
const insights = calculateInsights();
|
const insights = calculateInsights();
|
||||||
|
|
||||||
|
// Sort handler for column headers
|
||||||
|
const handleSort = (field) => {
|
||||||
|
if (sortBy === field) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setSortBy(field);
|
||||||
|
setSortDirection('asc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Filter function for units
|
// Filter function for units
|
||||||
const filteredUnits = availableUnits.filter(unit => {
|
const filteredUnits = availableUnits.filter(unit => {
|
||||||
const matchesPrice = (unit.currentPrice || 0) >= priceFilter.min && (unit.currentPrice || 0) <= priceFilter.max;
|
const matchesPrice = (unit.currentPrice || 0) >= priceFilter.min && (unit.currentPrice || 0) <= priceFilter.max;
|
||||||
const matchesBedroom = bedroomFilter === 'all' || (unit.bedCount != null && unit.bedCount.toString() === bedroomFilter);
|
const matchesBedroom = bedroomFilter === 'all' || (unit.bedCount != null && unit.bedCount.toString() === bedroomFilter);
|
||||||
return matchesPrice && matchesBedroom;
|
return matchesPrice && matchesBedroom;
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
|
const dir = sortDirection === 'asc' ? 1 : -1;
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'alphabetical': return (a.unitCode || '').localeCompare(b.unitCode || '');
|
case 'unitCode': return dir * (a.unitCode || '').localeCompare(b.unitCode || '');
|
||||||
case 'price': return (a.currentPrice || 0) - (b.currentPrice || 0);
|
case 'planName': return dir * (a.planName || '').localeCompare(b.planName || '');
|
||||||
case 'priceDesc': return (b.currentPrice || 0) - (a.currentPrice || 0);
|
case 'bedCount': return dir * ((a.bedCount || 0) - (b.bedCount || 0));
|
||||||
case 'size': return (b.area || 0) - (a.area || 0);
|
case 'area': return dir * ((a.area || 0) - (b.area || 0));
|
||||||
case 'daysAvailable': return (b.daysAvailable || 0) - (a.daysAvailable || 0);
|
case 'price': return dir * ((a.currentPrice || 0) - (b.currentPrice || 0));
|
||||||
case 'pricePerSqft': return ((a.currentPrice || 0) / (a.area || 1)) - ((b.currentPrice || 0) / (b.area || 1));
|
case 'pricePerSqft': return dir * (((a.currentPrice || 0) / (a.area || 1)) - ((b.currentPrice || 0) / (b.area || 1)));
|
||||||
|
case 'daysAvailable': return dir * ((a.daysAvailable || 0) - (b.daysAvailable || 0));
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -680,16 +693,24 @@ const ApartmentDashboard = () => {
|
|||||||
<option value="2">2 BR</option>
|
<option value="2">2 BR</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={`${sortBy}-${sortDirection}`}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const [field, dir] = e.target.value.split('-');
|
||||||
|
setSortBy(field);
|
||||||
|
setSortDirection(dir);
|
||||||
|
}}
|
||||||
className="border rounded-md px-3 py-2"
|
className="border rounded-md px-3 py-2"
|
||||||
>
|
>
|
||||||
<option value="alphabetical">Alphabetical</option>
|
<option value="unitCode-asc">Unit: A to Z</option>
|
||||||
<option value="price">Price: Low to High</option>
|
<option value="unitCode-desc">Unit: Z to A</option>
|
||||||
<option value="priceDesc">Price: High to Low</option>
|
<option value="price-asc">Price: Low to High</option>
|
||||||
<option value="size">Size: Large to Small</option>
|
<option value="price-desc">Price: High to Low</option>
|
||||||
<option value="daysAvailable">Days Available</option>
|
<option value="area-asc">Size: Small to Large</option>
|
||||||
<option value="pricePerSqft">Price per Sqft</option>
|
<option value="area-desc">Size: Large to Small</option>
|
||||||
|
<option value="daysAvailable-asc">Days: Low to High</option>
|
||||||
|
<option value="daysAvailable-desc">Days: High to Low</option>
|
||||||
|
<option value="pricePerSqft-asc">$/sqft: Low to High</option>
|
||||||
|
<option value="pricePerSqft-desc">$/sqft: High to Low</option>
|
||||||
</select>
|
</select>
|
||||||
<div className="flex items-center border rounded-md overflow-hidden ml-auto">
|
<div className="flex items-center border rounded-md overflow-hidden ml-auto">
|
||||||
<button
|
<button
|
||||||
@ -774,13 +795,69 @@ const ApartmentDashboard = () => {
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-50 border-b">
|
<thead className="bg-gray-50 border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Unit</th>
|
<th
|
||||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Plan</th>
|
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
<th className="text-center py-3 px-4 font-semibold text-gray-700">Bed/Bath</th>
|
onClick={() => handleSort('unitCode')}
|
||||||
<th className="text-center py-3 px-4 font-semibold text-gray-700">Area</th>
|
>
|
||||||
<th className="text-right py-3 px-4 font-semibold text-gray-700">Price</th>
|
<span className="flex items-center">
|
||||||
<th className="text-right py-3 px-4 font-semibold text-gray-700">$/sqft</th>
|
Unit
|
||||||
<th className="text-center py-3 px-4 font-semibold text-gray-700">Days</th>
|
{sortBy === 'unitCode' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('planName')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
Plan
|
||||||
|
{sortBy === 'planName' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('bedCount')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
Bed/Bath
|
||||||
|
{sortBy === 'bedCount' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('area')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
Area
|
||||||
|
{sortBy === 'area' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('price')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-end">
|
||||||
|
Price
|
||||||
|
{sortBy === 'price' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('pricePerSqft')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-end">
|
||||||
|
$/sqft
|
||||||
|
{sortBy === 'pricePerSqft' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||||
|
onClick={() => handleSort('daysAvailable')}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
Days
|
||||||
|
{sortBy === 'daysAvailable' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
<th className="text-center py-3 px-4 font-semibold text-gray-700">Actions</th>
|
<th className="text-center py-3 px-4 font-semibold text-gray-700">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user