2025-12-18 13:32:50 -03:00
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import SearchBar from '../components/SearchBar';
|
|
|
|
|
import ListingCard from '../components/ListingCard';
|
|
|
|
|
import { publicService } from '../services/publicService';
|
|
|
|
|
import type { Listing, Category } from '../types';
|
2025-12-23 15:12:57 -03:00
|
|
|
import { Filter, X } from 'lucide-react';
|
|
|
|
|
import { processCategoriesForSelect, type FlatCategory } from '../utils/categoryTreeUtils';
|
2025-12-18 13:32:50 -03:00
|
|
|
|
|
|
|
|
export default function HomePage() {
|
|
|
|
|
const [listings, setListings] = useState<Listing[]>([]);
|
2025-12-23 15:12:57 -03:00
|
|
|
|
|
|
|
|
// Usamos FlatCategory para el renderizado
|
|
|
|
|
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
|
|
|
|
const [rawCategories, setRawCategories] = useState<Category[]>([]); // Guardamos raw para los botones del home
|
|
|
|
|
|
2025-12-18 13:32:50 -03:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
// Estado de Búsqueda
|
|
|
|
|
const [searchText, setSearchText] = useState('');
|
|
|
|
|
const [selectedCatId, setSelectedCatId] = useState<number | null>(null);
|
|
|
|
|
const [dynamicFilters, setDynamicFilters] = useState<Record<string, string>>({});
|
|
|
|
|
|
2025-12-18 13:32:50 -03:00
|
|
|
useEffect(() => {
|
|
|
|
|
loadInitialData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const loadInitialData = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const [latestListings, cats] = await Promise.all([
|
|
|
|
|
publicService.getLatestListings(),
|
|
|
|
|
publicService.getCategories()
|
|
|
|
|
]);
|
|
|
|
|
setListings(latestListings);
|
2025-12-23 15:12:57 -03:00
|
|
|
setRawCategories(cats);
|
|
|
|
|
|
|
|
|
|
// Procesamos el árbol para el select
|
|
|
|
|
const processed = processCategoriesForSelect(cats);
|
|
|
|
|
setFlatCategories(processed);
|
|
|
|
|
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
finally { setLoading(false); }
|
2025-12-18 13:32:50 -03:00
|
|
|
};
|
|
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
const performSearch = async () => {
|
2025-12-18 13:32:50 -03:00
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2025-12-23 15:12:57 -03:00
|
|
|
const response = await import('../services/api').then(m => m.default.post('/listings/search', {
|
|
|
|
|
query: searchText,
|
|
|
|
|
categoryId: selectedCatId,
|
|
|
|
|
filters: dynamicFilters
|
|
|
|
|
}));
|
|
|
|
|
setListings(response.data);
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
finally { setLoading(false); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (searchText || selectedCatId || Object.keys(dynamicFilters).length > 0) {
|
|
|
|
|
performSearch();
|
2025-12-18 13:32:50 -03:00
|
|
|
}
|
2025-12-23 15:12:57 -03:00
|
|
|
}, [selectedCatId, dynamicFilters]);
|
|
|
|
|
|
|
|
|
|
const handleSearchText = (q: string) => {
|
|
|
|
|
setSearchText(q);
|
|
|
|
|
performSearch();
|
2025-12-18 13:32:50 -03:00
|
|
|
};
|
|
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
const clearFilters = () => {
|
|
|
|
|
setDynamicFilters({});
|
|
|
|
|
setSelectedCatId(null);
|
|
|
|
|
setSearchText('');
|
|
|
|
|
publicService.getLatestListings().then(setListings); // Reset list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Para los botones del Home, solo mostramos los Raíz
|
|
|
|
|
const rootCategories = rawCategories.filter(c => !c.parentId);
|
2025-12-18 13:32:50 -03:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 pb-20">
|
2025-12-23 15:12:57 -03:00
|
|
|
{/* Hero */}
|
|
|
|
|
<div className="bg-primary-900 text-white py-16 px-4 relative overflow-hidden">
|
2025-12-18 13:32:50 -03:00
|
|
|
<div className="absolute inset-0 bg-gradient-to-br from-primary-900 to-gray-900 opacity-90"></div>
|
|
|
|
|
<div className="max-w-4xl mx-auto text-center relative z-10">
|
2025-12-23 15:12:57 -03:00
|
|
|
<h1 className="text-3xl md:text-5xl font-bold mb-6">Encuentra tu próximo objetivo</h1>
|
2025-12-18 13:32:50 -03:00
|
|
|
<div className="flex justify-center">
|
2025-12-23 15:12:57 -03:00
|
|
|
<SearchBar onSearch={handleSearchText} />
|
2025-12-18 13:32:50 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
<div className="max-w-7xl mx-auto px-4 mt-8 flex flex-col lg:flex-row gap-8">
|
|
|
|
|
|
|
|
|
|
{/* SIDEBAR DE FILTROS */}
|
|
|
|
|
<div className="w-full lg:w-64 flex-shrink-0 space-y-6">
|
|
|
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100">
|
|
|
|
|
<div className="flex justify-between items-center mb-4">
|
|
|
|
|
<h3 className="font-bold text-gray-800 flex items-center gap-2">
|
|
|
|
|
<Filter size={18} /> Filtros
|
|
|
|
|
</h3>
|
|
|
|
|
{(selectedCatId || Object.keys(dynamicFilters).length > 0) && (
|
|
|
|
|
<button onClick={clearFilters} className="text-xs text-red-500 hover:underline flex items-center">
|
|
|
|
|
<X size={12} /> Limpiar
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Filtro Categoría (MEJORADO) */}
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">Categoría</label>
|
|
|
|
|
<select
|
|
|
|
|
className="w-full border border-gray-300 rounded p-2 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
|
|
|
|
|
value={selectedCatId || ''}
|
|
|
|
|
onChange={(e) => setSelectedCatId(Number(e.target.value) || null)}
|
|
|
|
|
>
|
|
|
|
|
<option value="">Todas las categorías</option>
|
|
|
|
|
{flatCategories.map(cat => (
|
|
|
|
|
<option
|
|
|
|
|
key={cat.id}
|
|
|
|
|
value={cat.id}
|
|
|
|
|
className={cat.level === 0 ? "font-bold text-gray-900" : "text-gray-600"}
|
|
|
|
|
>
|
|
|
|
|
{cat.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Filtros Dinámicos */}
|
|
|
|
|
{selectedCatId && (
|
|
|
|
|
<div className="space-y-3 pt-3 border-t">
|
|
|
|
|
<p className="text-xs font-bold text-gray-400 uppercase">Atributos</p>
|
|
|
|
|
<div>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Filtrar por Kilometraje..."
|
|
|
|
|
className="w-full border p-2 rounded text-sm"
|
|
|
|
|
onChange={(e) => setDynamicFilters({ ...dynamicFilters, 'Kilometraje': e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-12-18 13:32:50 -03:00
|
|
|
</div>
|
2025-12-23 15:12:57 -03:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-18 13:32:50 -03:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
{/* LISTADO */}
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
|
|
|
|
{loading ? 'Cargando...' : selectedCatId
|
|
|
|
|
? `${listings.length} Resultados en ${flatCategories.find(c => c.id === selectedCatId)?.name}`
|
|
|
|
|
: 'Resultados Recientes'}
|
|
|
|
|
</h2>
|
2025-12-18 13:32:50 -03:00
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
{!loading && listings.length === 0 && (
|
|
|
|
|
<div className="text-center text-gray-500 py-20 bg-white rounded-lg border border-dashed border-gray-300">
|
|
|
|
|
No se encontraron avisos con esos criterios.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-18 13:32:50 -03:00
|
|
|
|
2025-12-23 15:12:57 -03:00
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{listings.map(listing => (
|
|
|
|
|
<ListingCard key={listing.id} listing={listing} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-12-18 13:32:50 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|