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

View File

@@ -326,6 +326,9 @@ public class AdsV2Controller : ControllerBase
contactPhone = ad.ContactPhone, contactPhone = ad.ContactPhone,
contactEmail = ad.ContactEmail, contactEmail = ad.ContactEmail,
displayContactInfo = ad.DisplayContactInfo, displayContactInfo = ad.DisplayContactInfo,
showPhone = ad.ShowPhone,
allowWhatsApp = ad.AllowWhatsApp,
showEmail = ad.ShowEmail,
photos = ad.Photos.Select(p => new { p.PhotoID, p.FilePath, p.IsCover, p.SortOrder }), photos = ad.Photos.Select(p => new { p.PhotoID, p.FilePath, p.IsCover, p.SortOrder }),
features = ad.Features.Select(f => new { f.FeatureKey, f.FeatureValue }), features = ad.Features.Select(f => new { f.FeatureKey, f.FeatureValue }),
brand = ad.Brand != null ? new { id = ad.Brand.BrandID, name = ad.Brand.Name } : null, brand = ad.Brand != null ? new { id = ad.Brand.BrandID, name = ad.Brand.Name } : null,
@@ -432,6 +435,9 @@ public class AdsV2Controller : ControllerBase
ContactPhone = request.ContactPhone, ContactPhone = request.ContactPhone,
ContactEmail = request.ContactEmail, ContactEmail = request.ContactEmail,
DisplayContactInfo = request.DisplayContactInfo, DisplayContactInfo = request.DisplayContactInfo,
ShowPhone = request.ShowPhone,
AllowWhatsApp = request.AllowWhatsApp,
ShowEmail = request.ShowEmail,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}; };
@@ -655,6 +661,9 @@ public class AdsV2Controller : ControllerBase
ad.ContactPhone = updatedAdDto.ContactPhone; ad.ContactPhone = updatedAdDto.ContactPhone;
ad.ContactEmail = updatedAdDto.ContactEmail; ad.ContactEmail = updatedAdDto.ContactEmail;
ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo; ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo;
ad.ShowPhone = updatedAdDto.ShowPhone;
ad.AllowWhatsApp = updatedAdDto.AllowWhatsApp;
ad.ShowEmail = updatedAdDto.ShowEmail;
// Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin) // Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin)
// LÓGICA DE ESTADO TRAS RECHAZO // LÓGICA DE ESTADO TRAS RECHAZO

View File

@@ -95,6 +95,15 @@ public class CreateAdRequestDto
[JsonPropertyName("displayContactInfo")] [JsonPropertyName("displayContactInfo")]
public bool DisplayContactInfo { get; set; } = true; public bool DisplayContactInfo { get; set; } = true;
[JsonPropertyName("showPhone")]
public bool ShowPhone { get; set; } = true;
[JsonPropertyName("allowWhatsApp")]
public bool AllowWhatsApp { get; set; } = true;
[JsonPropertyName("showEmail")]
public bool ShowEmail { get; set; } = true;
// --- Admin Only --- // --- Admin Only ---
[JsonPropertyName("targetUserID")] [JsonPropertyName("targetUserID")]

View File

