Files
MotoresArgentinosV2/Frontend/src/App.tsx
dmolinari e096ed1590 Feat: Seguridad avanzada para cambio de email y gestión de MFA
- Backend: Implementada lógica de tokens para cambio de mail y desactivación de 2FA.
- Frontend: Nuevos flujos de verificación en Perfil y Panel de Seguridad.
2026-02-12 15:24:32 -03:00

338 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
import HomePage from './pages/HomePage';
import ExplorarPage from './pages/ExplorarPage';
import VehiculoDetailPage from './pages/VehiculoDetailPage';
import PublicarAvisoPage from './pages/PublicarAvisoPage';
import MisAvisosPage from './pages/MisAvisosPage';
import SuccessPage from './pages/SuccessPage';
import AdminPage from './pages/AdminPage';
import LoginModal from './components/LoginModal';
import { type UserSession } from './services/auth.service';
import VerifyEmailPage from './pages/VerifyEmailPage';
import ResetPasswordPage from './pages/ResetPasswordPage';
import PerfilPage from './pages/PerfilPage';
import SeguridadPage from './pages/SeguridadPage';
import { FaHome, FaSearch, FaCar, FaUser, FaShieldAlt } from 'react-icons/fa';
import { initMercadoPago } from '@mercadopago/sdk-react';
import { AuthProvider, useAuth } from './context/AuthContext';
import ConfirmEmailChangePage from './pages/ConfirmEmailChangePage';
function AdminGuard({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
if (loading) return <div className="min-h-screen flex items-center justify-center bg-[#0a0c10]"><div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div></div>;
if (!user || user.userType !== 3) return <Navigate to="/" replace />;
return <>{children}</>;
}
// COMPONENTE NAVBAR CON DROPDOWN
function Navbar() {
const { user, logout, login, unreadCount } = useAuth();
const [showLoginModal, setShowLoginModal] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const [showMobileMenu, setShowMobileMenu] = useState(false);
const isAdmin = user?.userType === 3;
const navigate = useNavigate();
const location = useLocation();
const handleLogout = () => {
logout();
setShowUserMenu(false);
setShowMobileMenu(false);
navigate('/');
};
const handleMobileNavClick = (path: string) => {
setShowMobileMenu(false);
navigate(path);
};
const handleLoginSuccess = (userSession: UserSession) => {
login(userSession);
setShowLoginModal(false);
};
const getLinkClass = (path: string) => {
const isActive = location.pathname === path;
return `transition-all duration-300 font-bold tracking-widest text-s hover:text-white ${isActive ? 'text-blue-400 drop-shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'text-gray-300'}`;
};
return (
<>
<nav className="sticky top-0 z-[100] border-b border-white/10 bg-[#0a0c10]/80 backdrop-blur-xl shadow-2xl transition-all duration-300">
<div className="container mx-auto px-6 h-20 flex justify-between items-center">
<Link to="/" className="flex items-center gap-3 group">
<div className="w-10 h-10 bg-gradient-to-tr from-blue-600 to-cyan-400 rounded-xl flex items-center justify-center shadow-lg shadow-blue-600/20 group-hover:shadow-blue-500/40 group-hover:scale-105 transition-all duration-300">
<span className="text-white font-black text-xl">M</span>
</div>
<div className="flex flex-col">
<span className="text-xl font-black tracking-tighter leading-none text-white group-hover:text-blue-100 transition-colors">MOTORES</span>
<span className="text-[10px] tracking-[0.3em] font-bold text-blue-400 leading-none group-hover:text-blue-300 transition-colors">ARGENTINOS</span>
</div>
</Link>
<div className="hidden md:flex items-center gap-10">
<Link to="/" className={getLinkClass('/')}>HOME</Link>
<Link to="/explorar" className={getLinkClass('/explorar')}>EXPLORAR</Link>
<Link to="/vender" className={`transition-all duration-300 font-bold tracking-widest hover:text-white ${location.pathname === '/vender' || location.pathname === '/publicar' ? 'text-blue-400 drop-shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'text-gray-300'}`}>
PUBLICAR
</Link>
{/* --- 2. ENLACE DE GESTIÓN --- */}
<Link to="/mis-avisos" className="relative">
<span className={getLinkClass('/mis-avisos')}>MIS AVISOS</span>
{user && unreadCount > 0 && (
<span className="absolute -top-1.5 -right-3.5 w-4 h-4 bg-red-600 text-white text-[9px] font-bold rounded-full flex items-center justify-center border-2 border-[#0a0c10] animate-pulse">
{unreadCount}
</span>
)}
</Link>
{isAdmin && (
<Link to="/admin" className={`transition-all duration-300 font-bold tracking-widest text-xs flex items-center gap-2 hover:text-white ${location.pathname === '/admin' ? 'text-blue-400' : 'text-gray-300'}`}>
<span className="text-sm">🛡</span> ADMIN
</Link>
)}
</div>
<div className="flex items-center gap-4">
{/* Botón de menú hamburguesa para móvil - Mejorado */}
<button
onClick={() => setShowMobileMenu(!showMobileMenu)}
className="md:hidden w-11 h-11 rounded-xl glass border border-white/10 flex flex-col items-center justify-center gap-1.5 group focus:outline-none hover:border-blue-500/50 transition-all shadow-lg"
aria-label="Menú de navegación"
>
<span className={`w-5 h-0.5 bg-gradient-to-r from-blue-400 to-cyan-400 rounded-full transition-all duration-300 ${showMobileMenu ? 'rotate-45 translate-y-2' : 'group-hover:w-6'}`}></span>
<span className={`w-5 h-0.5 bg-gradient-to-r from-blue-400 to-cyan-400 rounded-full transition-all duration-300 ${showMobileMenu ? 'opacity-0' : 'group-hover:w-6'}`}></span>
<span className={`w-5 h-0.5 bg-gradient-to-r from-blue-400 to-cyan-400 rounded-full transition-all duration-300 ${showMobileMenu ? '-rotate-45 -translate-y-2' : 'group-hover:w-6'}`}></span>
</button>
{user ? (
<div className="relative">
<button
onClick={() => setShowUserMenu(!showUserMenu)}
onBlur={() => setTimeout(() => setShowUserMenu(false), 200)}
className="flex items-center gap-3 group focus:outline-none"
>
<div className="text-right hidden lg:block">
<span className="text-[9px] font-bold text-gray-400 uppercase tracking-widest block group-hover:text-blue-400 transition-colors">Hola</span>
<span className="text-xs font-black text-white uppercase tracking-wider group-hover:text-blue-200 transition-colors">
{user.firstName || user.username}
</span>
</div>
<div className="w-10 h-10 bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl border border-white/10 flex items-center justify-center group-hover:border-blue-500/50 transition-all shadow-lg">
<span className="text-sm">👤</span>
</div>
</button>
{showUserMenu && (
<div className="absolute right-0 top-full mt-2 w-48 bg-[#161a22] border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-fade-in z-50">
<Link to="/perfil" className="block px-5 py-3 text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors border-b border-white/5">
👤 Mi Perfil
</Link>
<Link to="/seguridad" className="block px-5 py-3 text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors border-b border-white/5">
🛡 Seguridad
</Link>
<button
onClick={handleLogout}
className="block w-full text-left px-5 py-3 text-xs font-black text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-colors uppercase tracking-widest"
>
Cerrar Sesión
</button>
</div>
)}
</div>
) : (
<button
onClick={() => setShowLoginModal(true)}
className="bg-blue-600 hover:bg-blue-500 text-white px-4 md:px-7 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all shadow-lg shadow-blue-600/30 hover:shadow-blue-500/50 hover:-translate-y-0.5 flex items-center gap-2"
>
Ingresar
</button>
)}
</div>
</div>
</nav>
{/* Menú móvil overlay MODERNIZADO */}
{showMobileMenu && (
<div
className="fixed inset-0 top-[70px] z-[90] bg-black/95 backdrop-blur-3xl md:hidden animate-fade-in-up flex flex-col p-5 overflow-y-auto border-t border-white/10"
>
<div className="flex flex-col gap-3 mt-2">
<button
onClick={() => handleMobileNavClick('/')}
className={`group flex items-center gap-3.5 p-2.5 rounded-2xl transition-all ${location.pathname === '/' ? 'bg-white/10' : 'hover:bg-white/5'}`}
>
<div className={`w-9 h-9 rounded-xl flex items-center justify-center text-lg transition-all ${location.pathname === '/' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30' : 'bg-white/5 text-gray-400 group-hover:bg-white/10 group-hover:text-white'}`}>
<FaHome />
</div>
<span className={`text-lg font-black uppercase italic tracking-tighter ${location.pathname === '/' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
Home
</span>
</button>
<button
onClick={() => handleMobileNavClick('/explorar')}
className={`group flex items-center gap-3.5 p-2.5 rounded-2xl transition-all ${location.pathname === '/explorar' ? 'bg-white/10' : 'hover:bg-white/5'}`}
>
<div className={`w-9 h-9 rounded-xl flex items-center justify-center text-lg transition-all ${location.pathname === '/explorar' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30' : 'bg-white/5 text-gray-400 group-hover:bg-white/10 group-hover:text-white'}`}>
<FaSearch />
</div>
<span className={`text-lg font-black uppercase italic tracking-tighter ${location.pathname === '/explorar' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
Explorar
</span>
</button>
<button
onClick={() => handleMobileNavClick('/vender')}
className={`group flex items-center gap-3.5 p-2.5 rounded-2xl transition-all ${(location.pathname === '/vender' || location.pathname === '/publicar') ? 'bg-white/10' : 'hover:bg-white/5'}`}
>
<div className={`w-9 h-9 rounded-xl flex items-center justify-center text-lg transition-all ${(location.pathname === '/vender' || location.pathname === '/publicar') ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30' : 'bg-white/5 text-gray-400 group-hover:bg-white/10 group-hover:text-white'}`}>
<FaCar />
</div>
<span className={`text-lg font-black uppercase italic tracking-tighter ${(location.pathname === '/vender' || location.pathname === '/publicar') ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
Publicar
</span>
</button>
<button
onClick={() => handleMobileNavClick('/mis-avisos')}
className={`group flex items-center gap-3.5 p-2.5 rounded-2xl transition-all relative ${location.pathname === '/mis-avisos' ? 'bg-white/10' : 'hover:bg-white/5'}`}
>
<div className={`w-9 h-9 rounded-xl flex items-center justify-center text-lg transition-all ${location.pathname === '/mis-avisos' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30' : 'bg-white/5 text-gray-400 group-hover:bg-white/10 group-hover:text-white'}`}>
<FaUser />
</div>
<div className="flex flex-col">
<span className={`text-lg font-black uppercase italic tracking-tighter ${location.pathname === '/mis-avisos' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
Mis Avisos
</span>
{user && unreadCount > 0 && (
<span className="text-xs font-bold text-red-400 uppercase tracking-widest mt-1">
{unreadCount} mensaje(s) nuevo(s)
</span>
)}
</div>
{user && unreadCount > 0 && (
<span className="absolute top-6 right-6 w-3 h-3 bg-red-500 rounded-full animate-pulse shadow-lg shadow-red-500/50"></span>
)}
</button>
{isAdmin && (
<button
onClick={() => handleMobileNavClick('/admin')}
className={`group flex items-center gap-3.5 p-2.5 rounded-2xl transition-all ${location.pathname === '/admin' ? 'bg-white/10' : 'hover:bg-white/5'}`}
>
<div className={`w-9 h-9 rounded-xl flex items-center justify-center text-lg transition-all ${location.pathname === '/admin' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30' : 'bg-white/5 text-gray-400 group-hover:bg-white/10 group-hover:text-white'}`}>
<FaShieldAlt />
</div>
<span className={`text-lg font-black uppercase italic tracking-tighter ${location.pathname === '/admin' ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
Admin
</span>
</button>
)}
</div>
<div className="mt-auto pt-10 pb-6 text-center">
<p className="text-xs text-gray-600 font-bold uppercase tracking-[0.2em]">Motores Argentinos v2.0</p>
</div>
</div>
)}
{showLoginModal && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-md animate-fade-in p-4">
<div className="relative w-full max-w-md">
<LoginModal
onSuccess={handleLoginSuccess}
onClose={() => setShowLoginModal(false)}
/>
</div>
</div>
)}
</>
);
}
function FooterLegal() {
const currentYear = new Date().getFullYear();
const baseEdition = 5858;
const baseDate = new Date('2026-01-21T00:00:00');
const today = new Date();
today.setHours(0, 0, 0, 0);
baseDate.setHours(0, 0, 0, 0);
const diffTime = today.getTime() - baseDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
const currentEdition = baseEdition + (diffDays > 0 ? diffDays : 0);
return (
<footer className="border-t border-white/5 py-8 md:py-12 bg-black/40 mt-auto backdrop-blur-lg">
<div className="container mx-auto px-4 md:px-6 text-center">
<div className="flex flex-col gap-2 md:gap-2 text-[11px] md:text-[10px] text-gray-500 uppercase tracking-wider font-medium leading-relaxed max-w-4xl mx-auto">
<p>© {currentYear} MotoresArgentinos. Todos los derechos reservados. <span className="text-gray-400 font-bold ml-1">Edición número: {currentEdition}.</span></p>
<p>Registro DNDA : RL-2024-70042723-APN-DNDA#MJ - Propietario: Publiéxito S.A.</p>
<p>Director: Leonardo Mario Forclaz - 46 N 423 - La Plata - Pcia. de Bs. As.</p>
</div>
</div>
</footer>
);
}
function MainLayout() {
const { loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen bg-[#0a0c10] flex items-center justify-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<div className="min-h-screen bg-[#0a0c10] text-gray-100 font-sans selection:bg-blue-500/30 flex flex-col">
<Navbar />
<main className="relative flex-grow">
<div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-600/5 blur-[120px] rounded-full z-0 pointer-events-none"></div>
<div className="fixed bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-cyan-400/5 blur-[120px] rounded-full z-0 pointer-events-none"></div>
<div className="relative z-10">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/explorar" element={<ExplorarPage />} />
<Route path="/vehiculo/:id" element={<VehiculoDetailPage />} />
<Route path="/publicar" element={<PublicarAvisoPage />} />
<Route path="/vender" element={<PublicarAvisoPage />} />
<Route path="/mis-avisos" element={<MisAvisosPage />} />
<Route path="/pago-confirmado" element={<SuccessPage />} />
<Route path="/restablecer-clave" element={<ResetPasswordPage />} />
<Route path="/verificar-email" element={<VerifyEmailPage />} />
<Route path="/perfil" element={<PerfilPage />} />
<Route path="/seguridad" element={<SeguridadPage />} />
<Route path="/confirmar-cambio-email" element={<ConfirmEmailChangePage />} />
<Route path="/admin" element={
<AdminGuard>
<AdminPage />
</AdminGuard>
} />
</Routes>
</div>
</main>
<FooterLegal />
</div>
);
}
function App() {
useEffect(() => {
const mpPublicKey = import.meta.env.VITE_MP_PUBLIC_KEY;
if (mpPublicKey) initMercadoPago(mpPublicKey);
}, []);
return (
<AuthProvider>
<Router>
<MainLayout />
</Router>
</AuthProvider>
);
}
export default App;