Sistema de Notificaciones y Baja One-Click

This commit is contained in:
2026-03-12 13:52:33 -03:00
parent f1a9bb9099
commit 96fca4d9c7
21 changed files with 1384 additions and 79 deletions

View File

@@ -17,6 +17,10 @@ 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';
import BajaExitosaPage from './pages/BajaExitosaPage';
import BajaErrorPage from './pages/BajaErrorPage';
import ProcessUnsubscribePage from './pages/ProcessUnsubscribePage';
import CondicionesPage from './pages/CondicionesPage';
function AdminGuard({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
@@ -270,6 +274,11 @@ function FooterLegal() {
<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 className="mt-1 pt-1 border-t border-white/5">
<Link to="/condiciones" className="text-blue-400/60 hover:text-blue-400 transition-colors font-bold tracking-widest text-[9px] uppercase">
Términos y Condiciones
</Link>
</div>
</div>
</div>
</footer>
@@ -307,6 +316,10 @@ function MainLayout() {
<Route path="/perfil" element={<PerfilPage />} />
<Route path="/seguridad" element={<SeguridadPage />} />
<Route path="/confirmar-cambio-email" element={<ConfirmEmailChangePage />} />
<Route path="/baja/procesar" element={<ProcessUnsubscribePage />} />
<Route path="/baja-exitosa" element={<BajaExitosaPage />} />
<Route path="/baja-error" element={<BajaErrorPage />} />
<Route path="/condiciones" element={<CondicionesPage />} />
<Route path="/admin" element={
<AdminGuard>
<AdminPage />

View File

@@ -0,0 +1,60 @@
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
/**
* Página pública que el backend redirige cuando el token de baja es inválido o expirado.
*/
export default function BajaErrorPage() {
const [show, setShow] = useState(false);
useEffect(() => {
const t = setTimeout(() => setShow(true), 80);
return () => clearTimeout(t);
}, []);
return (
<div className="min-h-screen bg-[#0a0c10] flex items-center justify-center p-6">
<div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-red-600/5 blur-[120px] rounded-full pointer-events-none" />
<div className="fixed bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-orange-400/5 blur-[120px] rounded-full pointer-events-none" />
<div
className={`relative max-w-md w-full glass p-10 rounded-[2.5rem] border border-white/10 shadow-2xl text-center
transition-all duration-700 ${show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'}`}
>
{/* Ícono */}
<div className="w-20 h-20 bg-red-600/15 rounded-full flex items-center justify-center mx-auto mb-6 border border-red-500/20 shadow-lg shadow-red-500/10">
<svg className="w-10 h-10 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
</div>
{/* Título */}
<h1 className="text-3xl font-black uppercase tracking-tighter text-white mb-3">
Enlace <span className="text-red-400">Inválido</span>
</h1>
<p className="text-gray-400 text-sm font-medium leading-relaxed mb-8">
El enlace de baja que usaste es inválido, ya fue utilizado o expiró.
Si querés gestionar tus preferencias, iniciá sesión y accedé a tu perfil.
</p>
<div className="flex flex-col gap-3">
<Link
to="/perfil"
className="inline-block bg-blue-600 hover:bg-blue-500 text-white py-3.5 px-10 rounded-2xl
text-[11px] font-black uppercase tracking-widest transition-all shadow-lg shadow-blue-600/20
hover:shadow-blue-500/40 hover:-translate-y-0.5 active:scale-95"
>
Ir a Mi Perfil
</Link>
<Link
to="/"
className="inline-block text-gray-500 hover:text-gray-300 text-xs font-bold uppercase
tracking-widest transition-colors"
>
Ir al Inicio
</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import { useSearchParams, Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
/**
* Página pública que el backend redirige tras procesar una baja exitosa.
* No requiere inicio de sesión.
*/
export default function BajaExitosaPage() {
const [params] = useSearchParams();
const categoria = params.get('categoria') ?? 'la lista de correos';
const [show, setShow] = useState(false);
// Animación de entrada suave
useEffect(() => {
const t = setTimeout(() => setShow(true), 80);
return () => clearTimeout(t);
}, []);
return (
<div className="min-h-screen bg-[#0a0c10] flex items-center justify-center p-6">
{/* Fondo decorativo */}
<div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-green-600/5 blur-[120px] rounded-full pointer-events-none" />
<div className="fixed bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-cyan-400/5 blur-[120px] rounded-full pointer-events-none" />
<div
className={`relative max-w-md w-full glass p-10 rounded-[2.5rem] border border-white/10 shadow-2xl text-center
transition-all duration-700 ${show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'}`}
>
{/* Ícono */}
<div className="w-20 h-20 bg-green-600/15 rounded-full flex items-center justify-center mx-auto mb-6 border border-green-500/20 shadow-lg shadow-green-500/10">
<svg className="w-10 h-10 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
{/* Título */}
<h1 className="text-3xl font-black uppercase tracking-tighter text-white mb-3">
Baja <span className="text-green-400">Procesada</span>
</h1>
<p className="text-gray-400 text-sm font-medium leading-relaxed mb-2">
Te diste de baja exitosamente de:
</p>
<p className="text-white font-black text-base uppercase tracking-widest mb-8 px-4 py-2 bg-white/5 rounded-xl border border-white/10">
{categoria}
</p>
<p className="text-gray-500 text-xs font-medium mb-8 leading-relaxed">
Ya no recibirás correos de esta categoría.
Podés volver a activarlos en cualquier momento desde la sección{' '}
<span className="text-blue-400 font-bold">Preferencias de Notificación</span> en tu perfil.
</p>
<Link
to="/"
className="inline-block bg-blue-600 hover:bg-blue-500 text-white py-3.5 px-10 rounded-2xl
text-[11px] font-black uppercase tracking-widest transition-all shadow-lg shadow-blue-600/20
hover:shadow-blue-500/40 hover:-translate-y-0.5 active:scale-95"
>
Ir al Inicio
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,177 @@
import { useEffect, useState } from 'react';
/**
* Página de Términos y Condiciones.
*/
export default function CondicionesPage() {
const [show, setShow] = useState(false);
useEffect(() => {
window.scrollTo(0, 0);
const t = setTimeout(() => setShow(true), 100);
return () => clearTimeout(t);
}, []);
return (
<div className="container mx-auto px-6 py-12 max-w-4xl">
<div className={`transition-all duration-700 ${show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'}`}>
<div className="mb-12">
<h1 className="text-5xl font-black tracking-tighter uppercase mb-2">
Términos y <span className="text-blue-500">Condiciones</span>
</h1>
<p className="text-gray-500 font-bold tracking-widest uppercase text-xs">
Información legal y condiciones de uso del sitio
</p>
</div>
<div className="glass p-8 md:p-12 rounded-[2.5rem] border border-white/5 space-y-8 relative overflow-hidden">
{/* Decoración sutil de fondo */}
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 blur-[100px] rounded-full -mr-32 -mt-32 pointer-events-none" />
<div className="prose prose-invert max-w-none">
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
1. ACEPTACIÓN DE LAS CONDICIONES.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
El servicio de publicación de avisos de venta de vehículos (en adelante el "Servicio") es ofrecido por El Día S.A., en su carácter de responsable de la operación y explotación del sitio motoresargentinos.com (de aquí en más el "Sitio") a los anunciantes y/o usuarios (en adelante individualmente el "Usuario" y genéricamente los "Usuarios") que accedan y se registren en el Sitio motoresargentinos.com a fines de proceder con una o más publicaciones, con la condición de que acepten sin ninguna objeción todas las condiciones que se describen a continuación. Asimismo, debido a que ciertos contenidos que puedan ser accedidos a través del Sitio podrán estar alcanzados por normas específicas que reglamenten y complementen las presentes, se recomienda a los Usuarios tomar conocimiento específico de ellas a través del sitio.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
2. ALCANCE DE LAS CONDICIONES.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Además de lo estipulado en la última parte del apartado precedente, las presentes condiciones y las normas que los complementan sólo serán aplicables a los servicios y contenidos prestados y/o accesibles directamente en el Sitio y no a aquellos a los que los Usuarios puedan acceder a través de un hipervínculo (link), una barra co-branded, y/o cualquier otra herramienta de navegación ubicada en el Sitio que los lleve a navegar un recurso diferente.
La probable aparición de dichos links en el Sitio no implica de modo alguno la asunción de garantía por parte del Sitio sobre los productos, servicios, o programas contenidos en ninguna página vinculada al Sitio por tales links. Se declara no haber revisado ninguna de las páginas a las que pueda llegar a accederse desde el Sitio y, en consecuencia, se deslinda toda responsabilidad por el contenido de las mismas. En atención a ello, la utilización de los links para navegar hacia cualquier otra página queda al exclusivo criterio y riesgo de los Usuarios.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
3. INGRESO Y PUBLICACIÓN DE CLASIFICADOS.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
A fin de publicar un aviso clasificado a través del Sitio, el Usuario deberá registrarse e ingresar de manera correcta sus datos personales, así como el texto, fotos y demás información del aviso que pretende publicar. El Usuario deberá aceptar la totalidad de las condiciones de contratación.
Los Usuarios aceptan que sólo podrán publicarse aquellos vehículos cuyas características se encuentren contenidas en los nomencladores de marcas, modelos y versiones bajo los cuales opera el Sitio.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
4. PUBLICACIÓN EN EL SITIO WEB.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Cumplidos los requisitos del apartado anterior, se publicarán los avisos en el Sitio por treinta (30) días, pudiendo ser los mismos modificados (excepto la información de marca, modelo y año de fabricación) y/o dados de baja por el Usuario.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
5. ESPACIO ASIGNADO EN EL SERVICIO.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
El Sitio tiene la libre facultad de establecer y modificar la cantidad de espacio mínimo y máximo de MB que el Usuario oferente puede utilizar, para publicar los avisos clasificados.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
5. ESPACIO ASIGNADO EN EL SERVICIO.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
El Sitio tiene la libre facultad de establecer y modificar la cantidad de espacio mínimo y máximo de MB que el Usuario oferente puede utilizar, para publicar los avisos clasificados.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
6. CALIDAD DE LOS PRODUCTOS PROMOCIONADOS.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
El Sitio no pretende contener una lista exhaustiva de todos los productos del mercado y no manifiesta ni garantiza de modo alguno que no existan otros productos en el mercado, incluso más convenientes, en precio o condiciones, como así tampoco que no existan otros productos que cumplan la misma función que los publicados en el Sitio.
En consecuencia, sugiere firmemente que la información brindada por el Sitio respecto de los productos publicados, sea objeto de una investigación independiente y propia de quien esté interesado en la misma, no asumiendo ningún tipo de responsabilidad por la incorrección de la información, su desactualización o falsedad.
Tampoco asume ninguna obligación respecto del Usuario y/o los visitantes en general y se limita tan sólo a publicar en el Sitio en forma similar a aquella en que lo haría una guía telefónica o la sección clasificados de un periódico impreso, los datos de los Usuarios proveedores de productos o servicios que han solicitado tal publicación y en la forma en que tales datos han sido proporcionados por tales Usuarios.
Tampoco garantiza en forma alguna dichos productos y servicios, ya sea respecto de su calidad, condiciones de entrega, como respecto de ningún otro aspecto, ni garantiza a los Usuarios y/o visitantes en general respecto de la existencia, crédito, capacidad ni sobre ningún otro aspecto de los Usuarios proveedores de tales productos y servicios.
El contrato de compraventa de productos o la contratación de los servicios con sus proveedores se realiza fuera de la esfera de participación del sitio y sin su intervención, directamente entre el Usuario oferente y quien resulte comprador del producto o servicio por aquél ofrecido. En virtud de ello, no otorga garantía de evicción ni por vicios ocultos o aparentes de los bienes publicados por el Usuario oferente y adquiridos por quien resulte comprador.
Asimismo, los Usuarios aceptan que el sitio no controla, ni supervisa, el cumplimiento de los requisitos legales para ofrecer y vender los productos o servicios, ni sobre la capacidad y legitimación de los Usuarios oferentes para promocionar, ofrecer y/o vender sus bienes o servicios.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
7. PROCEDIMIENTO
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
MotoresArgentinos.com coloca gratuitamente a disposición de los usuarios, cierta cantidad de precios informados por ciertos proveedores de determinados bienes y servicios del sector automotor.
Si el usuario está interesado en adquirir alguno de los productos que figuran en MotoresArgentinos.com, deberá hacerlo saber a través de la opción existente en la página.
MotoresArgentinos.com informará al proveedor del producto en que el usuario esté interesado a fin de que tal proveedor contacte directamente al usuario.
La remuneración que percibe MotoresArgentinos.com del proveedor está determinada por un monto fijo en relación a la publicación (clasificado) activa en MotoresArgentinos.com. que no garantiza en modo alguno que el Proveedor respete tal precio o tales condiciones al momento de contratar y, en consecuencia, no se hace responsable de ningún gasto que hubiera podido realizar el usuario ni de ningún daño que hubiera podido sufrir el usuario sobre la base de su asunción de que tal precio o tales condiciones serían mantenidos.
Debe tenerse presente que se debe cumplir con el artículo 7 de la Ley de Defensa del Consumidor e informar la fecha precisa de comienzo y finalización de la oferta así como sus modalidades condiciones y limitaciones y, si el proveedor limita cuantitativamente su oferta, informar la cantidad con que cuenta para cubrirla; en caso de que se otorgue financiamiento deberá consignarse precio de contado, saldo de deuda, el total de los intereses a pagar, la tasa de interés efectiva anual, la forma de amortización de los intereses, otros gastos si los hubiere, cantidad de pagos a realizar y su periodicidad, gastos extras o adicionales si los hubiera y monto total financiado a pagar.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
8. CONDUCTA DE LOS USUARIOS.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Los Usuarios se comprometen a no utilizar el Servicio para:
a. ofrecer o publicar material ilegal, difamatorio, obsceno, pornográfico, racista, discriminatorio, agraviante, injurioso o que afecte la privacidad de las personas;
b. ofrecer o publicar fotografías, propias o de terceros, cuya imagen sea obscena, inmoral o contraria a las buenas costumbres;
c. ofrecer o publicar material mediante la falsificación de su identidad;
d. ofrecer o publicar material en infracción a la ley; violar cualquier legislación aplicable local, federal, nacional o internacional.
e. ofrecer, publicar o crear una base de datos personales de terceros.
El incumplimiento por parte de los Usuarios de cualquiera de las condiciones precedentes, implicará de inmediato la no publicación o baja del aviso en el Sitio, conforme lo estipulado en el apartado 9.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
9. LOS USUARIOS EXPRESAMENTE COMPRENDEN Y ACEPTAN QUE:
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
a. La utilización del Servicio es a su solo riesgo;
b. No se garantiza que el Servicio sea el adecuado a sus necesidades;
c. El Servicio puede ser suspendido o interrumpido;
d. El Servicio puede contener errores;
e. El Sitio no será responsable por ningún daño o perjuicio, directo o indirecto, incluyendo, sin ningún tipo de limitación, daños producidos por la pérdida o deterioro de información;
f. Los Usuarios son los únicos responsables de los contenidos de la información que se publica a través del Servicio;
g. El Sitio se reserva el derecho a terminar el Servicio.
h. El Servicio puede no siempre estar disponible debido a dificultades técnicas o fallas de Internet, o por cualquier otro motivo ajeno, por lo que no podrá imputarse responsabilidad alguna.
i. El contenido de las distintas pantallas del Sitio, junto con sus programas, bases de datos, redes y archivos, son de propiedad de MotoresArgentinos.com., en forma alternativa o conjunta sin limitación alguna Su uso indebido así como su reproducción no autorizada podrá dar lugar a las acciones judiciales que correspondan.
j. La utilización del Servicio no podrá, en ningún supuesto, ser interpretada como una autorización y/o concesión de licencia para la utilización de los derechos intelectuales del Sitio y/o de un tercero.
k. La utilización de Internet en general y del Sitio en particular, implica la asunción de riesgos de potenciales daños al software y al hardware del Usuario. Por tal motivo, el equipo terminal desde el cual acceda al Sitio el Usuario, estaría en condiciones de resultar atacado y dañado por la acción de hackers quienes podrían incluso acceder a la información contenida en el equipo terminal del Usuario, extraerla, sustraerla y/o dañarla.
l. Paralelamente, el intercambio de información a través de Internet tiene el riesgo de que tal información pueda ser captada por un tercero y el Sitio no se hace responsable de las consecuencias que pudiera acarrear al Usuario tal hipótesis.
m. No existe obligación alguna de conservar información que haya estado disponible para los Usuarios, ni que le haya sido enviada por éstos últimos.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
10. RESPONSABILIDAD.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Los Usuarios resultan responsables de toda afirmación y/o expresión y/o acto celebrado con su nombre de usuario y contraseña.
Los Usuarios aceptan y reconocen que el Sitio no será responsable, contractual o extracontractualmente, por ningún daño o perjuicio, directo o indirecto, derivado de la utilización del Servicio.
Los usuarios resultan responsables de toda información suministrada en los campos de texto disponibles. MotoresArgentinos.com no se responsabiliza por la publicación de números telefónicos en dichos campos, siendo esto total responsabilidad del cliente.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
11. INDEMNIDAD.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Los Usuarios asumen total responsabilidad frente al Sitio y a terceros por los daños y/o perjuicios de toda clase que se generen como consecuencia del uso del Servicio, debiendo indemnizar y mantener indemne al Sitio y a terceros ante cualquier reclamo (incluyendo honorarios profesionales) que pudiera corresponder en los supuestos indicados.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
12. BASE DE DATOS.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
motoresargentinos.com se compromete a no ceder, vender, ni entregar a otras empresas o personas físicas, la información suministrada por los Usuarios.
Los Usuarios aceptan por el hecho de registrarse como tales en el Sitio, el derecho de comunicarse con ellos en forma telefónica o vía electrónica; ello, hasta tanto los Usuarios hagan saber su decisión en contrario por medio fehaciente.
Los Usuarios no podrán hacer responsable a MotoresArgentinos.com y/o a ningún tercero por la suspensión o terminación del Servicio.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
13. ARBITRAJE; LEY APLICABLE Y JURISDICCION.
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Las presentes condiciones y las normas que lo complementan, constituyen un acuerdo legal entre los Usuarios y MotoresArgentinos.com. Toda controversia que se suscite en relación a su existencia, validez, calificación, interpretación, alcance o cumplimiento, se resolverá definitivamente por el Tribunal de Arbitraje General de la Bolsa de Comercio de Buenos Aires de acuerdo con la reglamentación vigente de dicho tribunal.
El presente contrato será interpretado y hecho valer de acuerdo con las leyes de la República Argentina, sin tomar en cuenta sus normas sobre conflictos de leyes. Respecto de la ejecución del laudo arbitral, las partes se someterán a la jurisdicción de los tribunales de competencia comercial de la ciudad de Buenos Aires. Las partes irrevocablemente renuncian a ejecutar cualquier objeción basada en la jurisdicción o contender esta bajo cualquier argumento por razón de sus domicilios presentes, futuros o por cualquier otra causa.
La utilización del Servicio está expresamente prohibida en toda jurisdicción en donde no puedan ser aplicadas las condiciones aquí establecidas.
Si los Usuarios utilizan el Servicio, significa que han leído, entendido y acordado las normas antes expuestas. Si no están de acuerdo con ellas, tienen la opción de no utilizar el Servicio.
</p>
<h2 className="text-xl font-bold text-white uppercase tracking-tight mb-4 border-b border-white/10 pb-2">
14. DOMICILIO
</h2>
<p className="text-gray-400 leading-relaxed mb-8">
Toda notificación u otra comunicación que deba efectuarse bajo estas condiciones, deberá realizarse por escrito: I- al Usuario: a la cuenta de correo electrónico por él ingresada o por carta documento dirigida al domicilio declarado en su ficha de registración o II- al Sitio a la cuenta de correo electrónico contacto@motoresargentinos.com.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -2,6 +2,10 @@ import { useState, useEffect } from "react";
import { ProfileService } from "../services/profile.service";
import { useAuth } from "../context/AuthContext";
import { AuthService } from "../services/auth.service";
import {
NotificationPreferencesService,
type NotificationPreferences,
} from "../services/notification-preferences.service";
export default function PerfilPage() {
const { user, refreshSession } = useAuth();
@@ -19,10 +23,46 @@ export default function PerfilPage() {
phoneNumber: "",
});
// Estado de preferencias de notificación
const [notifPrefs, setNotifPrefs] = useState<NotificationPreferences>({
sistema: true,
marketing: true,
rendimiento: true,
mensajes: true,
});
const [savingPrefs, setSavingPrefs] = useState(false);
const [prefsSaved, setPrefsSaved] = useState(false);
const [loadingPrefs, setLoadingPrefs] = useState(true);
useEffect(() => {
loadProfile();
loadNotifPrefs();
}, []);
const loadNotifPrefs = async () => {
try {
const data = await NotificationPreferencesService.getPreferences();
setNotifPrefs(data);
} catch (err) {
console.error("Error cargando preferencias de notificación", err);
} finally {
setLoadingPrefs(false);
}
};
const handleSaveNotifPrefs = async () => {
setSavingPrefs(true);
try {
await NotificationPreferencesService.updatePreferences(notifPrefs);
setPrefsSaved(true);
setTimeout(() => setPrefsSaved(false), 3000);
} catch (err) {
alert("Error al guardar las preferencias de notificación.");
} finally {
setSavingPrefs(false);
}
};
const loadProfile = async () => {
try {
const data = await ProfileService.getProfile();
@@ -97,7 +137,7 @@ export default function PerfilPage() {
</div>
</div>
{/* Edit Form */}
{/* Formulario de datos personales */}
<div className="lg:col-span-2">
<form
onSubmit={handleSubmit}
@@ -189,6 +229,118 @@ export default function PerfilPage() {
</button>
</div>
</form>
{/* ─── Preferencias de Notificación ─── */}
<div className="mt-8 glass p-8 rounded-[2.5rem] border border-white/5">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-600/15 rounded-2xl flex items-center justify-center text-xl border border-blue-500/20">
🔔
</div>
<div>
<h3 className="text-sm font-black uppercase tracking-widest text-white">
Preferencias de Notificación
</h3>
<p className="text-[10px] text-gray-500 font-medium mt-0.5">
Elegir qué correos querés recibir
</p>
</div>
</div>
{loadingPrefs ? (
<div className="flex items-center gap-3 py-4 text-gray-500 text-xs">
<div className="w-5 h-5 border-2 border-blue-500/40 border-t-blue-500 rounded-full animate-spin" />
Cargando preferencias...
</div>
) : (
<div className="space-y-4">
{([
{
key: "sistema" as const,
label: "Avisos del Sistema",
desc: "Vencimiento de avisos, renovaciones y alertas importantes.",
icon: "⚙️",
},
{
key: "marketing" as const,
label: "Promociones y Marketing",
desc: "Ofertas especiales, recordatorio de carrito abandonado.",
icon: "🎁",
},
{
key: "rendimiento" as const,
label: "Resumen Semanal",
desc: "Visitas y favoritos de tus avisos publicados.",
icon: "📊",
},
{
key: "mensajes" as const,
label: "Recordatorio de Mensajes",
desc: "Aviso cuando tenés mensajes sin leer por más de 4 horas.",
icon: "💬",
},
] as const).map(({ key, label, desc, icon }) => (
<label
key={key}
className="flex items-center justify-between gap-4 p-4 rounded-2xl bg-white/3
border border-white/5 hover:bg-white/5 transition-all cursor-pointer group"
>
<div className="flex items-center gap-3">
<span className="text-xl">{icon}</span>
<div>
<p className="text-xs font-black text-white uppercase tracking-wider">
{label}
</p>
<p className="text-[10px] text-gray-500 font-medium mt-0.5">{desc}</p>
</div>
</div>
{/* Toggle switch */}
<div className="relative flex-shrink-0">
<input
type="checkbox"
className="sr-only"
checked={notifPrefs[key]}
onChange={(e) =>
setNotifPrefs((prev) => ({ ...prev, [key]: e.target.checked }))
}
/>
<div
className={`w-12 h-6 rounded-full transition-all duration-300 cursor-pointer
${notifPrefs[key]
? 'bg-blue-600 shadow-lg shadow-blue-600/30'
: 'bg-white/10'}`}
>
<div
className={`absolute top-1 w-4 h-4 bg-white rounded-full shadow transition-all duration-300
${notifPrefs[key] ? 'left-7' : 'left-1'}`}
/>
</div>
</div>
</label>
))}
<div className="pt-4 border-t border-white/5 flex items-center gap-4">
<button
type="button"
onClick={handleSaveNotifPrefs}
disabled={savingPrefs}
className="bg-blue-600 hover:bg-blue-500 text-white py-3.5 px-10 rounded-2xl
text-[10px] font-black uppercase tracking-widest transition-all
shadow-lg shadow-blue-600/20 active:scale-95 disabled:opacity-50"
>
{savingPrefs ? "Guardando..." : "Guardar Preferencias"}
</button>
{prefsSaved && (
<span className="text-green-400 text-[10px] font-black uppercase tracking-widest
animate-pulse">
Preferencias guardadas
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
{showEmailModal && (

View File

@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { NotificationPreferencesService } from '../services/notification-preferences.service';
/**
* Página intermedia que toma el ?token=xxx de la URL (cuando el usuario hace clic en el mail)
* y llama silenciosamente a la API pública. Dependiendo del resultado, redirige
* a /baja-exitosa o a /baja-error.
*/
export default function ProcessUnsubscribePage() {
const [params] = useSearchParams();
const token = params.get('token');
const navigate = useNavigate();
useEffect(() => {
// Si entró sin token, mandarlo al error directamente
if (!token) {
navigate('/baja-error', { replace: true });
return;
}
// Procesa el token con el backend
NotificationPreferencesService.unsubscribeUsingToken(token)
.then((data) => {
if (data.success) {
// Todo bien -> Éxito
navigate(`/baja-exitosa?categoria=${encodeURIComponent(data.category)}`, { replace: true });
} else {
// El token no sirvió pero la API respondió
navigate('/baja-error', { replace: true });
}
})
.catch((_) => {
// Falló la red o dio 400/500
navigate('/baja-error', { replace: true });
});
}, [token, navigate]);
return (
<div className="min-h-screen bg-[#0a0c10] flex flex-col items-center justify-center p-6 text-center">
{/* Spinner llamativo para mostrar que algo está cargando */}
<div className="relative mb-6">
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full animate-pulse"></div>
<div className="relative animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<h1 className="text-2xl font-black uppercase tracking-widest text-white mb-2">
<span className="text-blue-400">Procesando</span> Baja
</h1>
<p className="text-gray-400 font-medium">
Estamos verificando tu solicitud, un momento por favor...
</p>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import apiClient from './axios.client';
export interface NotificationPreferences {
sistema: boolean;
marketing: boolean;
rendimiento: boolean;
mensajes: boolean;
}
export const NotificationPreferencesService = {
/** Obtiene las preferencias actuales del usuario autenticado */
getPreferences: async (): Promise<NotificationPreferences> => {
const response = await apiClient.get('/Profile/notification-preferences');
return response.data;
},
/** Actualiza las preferencias desde el panel de perfil */
updatePreferences: async (data: NotificationPreferences): Promise<void> => {
await apiClient.put('/Profile/notification-preferences', data);
},
/**
* Procesa la baja one-click tomando el token de la URL.
* Endpoint público (no requiere token JWT).
*/
unsubscribeUsingToken: async (token: string): Promise<{ success: boolean; category: string }> => {
const response = await apiClient.get(`/Unsubscribe?token=${encodeURIComponent(token)}`);
return response.data;
},
};