Feat: Selector de Contacto Independiente y Formato de Precio

- Se divide la selección de medios de contacto entre los datos del usuario, permitiendo mostras el tipo de contacto que prefiera.
- Cuando el precio es igual a 0, se muestra la palabra "Consultar" en lugar de $0 o ARS 0.
This commit is contained in:
2026-02-19 19:47:13 -03:00
parent 2dfd5f1fb8
commit 042cd8c6f1
9 changed files with 1374 additions and 486 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
import { useState, useEffect } from 'react';
import { useSearchParams, Link } from 'react-router-dom';
import { AdsV2Service, type AdListingDto } from '../services/ads.v2.service';
import { getImageUrl, formatCurrency } from '../utils/app.utils';
import SearchableSelect from '../components/SearchableSelect';
import AdStatusBadge from '../components/AdStatusBadge';
import { useState, useEffect } from "react";
import { useSearchParams, Link } from "react-router-dom";
import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
import { getImageUrl, formatCurrency } from "../utils/app.utils";
import SearchableSelect from "../components/SearchableSelect";
import AdStatusBadge from "../components/AdStatusBadge";
import {
AUTO_SEGMENTS,
MOTO_SEGMENTS,
AUTO_TRANSMISSIONS,
MOTO_TRANSMISSIONS,
FUEL_TYPES,
VEHICLE_CONDITIONS
} from '../constants/vehicleOptions';
VEHICLE_CONDITIONS,
} from "../constants/vehicleOptions";
export default function ExplorarPage() {
const [searchParams, setSearchParams] = useSearchParams();
@@ -19,25 +19,29 @@ export default function ExplorarPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [minPrice, setMinPrice] = useState(searchParams.get('minPrice') || '');
const [maxPrice, setMaxPrice] = useState(searchParams.get('maxPrice') || '');
const [currencyFilter, setCurrencyFilter] = useState(searchParams.get('currency') || '');
const [minYear, setMinYear] = useState(searchParams.get('minYear') || '');
const [maxYear, setMaxYear] = useState(searchParams.get('maxYear') || '');
const [brandId, setBrandId] = useState(searchParams.get('brandId') || '');
const [modelId, setModelId] = useState(searchParams.get('modelId') || '');
const [fuel, setFuel] = useState(searchParams.get('fuel') || '');
const [transmission, setTransmission] = useState(searchParams.get('transmission') || '');
const [minPrice, setMinPrice] = useState(searchParams.get("minPrice") || "");
const [maxPrice, setMaxPrice] = useState(searchParams.get("maxPrice") || "");
const [currencyFilter, setCurrencyFilter] = useState(
searchParams.get("currency") || "",
);
const [minYear, setMinYear] = useState(searchParams.get("minYear") || "");
const [maxYear, setMaxYear] = useState(searchParams.get("maxYear") || "");
const [brandId, setBrandId] = useState(searchParams.get("brandId") || "");
const [modelId, setModelId] = useState(searchParams.get("modelId") || "");
const [fuel, setFuel] = useState(searchParams.get("fuel") || "");
const [transmission, setTransmission] = useState(
searchParams.get("transmission") || "",
);
const [brands, setBrands] = useState<{ id: number, name: string }[]>([]);
const [models, setModels] = useState<{ id: number, name: string }[]>([]);
const [brands, setBrands] = useState<{ id: number; name: string }[]>([]);
const [models, setModels] = useState<{ id: number; name: string }[]>([]);
const q = searchParams.get('q') || '';
const c = searchParams.get('c') || 'ALL';
const q = searchParams.get("q") || "";
const c = searchParams.get("c") || "ALL";
useEffect(() => {
if (c !== 'ALL') {
const typeId = c === 'EAUTOS' ? 1 : 2;
if (c !== "ALL") {
const typeId = c === "EAUTOS" ? 1 : 2;
AdsV2Service.getBrands(typeId).then(setBrands);
} else {
setBrands([]);
@@ -61,7 +65,7 @@ export default function ExplorarPage() {
try {
const data = await AdsV2Service.getAll({
q,
c: c === 'ALL' ? undefined : c,
c: c === "ALL" ? undefined : c,
minPrice: minPrice ? Number(minPrice) : undefined,
maxPrice: maxPrice ? Number(maxPrice) : undefined,
currency: currencyFilter || undefined,
@@ -70,7 +74,7 @@ export default function ExplorarPage() {
brandId: brandId ? Number(brandId) : undefined,
modelId: modelId ? Number(modelId) : undefined,
fuel: fuel || undefined,
transmission: transmission || undefined
transmission: transmission || undefined,
});
setListings(data);
} catch (err) {
@@ -86,32 +90,47 @@ export default function ExplorarPage() {
const applyFilters = () => {
const newParams = new URLSearchParams(searchParams);
if (minPrice) newParams.set('minPrice', minPrice); else newParams.delete('minPrice');
if (maxPrice) newParams.set('maxPrice', maxPrice); else newParams.delete('maxPrice');
if (currencyFilter) newParams.set('currency', currencyFilter); else newParams.delete('currency');
if (minYear) newParams.set('minYear', minYear); else newParams.delete('minYear');
if (maxYear) newParams.set('maxYear', maxYear); else newParams.delete('maxYear');
if (brandId) newParams.set('brandId', brandId); else newParams.delete('brandId');
if (modelId) newParams.set('modelId', modelId); else newParams.delete('modelId');
if (fuel) newParams.set('fuel', fuel); else newParams.delete('fuel');
if (transmission) newParams.set('transmission', transmission); else newParams.delete('transmission');
if (minPrice) newParams.set("minPrice", minPrice);
else newParams.delete("minPrice");
if (maxPrice) newParams.set("maxPrice", maxPrice);
else newParams.delete("maxPrice");
if (currencyFilter) newParams.set("currency", currencyFilter);
else newParams.delete("currency");
if (minYear) newParams.set("minYear", minYear);
else newParams.delete("minYear");
if (maxYear) newParams.set("maxYear", maxYear);
else newParams.delete("maxYear");
if (brandId) newParams.set("brandId", brandId);
else newParams.delete("brandId");
if (modelId) newParams.set("modelId", modelId);
else newParams.delete("modelId");
if (fuel) newParams.set("fuel", fuel);
else newParams.delete("fuel");
if (transmission) newParams.set("transmission", transmission);
else newParams.delete("transmission");
setSearchParams(newParams);
};
const clearFilters = () => {
setMinPrice(''); setMaxPrice(''); setMinYear(''); setMaxYear('');
setCurrencyFilter('');
setBrandId(''); setModelId(''); setFuel(''); setTransmission('');
setMinPrice("");
setMaxPrice("");
setMinYear("");
setMaxYear("");
setCurrencyFilter("");
setBrandId("");
setModelId("");
setFuel("");
setTransmission("");
const newParams = new URLSearchParams();
if (q) newParams.set('q', q);
if (c !== 'ALL') newParams.set('c', c);
if (q) newParams.set("q", q);
if (c !== "ALL") newParams.set("c", c);
setSearchParams(newParams);
};
const handleCategoryFilter = (cat: string) => {
const newParams = new URLSearchParams();
if (q) newParams.set('q', q);
if (cat !== 'ALL') newParams.set('c', cat);
if (q) newParams.set("q", q);
if (cat !== "ALL") newParams.set("c", cat);
setSearchParams(newParams);
};
@@ -119,23 +138,29 @@ export default function ExplorarPage() {
<div className="container mx-auto px-2 md:px-6 py-4 md:py-8 flex flex-col md:flex-row gap-6 md:gap-8 relative items-start">
<button
onClick={() => setShowMobileFilters(true)}
className={`md:hidden fixed bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 z-[110] bg-blue-600 text-white px-6 md:px-8 py-3 md:py-4 rounded-xl md:rounded-2xl font-black uppercase tracking-widest shadow-2xl shadow-blue-600/40 border border-white/20 active:scale-95 transition-all flex items-center gap-2 md:gap-3 text-sm ${showMobileFilters ? 'opacity-0 pointer-events-none translate-y-20' : 'opacity-100 translate-y-0'}`}
className={`md:hidden fixed bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 z-[110] bg-blue-600 text-white px-6 md:px-8 py-3 md:py-4 rounded-xl md:rounded-2xl font-black uppercase tracking-widest shadow-2xl shadow-blue-600/40 border border-white/20 active:scale-95 transition-all flex items-center gap-2 md:gap-3 text-sm ${showMobileFilters ? "opacity-0 pointer-events-none translate-y-20" : "opacity-100 translate-y-0"}`}
>
<span>🔍 FILTRAR</span>
</button>
{/* Sidebar Filters - NATURAL FLOW (NO STICKY, NO SCROLL INTERNO) */}
<aside className={`
<aside
className={`
fixed inset-0 z-[105] bg-black/80 backdrop-blur-xl transition-all duration-500 overflow-y-auto md:overflow-visible
md:relative md:inset-auto md:bg-transparent md:backdrop-blur-none md:z-0 md:w-80 md:flex flex-col md:flex-shrink-0
${showMobileFilters ? 'opacity-100 pointer-events-auto translate-y-0' : 'opacity-0 pointer-events-none translate-y-10 md:opacity-100 md:pointer-events-auto md:translate-y-0'}
`}>
<div className="
${showMobileFilters ? "opacity-100 pointer-events-auto translate-y-0" : "opacity-0 pointer-events-none translate-y-10 md:opacity-100 md:pointer-events-auto md:translate-y-0"}
`}
>
<div
className="
glass p-6 rounded-[2rem] border border-white/5 shadow-2xl
h-fit m-6 mt-28 md:m-0
">
"
>
<div className="flex justify-between items-center mb-6 border-b border-white/5 pb-4">
<h3 className="text-xl font-black tracking-tighter uppercase">FILTROS</h3>
<h3 className="text-xl font-black tracking-tighter uppercase">
FILTROS
</h3>
<div className="flex items-center gap-2">
<button
onClick={clearFilters}
@@ -155,22 +180,32 @@ export default function ExplorarPage() {
<div className="flex flex-col gap-6">
{/* Categoría */}
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Tipo de Vehículo</label>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Tipo de Vehículo
</label>
<select
value={c}
onChange={(e) => handleCategoryFilter(e.target.value)}
className="w-full bg-blue-600/10 border border-blue-500/30 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none font-bold uppercase tracking-wide cursor-pointer hover:bg-blue-600/20"
>
<option value="ALL" className="bg-gray-900">Todos</option>
<option value="EAUTOS" className="bg-gray-900">Automóviles</option>
<option value="EMOTOS" className="bg-gray-900">Motos</option>
<option value="ALL" className="bg-gray-900">
Todos
</option>
<option value="EAUTOS" className="bg-gray-900">
Automóviles
</option>
<option value="EMOTOS" className="bg-gray-900">
Motos
</option>
</select>
</div>
{c !== 'ALL' && (
{c !== "ALL" && (
<div className="space-y-4 animate-fade-in">
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Marca</label>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Marca
</label>
<SearchableSelect
options={brands}
value={brandId}
@@ -181,15 +216,25 @@ export default function ExplorarPage() {
{brandId && (
<div className="animate-fade-in">
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Modelo</label>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Modelo
</label>
<select
value={modelId}
onChange={e => setModelId(e.target.value)}
onChange={(e) => setModelId(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none"
>
<option value="" className="bg-gray-900 text-gray-500">Todos los modelos</option>
{models.map(m => (
<option key={m.id} value={m.id} className="bg-gray-900 text-white">{m.name}</option>
<option value="" className="bg-gray-900 text-gray-500">
Todos los modelos
</option>
{models.map((m) => (
<option
key={m.id}
value={m.id}
className="bg-gray-900 text-white"
>
{m.name}
</option>
))}
</select>
</div>
@@ -198,85 +243,231 @@ export default function ExplorarPage() {
)}
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Moneda</label>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Moneda
</label>
<select
value={currencyFilter}
onChange={e => setCurrencyFilter(e.target.value)}
onChange={(e) => setCurrencyFilter(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none cursor-pointer"
>
<option value="" className="bg-gray-900 text-gray-500">Indistinto</option>
<option value="ARS" className="bg-gray-900 text-white">Pesos (ARS)</option>
<option value="USD" className="bg-gray-900 text-white">Dólares (USD)</option>
<option value="" className="bg-gray-900 text-gray-500">
Indistinto
</option>
<option value="ARS" className="bg-gray-900 text-white">
Pesos (ARS)
</option>
<option value="USD" className="bg-gray-900 text-white">
Dólares (USD)
</option>
</select>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Precio Máximo</label>
<input placeholder="Ej: 25000" type="number" value={maxPrice} onChange={e => setMaxPrice(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Precio Máximo
</label>
<input
placeholder="Ej: 25000"
type="number"
value={maxPrice}
onChange={(e) => setMaxPrice(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all"
/>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Desde Año</label>
<input placeholder="Ej: 2018" type="number" value={minYear} onChange={e => setMinYear(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Desde Año
</label>
<input
placeholder="Ej: 2018"
type="number"
value={minYear}
onChange={(e) => setMinYear(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500"
/>
</div>
<div className="space-y-4">
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Combustible</label>
<select value={fuel} onChange={e => setFuel(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none">
<option value="" className="bg-gray-900 text-gray-500">Todos</option>
{FUEL_TYPES.map(f => (<option key={f} value={f} className="bg-gray-900 text-white">{f}</option>))}
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Combustible
</label>
<select
value={fuel}
onChange={(e) => setFuel(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none"
>
<option value="" className="bg-gray-900 text-gray-500">
Todos
</option>
{FUEL_TYPES.map((f) => (
<option
key={f}
value={f}
className="bg-gray-900 text-white"
>
{f}
</option>
))}
</select>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Transmisión</label>
<select value={transmission} onChange={e => setTransmission(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none">
<option value="" className="bg-gray-900 text-gray-500">Todas</option>
{(c === 'EMOTOS' ? MOTO_TRANSMISSIONS : AUTO_TRANSMISSIONS).map(t => (
<option key={t} value={t} className="bg-gray-900 text-white">{t}</option>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Transmisión
</label>
<select
value={transmission}
onChange={(e) => setTransmission(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 transition-all appearance-none"
>
<option value="" className="bg-gray-900 text-gray-500">
Todas
</option>
{(c === "EMOTOS"
? MOTO_TRANSMISSIONS
: AUTO_TRANSMISSIONS
).map((t) => (
<option
key={t}
value={t}
className="bg-gray-900 text-white"
>
{t}
</option>
))}
</select>
</div>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Color</label>
<input placeholder="Ej: Blanco" type="text" value={searchParams.get('color') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('color', e.target.value); else p.delete('color'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Color
</label>
<input
placeholder="Ej: Blanco"
type="text"
value={searchParams.get("color") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("color", e.target.value);
else p.delete("color");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Ubicación</label>
<input placeholder="Ej: Buenos Aires" type="text" value={searchParams.get('location') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('location', e.target.value); else p.delete('location'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Ubicación
</label>
<input
placeholder="Ej: Buenos Aires"
type="text"
value={searchParams.get("location") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("location", e.target.value);
else p.delete("location");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500"
/>
</div>
<div className={`grid ${c === 'EMOTOS' ? 'grid-cols-1' : 'grid-cols-2'} gap-2`}>
<div
className={`grid ${c === "EMOTOS" ? "grid-cols-1" : "grid-cols-2"} gap-2`}
>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Estado</label>
<select value={searchParams.get('condition') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('condition', e.target.value); else p.delete('condition'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 appearance-none cursor-pointer">
<option value="" className="bg-gray-900">Todos</option>
{VEHICLE_CONDITIONS.map(o => <option key={o} value={o} className="bg-gray-900">{o}</option>)}
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Estado
</label>
<select
value={searchParams.get("condition") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("condition", e.target.value);
else p.delete("condition");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 appearance-none cursor-pointer"
>
<option value="" className="bg-gray-900">
Todos
</option>
{VEHICLE_CONDITIONS.map((o) => (
<option key={o} value={o} className="bg-gray-900">
{o}
</option>
))}
</select>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Segmento</label>
<select value={searchParams.get('segment') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('segment', e.target.value); else p.delete('segment'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 appearance-none cursor-pointer">
<option value="" className="bg-gray-900">Todos</option>
{(c === 'EMOTOS' ? MOTO_SEGMENTS : AUTO_SEGMENTS).map(o => (
<option key={o} value={o} className="bg-gray-900">{o}</option>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Segmento
</label>
<select
value={searchParams.get("segment") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("segment", e.target.value);
else p.delete("segment");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500 appearance-none cursor-pointer"
>
<option value="" className="bg-gray-900">
Todos
</option>
{(c === "EMOTOS" ? MOTO_SEGMENTS : AUTO_SEGMENTS).map((o) => (
<option key={o} value={o} className="bg-gray-900">
{o}
</option>
))}
</select>
</div>
</div>
{c !== 'EMOTOS' && (
{c !== "EMOTOS" && (
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Puertas</label>
<input placeholder="Ej: 4" type="number" value={searchParams.get('doorCount') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('doorCount', e.target.value); else p.delete('doorCount'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Puertas
</label>
<input
placeholder="Ej: 4"
type="number"
value={searchParams.get("doorCount") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("doorCount", e.target.value);
else p.delete("doorCount");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Dirección</label>
<input placeholder="Ej: Hidráulica" type="text" value={searchParams.get('steering') || ''} onChange={e => { const p = new URLSearchParams(searchParams); if (e.target.value) p.set('steering', e.target.value); else p.delete('steering'); setSearchParams(p); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500" />
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
Dirección
</label>
<input
placeholder="Ej: Hidráulica"
type="text"
value={searchParams.get("steering") || ""}
onChange={(e) => {
const p = new URLSearchParams(searchParams);
if (e.target.value) p.set("steering", e.target.value);
else p.delete("steering");
setSearchParams(p);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs text-white outline-none focus:border-blue-500"
/>
</div>
</div>
)}
</div>
<button onClick={applyFilters} className="w-full bg-blue-600 hover:bg-blue-500 text-white py-4 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-blue-600/20 active:scale-95 mb-4 mt-6">
<button
onClick={applyFilters}
className="w-full bg-blue-600 hover:bg-blue-500 text-white py-4 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-blue-600/20 active:scale-95 mb-4 mt-6"
>
Aplicar Filtros
</button>
</div>
@@ -285,10 +476,25 @@ export default function ExplorarPage() {
<div className="w-full md:flex-1 md:min-w-0">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 md:mb-8 gap-4 md:gap-6">
<div className="flex-1 w-full">
<h2 className="text-3xl md:text-4xl font-black tracking-tighter uppercase mb-2 md:mb-0">Explorar</h2>
<h2 className="text-3xl md:text-4xl font-black tracking-tighter uppercase mb-2 md:mb-0">
Explorar
</h2>
<div className="mt-3 md:mt-4 relative max-w-xl group">
<input type="text" placeholder="Buscar por marca, modelo o versión..." value={q} onChange={e => { const newParams = new URLSearchParams(searchParams); if (e.target.value) newParams.set('q', e.target.value); else newParams.delete('q'); setSearchParams(newParams); }} className="w-full bg-white/5 border border-white/10 rounded-xl md:rounded-2xl px-10 md:px-12 py-3 md:py-4 text-sm text-white outline-none focus:border-blue-500 transition-all focus:bg-white/10" />
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-blue-500 transition-colors">🔍</span>
<input
type="text"
placeholder="Buscar por marca, modelo o versión..."
value={q}
onChange={(e) => {
const newParams = new URLSearchParams(searchParams);
if (e.target.value) newParams.set("q", e.target.value);
else newParams.delete("q");
setSearchParams(newParams);
}}
className="w-full bg-white/5 border border-white/10 rounded-xl md:rounded-2xl px-10 md:px-12 py-3 md:py-4 text-sm text-white outline-none focus:border-blue-500 transition-all focus:bg-white/10"
/>
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-blue-500 transition-colors">
🔍
</span>
</div>
</div>
<span className="text-sm font-bold bg-white/5 border border-white/10 px-6 py-3 rounded-full text-gray-400 uppercase tracking-widest self-end md:self-center whitespace-nowrap">
@@ -297,22 +503,45 @@ export default function ExplorarPage() {
</div>
{loading ? (
<div className="flex justify-center p-20"><div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div></div>
<div className="flex justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
) : error ? (
<div className="glass p-12 rounded-[2.5rem] border border-red-500/20 text-center"><p className="text-red-400 font-bold">{error}</p></div>
<div className="glass p-12 rounded-[2.5rem] border border-red-500/20 text-center">
<p className="text-red-400 font-bold">{error}</p>
</div>
) : listings.length === 0 ? (
<div className="glass p-20 rounded-[2.5rem] text-center border-dashed border-2 border-white/10">
<span className="text-6xl mb-6 block">🔍</span>
<h3 className="text-2xl font-bold text-gray-400 uppercase tracking-tighter">Sin coincidencias</h3>
<p className="text-gray-600 max-w-xs mx-auto mt-2 italic">No encontramos vehículos que coincidan con los filtros seleccionados.</p>
<button onClick={clearFilters} className="mt-8 text-blue-400 font-black uppercase text-[10px] tracking-widest">Ver todos los avisos</button>
<h3 className="text-2xl font-bold text-gray-400 uppercase tracking-tighter">
Sin coincidencias
</h3>
<p className="text-gray-600 max-w-xs mx-auto mt-2 italic">
No encontramos vehículos que coincidan con los filtros
seleccionados.
</p>
<button
onClick={clearFilters}
className="mt-8 text-blue-400 font-black uppercase text-[10px] tracking-widest"
>
Ver todos los avisos
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-8">
{listings.map(car => (
<Link to={`/vehiculo/${car.id}`} key={car.id} className="glass-card rounded-2xl md:rounded-[2rem] overflow-hidden group animate-fade-in-up flex flex-col">
{listings.map((car) => (
<Link
to={`/vehiculo/${car.id}`}
key={car.id}
className="glass-card rounded-2xl md:rounded-[2rem] overflow-hidden group animate-fade-in-up flex flex-col"
>
<div className="aspect-[4/3] overflow-hidden relative bg-[#07090d] flex items-center justify-center border-b border-white/5">
<img src={getImageUrl(car.image)} className="max-w-full max-h-full object-contain group-hover:scale-110 transition-transform duration-700" alt={`${car.brandName} ${car.versionName}`} loading="lazy" />
<img
src={getImageUrl(car.image)}
className="max-w-full max-h-full object-contain group-hover:scale-110 transition-transform duration-700"
alt={`${car.brandName} ${car.versionName}`}
loading="lazy"
/>
{/* --- BLOQUE PARA EL BADGE --- */}
<div className="absolute top-4 left-4 z-10">
@@ -331,8 +560,12 @@ export default function ExplorarPage() {
</h3>
<div className="flex justify-between items-center mt-auto">
<div className="flex flex-col">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1">{car.year} {car.km.toLocaleString()} KM</span>
<span className="text-white font-black text-2xl tracking-tighter">{formatCurrency(car.price, car.currency)}</span>
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest mb-1">
{car.year} {car.km.toLocaleString()} KM
</span>
<span className="text-white font-black text-2xl tracking-tighter">
{formatCurrency(car.price, car.currency)}
</span>
</div>
</div>
</div>
@@ -341,6 +574,6 @@ export default function ExplorarPage() {
</div>
)}
</div>
</div >
</div>
);
}
}

View File

@@ -1,13 +1,13 @@
import { useState, useEffect, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { AdsV2Service, type AdListingDto } from '../services/ads.v2.service';
import { getImageUrl, formatCurrency } from '../utils/app.utils';
import AdStatusBadge from '../components/AdStatusBadge';
import { useState, useEffect, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
import { getImageUrl, formatCurrency } from "../utils/app.utils";
import AdStatusBadge from "../components/AdStatusBadge";
export default function HomePage() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [category, setCategory] = useState('ALL');
const [query, setQuery] = useState("");
const [category, setCategory] = useState("ALL");
const [featuredAds, setFeaturedAds] = useState<AdListingDto[]>([]);
const [loading, setLoading] = useState(true);
@@ -19,10 +19,10 @@ export default function HomePage() {
// Cargar destacados
useEffect(() => {
AdsV2Service.getAll({ isFeatured: true })
.then(data => {
.then((data) => {
setFeaturedAds(data.slice(0, 3));
})
.catch(err => console.error("Error cargando destacados:", err))
.catch((err) => console.error("Error cargando destacados:", err))
.finally(() => setLoading(false));
}, []);
@@ -45,7 +45,10 @@ export default function HomePage() {
// --- LÓGICA PARA CERRAR SUGERENCIAS AL HACER CLIC FUERA ---
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (searchWrapperRef.current && !searchWrapperRef.current.contains(event.target as Node)) {
if (
searchWrapperRef.current &&
!searchWrapperRef.current.contains(event.target as Node)
) {
setShowSuggestions(false);
}
}
@@ -57,7 +60,7 @@ export default function HomePage() {
const handleSearch = (searchTerm: string = query) => {
setShowSuggestions(false);
// Si la categoría es 'ALL', no enviamos el parámetro 'c'
const categoryParam = category === 'ALL' ? '' : `&c=${category}`;
const categoryParam = category === "ALL" ? "" : `&c=${category}`;
navigate(`/explorar?q=${searchTerm}${categoryParam}`);
};
@@ -87,7 +90,8 @@ export default function HomePage() {
ENCONTRÁ TU <span className="text-gradient">PRÓXIMO</span> VEHÍCULO
</h1>
<p className="text-sm sm:text-base md:text-xl text-gray-400 mb-6 md:mb-10 max-w-2xl mx-auto font-light px-2">
La web más avanzada para la compra y venta de Autos y Motos en Argentina.
La web más avanzada para la compra y venta de Autos y Motos en
Argentina.
</p>
{/* --- CONTENEDOR DEL BUSCADOR CON ref y onFocus --- */}
@@ -95,20 +99,20 @@ export default function HomePage() {
{/* Botones de categoría arriba del buscador */}
<div className="flex gap-2 mb-3 justify-center">
<button
onClick={() => setCategory('ALL')}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === 'ALL' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/40' : 'glass text-gray-400 hover:text-white'}`}
onClick={() => setCategory("ALL")}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === "ALL" ? "bg-blue-600 text-white shadow-lg shadow-blue-600/40" : "glass text-gray-400 hover:text-white"}`}
>
Todos
</button>
<button
onClick={() => setCategory('EAUTOS')}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === 'EAUTOS' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/40' : 'glass text-gray-400 hover:text-white'}`}
onClick={() => setCategory("EAUTOS")}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === "EAUTOS" ? "bg-blue-600 text-white shadow-lg shadow-blue-600/40" : "glass text-gray-400 hover:text-white"}`}
>
🚗 Automóviles
</button>
<button
onClick={() => setCategory('EMOTOS')}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === 'EMOTOS' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/40' : 'glass text-gray-400 hover:text-white'}`}
onClick={() => setCategory("EMOTOS")}
className={`px-4 md:px-6 py-2 rounded-xl font-bold text-xs md:text-sm uppercase tracking-widest transition-all ${category === "EMOTOS" ? "bg-blue-600 text-white shadow-lg shadow-blue-600/40" : "glass text-gray-400 hover:text-white"}`}
>
🏍 Motos
</button>
@@ -121,7 +125,7 @@ export default function HomePage() {
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setShowSuggestions(true)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
className="bg-transparent border-none px-4 md:px-6 py-3 md:py-4 flex-1 outline-none text-white text-base md:text-lg"
/>
<button
@@ -156,10 +160,19 @@ export default function HomePage() {
<section className="container mx-auto px-4 md:px-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-6 md:mb-10 gap-4">
<div>
<h2 className="text-3xl md:text-4xl font-bold mb-2">Avisos <span className="text-gradient">Destacados</span></h2>
<p className="text-gray-400 text-base md:text-lg italic">Las mejores ofertas seleccionadas para vos.</p>
<h2 className="text-3xl md:text-4xl font-bold mb-2">
Avisos <span className="text-gradient">Destacados</span>
</h2>
<p className="text-gray-400 text-base md:text-lg italic">
Las mejores ofertas seleccionadas para vos.
</p>
</div>
<Link to="/explorar" className="text-blue-400 hover:text-white transition text-sm md:text-base">Ver todos </Link>
<Link
to="/explorar"
className="text-blue-400 hover:text-white transition text-sm md:text-base"
>
Ver todos
</Link>
</div>
{loading ? (
@@ -168,12 +181,18 @@ export default function HomePage() {
</div>
) : featuredAds.length === 0 ? (
<div className="text-center p-10 glass rounded-3xl border border-white/5">
<p className="text-gray-500 text-xl font-bold uppercase tracking-widest">No hay avisos destacados por el momento.</p>
<p className="text-gray-500 text-xl font-bold uppercase tracking-widest">
No hay avisos destacados por el momento.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8">
{featuredAds.map(car => (
<Link to={`/vehiculo/${car.id}`} key={car.id} className="glass-card rounded-2xl md:rounded-3xl overflow-hidden group">
{featuredAds.map((car) => (
<Link
to={`/vehiculo/${car.id}`}
key={car.id}
className="glass-card rounded-2xl md:rounded-3xl overflow-hidden group"
>
<div className="aspect-[4/3] overflow-hidden relative bg-[#07090d] flex items-center justify-center border-b border-white/5">
<img
src={getImageUrl(car.image)}
@@ -184,10 +203,14 @@ export default function HomePage() {
<AdStatusBadge statusId={car.statusId || 4} />
</div>
{car.isFeatured && (
<div className="absolute top-4 right-4 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-widest shadow-lg shadow-blue-600/40">DESTACADO</div>
<div className="absolute top-4 right-4 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-widest shadow-lg shadow-blue-600/40">
DESTACADO
</div>
)}
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-4 py-2 rounded-xl border border-white/10">
<span className="text-xl font-bold">{formatCurrency(car.price, car.currency)}</span>
<span className="text-xl font-bold">
{formatCurrency(car.price, car.currency)}
</span>
</div>
</div>
<div className="p-6">
@@ -197,8 +220,12 @@ export default function HomePage() {
</h3>
</div>
<div className="flex gap-4 text-[10px] text-gray-400 font-black tracking-widest uppercase">
<span className="bg-gray-800/80 px-3 py-1.5 rounded-lg border border-white/5">{car.year}</span>
<span className="bg-gray-800/80 px-3 py-1.5 rounded-lg border border-white/5">{car.km.toLocaleString()} KM</span>
<span className="bg-gray-800/80 px-3 py-1.5 rounded-lg border border-white/5">
{car.year}
</span>
<span className="bg-gray-800/80 px-3 py-1.5 rounded-lg border border-white/5">
{car.km.toLocaleString()} KM
</span>
</div>
</div>
</Link>
@@ -208,4 +235,4 @@ export default function HomePage() {
</section>
</div>
);
}
}

View File

@@ -4,7 +4,7 @@ import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
import { useAuth } from "../context/AuthContext";
import { ChatService, type ChatMessage } from "../services/chat.service";
import ChatModal from "../components/ChatModal";
import { getImageUrl, parseUTCDate } from "../utils/app.utils";
import { formatCurrency, getImageUrl, parseUTCDate } from "../utils/app.utils";
import { AD_STATUSES, STATUS_CONFIG } from "../constants/adStatuses";
import ConfirmationModal from "../components/ConfirmationModal";
@@ -374,7 +374,7 @@ export default function MisAvisosPage() {
{av.brandName} {av.versionName}
</h3>
<span className="text-blue-400 font-bold text-lg">
{av.currency} {av.price.toLocaleString()}
{formatCurrency(av.price, av.currency)}
</span>
</div>
<div className="flex flex-wrap gap-3 justify-center md:justify-start">

View File

@@ -1,12 +1,17 @@
import { useState, useEffect, useRef } from 'react';
import { useParams, Link } from 'react-router-dom';
import { AdsV2Service } from '../services/ads.v2.service';
import { AuthService } from '../services/auth.service';
import ChatModal from '../components/ChatModal';
import { FaWhatsapp, FaMapMarkerAlt, FaInfoCircle, FaShareAlt } from 'react-icons/fa';
import { AD_STATUSES } from '../constants/adStatuses';
import AdStatusBadge from '../components/AdStatusBadge';
import PremiumGallery from '../components/PremiumGallery';
import { useState, useEffect, useRef } from "react";
import { useParams, Link } from "react-router-dom";
import { AdsV2Service } from "../services/ads.v2.service";
import { AuthService } from "../services/auth.service";
import ChatModal from "../components/ChatModal";
import {
FaWhatsapp,
FaMapMarkerAlt,
FaInfoCircle,
FaShareAlt,
} from "react-icons/fa";
import { AD_STATUSES } from "../constants/adStatuses";
import AdStatusBadge from "../components/AdStatusBadge";
import PremiumGallery from "../components/PremiumGallery";
export default function VehiculoDetailPage() {
const { id } = useParams();
@@ -43,7 +48,9 @@ export default function VehiculoDetailPage() {
}, [id, user?.id]);
useEffect(() => {
return () => { viewRegistered.current = false; };
return () => {
viewRegistered.current = false;
};
}, [id]);
const handleFavoriteToggle = async () => {
@@ -52,52 +59,102 @@ export default function VehiculoDetailPage() {
if (isFavorite) await AdsV2Service.removeFavorite(user.id, Number(id));
else await AdsV2Service.addFavorite(user.id, Number(id)!);
setIsFavorite(!isFavorite);
} catch (err) { console.error(err); }
} catch (err) {
console.error(err);
}
};
const getWhatsAppLink = (phone: string, title: string) => {
if (!phone) return '#';
let number = phone.replace(/[^\d]/g, '');
if (number.startsWith('0')) number = number.substring(1);
if (!number.startsWith('54')) number = `549${number}`;
if (!phone) return "#";
let number = phone.replace(/[^\d]/g, "");
if (number.startsWith("0")) number = number.substring(1);
if (!number.startsWith("54")) number = `549${number}`;
const message = `Hola, vi tu aviso "${title}" en Motores Argentinos y me interesa.`;
return `https://wa.me/${number}?text=${encodeURIComponent(message)}`;
};
const handleShare = (platform: 'wa' | 'fb' | 'copy') => {
const handleShare = (platform: "wa" | "fb" | "copy") => {
const url = window.location.href;
const vehicleTitle = `${vehicle.brand?.name || ''} ${vehicle.versionName}`.trim();
const vehicleTitle =
`${vehicle.brand?.name || ""} ${vehicle.versionName}`.trim();
const text = `Mira este ${vehicleTitle} en Motores Argentinos!`;
switch (platform) {
case 'wa': window.open(`https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`, '_blank'); break;
case 'fb': window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank'); break;
case 'copy': navigator.clipboard.writeText(url); alert('Enlace copiado al portapapeles'); break;
case "wa":
window.open(
`https://wa.me/?text=${encodeURIComponent(text + " " + url)}`,
"_blank",
);
break;
case "fb":
window.open(
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
"_blank",
);
break;
case "copy":
navigator.clipboard.writeText(url);
alert("Enlace copiado al portapapeles");
break;
}
};
if (loading) return (
<div className="flex flex-col items-center justify-center p-40 gap-6">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500"></div>
<span className="text-gray-500 font-black uppercase tracking-widest text-xs animate-pulse">Cargando...</span>
</div>
);
if (loading)
return (
<div className="flex flex-col items-center justify-center p-40 gap-6">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500"></div>
<span className="text-gray-500 font-black uppercase tracking-widest text-xs animate-pulse">
Cargando...
</span>
</div>
);
if (error || !vehicle) return <div className="text-white p-20 text-center">{error || "Vehículo no encontrado"}</div>;
if (error || !vehicle)
return (
<div className="text-white p-20 text-center">
{error || "Vehículo no encontrado"}
</div>
);
// HELPER: Valida que el dato exista y no sea basura ("0", vacío, etc)
const hasData = (val: string | null | undefined) => {
return val && val.trim().length > 0 && val !== "0";
};
const isOwnerAdmin = vehicle.ownerUserType === 3;
const isAdActive = vehicle.statusID === AD_STATUSES.ACTIVE;
const isContactable = isAdActive && vehicle.displayContactInfo;
// CALCULAMOS LA DISPONIBILIDAD REAL
// 1. WhatsApp: Debe estar habilitado Y tener un teléfono válido
const canShowWhatsApp =
vehicle.allowWhatsApp && hasData(vehicle.contactPhone);
// 2. Teléfono: Debe estar habilitado ("Mostrar Número") Y tener un teléfono válido
const canShowPhone = vehicle.showPhone && hasData(vehicle.contactPhone);
// 3. Email: Debe estar habilitado Y tener un email válido
const canShowEmail = vehicle.showEmail && hasData(vehicle.contactEmail);
// Es contactable si está Activo Y (Tiene WhatsApp O Tiene Teléfono O Tiene Email)
const isContactable =
isAdActive && (canShowWhatsApp || canShowPhone || canShowEmail);
return (
<div className="container mx-auto px-4 md:px-6 py-6 md:py-12 animate-fade-in-up">
<nav className="flex gap-2 text-xs font-bold uppercase tracking-widest text-gray-500 mb-6 md:mb-8 items-center overflow-x-auto whitespace-nowrap">
<Link to="/" className="hover:text-white transition-colors">Inicio</Link> /
<Link to="/explorar" className="hover:text-white transition-colors">Explorar</Link> /
<span className="text-blue-400 truncate">{vehicle.brand?.name} {vehicle.versionName || 'Detalle'}</span>
<Link to="/" className="hover:text-white transition-colors">
Inicio
</Link>{" "}
/
<Link to="/explorar" className="hover:text-white transition-colors">
Explorar
</Link>{" "}
/
<span className="text-blue-400 truncate">
{vehicle.brand?.name} {vehicle.versionName || "Detalle"}
</span>
</nav>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-12 relative items-start">
{/* COLUMNA IZQUIERDA: Galería + Descripción (Desktop) */}
<div className="lg:col-span-2 space-y-8 md:space-y-12 order-1 lg:order-1">
<PremiumGallery
@@ -106,8 +163,21 @@ export default function VehiculoDetailPage() {
isFavorite={isFavorite}
onFavoriteToggle={handleFavoriteToggle}
statusBadge={<AdStatusBadge statusId={vehicle.statusID} />}
featuredBadge={vehicle.isFeatured && <span className="bg-gradient-to-r from-blue-600 to-cyan-500 text-white px-3 md:px-4 py-1 md:py-1.5 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-widest shadow-lg animate-glow flex items-center gap-1"> DESTACADO</span>}
locationBadge={vehicle.location && <span className="bg-black/60 backdrop-blur-md text-white px-3 md:px-4 py-1 md:py-1.5 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-widest border border-white/10 flex items-center gap-1.5"><FaMapMarkerAlt className="text-blue-500" /> {vehicle.location}</span>}
featuredBadge={
vehicle.isFeatured && (
<span className="bg-gradient-to-r from-blue-600 to-cyan-500 text-white px-3 md:px-4 py-1 md:py-1.5 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-widest shadow-lg animate-glow flex items-center gap-1">
DESTACADO
</span>
)
}
locationBadge={
vehicle.location && (
<span className="bg-black/60 backdrop-blur-md text-white px-3 md:px-4 py-1 md:py-1.5 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-widest border border-white/10 flex items-center gap-1.5">
<FaMapMarkerAlt className="text-blue-500" />{" "}
{vehicle.location}
</span>
)
}
/>
{/* BLOQUE 3: Información General y Técnica (Acomodado debajo de la galería) */}
@@ -118,7 +188,10 @@ export default function VehiculoDetailPage() {
<div className="w-14 h-14 bg-blue-600/10 rounded-2xl flex items-center justify-center text-blue-400 text-2xl border border-blue-500/20 shadow-inner">
<span className="text-2xl">📝</span>
</div>
<h3 className="text-2xl md:text-3xl font-black uppercase tracking-tighter text-white">Descripción del <span className="text-blue-500">Vendedor</span></h3>
<h3 className="text-2xl md:text-3xl font-black uppercase tracking-tighter text-white">
Descripción del{" "}
<span className="text-blue-500">Vendedor</span>
</h3>
</div>
<p className="text-gray-300 leading-relaxed font-light whitespace-pre-wrap text-base md:text-lg">
{vehicle.description}
@@ -130,18 +203,54 @@ export default function VehiculoDetailPage() {
<div className="w-14 h-14 bg-blue-600/10 rounded-2xl flex items-center justify-center text-blue-400 text-2xl border border-blue-500/20 shadow-inner">
<FaInfoCircle />
</div>
<h3 className="text-2xl md:text-3xl font-black uppercase tracking-tighter text-white">Información <span className="text-blue-500">Técnica</span></h3>
<h3 className="text-2xl md:text-3xl font-black uppercase tracking-tighter text-white">
Información <span className="text-blue-500">Técnica</span>
</h3>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 md:gap-8">
<TechnicalItem label="Kilómetros" value={`${vehicle.km?.toLocaleString()} KM`} icon="🏎️" />
<TechnicalItem label="Combustible" value={vehicle.fuelType} icon="⛽" />
<TechnicalItem label="Transmisión" value={vehicle.transmission} icon="⚙️" />
<TechnicalItem
label="Kilómetros"
value={`${vehicle.km?.toLocaleString()} KM`}
icon="🏎️"
/>
<TechnicalItem
label="Combustible"
value={vehicle.fuelType}
icon="⛽"
/>
<TechnicalItem
label="Transmisión"
value={vehicle.transmission}
icon="⚙️"
/>
<TechnicalItem label="Color" value={vehicle.color} icon="🎨" />
<TechnicalItem label="Segmento" value={vehicle.segment} icon="🚗" />
{vehicle.condition && <TechnicalItem label="Estado" value={vehicle.condition} icon="✨" />}
{vehicle.doorCount && <TechnicalItem label="Puertas" value={vehicle.doorCount} icon="🚪" />}
{vehicle.engineSize && <TechnicalItem label="Motor" value={vehicle.engineSize} icon="⚡" />}
<TechnicalItem
label="Segmento"
value={vehicle.segment}
icon="🚗"
/>
{vehicle.condition && (
<TechnicalItem
label="Estado"
value={vehicle.condition}
icon="✨"
/>
)}
{vehicle.doorCount && (
<TechnicalItem
label="Puertas"
value={vehicle.doorCount}
icon="🚪"
/>
)}
{vehicle.engineSize && (
<TechnicalItem
label="Motor"
value={vehicle.engineSize}
icon="⚡"
/>
)}
</div>
</div>
</div>
@@ -158,7 +267,9 @@ export default function VehiculoDetailPage() {
</div>
<h1 className="text-3xl md:text-4xl font-black tracking-tighter uppercase leading-tight mb-4 text-white">
<span className="text-blue-500 mr-2">{vehicle.brand?.name}</span>
<span className="text-blue-500 mr-2">
{vehicle.brand?.name}
</span>
{vehicle.versionName}
</h1>
@@ -169,57 +280,99 @@ export default function VehiculoDetailPage() {
</div>
<div className="bg-gradient-to-br from-white/5 to-transparent rounded-2xl md:rounded-3xl p-6 md:p-8 mb-8 border border-white/10 shadow-inner group">
<span className="text-gray-500 text-[10px] font-black tracking-widest uppercase block mb-1 opacity-60">Precio</span>
<span className="text-gray-500 text-[10px] font-black tracking-widest uppercase block mb-1 opacity-60">
Precio
</span>
<div className="flex items-baseline gap-2">
<span className="text-blue-400 text-3xl md:text-5xl font-black tracking-tighter">{vehicle.currency} {vehicle.price?.toLocaleString()}</span>
<span className="text-blue-400 text-3xl md:text-5xl font-black tracking-tighter">
{vehicle.price === 0
? "CONSULTAR"
: `${vehicle.currency} ${vehicle.price?.toLocaleString()}`}
</span>
</div>
</div>
{isContactable ? (
<div className="space-y-4">
<a href={getWhatsAppLink(vehicle.contactPhone, `${vehicle.brand?.name} ${vehicle.versionName}`)} target="_blank" rel="noopener noreferrer"
className="w-full glass border border-green-500/30 hover:bg-green-600 text-white py-5 rounded-2xl font-black uppercase tracking-widest transition-all shadow-lg shadow-green-600/20 flex items-center justify-center gap-3 group hover:border-green-500/50">
<FaWhatsapp className="text-3xl group-hover:scale-110 transition-transform text-green-400 group-hover:text-white" />
<span>Contactar</span>
</a>
{/* BOTÓN WHATSAPP: Usamos la nueva variable canShowWhatsApp */}
{canShowWhatsApp && (
<a
href={getWhatsAppLink(
vehicle.contactPhone,
`${vehicle.brand?.name} ${vehicle.versionName}`,
)}
target="_blank"
rel="noopener noreferrer"
className="w-full glass border border-green-500/30 hover:bg-green-600 text-white py-5 rounded-2xl font-black uppercase tracking-widest transition-all shadow-lg shadow-green-600/20 flex items-center justify-center gap-3 group hover:border-green-500/50"
>
<FaWhatsapp className="text-3xl group-hover:scale-110 transition-transform text-green-400 group-hover:text-white" />
<span>Contactar</span>
</a>
)}
{vehicle.contactPhone && (
{/* CAJA DE TELÉFONO: Usamos canShowPhone */}
{canShowPhone && (
<div className="w-full bg-white/5 py-4 rounded-xl border border-white/10 flex items-center justify-center gap-3 text-gray-300 font-black uppercase tracking-[0.2em] text-[10px] shadow-sm">
<span className="text-blue-400">📞</span>
<span className="opacity-60 font-bold">Llamar:</span>
<span className="tracking-widest">{vehicle.contactPhone}</span>
<span className="tracking-widest select-all">
{vehicle.contactPhone}
</span>
</div>
)}
{/* CAJA DE EMAIL: Usamos canShowEmail */}
{canShowEmail && (
<div className="w-full bg-white/5 py-4 rounded-xl border border-white/10 flex items-center justify-center gap-3 text-gray-300 font-black uppercase tracking-[0.1em] text-[9px] shadow-sm overflow-hidden">
<span className="text-blue-400 text-base"></span>
<span className="tracking-widest truncate max-w-[200px] select-all">
{vehicle.contactEmail}
</span>
</div>
)}
</div>
) : (
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-center">
<div className="text-3xl mb-3">
{isOwnerAdmin ? '' :
vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? '⏳' :
vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? '💳' :
vehicle.statusID === AD_STATUSES.SOLD ? '🤝' : '🔒'}
{isOwnerAdmin
? ""
: vehicle.statusID === AD_STATUSES.MODERATION_PENDING
? "⏳"
: vehicle.statusID === AD_STATUSES.PAYMENT_PENDING
? "💳"
: vehicle.statusID === AD_STATUSES.SOLD
? "🤝"
: "🔒"}
</div>
<h3 className="text-lg font-bold text-white uppercase tracking-tight mb-2">
{isOwnerAdmin ? 'Contacto en descripción' :
vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? 'Aviso en Revisión' :
vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? 'Pago Pendiente' :
vehicle.statusID === AD_STATUSES.SOLD ? 'Vehículo Vendido' : 'No disponible'}
{isOwnerAdmin
? "Contacto en descripción"
: vehicle.statusID === AD_STATUSES.MODERATION_PENDING
? "Aviso en Revisión"
: vehicle.statusID === AD_STATUSES.PAYMENT_PENDING
? "Pago Pendiente"
: vehicle.statusID === AD_STATUSES.SOLD
? "Vehículo Vendido"
: "No disponible"}
</h3>
<p className="text-xs text-gray-400 leading-relaxed">
{isOwnerAdmin
? 'Revisa la descripción para contactar al vendedor.'
? "Revisa la descripción para contactar al vendedor."
: vehicle.statusID === AD_STATUSES.MODERATION_PENDING
? 'Este aviso está siendo verificado por un moderador. Estará activo pronto.'
? "Este aviso está siendo verificado por un moderador. Estará activo pronto."
: vehicle.statusID === AD_STATUSES.PAYMENT_PENDING
? 'El pago de este aviso aún no ha sido procesado completamente.'
? "El pago de este aviso aún no ha sido procesado completamente."
: vehicle.statusID === AD_STATUSES.SOLD
? 'Este vehículo ya ha sido vendido a otro usuario.'
: 'Revisa la descripción para contactar al vendedor.'}
? "Este vehículo ya ha sido vendido a otro usuario."
: "Revisa la descripción para contactar al vendedor."}
</p>
</div>
)}
<button onClick={() => handleShare('copy')} className="w-full mt-6 py-4 rounded-xl border border-white/5 text-[9px] font-black uppercase tracking-[0.2em] text-gray-500 hover:text-white hover:bg-white/5 transition-all flex items-center justify-center gap-2">
<button
onClick={() => handleShare("copy")}
className="w-full mt-6 py-4 rounded-xl border border-white/5 text-[9px] font-black uppercase tracking-[0.2em] text-gray-500 hover:text-white hover:bg-white/5 transition-all flex items-center justify-center gap-2"
>
<FaShareAlt /> Compartir Aviso
</button>
</div>
@@ -240,15 +393,26 @@ export default function VehiculoDetailPage() {
);
}
function TechnicalItem({ label, value, icon }: { label: string, value: string, icon: string }) {
if ((value === undefined || value === null || value === '') || value === 'N/A') return null;
function TechnicalItem({
label,
value,
icon,
}: {
label: string;
value: string;
icon: string;
}) {
if (value === undefined || value === null || value === "" || value === "N/A")
return null;
return (
<div className="bg-white/5 p-4 rounded-xl md:rounded-2xl border border-white/5 hover:bg-white/10 transition-colors">
<span className="text-gray-500 text-[9px] md:text-[10px] uppercase font-black tracking-widest block mb-1">{label}</span>
<span className="text-gray-500 text-[9px] md:text-[10px] uppercase font-black tracking-widest block mb-1">
{label}
</span>
<div className="flex items-center gap-2">
<span className="text-sm md:text-base">{icon}</span>
<span className="text-white font-bold text-xs md:text-sm">{value}</span>
</div>
</div>
);
}
}

View File

@@ -31,6 +31,11 @@ export const getImageUrl = (path?: string): string => {
* Formatea un número como moneda ARS o USD.
*/
export const formatCurrency = (amount: number, currency: string = 'ARS') => {
// Lógica para precio 0
if (amount === 0) {
return 'CONSULTAR';
}
return new Intl.NumberFormat('es-AR', {
style: 'currency',
currency: currency,