resetea.net/public/plantillas.html
hacklab 614d5af397 Add full project structure: backend API + frontend
- Move repo to project root to include both public/ and api/
- Add .gitignore excluding node_modules and .env
- Include API routes (erase, gmail_oauth), services (mailer), and config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:09:54 +02:00

879 lines
37 KiB
HTML

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RESETEA.NET · Plantillas legales GDPR</title>
<meta name="description"
content="Generador de cartas GDPR/RGPD para ejercer el derecho al olvido, supresión, acceso y portabilidad ante redes sociales y data brokers. Sin enviar tus datos a ningún servidor.">
<link rel="stylesheet" href="index.css">
<style>
/* ── Formulario de datos ── */
.form-section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.8rem;
margin-bottom: 2rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 1.2rem;
}
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
.form-group label { font-size: 0.8rem; color: var(--muted); font-weight: 600; letter-spacing: 0.03em; }
.form-group input, .form-group select, .form-group textarea {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
padding: 0.55rem 0.75rem;
font-size: 0.9rem;
font-family: inherit;
transition: border-color 160ms ease;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-group select option { background: var(--panel); }
.form-full { grid-column: 1 / -1; }
/* ── Selector de tipo de solicitud ── */
.type-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-top: 1rem;
}
.type-btn {
padding: 0.75rem 0.5rem;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--muted);
font-size: 0.8rem;
font-weight: 600;
text-align: center;
cursor: pointer;
transition: all 160ms ease;
}
.type-btn:hover { border-color: var(--accent); color: var(--text); }
.type-btn.active {
border-color: var(--accent);
background: rgba(255,61,0,0.1);
color: var(--accent);
}
/* ── Resultado ── */
.result-section {
display: none;
margin-top: 2rem;
}
.result-section.visible { display: block; }
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.result-header h3 { font-size: 1rem; }
.copy-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
background: var(--accent);
color: #000;
border: none;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
transition: opacity 160ms ease;
}
.copy-btn:hover { opacity: 0.85; }
.copy-btn.copied { background: var(--neon); }
.letter-box {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.4rem;
font-size: 0.88rem;
line-height: 1.7;
white-space: pre-wrap;
color: var(--text);
font-family: 'Courier New', monospace;
min-height: 280px;
}
/* ── Plataformas grid ── */
.platform-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-top: 1.2rem;
}
.platform-btn {
padding: 0.65rem 0.5rem;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--muted);
font-size: 0.82rem;
font-weight: 600;
text-align: center;
cursor: pointer;
transition: all 160ms ease;
}
.platform-btn:hover { border-color: var(--accent); color: var(--text); }
.platform-btn.active {
border-color: var(--accent);
background: rgba(255,61,0,0.1);
color: var(--accent);
}
/* ── Info card de plataforma ── */
.platform-info {
display: none;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.2rem;
margin-top: 1rem;
font-size: 0.85rem;
}
.platform-info.visible { display: block; }
.platform-info .pi-row {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
align-items: baseline;
flex-wrap: wrap;
}
.platform-info .pi-label { color: var(--muted); min-width: 120px; font-size: 0.78rem; }
.platform-info a { color: var(--accent); }
/* ── Generate btn ── */
.generate-btn {
display: block;
width: 100%;
margin-top: 1.5rem;
padding: 0.9rem;
background: var(--accent);
color: #000;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: opacity 160ms ease, transform 160ms ease;
}
.generate-btn:hover { opacity: 0.9; transform: translateY(-1px); }
/* ── Info notice ── */
.privacy-notice {
display: flex;
gap: 0.6rem;
align-items: flex-start;
background: rgba(57,255,20,0.05);
border: 1px solid rgba(57,255,20,0.2);
border-radius: 10px;
padding: 0.9rem 1rem;
font-size: 0.8rem;
color: var(--muted);
margin-bottom: 1.5rem;
}
.privacy-notice strong { color: var(--neon); }
/* ── Pasos post-generación ── */
.next-steps {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.2rem 1.4rem;
margin-top: 1.5rem;
}
.next-steps h4 { margin-bottom: 0.75rem; font-size: 0.95rem; }
.next-steps ol { padding-left: 1.2rem; color: var(--muted); font-size: 0.85rem; line-height: 2; }
.next-steps ol li strong { color: var(--text); }
@media (max-width: 900px) {
.form-grid { grid-template-columns: 1fr; }
.type-grid { grid-template-columns: repeat(2, 1fr); }
.platform-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<header class="topbar">
<div class="container topbar-inner">
<div class="brand">
<div class="brand-logo">R</div>
<div class="brand-text">
<div class="brand-name">RESETEA<span>.NET</span></div>
<div class="brand-tag">Privacidad sin custodios</div>
</div>
</div>
<nav class="nav" aria-label="Navegación principal">
<a class="nav-btn" href="tipos.html">Tipos de información</a>
<a class="nav-btn" href="concienciacion.html">Concienciación</a>
<a class="nav-btn" href="index.html">Resetea</a>
<a class="nav-btn" href="egosurfing.html">Egosurfing</a>
</nav>
</div>
</header>
<main>
<section class="hero">
<div class="container hero-inner">
<div class="hero-text">
<h1>Plantillas legales GDPR.<br>Listas en segundos.</h1>
<p>
Rellena tus datos una sola vez, elige la plataforma y el tipo de solicitud.
La carta se genera al instante en tu navegador: <strong>nunca sale de tu dispositivo</strong>.
Cópiala y envíala tú mismo al DPO de la empresa.
</p>
<div class="hero-actions">
<a class="btn primary" href="#generador">Abrir generador</a>
<a class="btn ghost" href="concienciacion.html">Ver mis derechos</a>
</div>
<div class="notice">
<strong>Privacidad absoluta:</strong><br>
Todo el procesamiento ocurre en tu navegador (JavaScript local).
No se envía ningún dato a ningún servidor de RESETEA.NET.
Puedes usar esta página sin conexión a internet una vez cargada.
</div>
</div>
<div class="hero-card">
<div class="stats">
<div class="stat"><div class="stat-num">4</div><div class="stat-label">Tipos de solicitud</div></div>
<div class="stat"><div class="stat-num">12+</div><div class="stat-label">Plataformas cubiertas</div></div>
<div class="stat"><div class="stat-num">0</div><div class="stat-label">Datos enviados al servidor</div></div>
</div>
<ul class="steps">
<li><span>1</span> Rellena tus datos</li>
<li><span>2</span> Elige plataforma y tipo</li>
<li><span>3</span> Copia y envía la carta</li>
</ul>
</div>
</div>
</section>
<!-- GENERADOR -->
<section id="generador" class="panel">
<div class="container">
<h2>Generador de cartas GDPR</h2>
<p class="section-desc">
Todos los campos son opcionales excepto nombre y email.
Cuantos más rellenes, más sólida y completa quedará la carta.
</p>
<div class="privacy-notice">
<strong>Privacidad:</strong>&nbsp;Tus datos solo existen en la memoria de esta pestaña del navegador.
No se almacenan ni se transmiten. Cierra la pestaña y desaparecen.
</div>
<!-- DATOS DEL SOLICITANTE -->
<div class="form-section">
<h3 style="margin-bottom:0.3rem;">Tus datos (solicitante)</h3>
<p style="font-size:0.82rem;color:var(--muted);">Son los que figurarán en la carta. La plataforma puede pedirte una copia de tu DNI para verificar tu identidad.</p>
<div class="form-grid">
<div class="form-group">
<label for="inp-name">Nombre completo *</label>
<input id="inp-name" type="text" placeholder="María García López" autocomplete="name">
</div>
<div class="form-group">
<label for="inp-email">Email de contacto *</label>
<input id="inp-email" type="email" placeholder="tu@email.com" autocomplete="email">
</div>
<div class="form-group">
<label for="inp-nick">Nombre de usuario / alias (en esa plataforma)</label>
<input id="inp-nick" type="text" placeholder="@usuario o nick">
</div>
<div class="form-group">
<label for="inp-phone">Teléfono asociado a la cuenta (opcional)</label>
<input id="inp-phone" type="tel" placeholder="+34 600 000 000">
</div>
<div class="form-group form-full">
<label for="inp-address">Dirección postal (opcional, refuerza la solicitud)</label>
<input id="inp-address" type="text" placeholder="Calle Ejemplo 1, 28001 Madrid, España" autocomplete="street-address">
</div>
<div class="form-group form-full">
<label for="inp-extra">Información adicional (IDs de cuenta, URLs de perfil, etc.)</label>
<input id="inp-extra" type="text" placeholder="ID de usuario: 123456789 · URL: https://...">
</div>
</div>
</div>
<!-- PLATAFORMA -->
<div class="form-section">
<h3>Plataforma de destino</h3>
<div class="platform-grid" id="platform-grid">
<!-- generado por JS -->
</div>
<div class="platform-info" id="platform-info">
<!-- generado por JS -->
</div>
</div>
<!-- TIPO DE SOLICITUD -->
<div class="form-section">
<h3>Tipo de solicitud</h3>
<p style="font-size:0.82rem;color:var(--muted);margin-top:0.3rem;">
¿Qué derecho quieres ejercer? Si tienes dudas, consulta <a href="concienciacion.html">Concienciación</a>.
</p>
<div class="type-grid" id="type-grid">
<!-- generado por JS -->
</div>
</div>
<button class="generate-btn" id="generate-btn" onclick="generateLetter()">
Generar carta
</button>
<!-- RESULTADO -->
<div class="result-section" id="result-section">
<div class="result-header">
<h3 id="result-title">Carta generada</h3>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<button class="copy-btn" id="copy-btn" onclick="copyLetter()">Copiar carta</button>
<button class="copy-btn" id="gmail-btn" onclick="sendViaGmail()"
style="background:#1a1714;color:#fff;" title="Envía la carta directamente desde tu Gmail">
Enviar con Gmail
</button>
</div>
</div>
<div id="oauth-banner" style="display:none;"></div>
<div class="letter-box" id="letter-box"></div>
<div class="next-steps" id="next-steps"></div>
</div>
</div>
</section>
<!-- GUIA DE USO -->
<section class="info alt">
<div class="container">
<h2>Cómo usar esta carta</h2>
<div class="grid">
<div class="group">
<h3>Opción A — Por email</h3>
<div class="item"><label><input type="checkbox"> Copia el texto generado</label></div>
<div class="item"><label><input type="checkbox"> Abre tu cliente de correo</label></div>
<div class="item"><label><input type="checkbox"> Envía al email del DPO que aparece en la carta</label></div>
<div class="item"><label><input type="checkbox"> Asunto: "Ejercicio de derecho — [Art. X RGPD]"</label></div>
<div class="item"><label><input type="checkbox"> Adjunta copia de tu DNI/pasaporte si es requerido</label></div>
<div class="item"><label><input type="checkbox"> Guarda el email enviado como prueba</label></div>
</div>
<div class="group">
<h3>Opción B — Por formulario oficial</h3>
<div class="item"><label><input type="checkbox"> Haz clic en el enlace "Formulario oficial" que aparece al generar</label></div>
<div class="item"><label><input type="checkbox"> Copia los datos de la carta en los campos del formulario</label></div>
<div class="item"><label><input type="checkbox"> Haz captura de pantalla de la confirmación</label></div>
<div class="item"><label><input type="checkbox"> Apunta la fecha de envío</label></div>
</div>
<div class="group">
<h3>Si no responden en 30 días</h3>
<div class="item"><label><input type="checkbox"> Recopila pruebas: email enviado + silencio</label></div>
<div class="item"><label><input type="checkbox"> Presenta reclamación gratuita en la AEPD</label></div>
<div class="item"><label><input type="checkbox"> Adjunta toda la correspondencia</label></div>
<div class="actions" style="margin-top:0.8rem;">
<a href="https://sedeagpd.gob.es/sede-electronica-web/" target="_blank" rel="noopener">Sede AEPD</a>
</div>
</div>
</div>
</div>
</section>
<!-- TIPOS DE CARTA -->
<section class="info">
<div class="container">
<h2>Qué cubre cada tipo de carta</h2>
<div class="grid">
<div class="group">
<h3>Supresión / Derecho al olvido</h3>
<p class="section-desc">Art. 17 RGPD</p>
<p style="font-size:0.85rem;color:var(--muted);">
Solicita la eliminación completa de todos tus datos personales:
cuenta, historial, publicaciones, metadatos, backups y datos compartidos con terceros.
La plataforma tiene 30 días para confirmar el borrado o justificar por qué no puede hacerlo.
</p>
</div>
<div class="group">
<h3>Acceso a tus datos</h3>
<p class="section-desc">Art. 15 RGPD</p>
<p style="font-size:0.85rem;color:var(--muted);">
Solicita una copia de todos los datos que tienen sobre ti, incluyendo datos inferidos,
con quién los comparten, cuánto tiempo los conservan y cuál es la base legal del tratamiento.
Útil antes de pedir la supresión para saber exactamente qué borrar.
</p>
</div>
<div class="group">
<h3>Portabilidad</h3>
<p class="section-desc">Art. 20 RGPD</p>
<p style="font-size:0.85rem;color:var(--muted);">
Solicita una exportación de tus datos en formato estructurado y legible por máquina
(JSON, CSV, XML). Úsalo para descargarte tu historial antes de borrar la cuenta
y llevarte el contenido a otra plataforma.
</p>
</div>
<div class="group">
<h3>Oposición al perfilado</h3>
<p class="section-desc">Art. 21 RGPD</p>
<p style="font-size:0.85rem;color:var(--muted);">
Solicita que dejen de usar tus datos para publicidad personalizada, perfilado
o decisiones automatizadas. En marketing directo es un derecho absoluto:
no necesitas justificación y deben parar inmediatamente.
</p>
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<p>RESETEA.NET · Plantillas legales · Estático · Sin cookies · Sin tracking · Generación 100 % local</p>
</div>
</footer>
<script>
'use strict';
/* ============================================================
DATOS DE PLATAFORMAS
============================================================ */
const PLATFORMS = {
instagram: {
name: 'Instagram',
company: 'Meta Platforms Ireland Limited',
address: '4 Grand Canal Square, Grand Canal Harbour, Dublín 2, Irlanda',
dpoEmail: 'privacidad-dpo@fb.com',
deleteUrl: 'https://www.instagram.com/accounts/remove/request/permanent/',
privacyUrl: 'https://privacycenter.instagram.com/policy',
formUrl: 'https://www.facebook.com/help/contact/2364547050428001',
notes: 'Los datos se retienen hasta 90 días en backups tras la eliminación de cuenta.'
},
facebook: {
name: 'Facebook',
company: 'Meta Platforms Ireland Limited',
address: '4 Grand Canal Square, Grand Canal Harbour, Dublín 2, Irlanda',
dpoEmail: 'privacidad-dpo@fb.com',
deleteUrl: 'https://www.facebook.com/help/delete_account',
privacyUrl: 'https://www.facebook.com/privacy/policy/',
formUrl: 'https://www.facebook.com/help/contact/2364547050428001',
notes: 'Los datos se retienen hasta 90 días en backups.'
},
twitter_x: {
name: 'X (Twitter)',
company: 'Twitter International Unlimited Company',
address: 'One Cumberland Place, Fenian Street, Dublín 2, D02 AX07, Irlanda',
dpoEmail: 'gdpr@twitter.com',
deleteUrl: 'https://x.com/settings/deactivate',
privacyUrl: 'https://x.com/es/privacy',
formUrl: null,
notes: 'La desactivación inicia un periodo de gracia de 30 días antes del borrado definitivo.'
},
google: {
name: 'Google',
company: 'Google Ireland Limited',
address: 'Gordon House, Barrow Street, Dublín 4, Irlanda',
dpoEmail: null,
deleteUrl: 'https://myaccount.google.com/delete-services-or-account',
privacyUrl: 'https://myaccount.google.com/data-and-privacy',
formUrl: 'https://support.google.com/policies/contact/sar_data_protection',
notes: 'Para desindexación de resultados de búsqueda usa el formulario RTBF: https://reportcontent.google.com/forms/rtbf'
},
linkedin: {
name: 'LinkedIn',
company: 'LinkedIn Ireland Unlimited Company',
address: 'Wilton Place, Dublín 2, Irlanda',
dpoEmail: 'privacy@linkedin.com',
deleteUrl: 'https://www.linkedin.com/mypreferences/d/close-your-account',
privacyUrl: 'https://www.linkedin.com/legal/privacy-policy',
formUrl: 'https://www.linkedin.com/help/linkedin/ask/PPQ',
notes: 'Los datos se eliminan en un plazo de 30 días desde el cierre de cuenta.'
},
tiktok: {
name: 'TikTok',
company: 'TikTok Technology Limited',
address: '10 Earlsfort Terrace, Dublín, D02 T380, Irlanda',
dpoEmail: 'privacy@tiktok.com',
deleteUrl: 'https://support.tiktok.com/es/safety-hic/account-and-user-safety/account-deletion',
privacyUrl: 'https://www.tiktok.com/legal/page/eea/privacy-policy/es',
formUrl: null,
notes: 'La eliminación de cuenta solo puede hacerse desde la app móvil. Hay 30 días de periodo de reflexión.'
},
snapchat: {
name: 'Snapchat',
company: 'Snap Group Limited',
address: '77 Shaftesbury Avenue, Londres W1D 5DU, Reino Unido',
dpoEmail: 'privacy@snap.com',
deleteUrl: 'https://accounts.snapchat.com/accounts/delete_account',
privacyUrl: 'https://www.snap.com/es-ES/privacy/privacy-policy',
formUrl: 'https://support.snapchat.com/es-ES/i-need-help',
notes: 'La cuenta se desactiva 30 días antes del borrado definitivo.'
},
microsoft: {
name: 'Microsoft',
company: 'Microsoft Ireland Operations Limited',
address: 'One Microsoft Place, South County Business Park, Leopardstown, Dublín 18, Irlanda',
dpoEmail: 'msprivacy@microsoft.com',
deleteUrl: 'https://account.live.com/closeaccount.aspx',
privacyUrl: 'https://privacy.microsoft.com/es-es/privacystatement',
formUrl: 'https://www.microsoft.com/es-es/concern/privacy',
notes: null
},
apple: {
name: 'Apple',
company: 'Apple Distribution International Ltd.',
address: 'Hollyhill Industrial Estate, Hollyhill, Cork, Irlanda',
dpoEmail: 'privacy@apple.com',
deleteUrl: 'https://privacy.apple.com/',
privacyUrl: 'https://www.apple.com/es/legal/privacy/',
formUrl: 'https://privacy.apple.com/',
notes: 'Usa el portal privacy.apple.com para solicitar acceso, portabilidad y eliminación.'
},
amazon: {
name: 'Amazon',
company: 'Amazon Europe Core S.à r.l.',
address: '38 avenue John F. Kennedy, L-1855 Luxemburgo',
dpoEmail: null,
deleteUrl: 'https://www.amazon.es/gp/help/customer/display.html?nodeId=GXPU3YPMBDQWWHGZ',
privacyUrl: 'https://www.amazon.es/gp/help/customer/display.html?nodeId=GX7NJQ4ZB8MHFRNJ',
formUrl: 'https://www.amazon.es/hz/contact-us/privacy-disclosure/ref=hp_bc_nav',
notes: null
},
reddit: {
name: 'Reddit',
company: 'Reddit Inc.',
address: '548 Market St. #16093, San Francisco, CA 94104-5401, EE.UU.',
dpoEmail: 'gdpr@reddit.com',
deleteUrl: 'https://www.reddit.com/settings/account',
privacyUrl: 'https://www.reddit.com/policies/privacy-policy',
formUrl: 'https://www.reddithelp.com/hc/es/requests/new',
notes: 'Los posts no se borran automáticamente al cerrar la cuenta; hay que borrarlos manualmente primero.'
},
discord: {
name: 'Discord',
company: 'Discord Inc.',
address: '444 De Haro Street, Suite 200, San Francisco, CA 94107, EE.UU.',
dpoEmail: 'privacy@discord.com',
deleteUrl: 'https://discord.com/channels/@me',
privacyUrl: 'https://discord.com/privacy',
formUrl: 'https://support.discord.com/hc/es/requests/new',
notes: 'Para eliminar la cuenta: Configuración → Mi cuenta → Eliminar cuenta.'
}
};
/* ============================================================
TIPOS DE SOLICITUD
============================================================ */
const REQUEST_TYPES = {
suppression: {
label: 'Supresión (Art. 17)',
short: 'Supresión',
article: 'artículo 17 del Reglamento (UE) 2016/679 (RGPD)',
right: 'derecho de supresión ("derecho al olvido")',
body: (p) => `En ejercicio del ${p.right} reconocido en el ${p.article}, y en concordancia con el artículo 94 de la Ley Orgánica 3/2018 (LOPDGDD), solicito la supresión completa e irreversible de todos mis datos personales de los sistemas de ${p.company}, incluyendo sin limitación:
- Datos de cuenta e identificadores (nombre, email, teléfono, dirección IP, identificadores de dispositivo).
- Contenidos publicados, comentarios, mensajes, imágenes y archivos.
- Datos de comportamiento, historial de navegación, preferencias e intereses inferidos.
- Datos en sistemas de backup y réplicas, en el plazo razonable comprometido.
- Datos que hayan sido cedidos o compartidos con terceros, con la correspondiente notificación a dichos terceros para que también procedan a la supresión.
Fundamento mi solicitud en que el tratamiento de mis datos ya no es necesario para los fines para los cuales fueron recogidos, y en la retirada del consentimiento que en su momento presté.`,
footer: 'Solicito confirmación escrita de la supresión una vez completada, con indicación del plazo previsto para la eliminación de los backups.'
},
access: {
label: 'Acceso (Art. 15)',
short: 'Acceso',
article: 'artículo 15 del Reglamento (UE) 2016/679 (RGPD)',
right: 'derecho de acceso',
body: (p) => `En ejercicio del ${p.right} reconocido en el ${p.article}, solicito que ${p.company} me proporcione la siguiente información relativa a mis datos personales:
a) Confirmación de si se están tratando datos personales míos.
b) Copia de todos los datos personales que obran en poder de ${p.company}, en formato legible.
c) Finalidades del tratamiento para cada categoría de datos.
d) Categorías de datos tratados.
e) Destinatarios o categorías de destinatarios con quienes se hayan comunicado o vayan a comunicarse los datos.
f) Plazos previstos de conservación de los datos.
g) Base jurídica del tratamiento.
h) Información sobre el origen de los datos cuando no hayan sido recabados directamente de mí.
i) Existencia de decisiones automatizadas (incluida la elaboración de perfiles) y lógica aplicada.`,
footer: 'Solicito que la información se facilite en formato digital estructurado.'
},
portability: {
label: 'Portabilidad (Art. 20)',
short: 'Portabilidad',
article: 'artículo 20 del Reglamento (UE) 2016/679 (RGPD)',
right: 'derecho a la portabilidad de los datos',
body: (p) => `En ejercicio del ${p.right} reconocido en el ${p.article}, solicito recibir los datos personales que me conciernen y que haya facilitado a ${p.company}, en un formato estructurado, de uso común y lectura mecánica (por ejemplo, JSON, CSV o XML).
Los datos solicitados incluyen:
- Datos de perfil y cuenta.
- Contenidos generados (publicaciones, imágenes, mensajes enviados).
- Historial de actividad e interacciones.
- Configuraciones y preferencias.
- Cualquier otro dato facilitado directamente por mí o generado como resultado de mi uso del servicio.`,
footer: 'Si es técnicamente posible, solicito que los datos sean transmitidos directamente a otro responsable del tratamiento.'
},
opposition: {
label: 'Oposición (Art. 21)',
short: 'Oposición',
article: 'artículo 21 del Reglamento (UE) 2016/679 (RGPD)',
right: 'derecho de oposición',
body: (p) => `En ejercicio del ${p.right} reconocido en el ${p.article}, me opongo al tratamiento de mis datos personales por parte de ${p.company} para los siguientes fines:
a) Marketing directo y comunicaciones comerciales de cualquier tipo.
b) Elaboración de perfiles con fines publicitarios o de análisis del comportamiento.
c) Cesión o comunicación de mis datos a terceros (anunciantes, socios, plataformas de AdTech) con fines de marketing o monetización.
d) Decisiones automatizadas que produzcan efectos jurídicos o me afecten de modo significativo.
En aplicación del artículo 21.3 RGPD, en el caso de tratamiento con fines de marketing directo, el responsable del tratamiento deberá dejar de tratar los datos para dichos fines sin necesidad de que motive mi solicitud.`,
footer: 'Solicito confirmación de que el tratamiento ha cesado para los fines indicados.'
}
};
/* ============================================================
ESTADO
============================================================ */
let selectedPlatform = null;
let selectedType = null;
/* ============================================================
INIT
============================================================ */
document.addEventListener('DOMContentLoaded', () => {
renderPlatformGrid();
renderTypeGrid();
});
function renderPlatformGrid() {
const grid = document.getElementById('platform-grid');
grid.innerHTML = Object.entries(PLATFORMS).map(([key, p]) => `
<button class="platform-btn" data-key="${key}" onclick="selectPlatform('${key}')">
${p.name}
</button>
`).join('');
}
function renderTypeGrid() {
const grid = document.getElementById('type-grid');
grid.innerHTML = Object.entries(REQUEST_TYPES).map(([key, t]) => `
<button class="type-btn" data-key="${key}" onclick="selectType('${key}')">
${t.label}
</button>
`).join('');
}
function selectPlatform(key) {
selectedPlatform = key;
document.querySelectorAll('.platform-btn').forEach(b => b.classList.remove('active'));
document.querySelector(`.platform-btn[data-key="${key}"]`).classList.add('active');
showPlatformInfo(key);
hideResult();
}
function showPlatformInfo(key) {
const p = PLATFORMS[key];
const info = document.getElementById('platform-info');
info.innerHTML = `
<div class="pi-row"><span class="pi-label">Empresa:</span><span>${p.company}</span></div>
<div class="pi-row"><span class="pi-label">Dirección:</span><span>${p.address}</span></div>
<div class="pi-row"><span class="pi-label">DPO / Privacidad:</span><span>${
p.dpoEmail ? `<a href="mailto:${p.dpoEmail}">${p.dpoEmail}</a>` : '—'
}</span></div>
<div class="pi-row"><span class="pi-label">Eliminar cuenta:</span><span><a href="${p.deleteUrl}" target="_blank" rel="noopener">Abrir enlace oficial</a></span></div>
${p.formUrl ? `<div class="pi-row"><span class="pi-label">Formulario GDPR:</span><span><a href="${p.formUrl}" target="_blank" rel="noopener">Formulario oficial</a></span></div>` : ''}
${p.notes ? `<div class="pi-row" style="margin-top:0.5rem;"><span class="pi-label" style="color:var(--accent);">Nota:</span><span style="color:var(--muted);font-size:0.82rem;">${p.notes}</span></div>` : ''}
`;
info.classList.add('visible');
}
function selectType(key) {
selectedType = key;
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
document.querySelector(`.type-btn[data-key="${key}"]`).classList.add('active');
hideResult();
}
function hideResult() {
document.getElementById('result-section').classList.remove('visible');
}
function generateLetter() {
const name = document.getElementById('inp-name').value.trim();
const email = document.getElementById('inp-email').value.trim();
const nick = document.getElementById('inp-nick').value.trim();
const phone = document.getElementById('inp-phone').value.trim();
const address = document.getElementById('inp-address').value.trim();
const extra = document.getElementById('inp-extra').value.trim();
if (!name || !email) {
alert('Por favor, rellena al menos tu nombre completo y tu email de contacto.');
return;
}
if (!selectedPlatform) {
alert('Por favor, selecciona una plataforma de destino.');
return;
}
if (!selectedType) {
alert('Por favor, selecciona el tipo de solicitud que quieres ejercer.');
return;
}
const p = PLATFORMS[selectedPlatform];
const rt = REQUEST_TYPES[selectedType];
const today = new Date().toLocaleDateString('es-ES', { day: 'numeric', month: 'long', year: 'numeric' });
const deadline = new Date(Date.now() + 30 * 86400000).toLocaleDateString('es-ES', { day: 'numeric', month: 'long', year: 'numeric' });
const identLines = [
`Nombre completo: ${name}`,
`Correo electrónico: ${email}`,
nick ? `Usuario en la plataforma: ${nick}` : null,
phone ? `Teléfono: ${phone}` : null,
address ? `Dirección postal: ${address}` : null,
extra ? `Datos adicionales de identificación: ${extra}` : null,
].filter(Boolean).join('\n');
const bodyParams = {
company: p.company,
right: rt.right,
article: rt.article,
};
const letter = `${p.company}
${p.address}
${p.dpoEmail ? `Delegado de Protección de Datos: ${p.dpoEmail}` : 'Departamento de Privacidad'}
${today}
Asunto: Ejercicio del ${rt.right}${rt.article}
A quien corresponda:
Yo, ${name}, con los siguientes datos de contacto:
${identLines}
En ejercicio de mis derechos reconocidos en el Reglamento (UE) 2016/679 del Parlamento Europeo y del Consejo (RGPD) y en la Ley Orgánica 3/2018, de Protección de Datos Personales y garantía de los derechos digitales (LOPDGDD), dirijo la presente comunicación a ${p.company} como responsable del tratamiento de mis datos personales.
─────────────────────────────────────────
${rt.body(bodyParams)}
─────────────────────────────────────────
${rt.footer}
De conformidad con el artículo 12.3 del RGPD, disponen de un plazo máximo de un mes desde la recepción de la presente solicitud para darle respuesta (antes del ${deadline}). En caso de denegación o ausencia de respuesta, me reservo el derecho a presentar una reclamación ante la Agencia Española de Protección de Datos (sedeagpd.gob.es) y/o ejercer las acciones judiciales que estime oportunas.
Quedo a su disposición para cualquier aclaración o para aportar documentación adicional que acredite mi identidad.
Atentamente,
${name}
${email}
${address ? address + '\n' : ''}${today}`;
document.getElementById('letter-box').textContent = letter;
document.getElementById('result-title').textContent = `Carta: ${rt.short} ante ${p.name}`;
const nextSteps = document.getElementById('next-steps');
nextSteps.innerHTML = `
<h4>Próximos pasos</h4>
<ol>
<li><strong>Copia la carta</strong> con el botón "Copiar carta".</li>
<li>${p.dpoEmail
? `<strong>Envíala por email</strong> a <a href="mailto:${p.dpoEmail}">${p.dpoEmail}</a>.`
: `<strong>Usa el formulario oficial</strong>: <a href="${p.formUrl || p.deleteUrl}" target="_blank" rel="noopener">abrir formulario</a>.`
}</li>
<li><strong>Guarda el acuse de recibo</strong> (captura o email de confirmación). El plazo de 30 días empieza desde la recepción.</li>
<li>Si no responden antes del <strong>${deadline}</strong>, presenta reclamación en la <a href="https://sedeagpd.gob.es/sede-electronica-web/" target="_blank" rel="noopener">sede AEPD</a>.</li>
${p.deleteUrl ? `<li>Si también quieres eliminar tu cuenta, accede aquí: <a href="${p.deleteUrl}" target="_blank" rel="noopener">${p.name} — eliminación de cuenta</a>.</li>` : ''}
</ol>
`;
const resultSection = document.getElementById('result-section');
resultSection.classList.add('visible');
resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* ── OAuth Gmail ── */
function sendViaGmail() {
if (!selectedPlatform || !selectedType) {
alert('Genera primero la carta antes de enviarla.');
return;
}
const name = document.getElementById('inp-name').value.trim();
const email = document.getElementById('inp-email').value.trim();
if (!name || !email) { alert('Rellena nombre y email antes de enviar.'); return; }
const params = new URLSearchParams({
provider: selectedPlatform,
requestType: selectedType,
name,
email,
nickname: document.getElementById('inp-nick').value.trim(),
phone: document.getElementById('inp-phone').value.trim(),
address: document.getElementById('inp-address').value.trim(),
extra: document.getElementById('inp-extra').value.trim(),
});
// Redirige al backend OAuth — el callback devuelve a plantillas.html
window.location.href = '/api/gmail/auth?' + params.toString();
}
/* ── Notificación post-OAuth ── */
(function handleOAuthResult() {
const p = new URLSearchParams(window.location.search);
const banner = document.getElementById('oauth-banner');
if (!banner) return;
const status = p.get('oauth');
if (!status) return;
const msgs = {
ok: { bg: '#e8f2eb', color: '#2d6b3f', text: `Carta enviada desde tu Gmail a ${p.get('provider') || 'la plataforma'}. Guarda el email enviado como prueba.` },
cancelled: { bg: '#f0e6df', color: '#7b3f2e', text: 'Autorización cancelada. Puedes copiar la carta y enviarla manualmente.' },
error: { bg: '#fde8e8', color: '#b91c1c', text: 'Error al enviar. Copia la carta y envíala manualmente desde tu correo.' },
no_email: { bg: '#f0e6df', color: '#7b3f2e', text: 'Este proveedor no acepta solicitudes por email. Usa su formulario oficial.' },
};
const m = msgs[status] || msgs.error;
banner.style.cssText = `display:block;padding:0.85rem 1rem;border-radius:10px;margin-bottom:1rem;font-size:0.88rem;background:${m.bg};color:${m.color};border:1px solid ${m.color}33;`;
banner.textContent = m.text;
// Limpiar el parámetro de la URL sin recargar
const clean = window.location.pathname;
window.history.replaceState({}, '', clean);
})();
function copyLetter() {
const text = document.getElementById('letter-box').textContent;
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copy-btn');
btn.textContent = '¡Copiado!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copiar carta';
btn.classList.remove('copied');
}, 2500);
}).catch(() => {
// Fallback para navegadores sin clipboard API
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
</script>
</body>
</html>