Frontend improvements (#2)
Some checks failed
Deploy Apartment Dashboard / deploy (push) Failing after 1s
Some checks failed
Deploy Apartment Dashboard / deploy (push) Failing after 1s
Co-authored-by: Stephen Minakian <stephenminakian@gmail.com> Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
This commit is contained in:
212
src/App.jsx
212
src/App.jsx
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Routes, Route, Link, useLocation, Navigate } from 'react-router-dom';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Area, AreaChart, ScatterChart, Scatter, ComposedChart, ReferenceLine } from 'recharts';
|
||||
import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star, RefreshCw, Wifi, WifiOff, Bell, Target, Filter, Search, ArrowUp, ArrowDown, Zap, Clock, Eye } from 'lucide-react';
|
||||
import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star, RefreshCw, Wifi, WifiOff, Bell, Target, Filter, Search, ArrowUp, ArrowDown, Zap, Clock, Eye, LayoutGrid, List } from 'lucide-react';
|
||||
|
||||
const ApartmentDashboard = () => {
|
||||
// Router
|
||||
@ -19,8 +19,10 @@ const ApartmentDashboard = () => {
|
||||
// Filters
|
||||
const [priceFilter, setPriceFilter] = useState({ min: 0, max: 10000 });
|
||||
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 [unitsViewMode, setUnitsViewMode] = useState('list');
|
||||
|
||||
// API Data state - using your working endpoints
|
||||
const [dailySummary, setDailySummary] = useState(null);
|
||||
@ -370,19 +372,32 @@ const ApartmentDashboard = () => {
|
||||
|
||||
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
|
||||
const filteredUnits = availableUnits.filter(unit => {
|
||||
const matchesPrice = (unit.currentPrice || 0) >= priceFilter.min && (unit.currentPrice || 0) <= priceFilter.max;
|
||||
const matchesBedroom = bedroomFilter === 'all' || (unit.bedCount != null && unit.bedCount.toString() === bedroomFilter);
|
||||
return matchesPrice && matchesBedroom;
|
||||
}).sort((a, b) => {
|
||||
const dir = sortDirection === 'asc' ? 1 : -1;
|
||||
switch (sortBy) {
|
||||
case 'alphabetical': return (a.unitCode || '').localeCompare(b.unitCode || '');
|
||||
case 'price': return (a.currentPrice || 0) - (b.currentPrice || 0);
|
||||
case 'priceDesc': return (b.currentPrice || 0) - (a.currentPrice || 0);
|
||||
case 'size': return (b.area || 0) - (a.area || 0);
|
||||
case 'daysAvailable': return (b.daysAvailable || 0) - (a.daysAvailable || 0);
|
||||
case 'pricePerSqft': return ((a.currentPrice || 0) / (a.area || 1)) - ((b.currentPrice || 0) / (b.area || 1));
|
||||
case 'unitCode': return dir * (a.unitCode || '').localeCompare(b.unitCode || '');
|
||||
case 'planName': return dir * (a.planName || '').localeCompare(b.planName || '');
|
||||
case 'bedCount': return dir * ((a.bedCount || 0) - (b.bedCount || 0));
|
||||
case 'area': return dir * ((a.area || 0) - (b.area || 0));
|
||||
case 'price': return dir * ((a.currentPrice || 0) - (b.currentPrice || 0));
|
||||
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));
|
||||
case 'soonest': return dir * (a.soonest || '').localeCompare(b.soonest || '');
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
@ -679,21 +694,46 @@ const ApartmentDashboard = () => {
|
||||
<option value="2">2 BR</option>
|
||||
</select>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
value={`${sortBy}-${sortDirection}`}
|
||||
onChange={(e) => {
|
||||
const [field, dir] = e.target.value.split('-');
|
||||
setSortBy(field);
|
||||
setSortDirection(dir);
|
||||
}}
|
||||
className="border rounded-md px-3 py-2"
|
||||
>
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
<option value="price">Price: Low to High</option>
|
||||
<option value="priceDesc">Price: High to Low</option>
|
||||
<option value="size">Size: Large to Small</option>
|
||||
<option value="daysAvailable">Days Available</option>
|
||||
<option value="pricePerSqft">Price per Sqft</option>
|
||||
<option value="unitCode-asc">Unit: A to Z</option>
|
||||
<option value="unitCode-desc">Unit: Z to A</option>
|
||||
<option value="price-asc">Price: Low to High</option>
|
||||
<option value="price-desc">Price: High to Low</option>
|
||||
<option value="area-asc">Size: Small to Large</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>
|
||||
<div className="flex items-center border rounded-md overflow-hidden ml-auto">
|
||||
<button
|
||||
onClick={() => setUnitsViewMode('grid')}
|
||||
className={`p-2 ${unitsViewMode === 'grid' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-100'}`}
|
||||
title="Grid view"
|
||||
>
|
||||
<LayoutGrid className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUnitsViewMode('list')}
|
||||
className={`p-2 ${unitsViewMode === 'list' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-100'}`}
|
||||
title="List view"
|
||||
>
|
||||
<List className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unit Cards */}
|
||||
{/* Unit Cards - Grid View */}
|
||||
{unitsViewMode === 'grid' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredUnits.map((unit) => (
|
||||
<div key={unit.unitCode} className={`bg-white rounded-lg shadow-md overflow-hidden ${watchedUnits.has(unit.unitCode) ? 'ring-2 ring-blue-400' : ''}`}>
|
||||
@ -726,28 +766,127 @@ const ApartmentDashboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => openUnitDetail(unit)}
|
||||
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleWatchUnit(unit.unitCode)}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
watchedUnits.has(unit.unitCode)
|
||||
? 'bg-yellow-500 text-white hover:bg-yellow-600'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unit Cards - List View */}
|
||||
{unitsViewMode === 'list' && (
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th
|
||||
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('unitCode')}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
Unit
|
||||
{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 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('soonest')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Soonest
|
||||
{sortBy === 'soonest' && (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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUnits.map((unit, index) => (
|
||||
<tr
|
||||
key={unit.unitCode}
|
||||
className={`border-b ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} ${watchedUnits.has(unit.unitCode) ? 'ring-2 ring-inset ring-blue-400' : ''} hover:bg-blue-50`}
|
||||
>
|
||||
<td className="py-3 px-4 font-bold">{unit.unitCode}</td>
|
||||
<td className="py-3 px-4 text-gray-600">{unit.planName}</td>
|
||||
<td className="py-3 px-4 text-center">{unit.bedCount}/{unit.bathCount}</td>
|
||||
<td className="py-3 px-4 text-center">{unit.area} sqft</td>
|
||||
<td className="py-3 px-4 text-right font-bold text-green-600">{formatPrice(unit.currentPrice)}</td>
|
||||
<td className="py-3 px-4 text-right text-gray-500">${((unit.currentPrice || 0) / (unit.area || 1)).toFixed(2)}</td>
|
||||
<td className="py-3 px-4 text-center">{unit.daysAvailable || 0}</td>
|
||||
<td className="py-3 px-4 text-center">{unit.soonest ? (new Date(unit.soonest) <= new Date() ? 'Now' : formatDate(unit.soonest)) : '-'}</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<button
|
||||
onClick={() => openUnitDetail(unit)}
|
||||
className="bg-blue-600 text-white py-1 px-3 rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredUnits.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
@ -1107,19 +1246,6 @@ const ApartmentDashboard = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => toggleWatchUnit(selectedUnit.unitCode)}
|
||||
className={`flex-1 py-2 px-4 rounded-md transition-colors ${
|
||||
watchedUnits.has(selectedUnit.unitCode)
|
||||
? 'bg-yellow-500 text-white hover:bg-yellow-600'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{watchedUnits.has(selectedUnit.unitCode) ? 'Stop Watching' : 'Watch This Unit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user