Files
url-shrinker/index.html
Fable 309661c9e4 URL Shrinker: acortador de URLs con Node 24 + node:sqlite
App sin dependencias: UI web, POST /api/shorten (codigo aleatorio o
personalizado), redireccion 302 con contador de clics, /api/stats/<code>
y /healthz. SQLite en DATA_DIR (/data) para montar volumen persistente.
2026-09-12 03:38:39 +02:00

119 lines
4.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>URL Shrinker</title>
<style>
:root {
--bg: #0f1117; --card: #181b24; --text: #e8eaf0; --muted: #9aa1b2;
--accent: #5b8def; --accent-hover: #7aa2f2; --error: #e06c75; --ok: #7fbf7f;
--border: #2a2f3d; --radius: 10px;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f4f5f8; --card: #ffffff; --text: #1c1f26; --muted: #5c6270;
--accent: #3b6fd4; --accent-hover: #2a5cbf; --error: #c0392b; --ok: #2e7d32;
--border: #dde0e8;
}
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: var(--bg); color: var(--text);
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; padding: 1.5rem;
}
main { width: 100%; max-width: 34rem; }
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
h1 span { color: var(--accent); }
p.sub { color: var(--muted); margin: 0 0 1.5rem; }
form {
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
padding: 1.25rem; display: grid; gap: .75rem;
}
label { font-size: .85rem; color: var(--muted); display: grid; gap: .3rem; }
input {
width: 100%; padding: .6rem .75rem; border-radius: 6px; border: 1px solid var(--border);
background: var(--bg); color: var(--text); font-size: 1rem;
}
input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
button {
padding: .65rem; border: 0; border-radius: 6px; background: var(--accent); color: #fff;
font-size: 1rem; font-weight: 600; cursor: pointer;
}
button:hover { background: var(--accent-hover); }
button:disabled { opacity: .6; cursor: wait; }
#result {
margin-top: 1rem; padding: 1rem 1.25rem; border-radius: var(--radius); display: none;
background: var(--card); border: 1px solid var(--border); word-break: break-all;
}
#result.ok { display: block; border-color: var(--ok); }
#result.err { display: block; border-color: var(--error); color: var(--error); }
#result a { color: var(--accent); font-weight: 600; text-decoration: none; font-size: 1.1rem; }
#result a:hover { text-decoration: underline; }
#copy {
margin-left: .6rem; padding: .25rem .6rem; font-size: .8rem; font-weight: 500;
background: transparent; color: var(--muted); border: 1px solid var(--border);
}
#copy:hover { color: var(--text); background: transparent; }
footer { margin-top: 1.25rem; text-align: center; color: var(--muted); font-size: .8rem; }
</style>
</head>
<body>
<main>
<h1>URL <span>Shrinker</span></h1>
<p class="sub">Pega una URL larga, llévate una corta.</p>
<form id="form">
<label>URL a acortar
<input id="url" type="url" placeholder="https://ejemplo.com/una/ruta/muy/larga" required autofocus>
</label>
<label>Código personalizado <small>(opcional, 332 caracteres)</small>
<input id="code" type="text" pattern="[A-Za-z0-9_\-]{3,32}" placeholder="mi-enlace">
</label>
<button type="submit" id="submit">Acortar</button>
</form>
<div id="result"></div>
<footer>montlab.dev · los enlaces cuentan sus clics: <code>/api/stats/&lt;código&gt;</code></footer>
</main>
<script>
const form = document.getElementById('form');
const result = document.getElementById('result');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('submit');
btn.disabled = true;
result.className = '';
try {
const body = { url: document.getElementById('url').value };
const code = document.getElementById('code').value.trim();
if (code) body.code = code;
const res = await fetch('/api/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'error ' + res.status);
result.className = 'ok';
result.innerHTML = '';
const a = document.createElement('a');
a.href = data.short_url; a.textContent = data.short_url; a.target = '_blank'; a.rel = 'noopener';
const copy = document.createElement('button');
copy.id = 'copy'; copy.type = 'button'; copy.textContent = 'Copiar';
copy.onclick = async () => {
await navigator.clipboard.writeText(data.short_url);
copy.textContent = '¡Copiado!';
setTimeout(() => (copy.textContent = 'Copiar'), 1500);
};
result.append(a, copy);
} catch (err) {
result.className = 'err';
result.textContent = err.message;
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>