feat: pagina generadora de enlaces de invitado protegida por contrasena
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<title>Nuevo enlace de invitado · AQNSN</title>
|
||||
<meta name="theme-color" content="#06080d">
|
||||
<link rel="icon" href="img/avatar.jpg">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.gen-form { display: grid; gap: 20px; max-width: 560px; }
|
||||
.gen-form label { display: grid; gap: 6px; font-weight: 600; font-size: .95rem; }
|
||||
.gen-form input[type=text], .gen-form input[type=password], .gen-form input[type=datetime-local] {
|
||||
width: 100%; padding: 13px 15px; border-radius: 12px; border: 1px solid var(--line);
|
||||
background: var(--card); color: var(--ink); font: inherit; font-weight: 400; transition: .2s;
|
||||
}
|
||||
.gen-form input:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 3px rgba(46,230,255,.18); }
|
||||
.dias-list { display: grid; gap: 10px; }
|
||||
.dia-row { display: flex; gap: 10px; align-items: center; }
|
||||
.dia-row input { flex: 1; }
|
||||
.dia-row .del-dia { background: transparent; border: 1px solid var(--line); color: var(--muted);
|
||||
padding: 8px 13px; border-radius: 8px; font: inherit; cursor: pointer; transition: .2s; flex: none; }
|
||||
.dia-row .del-dia:hover { border-color: #ff8a8a; color: #ff8a8a; }
|
||||
.add-dia { background: transparent; border: 1px dashed var(--line); color: var(--muted);
|
||||
padding: 11px 18px; border-radius: 12px; font: inherit; cursor: pointer; width: 100%; transition: .2s; }
|
||||
.add-dia:hover { border-color: var(--neon); color: var(--neon); }
|
||||
.result-box { display: none; margin-top: 8px; padding: 16px 18px; border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--neon); border-radius: 12px; background: var(--card); }
|
||||
.result-box .url { word-break: break-all; font-size: .92rem; color: var(--neon); margin: 0 0 12px; }
|
||||
.copy-btn { padding: 9px 20px; border-radius: 999px; border: 1px solid var(--neon);
|
||||
background: transparent; color: var(--neon); font: inherit; font-weight: 700; cursor: pointer; transition: .2s; }
|
||||
.copy-btn:hover { background: var(--neon); color: #001a20; }
|
||||
.lock { max-width: 340px; }
|
||||
small { color: var(--muted); font-weight: 400; font-size: .88rem; }
|
||||
#login-err { color: #ff8a8a; font-size: .92rem; min-height: 1.4em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||
</header>
|
||||
|
||||
<main class="legal contact">
|
||||
<h1>Nuevo <em>enlace</em> de invitado</h1>
|
||||
|
||||
<div id="login-panel">
|
||||
<p class="lead-c">Solo para el equipo del programa.</p>
|
||||
<form id="login-form" class="gen-form lock" novalidate>
|
||||
<label>Contraseña
|
||||
<input id="pwd" type="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<p id="login-err"></p>
|
||||
<button class="btn primary" type="submit">Entrar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="gen-panel" hidden>
|
||||
<p class="lead-c">Rellena el nombre del invitado y los días propuestos. Se generará un enlace que podrás enviarle.</p>
|
||||
<form id="gen-form" class="gen-form" novalidate autocomplete="off">
|
||||
<label>Nombre del invitado <span class="req">*</span>
|
||||
<input id="nombre" type="text" maxlength="60" placeholder="Ej: Ana García" required>
|
||||
<small>Tal como quieres presentarle en el programa.</small>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.95rem;margin-bottom:8px">Días propuestos <span class="req">*</span></div>
|
||||
<small style="display:block;margin-bottom:12px">Añade entre 1 y 5 opciones. La invitada elegirá la que mejor le venga.</small>
|
||||
<div id="dias-list" class="dias-list"></div>
|
||||
<button type="button" id="add-dia" class="add-dia">+ Añadir otro día</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.95rem;margin-bottom:8px">Modo de enlace</div>
|
||||
<label class="check" style="margin-bottom:8px">
|
||||
<input type="radio" name="modo" value="propuestos" checked>
|
||||
<span>Días propuestos — la invitada elige</span>
|
||||
</label>
|
||||
<label class="check">
|
||||
<input type="radio" name="modo" value="fijo">
|
||||
<span>Fecha fija — ya está confirmada</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p id="gen-err" class="status err" hidden></p>
|
||||
<button class="btn primary" type="submit">Generar enlace</button>
|
||||
</form>
|
||||
|
||||
<div id="result" class="result-box">
|
||||
<p class="url" id="result-url"></p>
|
||||
<button class="copy-btn" id="copy-btn">Copiar enlace</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var HASH = '%%PASS_HASH%%';
|
||||
|
||||
async function sha256hex(str) {
|
||||
var buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
|
||||
return Array.from(new Uint8Array(buf)).map(function (b) { return b.toString(16).padStart(2, '0'); }).join('');
|
||||
}
|
||||
|
||||
var loginPanel = document.getElementById('login-panel');
|
||||
var genPanel = document.getElementById('gen-panel');
|
||||
var loginForm = document.getElementById('login-form');
|
||||
var loginErr = document.getElementById('login-err');
|
||||
var pwd = document.getElementById('pwd');
|
||||
|
||||
var SESSION_KEY = 'aqnsn-gen-auth';
|
||||
try { if (sessionStorage.getItem(SESSION_KEY) === HASH) { loginPanel.hidden = true; genPanel.hidden = false; } } catch (e) {}
|
||||
|
||||
loginForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
sha256hex(pwd.value).then(function (h) {
|
||||
if (h === HASH) {
|
||||
try { sessionStorage.setItem(SESSION_KEY, h); } catch (e) {}
|
||||
loginPanel.hidden = true; genPanel.hidden = false; loginErr.textContent = '';
|
||||
} else {
|
||||
loginErr.textContent = 'Contraseña incorrecta.';
|
||||
pwd.value = ''; pwd.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Generador ---
|
||||
var diasList = document.getElementById('dias-list');
|
||||
var addDiaBtn = document.getElementById('add-dia');
|
||||
var genForm = document.getElementById('gen-form');
|
||||
var genErr = document.getElementById('gen-err');
|
||||
var result = document.getElementById('result');
|
||||
var resultUrl = document.getElementById('result-url');
|
||||
var copyBtn = document.getElementById('copy-btn');
|
||||
|
||||
function addDia(value) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'dia-row';
|
||||
var inp = document.createElement('input');
|
||||
inp.type = 'datetime-local'; inp.className = 'dia-inp';
|
||||
if (value) inp.value = value;
|
||||
var del = document.createElement('button');
|
||||
del.type = 'button'; del.className = 'del-dia'; del.textContent = '✕';
|
||||
del.addEventListener('click', function () {
|
||||
row.remove();
|
||||
actualizarAddBtn();
|
||||
});
|
||||
row.appendChild(inp); row.appendChild(del);
|
||||
diasList.appendChild(row);
|
||||
actualizarAddBtn();
|
||||
}
|
||||
|
||||
function actualizarAddBtn() {
|
||||
addDiaBtn.style.display = diasList.children.length >= 5 ? 'none' : '';
|
||||
}
|
||||
|
||||
addDia(); // una fila por defecto
|
||||
addDiaBtn.addEventListener('click', function () { addDia(); });
|
||||
|
||||
genForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
genErr.hidden = true;
|
||||
var nombre = document.getElementById('nombre').value.trim();
|
||||
if (!nombre) { genErr.textContent = 'Escribe el nombre del invitado.'; genErr.hidden = false; return; }
|
||||
|
||||
var modo = genForm.querySelector('input[name="modo"]:checked').value;
|
||||
var inps = Array.prototype.slice.call(diasList.querySelectorAll('input.dia-inp'));
|
||||
var timestamps = [];
|
||||
for (var i = 0; i < inps.length; i++) {
|
||||
var v = inps[i].value;
|
||||
if (!v) continue;
|
||||
var ts = Math.round(new Date(v).getTime() / 1000);
|
||||
if (isNaN(ts)) continue;
|
||||
timestamps.push(ts);
|
||||
}
|
||||
if (timestamps.length === 0) { genErr.textContent = 'Añade al menos un día.'; genErr.hidden = false; return; }
|
||||
|
||||
var base = location.origin + '/invitados.html';
|
||||
var url;
|
||||
if (modo === 'fijo') {
|
||||
if (timestamps.length > 1) { genErr.textContent = 'En modo fecha fija solo puedes indicar un día.'; genErr.hidden = false; return; }
|
||||
url = base + '?n=' + encodeURIComponent(nombre) + '&f=' + timestamps[0];
|
||||
} else {
|
||||
url = base + '?n=' + encodeURIComponent(nombre) + '&d=' + timestamps.join(',');
|
||||
}
|
||||
|
||||
resultUrl.textContent = url;
|
||||
result.style.display = 'block';
|
||||
result.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
|
||||
copyBtn.addEventListener('click', function () {
|
||||
var url = resultUrl.textContent;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(function () {
|
||||
copyBtn.textContent = '¡Copiado!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copiar enlace'; }, 2000);
|
||||
});
|
||||
} else {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = url; document.body.appendChild(ta); ta.select(); document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
copyBtn.textContent = '¡Copiado!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copiar enlace'; }, 2000);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user