// Resenas.jsx — Reseñas y sugerencias de los usuarios. // // Tres piezas: // · ResenaModal → el formulario (pop-up) // · ResenaNudge → el aviso pequeño y discreto dentro de cada función // · ResenasSection → la lista pública, con promedio y 3 órdenes de lectura // // SEGURIDAD Y PRIVACIDAD // La moderación de verdad ocurre en el SERVIDOR (sql/resenas.sql): un filtro // en JavaScript se saltaría abriendo las herramientas del navegador. Aquí solo // se avisa al usuario cuanto antes, por cortesía. // La lista pública llega por la función resenas_publicas(), que jamás devuelve // user_id ni correos. const { useState: useReS, useEffect: useReE, useCallback: useReC } = React; const RESENA_VISTA_KEY = 'cyna_resena_popup_visto'; const RESENA_NUDGE_KEY = 'cyna_resena_nudge_cerrado'; function sbClient() { return (window.cynaAuth && window.cynaAuth.client) || null; } // ─── Estrellas ────────────────────────────────────────────── function Estrellas({ valor, onChange, size = 20, readOnly }) { const [hover, setHover] = useReS(0); const activo = hover || valor; return (
{[1, 2, 3, 4, 5].map(n => ( ))}
); } // ─── Formulario (pop-up) ──────────────────────────────────── function ResenaModal({ onClose }) { const user = window.__cynaUser || null; const [estrellas, setEstrellas] = useReS(0); const [texto, setTexto] = useReS(''); const [sugerencia, setSugerencia] = useReS(''); const [estado, setEstado] = useReS('idle'); // idle | enviando | ok | retenida | error const [error, setError] = useReS(''); const [yaTenia, setYaTenia] = useReS(false); // Si ya dejó una reseña, la traemos para que la edite en vez de duplicarla. useReE(() => { const sb = sbClient(); if (!sb || !user) return; let cancel = false; sb.rpc('mi_resena').then(({ data }) => { if (cancel || !data || !data.length) return; const r = data[0]; setEstrellas(r.estrellas); setTexto(r.texto || ''); setSugerencia(r.sugerencia || ''); setYaTenia(true); }, () => {}); return () => { cancel = true; }; }, []); const enviar = async () => { setError(''); const sec = window.cynaSec; const sb = sbClient(); if (!user) { setError('Necesitas iniciar sesión para dejar tu reseña.'); return; } if (!sb) { setError('El servicio no está disponible ahora mismo.'); return; } if (!estrellas) { setError('Elige cuántas estrellas le das.'); return; } const limpio = sec ? sec.sanitizeText(texto, 600) : texto.trim(); const sug = sec ? sec.sanitizeText(sugerencia, 400) : sugerencia.trim(); if (limpio.length < 5) { setError('Cuéntanos un poco más (al menos 5 caracteres).'); return; } // Límite en el navegador: cortesía para no golpear el servidor. El límite // real lo impone la base de datos. if (sec) { const rl = sec.rateLimit('resena', { minGap: 60, maxPerHour: 5 }); if (!rl.ok) { setError('Espera un momento antes de volver a enviar.'); return; } } setEstado('enviando'); const nombre = (user.user_metadata && (user.user_metadata.full_name || user.user_metadata.nombre)) || (user.email ? user.email.split('@')[0] : 'Usuario'); const { error: err } = await sb.from('cyna_resenas').upsert({ user_id: user.id, nombre: String(nombre).slice(0, 60), estrellas, texto: limpio, sugerencia: sug || null, }, { onConflict: 'user_id' }); if (err) { setEstado('error'); // El error completo va a la consola: sin esto, un fallo de permisos y uno // de red se ven igual en pantalla y no hay forma de diagnosticarlos. console.error('[Cynamonurs] Error al guardar la reseña:', err); const msg = err.message || ''; if (/Espera un momento/i.test(msg)) { setError('Espera un minuto antes de volver a editar tu reseña.'); } else if (err.code === '42501' || /row-level security|permission denied/i.test(msg)) { setError('No pudimos guardar tu reseña en este momento. Inténtalo de nuevo en un rato.'); } else if (/check constraint|violates check/i.test(msg)) { setError('Revisa el texto: debe tener entre 5 y 600 caracteres.'); } else { setError('No se pudo guardar tu reseña. Inténtalo de nuevo más tarde.'); } return; } // Preguntamos cómo quedó: el servidor pudo retenerla por el filtro. const { data } = await sb.rpc('mi_resena'); const retenida = data && data[0] && data[0].estado === 'retenida'; setEstado(retenida ? 'retenida' : 'ok'); if (window.cynaTrack) window.cynaTrack('resena_enviada', { estrellas, retenida: !!retenida }); try { localStorage.setItem(RESENA_VISTA_KEY, '1'); } catch (e) {} window.dispatchEvent(new CustomEvent('cyna-resenas')); }; const cerrar = () => { try { localStorage.setItem(RESENA_VISTA_KEY, '1'); } catch (e) {} onClose(); }; return (
e.stopPropagation()}>

{yaTenia ? 'Edita tu reseña' : 'Cuéntanos qué te pareció'}

{!user ? (

Para dejar una reseña necesitas iniciar sesión. Así evitamos reseñas falsas y spam.

) : estado === 'ok' || estado === 'retenida' ? (
{estado === 'ok' ? (

¡Gracias! Tu reseña ya está publicada.

) : (

Recibimos tu reseña. La revisaremos antes de publicarla — nuestro filtro detectó algo que conviene mirar con calma. Si todo está bien, aparecerá en breve.

)}
) : ( <>