Init Commit

This commit is contained in:
2026-01-29 13:43:44 -03:00
commit b9aa8478db
126 changed files with 20649 additions and 0 deletions

336
Frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,336 @@
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';
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="/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;