@@ -125,6 +125,11 @@ public class Ad
public string? ContactPhone { get; set; } public string? ContactPhone { get; set; }
public string? ContactEmail { get; set; } public string? ContactEmail { get; set; }
public bool DisplayContactInfo { get; set; } = true; public bool DisplayContactInfo { get; set; } = true;
public bool ShowPhone { get; set; } = true;
public bool AllowWhatsApp { get; set; } = true;
public bool ShowEmail { get; set; } = true;
public bool IsFeatured { get; set; } public bool IsFeatured { get; set; }
public int StatusID { get; set; } public int StatusID { get; set; }

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useSearchParams, Link } from 'react-router-dom'; import { useSearchParams, Link } from "react-router-dom";
import { AdsV2Service, type AdListingDto } from '../services/ads.v2.service'; import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
import { getImageUrl, formatCurrency } from '../utils/app.utils'; import { getImageUrl, formatCurrency } from "../utils/app.utils";
import SearchableSelect from '../components/SearchableSelect'; import SearchableSelect from "../components/SearchableSelect";
import AdStatusBadge from '../components/AdStatusBadge'; import AdStatusBadge from "../components/AdStatusBadge";
import { import {
AUTO_SEGMENTS, AUTO_SEGMENTS,
MOTO_SEGMENTS, MOTO_SEGMENTS,
AUTO_TRANSMISSIONS, AUTO_TRANSMISSIONS,
MOTO_TRANSMISSIONS, MOTO_TRANSMISSIONS,
FUEL_TYPES, FUEL_TYPES,
VEHICLE_CONDITIONS VEHICLE_CONDITIONS,
} from '../constants/vehicleOptions'; } from "../constants/vehicleOptions";
export default function ExplorarPage() { export default function ExplorarPage() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -19,25 +19,29 @@ export default function ExplorarPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [minPrice, setMinPrice] = useState(searchParams.get('minPrice') || ''); const [minPrice, setMinPrice] = useState(searchParams.get("minPrice") || "");
const [maxPrice, setMaxPrice] = useState(searchParams.get('maxPrice') || ''); const [maxPrice, setMaxPrice] = useState(searchParams.get("maxPrice") || "");
const [currencyFilter, setCurrencyFilter] = useState(searchParams.get('currency') || ''); const [currencyFilter, setCurrencyFilter] = useState(
const [minYear, setMinYear] = useState(searchParams.get('minYear') || ''); searchParams.get("currency") || "",
const [maxYear, setMaxYear] = useState(searchParams.get('maxYear') || ''); );
const [brandId, setBrandId] = useState(searchParams.get('brandId') || ''); const [minYear, setMinYear] = useState(searchParams.get("minYear") || "");
const [modelId, setModelId] = useState(searchParams.get('modelId') || ''); const [maxYear, setMaxYear] = useState(searchParams.get("maxYear") || "");
const [fuel, setFuel] = useState(searchParams.get('fuel') || ''); const [brandId, setBrandId] = useState(searchParams.get("brandId") || "");
const [transmission, setTransmission] = useState(searchParams.get('transmission') || ''); 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 [brands, setBrands] = useState<{ id: number; name: string }[]>([]);
const [models, setModels] = useState<{ id: number, name: string }[]>([]); const [models, setModels] = useState<{ id: number; name: string }[]>([]);
const q = searchParams.get('q') || ''; const q = searchParams.get("q") || "";
const c = searchParams.get('c') || 'ALL'; const c = searchParams.get("c") || "ALL";
useEffect(() => { useEffect(() => {
if (c !== 'ALL') { if (c !== "ALL") {
const typeId = c === 'EAUTOS' ? 1 : 2; const typeId = c === "EAUTOS" ? 1 : 2;
AdsV2Service.getBrands(typeId).then(setBrands); AdsV2Service.getBrands(typeId).then(setBrands);
} else { } else {
setBrands([]); setBrands([]);
@@ -61,7 +65,7 @@ export default function ExplorarPage() {
try { try {
const data = await AdsV2Service.getAll({ const data = await AdsV2Service.getAll({
q, q,
c: c === 'ALL' ? undefined : c, c: c === "ALL" ? undefined : c,
minPrice: minPrice ? Number(minPrice) : undefined, minPrice: minPrice ? Number(minPrice) : undefined,
maxPrice: maxPrice ? Number(maxPrice) : undefined, maxPrice: maxPrice ? Number(maxPrice) : undefined,
currency: currencyFilter || undefined, currency: currencyFilter || undefined,
@@ -70,7 +74,7 @@ export default function ExplorarPage() {
brandId: brandId ? Number(brandId) : undefined, brandId: brandId ? Number(brandId) : undefined,
modelId: modelId ? Number(modelId) : undefined, modelId: modelId ? Number(modelId) : undefined,
fuel: fuel || undefined, fuel: fuel || undefined,
transmission: transmission || undefined transmission: transmission || undefined,
}); });
setListings(data); setListings(data);
} catch (err) { } catch (err) {
@@ -86,32 +90,47 @@ export default function ExplorarPage() {
const applyFilters = () => { const applyFilters = () => {
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
if (minPrice) newParams.set('minPrice', minPrice); else newParams.delete('minPrice'); if (minPrice) newParams.set("minPrice", minPrice);
if (maxPrice) newParams.set('maxPrice', maxPrice); else newParams.delete('maxPrice'); else newParams.delete("minPrice");
if (currencyFilter) newParams.set('currency', currencyFilter); else newParams.delete('currency'); if (maxPrice) newParams.set("maxPrice", maxPrice);
if (minYear) newParams.set('minYear', minYear); else newParams.delete('minYear'); else newParams.delete("maxPrice");
if (maxYear) newParams.set('maxYear', maxYear); else newParams.delete('maxYear'); if (currencyFilter) newParams.set("currency", currencyFilter);
if (brandId) newParams.set('brandId', brandId); else newParams.delete('brandId'); else newParams.delete("currency");
if (modelId) newParams.set('modelId', modelId); else newParams.delete('modelId'); if (minYear) newParams.set("minYear", minYear);
if (fuel) newParams.set('fuel', fuel); else newParams.delete('fuel'); else newParams.delete("minYear");
if (transmission) newParams.set('transmission', transmission); else newParams.delete('transmission'); 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); setSearchParams(newParams);
}; };
const clearFilters = () => { const clearFilters = () => {
setMinPrice(''); setMaxPrice(''); setMinYear(''); setMaxYear(''); setMinPrice("");
setCurrencyFilter(''); setMaxPrice("");
setBrandId(''); setModelId(''); setFuel(''); setTransmission(''); setMinYear("");
setMaxYear("");
setCurrencyFilter("");
setBrandId("");
setModelId("");
setFuel("");
setTransmission("");
const newParams = new URLSearchParams(); const newParams = new URLSearchParams();
if (q) newParams.set('q', q); if (q) newParams.set("q", q);
if (c !== 'ALL') newParams.set('c', c); if (c !== "ALL") newParams.set("c", c);
setSearchParams(newParams); setSearchParams(newParams);
}; };
const handleCategoryFilter = (cat: string) => { const handleCategoryFilter = (cat: string) => {
const newParams = new URLSearchParams(); const newParams = new URLSearchParams();
if (q) newParams.set('q', q); if (q) newParams.set("q", q);
if (cat !== 'ALL') newParams.set('c', cat); if (cat !== "ALL") newParams.set("c", cat);
setSearchParams(newParams); 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"> <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 <button
onClick={() => setShowMobileFilters(true)} 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> <span>🔍 FILTRAR</span>
</button> </button>
{/* Sidebar Filters - NATURAL FLOW (NO STICKY, NO SCROLL INTERNO) */} {/* 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 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 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'} ${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=" >
<div
className="
glass p-6 rounded-[2rem] border border-white/5 shadow-2xl glass p-6 rounded-[2rem] border border-white/5 shadow-2xl
h-fit m-6 mt-28 md:m-0 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"> <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"> <div className="flex items-center gap-2">
<button <button
onClick={clearFilters} onClick={clearFilters}
@@ -155,22 +180,32 @@ export default function ExplorarPage() {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Categoría */} {/* Categoría */}
<div> <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 <select
value={c} value={c}
onChange={(e) => handleCategoryFilter(e.target.value)} 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" 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="ALL" className="bg-gray-900">
<option value="EAUTOS" className="bg-gray-900">Automóviles</option> Todos
<option value="EMOTOS" className="bg-gray-900">Motos</option> </option>
<option value="EAUTOS" className="bg-gray-900">
Automóviles
</option>
<option value="EMOTOS" className="bg-gray-900">
Motos
</option>
</select> </select>
</div> </div>
{c !== 'ALL' && ( {c !== "ALL" && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-4 animate-fade-in">
<div> <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 <SearchableSelect
options={brands} options={brands}
value={brandId} value={brandId}
@@ -181,15 +216,25 @@ export default function ExplorarPage() {
{brandId && ( {brandId && (
<div className="animate-fade-in"> <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 <select
value={modelId} 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" 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> <option value="" className="bg-gray-900 text-gray-500">
{models.map(m => ( Todos los modelos
<option key={m.id} value={m.id} className="bg-gray-900 text-white">{m.name}</option> </option>
{models.map((m) => (
<option
key={m.id}
value={m.id}
className="bg-gray-900 text-white"
>
{m.name}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -198,85 +243,231 @@ export default function ExplorarPage() {
)} )}
<div> <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 <select
value={currencyFilter} 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" 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="" className="bg-gray-900 text-gray-500">
<option value="ARS" className="bg-gray-900 text-white">Pesos (ARS)</option> Indistinto
<option value="USD" className="bg-gray-900 text-white">Dólares (USD)</option> </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> </select>
</div> </div>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Precio Máximo</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Desde Año</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Combustible</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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"> Combustible
<option value="" className="bg-gray-900 text-gray-500">Todos</option> </label>
{FUEL_TYPES.map(f => (<option key={f} value={f} className="bg-gray-900 text-white">{f}</option>))} <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> </select>
</div> </div>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Transmisión</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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"> Transmisión
<option value="" className="bg-gray-900 text-gray-500">Todas</option> </label>
{(c === 'EMOTOS' ? MOTO_TRANSMISSIONS : AUTO_TRANSMISSIONS).map(t => ( <select
<option key={t} value={t} className="bg-gray-900 text-white">{t}</option> 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> </select>
</div> </div>
</div> </div>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Color</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Ubicación</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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>
<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> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Estado</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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"> Estado
<option value="" className="bg-gray-900">Todos</option> </label>
{VEHICLE_CONDITIONS.map(o => <option key={o} value={o} className="bg-gray-900">{o}</option>)} <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> </select>
</div> </div>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Segmento</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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"> Segmento
<option value="" className="bg-gray-900">Todos</option> </label>
{(c === 'EMOTOS' ? MOTO_SEGMENTS : AUTO_SEGMENTS).map(o => ( <select
<option key={o} value={o} className="bg-gray-900">{o}</option> 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> </select>
</div> </div>
</div> </div>
{c !== 'EMOTOS' && ( {c !== "EMOTOS" && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Puertas</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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>
<div> <div>
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">Dirección</label> <label className="text-[10px] text-gray-500 uppercase tracking-widest font-black mb-2 block">
<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" /> 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> </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 Aplicar Filtros
</button> </button>
</div> </div>
@@ -285,10 +476,25 @@ export default function ExplorarPage() {
<div className="w-full md:flex-1 md:min-w-0"> <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 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"> <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"> <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" /> <input
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-blue-500 transition-colors">🔍</span> 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>
</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"> <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> </div>
{loading ? ( {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 ? ( ) : 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 ? ( ) : listings.length === 0 ? (
<div className="glass p-20 rounded-[2.5rem] text-center border-dashed border-2 border-white/10"> <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> <span className="text-6xl mb-6 block">🔍</span>
<h3 className="text-2xl font-bold text-gray-400 uppercase tracking-tighter">Sin coincidencias</h3> <h3 className="text-2xl font-bold text-gray-400 uppercase tracking-tighter">
<p className="text-gray-600 max-w-xs mx-auto mt-2 italic">No encontramos vehículos que coincidan con los filtros seleccionados.</p> Sin coincidencias
<button onClick={clearFilters} className="mt-8 text-blue-400 font-black uppercase text-[10px] tracking-widest">Ver todos los avisos</button> </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>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-8">
{listings.map(car => ( {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"> <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"> <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 --- */} {/* --- BLOQUE PARA EL BADGE --- */}
<div className="absolute top-4 left-4 z-10"> <div className="absolute top-4 left-4 z-10">
@@ -331,8 +560,12 @@ export default function ExplorarPage() {
</h3> </h3>
<div className="flex justify-between items-center mt-auto"> <div className="flex justify-between items-center mt-auto">
<div className="flex flex-col"> <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-gray-500 text-[10px] font-black uppercase tracking-widest mb-1">
<span className="text-white font-black text-2xl tracking-tighter">{formatCurrency(car.price, car.currency)}</span> {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> </div>
</div> </div>
@@ -341,6 +574,6 @@ export default function ExplorarPage() {
</div> </div>
)} )}
</div> </div>
</div > </div>
); );
} }

View File

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

View File

@@ -4,7 +4,7 @@ import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import { ChatService, type ChatMessage } from "../services/chat.service"; import { ChatService, type ChatMessage } from "../services/chat.service";
import ChatModal from "../components/ChatModal"; 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 { AD_STATUSES, STATUS_CONFIG } from "../constants/adStatuses";
import ConfirmationModal from "../components/ConfirmationModal"; import ConfirmationModal from "../components/ConfirmationModal";
@@ -374,7 +374,7 @@ export default function MisAvisosPage() {
{av.brandName} {av.versionName} {av.brandName} {av.versionName}
</h3> </h3>
<span className="text-blue-400 font-bold text-lg"> <span className="text-blue-400 font-bold text-lg">
{av.currency} {av.price.toLocaleString()} {formatCurrency(av.price, av.currency)}
</span> </span>
</div> </div>
<div className="flex flex-wrap gap-3 justify-center md:justify-start"> <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 { useState, useEffect, useRef } from "react";
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from "react-router-dom";
import { AdsV2Service } from '../services/ads.v2.service'; import { AdsV2Service } from "../services/ads.v2.service";
import { AuthService } from '../services/auth.service'; import { AuthService } from "../services/auth.service";
import ChatModal from '../components/ChatModal'; import ChatModal from "../components/ChatModal";
import { FaWhatsapp, FaMapMarkerAlt, FaInfoCircle, FaShareAlt } from 'react-icons/fa'; import {
import { AD_STATUSES } from '../constants/adStatuses'; FaWhatsapp,
import AdStatusBadge from '../components/AdStatusBadge'; FaMapMarkerAlt,
import PremiumGallery from '../components/PremiumGallery'; 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() { export default function VehiculoDetailPage() {
const { id } = useParams(); const { id } = useParams();
@@ -43,7 +48,9 @@ export default function VehiculoDetailPage() {
}, [id, user?.id]); }, [id, user?.id]);
useEffect(() => { useEffect(() => {
return () => { viewRegistered.current = false; }; return () => {
viewRegistered.current = false;
};
}, [id]); }, [id]);
const handleFavoriteToggle = async () => { const handleFavoriteToggle = async () => {
@@ -52,52 +59,102 @@ export default function VehiculoDetailPage() {
if (isFavorite) await AdsV2Service.removeFavorite(user.id, Number(id)); if (isFavorite) await AdsV2Service.removeFavorite(user.id, Number(id));
else await AdsV2Service.addFavorite(user.id, Number(id)!); else await AdsV2Service.addFavorite(user.id, Number(id)!);
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
} catch (err) { console.error(err); } } catch (err) {
console.error(err);
}
}; };
const getWhatsAppLink = (phone: string, title: string) => { const getWhatsAppLink = (phone: string, title: string) => {
if (!phone) return '#'; if (!phone) return "#";
let number = phone.replace(/[^\d]/g, ''); let number = phone.replace(/[^\d]/g, "");
if (number.startsWith('0')) number = number.substring(1); if (number.startsWith("0")) number = number.substring(1);
if (!number.startsWith('54')) number = `549${number}`; if (!number.startsWith("54")) number = `549${number}`;
const message = `Hola, vi tu aviso "${title}" en Motores Argentinos y me interesa.`; const message = `Hola, vi tu aviso "${title}" en Motores Argentinos y me interesa.`;
return `https://wa.me/${number}?text=${encodeURIComponent(message)}`; 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 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!`; const text = `Mira este ${vehicleTitle} en Motores Argentinos!`;
switch (platform) { switch (platform) {
case 'wa': window.open(`https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`, '_blank'); break; case "wa":
case 'fb': window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank'); break; window.open(
case 'copy': navigator.clipboard.writeText(url); alert('Enlace copiado al portapapeles'); break; `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 ( if (loading)
return (
<div className="flex flex-col items-center justify-center p-40 gap-6"> <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> <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> <span className="text-gray-500 font-black uppercase tracking-widest text-xs animate-pulse">
Cargando...
</span>
</div> </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 isOwnerAdmin = vehicle.ownerUserType === 3;
const isAdActive = vehicle.statusID === AD_STATUSES.ACTIVE; 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 ( return (
<div className="container mx-auto px-4 md:px-6 py-6 md:py-12 animate-fade-in-up"> <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"> <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="/" className="hover:text-white transition-colors">
<Link to="/explorar" className="hover:text-white transition-colors">Explorar</Link> / Inicio
<span className="text-blue-400 truncate">{vehicle.brand?.name} {vehicle.versionName || 'Detalle'}</span> </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> </nav>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-12 relative items-start"> <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) */} {/* 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"> <div className="lg:col-span-2 space-y-8 md:space-y-12 order-1 lg:order-1">
<PremiumGallery <PremiumGallery
@@ -106,8 +163,21 @@ export default function VehiculoDetailPage() {
isFavorite={isFavorite} isFavorite={isFavorite}
onFavoriteToggle={handleFavoriteToggle} onFavoriteToggle={handleFavoriteToggle}
statusBadge={<AdStatusBadge statusId={vehicle.statusID} />} 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>} featuredBadge={
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>} 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) */} {/* 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"> <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> <span className="text-2xl">📝</span>
</div> </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> </div>
<p className="text-gray-300 leading-relaxed font-light whitespace-pre-wrap text-base md:text-lg"> <p className="text-gray-300 leading-relaxed font-light whitespace-pre-wrap text-base md:text-lg">
{vehicle.description} {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"> <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 /> <FaInfoCircle />
</div> </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>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 md:gap-8"> <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
<TechnicalItem label="Combustible" value={vehicle.fuelType} icon="⛽" /> label="Kilómetros"
<TechnicalItem label="Transmisión" value={vehicle.transmission} icon="⚙️" /> 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="Color" value={vehicle.color} icon="🎨" />
<TechnicalItem label="Segmento" value={vehicle.segment} icon="🚗" /> <TechnicalItem
{vehicle.condition && <TechnicalItem label="Estado" value={vehicle.condition} icon="✨" />} label="Segmento"
{vehicle.doorCount && <TechnicalItem label="Puertas" value={vehicle.doorCount} icon="🚪" />} value={vehicle.segment}
{vehicle.engineSize && <TechnicalItem label="Motor" value={vehicle.engineSize} icon="⚡" />} 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> </div>
</div> </div>
@@ -158,7 +267,9 @@ export default function VehiculoDetailPage() {
</div> </div>
<h1 className="text-3xl md:text-4xl font-black tracking-tighter uppercase leading-tight mb-4 text-white"> <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} {vehicle.versionName}
</h1> </h1>
@@ -169,57 +280,99 @@ export default function VehiculoDetailPage() {
</div> </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"> <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"> <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>
</div> </div>
{isContactable ? ( {isContactable ? (
<div className="space-y-4"> <div className="space-y-4">
<a href={getWhatsAppLink(vehicle.contactPhone, `${vehicle.brand?.name} ${vehicle.versionName}`)} target="_blank" rel="noopener noreferrer" {/* BOTÓN WHATSAPP: Usamos la nueva variable canShowWhatsApp */}
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"> {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" /> <FaWhatsapp className="text-3xl group-hover:scale-110 transition-transform text-green-400 group-hover:text-white" />
<span>Contactar</span> <span>Contactar</span>
</a> </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"> <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="text-blue-400">📞</span>
<span className="opacity-60 font-bold">Llamar:</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> </div>
) : ( ) : (
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-center"> <div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-center">
<div className="text-3xl mb-3"> <div className="text-3xl mb-3">
{isOwnerAdmin ? '' : {isOwnerAdmin
vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? '⏳' : ? ""
vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? '💳' : : vehicle.statusID === AD_STATUSES.MODERATION_PENDING
vehicle.statusID === AD_STATUSES.SOLD ? '🤝' : '🔒'} ? "⏳"
: vehicle.statusID === AD_STATUSES.PAYMENT_PENDING
? "💳"
: vehicle.statusID === AD_STATUSES.SOLD
? "🤝"
: "🔒"}
</div> </div>
<h3 className="text-lg font-bold text-white uppercase tracking-tight mb-2"> <h3 className="text-lg font-bold text-white uppercase tracking-tight mb-2">
{isOwnerAdmin ? 'Contacto en descripción' : {isOwnerAdmin
vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? 'Aviso en Revisión' : ? "Contacto en descripción"
vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? 'Pago Pendiente' : : vehicle.statusID === AD_STATUSES.MODERATION_PENDING
vehicle.statusID === AD_STATUSES.SOLD ? 'Vehículo Vendido' : 'No disponible'} ? "Aviso en Revisión"
: vehicle.statusID === AD_STATUSES.PAYMENT_PENDING
? "Pago Pendiente"
: vehicle.statusID === AD_STATUSES.SOLD
? "Vehículo Vendido"
: "No disponible"}
</h3> </h3>
<p className="text-xs text-gray-400 leading-relaxed"> <p className="text-xs text-gray-400 leading-relaxed">
{isOwnerAdmin {isOwnerAdmin
? 'Revisa la descripción para contactar al vendedor.' ? "Revisa la descripción para contactar al vendedor."
: vehicle.statusID === AD_STATUSES.MODERATION_PENDING : 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 : 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 : vehicle.statusID === AD_STATUSES.SOLD
? 'Este vehículo ya ha sido vendido a otro usuario.' ? "Este vehículo ya ha sido vendido a otro usuario."
: 'Revisa la descripción para contactar al vendedor.'} : "Revisa la descripción para contactar al vendedor."}
</p> </p>
</div> </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 <FaShareAlt /> Compartir Aviso
</button> </button>
</div> </div>
@@ -240,11 +393,22 @@ export default function VehiculoDetailPage() {
); );
} }
function TechnicalItem({ label, value, icon }: { label: string, value: string, icon: string }) { function TechnicalItem({
if ((value === undefined || value === null || value === '') || value === 'N/A') return null; label,
value,
icon,
}: {
label: string;
value: string;
icon: string;
}) {
if (value === undefined || value === null || value === "" || value === "N/A")
return null;
return ( return (
<div className="bg-white/5 p-4 rounded-xl md:rounded-2xl border border-white/5 hover:bg-white/10 transition-colors"> <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"> <div className="flex items-center gap-2">
<span className="text-sm md:text-base">{icon}</span> <span className="text-sm md:text-base">{icon}</span>
<span className="text-white font-bold text-xs md:text-sm">{value}</span> <span className="text-white font-bold text-xs md:text-sm">{value}</span>

View File

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