From ed243ccb78320930aa24d98250e412938eb8f0a7 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 13 Feb 2026 11:23:16 -0300 Subject: [PATCH] =?UTF-8?q?Fix:=20Ajustes=20UI/UX=20y=20Seguridad=20Produc?= =?UTF-8?q?ci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AuthController.cs | 4 +- Frontend/src/components/LoginModal.tsx | 659 +++++++++++++----- Frontend/src/pages/PublicarAvisoPage.tsx | 218 ++++-- 3 files changed, 634 insertions(+), 247 deletions(-) diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs index f109819..2a782fd 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs @@ -34,8 +34,8 @@ public class AuthController : ControllerBase { HttpOnly = true, // Seguridad: JS no puede leer esto Expires = DateTime.UtcNow.AddMinutes(15), - Secure = false, // Solo HTTPS (Para tests locales 'Secure = false' temporalmente) - SameSite = SameSiteMode.Lax, // Protección CSRF (Strict para máxima seguridad, pero puede ser Lax si hay problemas con redirecciones y testeos locales) + Secure = true, // Solo HTTPS (Para tests locales 'Secure = false' temporalmente) + SameSite = SameSiteMode.Strict, // Protección CSRF (Strict para máxima seguridad, pero puede ser Lax si hay problemas con redirecciones y testeos locales) IsEssential = true }; Response.Cookies.Append(cookieName, token, cookieOptions); diff --git a/Frontend/src/components/LoginModal.tsx b/Frontend/src/components/LoginModal.tsx index 8b889e2..04e8987 100644 --- a/Frontend/src/components/LoginModal.tsx +++ b/Frontend/src/components/LoginModal.tsx @@ -1,8 +1,8 @@ // src/components/LoginModal.tsx -import { useState } from 'react'; -import { AuthService, type UserSession } from '../services/auth.service'; -import { QRCodeSVG } from 'qrcode.react'; +import { useState, useEffect, useRef } from "react"; +import { AuthService, type UserSession } from "../services/auth.service"; +import { QRCodeSVG } from "qrcode.react"; interface Props { onSuccess: (user: UserSession) => void; @@ -11,91 +11,190 @@ interface Props { // --- ICONOS SVG --- const EyeIcon = () => ( - - - + + + ); const EyeSlashIcon = () => ( - - + + ); const CheckCircleIcon = () => ( - - + + ); const XCircleIcon = () => ( - - + + ); const NeutralCircleIcon = () => ( - - + + ); export default function LoginModal({ onSuccess, onClose }: Props) { // Toggle entre Login y Registro - const [mode, setMode] = useState<'LOGIN' | 'REGISTER'>('LOGIN'); + const [mode, setMode] = useState<"LOGIN" | "REGISTER">("LOGIN"); // Estados de Login - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); // Para controlar si mostramos la opción de reenviar email no verificado const [showResend, setShowResend] = useState(false); - const [unverifiedEmail, setUnverifiedEmail] = useState(''); + const [unverifiedEmail, setUnverifiedEmail] = useState(""); // Estados de recuperación de clave - const [forgotEmail, setForgotEmail] = useState(''); + const [forgotEmail, setForgotEmail] = useState(""); // Estados de Registro const [regData, setRegData] = useState({ - firstName: '', - lastName: '', - email: '', - username: '', - phone: '', - password: '', - confirmPassword: '' + firstName: "", + lastName: "", + email: "", + username: "", + phone: "", + password: "", + confirmPassword: "", }); // Estados para Migración / Nueva Clave - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [showNewPass, setShowNewPass] = useState(false); const [showConfirmPass, setShowConfirmPass] = useState(false); // Estados Generales - const [mfaCode, setMfaCode] = useState(''); - const [qrData, setQrData] = useState<{ uri: string, secret: string } | null>(null); - const [step, setStep] = useState<'LOGIN' | 'MIGRATE' | 'MFA' | 'MFA_SETUP' | 'FORGOT' | 'MFA_PROMPT'>('LOGIN'); + const [mfaCode, setMfaCode] = useState(""); + const [qrData, setQrData] = useState<{ uri: string; secret: string } | null>( + null, + ); + const [step, setStep] = useState< + "LOGIN" | "MIGRATE" | "MFA" | "MFA_SETUP" | "FORGOT" | "MFA_PROMPT" + >("LOGIN"); const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [successMessage, setSuccessMessage] = useState(''); + const [error, setError] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); const [tempUser, setTempUser] = useState(null); + // Refs para los inputs + const usernameRef = useRef(null); + const mfaRef = useRef(null); + const forgotEmailRef = useRef(null); + const registerFirstNameRef = useRef(null); + const migratePassRef = useRef(null); + // Validaciones - const activePassword = step === 'MIGRATE' ? newPassword : (mode === 'REGISTER' ? regData.password : ''); - const activeConfirm = step === 'MIGRATE' ? confirmPassword : (mode === 'REGISTER' ? regData.confirmPassword : ''); + const activePassword = + step === "MIGRATE" + ? newPassword + : mode === "REGISTER" + ? regData.password + : ""; + const activeConfirm = + step === "MIGRATE" + ? confirmPassword + : mode === "REGISTER" + ? regData.confirmPassword + : ""; const validations = { length: activePassword.length >= 8, upper: /[A-Z]/.test(activePassword), number: /\d/.test(activePassword), special: /[\W_]/.test(activePassword), - match: activePassword.length > 0 && activePassword === activeConfirm + match: activePassword.length > 0 && activePassword === activeConfirm, }; + // Manejo automático de focos + useEffect(() => { + // 1. Foco en Login (al abrir o cambiar a modo login) + if (step === "LOGIN" && mode === "LOGIN") { + // Pequeño timeout para asegurar que el modal terminó de animar + setTimeout(() => usernameRef.current?.focus(), 100); + } + // 2. Foco en MFA (cuando el backend pide el código) + else if (step === "MFA" || step === "MFA_SETUP") { + setTimeout(() => mfaRef.current?.focus(), 100); + } + // 3. Foco en Recuperar Contraseña + else if (step === "FORGOT") { + setTimeout(() => forgotEmailRef.current?.focus(), 100); + } + // 4. Foco en Registro + else if (step === "LOGIN" && mode === "REGISTER") { + setTimeout(() => registerFirstNameRef.current?.focus(), 100); + } + // 5. Foco en Migración + else if (step === "MIGRATE") { + setTimeout(() => migratePassRef.current?.focus(), 100); + } + }, [step, mode]); + const handleSafeClose = () => { - if (step === 'MFA_PROMPT' && tempUser) { + if (step === "MFA_PROMPT" && tempUser) { onSuccess(tempUser); } else { onClose(); @@ -104,30 +203,30 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); - setSuccessMessage(''); + setError(""); + setSuccessMessage(""); setShowResend(false); setLoading(true); try { const res = await AuthService.login(username, password); - if (res.status === 'MIGRATION_REQUIRED') { - setStep('MIGRATE'); - } else if (res.status === 'MFA_SETUP_REQUIRED') { + if (res.status === "MIGRATION_REQUIRED") { + setStep("MIGRATE"); + } else if (res.status === "MFA_SETUP_REQUIRED") { setQrData({ uri: res.qrUri, secret: res.secret }); - setStep('MFA_SETUP'); - } else if (res.status === 'TOTP_REQUIRED') { - setStep('MFA'); - } else if (res.status === 'SUCCESS' && res.user) { + setStep("MFA_SETUP"); + } else if (res.status === "TOTP_REQUIRED") { + setStep("MFA"); + } else if (res.status === "SUCCESS" && res.user) { if (res.recommendMfa) { setTempUser(res.user); - setStep('MFA_PROMPT'); + setStep("MFA_PROMPT"); } else { onSuccess(res.user); } } } catch (err: any) { - const msg = err.response?.data?.message || 'Error al iniciar sesión'; + const msg = err.response?.data?.message || "Error al iniciar sesión"; setError(msg); if (msg.includes("verificar tu email") || msg === "EMAIL_NOT_VERIFIED") { setShowResend(true); @@ -141,14 +240,16 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleForgot = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); - setError(''); - setSuccessMessage(''); + setError(""); + setSuccessMessage(""); try { const res = await AuthService.forgotPassword(forgotEmail); setSuccessMessage(res.message); } catch (err: any) { - setError(err.response?.data?.message || 'Error al procesar la solicitud.'); + setError( + err.response?.data?.message || "Error al procesar la solicitud.", + ); } finally { setLoading(false); } @@ -157,7 +258,7 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleResendClick = async () => { if (!unverifiedEmail) return; setLoading(true); - setError(''); + setError(""); try { await AuthService.resendVerification(unverifiedEmail); setSuccessMessage("Correo reenviado. Revisa tu bandeja de entrada."); @@ -171,25 +272,31 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); - setSuccessMessage(''); + setError(""); + setSuccessMessage(""); // Validación de Email básica const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(regData.email)) { - setError('Por favor, ingresa un correo electrónico válido.'); + setError("Por favor, ingresa un correo electrónico válido."); return; } // Validación de Username (longitud) if (regData.username.length < 4) { - setError('El usuario debe tener al menos 4 caracteres.'); + setError("El usuario debe tener al menos 4 caracteres."); return; } // Validación de Contraseña (Requisitos) - if (!validations.length || !validations.upper || !validations.number || !validations.special || !validations.match) { - setError('Por favor, verifique los requisitos de la contraseña.'); + if ( + !validations.length || + !validations.upper || + !validations.number || + !validations.special || + !validations.match + ) { + setError("Por favor, verifique los requisitos de la contraseña."); return; } @@ -201,14 +308,24 @@ export default function LoginModal({ onSuccess, onClose }: Props) { firstName: regData.firstName, lastName: regData.lastName, phoneNumber: regData.phone, - password: regData.password + password: regData.password, }); - setSuccessMessage('¡Cuenta creada con éxito! Revisa tu email para activarla.'); - setRegData({ firstName: '', lastName: '', email: '', username: '', phone: '', password: '', confirmPassword: '' }); - setTimeout(() => setMode('LOGIN'), 3000); + setSuccessMessage( + "¡Cuenta creada con éxito! Revisa tu email para activarla.", + ); + setRegData({ + firstName: "", + lastName: "", + email: "", + username: "", + phone: "", + password: "", + confirmPassword: "", + }); + setTimeout(() => setMode("LOGIN"), 3000); } catch (err: any) { - setError(err.response?.data?.message || 'Error al crear la cuenta.'); + setError(err.response?.data?.message || "Error al crear la cuenta."); } finally { setLoading(false); } @@ -216,7 +333,7 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleVerifyMFA = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); + setError(""); setLoading(true); try { const userToVerify = tempUser?.username || username; @@ -225,7 +342,7 @@ export default function LoginModal({ onSuccess, onClose }: Props) { onSuccess(user); } } catch (err: any) { - setError('Código inválido o expirado'); + setError("Código inválido o expirado"); } finally { setLoading(false); } @@ -233,27 +350,34 @@ export default function LoginModal({ onSuccess, onClose }: Props) { const handleMigrate = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); + setError(""); - if (!validations.length || !validations.upper || !validations.number || !validations.special) { - setError('La contraseña no cumple con los requisitos de seguridad.'); + if ( + !validations.length || + !validations.upper || + !validations.number || + !validations.special + ) { + setError("La contraseña no cumple con los requisitos de seguridad."); return; } if (!validations.match) { - setError('Las contraseñas no coinciden.'); + setError("Las contraseñas no coinciden."); return; } setLoading(true); try { await AuthService.migratePassword(username, newPassword); - setSuccessMessage("¡Contraseña actualizada con éxito! Por favor, inicie sesión."); - setStep('LOGIN'); - setPassword(''); - setNewPassword(''); - setConfirmPassword(''); + setSuccessMessage( + "¡Contraseña actualizada con éxito! Por favor, inicie sesión.", + ); + setStep("LOGIN"); + setPassword(""); + setNewPassword(""); + setConfirmPassword(""); } catch (err: any) { - setError('Error al actualizar contraseña. Intente nuevamente.'); + setError("Error al actualizar contraseña. Intente nuevamente."); } finally { setLoading(false); } @@ -264,7 +388,7 @@ export default function LoginModal({ onSuccess, onClose }: Props) { try { const data = await AuthService.initMFA(); setQrData({ uri: data.qrUri, secret: data.secret }); - setStep('MFA_SETUP'); + setStep("MFA_SETUP"); } catch (err) { setError("No se pudo iniciar la configuración de seguridad."); } finally { @@ -277,11 +401,25 @@ export default function LoginModal({ onSuccess, onClose }: Props) { }; // --- COMPONENTE DE REQUISITOS (EN 2 COLUMNAS) --- - const RequirementItem = ({ isValid, text }: { isValid: boolean, text: string }) => { + const RequirementItem = ({ + isValid, + text, + }: { + isValid: boolean; + text: string; + }) => { const isNeutral = activePassword.length === 0; return ( -
  • - {isNeutral ? : isValid ? : } +
  • + {isNeutral ? ( + + ) : isValid ? ( + + ) : ( + + )} {text}
  • ); @@ -290,32 +428,41 @@ export default function LoginModal({ onSuccess, onClose }: Props) { // COMPONENTE DE LISTA DE REQUISITOS (GRID) const PasswordRequirements = () => (
    -

    Seguridad de la clave:

    +

    + Seguridad de la clave: +

    ); const getTitle = () => { - if (step === 'MIGRATE') return 'Renovar Clave'; - if (step === 'MFA' || step === 'MFA_SETUP') return 'Seguridad'; - if (step === 'FORGOT') return 'Recuperar Clave'; - if (step === 'MFA_PROMPT') return 'Protege tu Cuenta'; - return mode === 'LOGIN' ? 'Ingresar' : 'Crear Cuenta'; + if (step === "MIGRATE") return "Renovar Clave"; + if (step === "MFA" || step === "MFA_SETUP") return "Seguridad"; + if (step === "FORGOT") return "Recuperar Clave"; + if (step === "MFA_PROMPT") return "Protege tu Cuenta"; + return mode === "LOGIN" ? "Ingresar" : "Crear Cuenta"; }; return ( // CAMBIO: max-w-lg (más ancho) y overflow-hidden para evitar scrollbars feos
    - - {loading &&
    } + {loading && ( +
    + )} @@ -349,8 +508,17 @@ export default function LoginModal({ onSuccess, onClose }: Props) { {error && (
    - - + + {error}
    @@ -364,33 +532,42 @@ export default function LoginModal({ onSuccess, onClose }: Props) { )} {/* --- FORMULARIO LOGIN --- */} - {step === 'LOGIN' && mode === 'LOGIN' && ( + {step === "LOGIN" && mode === "LOGIN" && (
    - + setUsername(e.target.value)} + onChange={(e) => setUsername(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-white outline-none focus:border-blue-500 transition-all placeholder:text-gray-600 focus:bg-white/10" placeholder="Tu nombre de usuario" />
    - + setPassword(e.target.value)} + onChange={(e) => setPassword(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-white outline-none focus:border-blue-500 transition-all placeholder:text-gray-600 focus:bg-white/10" placeholder="••••••••" />
    - {showResend && ( @@ -413,7 +593,7 @@ export default function LoginModal({ onSuccess, onClose }: Props) { disabled={loading} className="bg-amber-500/20 hover:bg-amber-500/30 text-amber-400 text-xs font-bold py-2 px-4 rounded-lg uppercase tracking-widest transition-all w-full" > - {loading ? 'Enviando...' : 'Reenviar Email'} + {loading ? "Enviando..." : "Reenviar Email"}
    )} @@ -425,62 +605,115 @@ export default function LoginModal({ onSuccess, onClose }: Props) { )} {/* --- FORMULARIO OLVIDÉ CLAVE --- */} - {step === 'FORGOT' && ( + {step === "FORGOT" && (

    - Ingresa tu email o usuario. Te enviaremos un enlace para restablecer tu clave. + Ingresa tu email o usuario. Te enviaremos un enlace para + restablecer tu clave.

    - - setForgotEmail(e.target.value)} + + setForgotEmail(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-white outline-none focus:border-blue-500 transition-all placeholder:text-gray-600" - placeholder="ejemplo@email.com" /> + placeholder="ejemplo@email.com" + />
    - - )} {/* --- FORMULARIO REGISTRO --- */} - {step === 'LOGIN' && mode === 'REGISTER' && ( + {step === "LOGIN" && mode === "REGISTER" && (
    - - setRegData({ ...regData, firstName: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" /> + + + setRegData({ ...regData, firstName: e.target.value }) + } + className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" + />
    - - setRegData({ ...regData, lastName: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" /> + + + setRegData({ ...regData, lastName: e.target.value }) + } + className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" + />
    - - setRegData({ ...regData, email: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" placeholder="email@ejemplo.com" /> + + + setRegData({ ...regData, email: e.target.value }) + } + className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" + placeholder="email@ejemplo.com" + />
    - + { - const cleanValue = e.target.value.toLowerCase().replace(/[^a-z0-9]/g, ''); + onChange={(e) => { + const cleanValue = e.target.value + .toLowerCase() + .replace(/[^a-z0-9]/g, ""); setRegData({ ...regData, username: cleanValue }); }} className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500 placeholder:text-gray-700" @@ -491,121 +724,190 @@ export default function LoginModal({ onSuccess, onClose }: Props) {

    - - setRegData({ ...regData, phone: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" /> + + + setRegData({ ...regData, phone: e.target.value }) + } + className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-white text-sm outline-none focus:border-blue-500" + />
    {/* PASSWORD Y CONFIRMACIÓN */}
    - setRegData({ ...regData, password: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl pl-3 pr-10 py-2 text-white text-sm outline-none focus:border-blue-500 placeholder:text-gray-600" placeholder="Contraseña" /> -
    - setRegData({ ...regData, confirmPassword: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-xl pl-3 pr-10 py-2 text-white text-sm outline-none focus:border-blue-500 placeholder:text-gray-600" placeholder="Confirmar Contraseña" /> -
    {/* LISTA DE REQUISITOS EN GRID */} -
    -
    )} {/* --- MFA SETUP --- */} - {step === 'MFA_SETUP' && qrData && ( + {step === "MFA_SETUP" && qrData && (
    -

    Configuración de Seguridad

    +

    + Configuración de Seguridad +

    -

    Escanea con Google Authenticator o Authy

    +

    + Escanea con Google Authenticator o Authy +

    - {qrData.secret} + + {qrData.secret} +
    - + setMfaCode(e.target.value.replace(/\D/g, ''))} + onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ""))} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-4 text-center text-4xl font-black tracking-[0.5em] text-white outline-none focus:border-blue-500 transition-all placeholder:opacity-10" />
    -
    )} {/* --- MFA LOGIN --- */} - {step === 'MFA' && ( + {step === "MFA" && (
    -
    🛡️
    -

    Autenticación de 2 Factores

    +
    + 🛡️ +
    +

    + Autenticación de 2 Factores +

    - Tu cuenta está protegida. Ingresa el código temporal de tu aplicación. + Tu cuenta está protegida. Ingresa el código temporal de tu + aplicación.

    setMfaCode(e.target.value.replace(/\D/g, ''))} + onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ""))} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-4 text-center text-3xl font-black tracking-[0.2em] text-white outline-none focus:border-blue-500 transition-all placeholder:opacity-10" />
    - -
    )} {/* --- MIGRACIÓN PASSWORD --- */} - {step === 'MIGRATE' && ( + {step === "MIGRATE" && (

    - Actualización Requerida - Bienvenido a la nueva plataforma. Por seguridad, detectamos que tu usuario proviene del sistema anterior. + + Actualización Requerida + + Bienvenido a la nueva plataforma. Por seguridad, detectamos que tu + usuario proviene del sistema anterior.

    {/* Nueva Contraseña */}
    - +
    setNewPassword(e.target.value)} + onChange={(e) => setNewPassword(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl pl-4 pr-12 py-3.5 text-white outline-none focus:border-green-500 transition-all placeholder:text-gray-600" placeholder="Escribe tu nueva clave" /> @@ -621,15 +923,20 @@ export default function LoginModal({ onSuccess, onClose }: Props) { {/* Repetir Contraseña */}
    - +
    setConfirmPassword(e.target.value)} - className={`w-full bg-white/5 border rounded-xl pl-4 pr-12 py-3.5 text-white outline-none transition-all placeholder:text-gray-600 ${confirmPassword && confirmPassword !== newPassword ? 'border-red-500/50' : 'border-white/10 focus:border-green-500' - }`} + onChange={(e) => setConfirmPassword(e.target.value)} + className={`w-full bg-white/5 border rounded-xl pl-4 pr-12 py-3.5 text-white outline-none transition-all placeholder:text-gray-600 ${ + confirmPassword && confirmPassword !== newPassword + ? "border-red-500/50" + : "border-white/10 focus:border-green-500" + }`} placeholder="Confirma tu nueva clave" />
    - )} {/* --- MFA PROMPT --- */} - {step === 'MFA_PROMPT' && ( + {step === "MFA_PROMPT" && (
    -
    @@ -664,11 +973,17 @@ export default function LoginModal({ onSuccess, onClose }: Props) {
    -

    ¡Protege tu Cuenta!

    +

    + ¡Protege tu Cuenta! +

    Detectamos que no tienes activada la autenticación de dos pasos. -

    - Actívala ahora para evitar accesos no autorizados y asegurar tus publicaciones. +
    +
    + + Actívala ahora para evitar accesos no autorizados y asegurar tus + publicaciones. +

    @@ -691,4 +1006,4 @@ export default function LoginModal({ onSuccess, onClose }: Props) { )}
    ); -} \ No newline at end of file +} diff --git a/Frontend/src/pages/PublicarAvisoPage.tsx b/Frontend/src/pages/PublicarAvisoPage.tsx index 5743a6e..62f9f06 100644 --- a/Frontend/src/pages/PublicarAvisoPage.tsx +++ b/Frontend/src/pages/PublicarAvisoPage.tsx @@ -1,29 +1,42 @@ -import { useState, useEffect } from 'react'; -import { useSearchParams, useNavigate } from 'react-router-dom'; // Importar useNavigate -import { AvisosService } from '../services/avisos.service'; -import { AdsV2Service } from '../services/ads.v2.service'; -import { AuthService, type UserSession } from '../services/auth.service'; -import type { DatosAvisoDto } from '../types/aviso.types'; -import FormularioAviso from '../components/FormularioAviso'; -import LoginModal from '../components/LoginModal'; +import { useState, useEffect } from "react"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import { AvisosService } from "../services/avisos.service"; +import { AdsV2Service } from "../services/ads.v2.service"; +import { AuthService, type UserSession } from "../services/auth.service"; +import type { DatosAvisoDto } from "../types/aviso.types"; +import FormularioAviso from "../components/FormularioAviso"; +import LoginModal from "../components/LoginModal"; const TAREAS_DISPONIBLES = [ - { id: 'EAUTOS', label: 'Automóviles', icon: '🚗', description: 'Venta de Autos, Camionetas y Utilitarios' }, - { id: 'EMOTOS', label: 'Motos', icon: '🏍️', description: 'Venta de Motos, Cuatriciclos y Náutica' }, + { + id: "EAUTOS", + label: "Automóviles", + icon: "🚗", + description: "Venta de Autos, Camionetas y Utilitarios", + }, + { + id: "EMOTOS", + label: "Motos", + icon: "🏍️", + description: "Venta de Motos, Cuatriciclos y Náutica", + }, ]; export default function PublicarAvisoPage() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); // Hook de navegación - const editId = searchParams.get('edit'); + const editId = searchParams.get("edit"); - const [categorySelection, setCategorySelection] = useState(''); + const [categorySelection, setCategorySelection] = useState(""); const [tarifas, setTarifas] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [planSeleccionado, setPlanSeleccionado] = useState(null); + const [planSeleccionado, setPlanSeleccionado] = + useState(null); const [fixedCategory, setFixedCategory] = useState(null); - const [user, setUser] = useState(AuthService.getCurrentUser()); + const [user, setUser] = useState( + AuthService.getCurrentUser(), + ); useEffect(() => { if (editId) { @@ -31,30 +44,24 @@ export default function PublicarAvisoPage() { } }, [editId]); + useEffect(() => { + if (planSeleccionado) { + window.scrollTo({ top: 0, behavior: "instant" }); + } + }, [planSeleccionado]); + const cargarAvisoParaEdicion = async (id: number) => { setLoading(true); try { const ad = await AdsV2Service.getById(id); // Determinamos la categoría para cargar las tarifas correspondientes - const categoryCode = ad.vehicleTypeID === 1 ? 'EAUTOS' : 'EMOTOS'; + const categoryCode = ad.vehicleTypeID === 1 ? "EAUTOS" : "EMOTOS"; setCategorySelection(categoryCode); - // 🟢 FIX: Bloquear el cambio de categoría + // Bloquear el cambio de categoría setFixedCategory(categoryCode); - - // 🟢 FIX: NO seleccionamos plan automáticamente. - // Dejamos que el usuario elija el plan en las tarjetas. - // (Eliminamos todo el bloque de setPlanSeleccionado) - - /* BLOQUE ELIMINADO: - const tarifasData = await AvisosService.obtenerConfiguracion('EMOTORES', ad.isFeatured ? 1 : 0); - const tarifaReal = tarifasData[0]; - if (!tarifaReal) throw new Error("Tarifa no encontrada"); - setPlanSeleccionado({ ... }); - */ - } catch (err) { console.error(err); setError("Error al cargar el aviso."); @@ -71,8 +78,8 @@ export default function PublicarAvisoPage() { setError(null); try { const [simple, destacado] = await Promise.all([ - AvisosService.obtenerConfiguracion('EMOTORES', 0), - AvisosService.obtenerConfiguracion('EMOTORES', 1) + AvisosService.obtenerConfiguracion("EMOTORES", 0), + AvisosService.obtenerConfiguracion("EMOTORES", 1), ]); const planes = [...simple, ...destacado]; @@ -90,26 +97,30 @@ export default function PublicarAvisoPage() { }, [categorySelection]); const handleSelectPlan = (plan: DatosAvisoDto) => { - const vehicleTypeId = categorySelection === 'EAUTOS' ? 1 : 2; - const nombrePlanAmigable = plan.paquete === 1 ? 'PLAN DESTACADO' : 'PLAN ESTÁNDAR'; + const vehicleTypeId = categorySelection === "EAUTOS" ? 1 : 2; + const nombrePlanAmigable = + plan.paquete === 1 ? "PLAN DESTACADO" : "PLAN ESTÁNDAR"; setPlanSeleccionado({ ...plan, idRubro: vehicleTypeId, - nomavi: nombrePlanAmigable + nomavi: nombrePlanAmigable, }); }; // Manejador centralizado de éxito const handleSuccess = (adId: number, isAdminAction: boolean = false) => { - const status = isAdminAction ? 'admin_created' : 'approved'; + const status = isAdminAction ? "admin_created" : "approved"; navigate(`/pago-confirmado?status=${status}&adId=${adId}`); }; if (!user) { return (
    - setUser(u)} onClose={() => navigate('/')} /> + setUser(u)} + onClose={() => navigate("/")} + />
    ); } @@ -120,11 +131,15 @@ export default function PublicarAvisoPage() { return (
    -
    - Publicando como: {user.username} + Publicando como:{" "} + {user.username}
    -

    Comienza a Vender

    -

    Selecciona una categoría para ver los planes de publicación.

    +

    + Comienza a Vender +

    +

    + Selecciona una categoría para ver los planes de publicación. +

    {/* SECCIÓN DE CATEGORÍA */}
    - {TAREAS_DISPONIBLES.map(t => { + {TAREAS_DISPONIBLES.map((t) => { // Lógica de bloqueo const isDisabled = fixedCategory && fixedCategory !== t.id; @@ -157,16 +176,24 @@ export default function PublicarAvisoPage() { disabled={!!isDisabled} // Deshabilitar botón className={` glass-card p-6 md:p-10 rounded-[2rem] md:rounded-[2.5rem] flex items-center justify-between group transition-all text-left - ${categorySelection === t.id ? 'border-blue-500 scale-[1.02] shadow-2xl shadow-blue-600/10 bg-white/5' : 'hover:bg-white/5'} - ${isDisabled ? 'opacity-30 cursor-not-allowed grayscale' : 'cursor-pointer'} + ${categorySelection === t.id ? "border-blue-500 scale-[1.02] shadow-2xl shadow-blue-600/10 bg-white/5" : "hover:bg-white/5"} + ${isDisabled ? "opacity-30 cursor-not-allowed grayscale" : "cursor-pointer"} `} >
    - Categoría -

    {t.label}

    -

    {t.description}

    + + Categoría + +

    + {t.label} +

    +

    + {t.description} +

    - {t.icon} + + {t.icon} + ); })} @@ -177,27 +204,38 @@ export default function PublicarAvisoPage() {
    -

    Planes Disponibles

    -

    Para {categorySelection === 'EAUTOS' ? 'Automóviles' : 'Motos'}

    +

    + Planes Disponibles +

    +

    + Para {categorySelection === "EAUTOS" ? "Automóviles" : "Motos"} +

    - Precios finales (IVA Incluido) + + Precios finales (IVA Incluido) +
    {error && (
    -

    Error de Conexión

    +

    + Error de Conexión +

    {error}

    )} {loading ? ( -
    +
    +
    +
    ) : (
    {tarifas.map((tarifa, idx) => { - const precioRaw = tarifa.importeTotsiniva > 0 - ? tarifa.importeTotsiniva * 1.105 - : tarifa.importeSiniva * 1.105; + const precioRaw = + tarifa.importeTotsiniva > 0 + ? tarifa.importeTotsiniva * 1.105 + : tarifa.importeSiniva * 1.105; const precioFinal = Math.round(precioRaw); const esDestacado = tarifa.paquete === 1; const tituloPlan = esDestacado ? "DESTACADO" : "ESTÁNDAR"; @@ -206,14 +244,20 @@ export default function PublicarAvisoPage() { : "Presencia esencial. Tu aviso aparecerá en el listado general de búsqueda."; return ( -
    handleSelectPlan(tarifa)} - className={`glass-card p-8 rounded-[2.5rem] flex flex-col group cursor-pointer relative overflow-hidden transition-all hover:-translate-y-2 hover:shadow-2xl ${esDestacado ? 'border-blue-500/30 hover:border-blue-500 hover:shadow-blue-900/20' : 'hover:border-white/30'}`}> - -
    - {esDestacado ? 'RECOMENDADO' : 'BÁSICO'} +
    handleSelectPlan(tarifa)} + className={`glass-card p-8 rounded-[2.5rem] flex flex-col group cursor-pointer relative overflow-hidden transition-all hover:-translate-y-2 hover:shadow-2xl ${esDestacado ? "border-blue-500/30 hover:border-blue-500 hover:shadow-blue-900/20" : "hover:border-white/30"}`} + > +
    + {esDestacado ? "RECOMENDADO" : "BÁSICO"}
    -

    +

    {tituloPlan}

    @@ -224,33 +268,61 @@ export default function PublicarAvisoPage() {
    • - Plataforma - SOLO WEB + + Plataforma + + + SOLO WEB +
    • - Duración - {tarifa.cantidadDias} Días + + Duración + + + {tarifa.cantidadDias} Días +
    • - Fotos - Hasta 5 + + Fotos + + + Hasta 5 +
    • - Visibilidad - {esDestacado ? 'ALTA ⭐' : 'NORMAL'} + + Visibilidad + + + {esDestacado ? "ALTA ⭐" : "NORMAL"} +
    - Precio Final + + Precio Final +
    - ${precioFinal.toLocaleString('es-AR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + $ + {precioFinal.toLocaleString("es-AR", { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + })} + + + ARS - ARS
    -
    @@ -263,4 +335,4 @@ export default function PublicarAvisoPage() { )}
    ); -} \ No newline at end of file +}