This commit is contained in:
thePR0M3TH3AN
2025-07-01 15:39:38 -04:00
parent a381c91a63
commit fb0be95d0d

View File

@@ -12,11 +12,16 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style> <style>
:root { --accent-purple: #a855f7; --accent-orange: #f97316; } :root {
--accent-purple: #a855f7;
--accent-orange: #f97316;
}
/* Body is now a column so the footer lives at the very bottom */
body { body {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center; /* keeps <main> vertically centred when possible */
min-height: 100vh; min-height: 100vh;
margin: 0; margin: 0;
} }
@@ -31,6 +36,16 @@
<body class="bg-gray-900 text-gray-200"> <body class="bg-gray-900 text-gray-200">
<main id="app" class="w-full max-w-sm p-6 space-y-6 text-center"></main> <main id="app" class="w-full max-w-sm p-6 space-y-6 text-center"></main>
<!-- Subtle footer on every page -->
<footer class="mt-auto mb-2 text-center text-xs text-gray-500 opacity-70">
Enjoying this?&nbsp;
<a
href="https://nostrtipjar.netlify.app/?n=npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx"
class="underline hover:text-purple-400"
>Leave&nbsp;a&nbsp;tip!</a
>
</footer>
<script type="module"> <script type="module">
import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.7.5?bundle"; import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.7.5?bundle";
@@ -46,8 +61,10 @@
const log = (...a) => console.log("[tipjar]", ...a); const log = (...a) => console.log("[tipjar]", ...a);
const warn = (...a) => console.warn("[tipjar]", ...a); const warn = (...a) => console.warn("[tipjar]", ...a);
const error = (...a) => console.error("[tipjar]", ...a); const error = (...a) => console.error("[tipjar]", ...a);
window.addEventListener("error", e => error("Uncaught:", e.message)); window.addEventListener("error", (e) => error("Uncaught:", e.message));
window.addEventListener("unhandledrejection", e => error("Promise rejection:", e.reason)); window.addEventListener("unhandledrejection", (e) =>
error("Promise rejection:", e.reason)
);
init(); init();
@@ -63,34 +80,37 @@
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const npub = params.get("n"); const npub = params.get("n");
// Base is the URL of the HTML file (strip query and hash) // Base is the URL of the HTML file (strip query and hash)
const base = window.location.href.split('?')[0].split('#')[0]; const base = window.location.href.split("?")[0].split("#")[0];
return { npub, base }; return { npub, base };
} }
function renderLanding(base, msg = '') { function renderLanding(base, msg = "") {
document.getElementById('app').innerHTML = ` document.getElementById("app").innerHTML = `
<div class="space-y-6"> <div class="space-y-6">
<h1 class="text-3xl font-extrabold accent-gradient">Lightning Tip Jar</h1> <h1 class="text-3xl font-extrabold accent-gradient">Lightning Tip Jar</h1>
<p class="text-gray-400">Enter a Nostr <span class="font-mono">npub…</span> to create a tipping page.</p> <p class="text-gray-400">Enter a Nostr <span class="font-mono">npub…</span> to create a tipping page.</p>
${msg ? `<p class='text-red-500'>${msg}</p>` : ''} ${msg ? `<p class='text-red-500'>${msg}</p>` : ""}
<form id="form" class="space-y-4"> <form id="form" class="space-y-4">
<input id="input" type="text" placeholder="npub1…" required class="w-full px-4 py-2 rounded-lg bg-gray-800 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600" /> <input id="input" type="text" placeholder="npub1…" required class="w-full px-4 py-2 rounded-lg bg-gray-800 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600" />
<button class="w-full py-2 rounded-lg bg-gradient-to-r from-purple-600 to-orange-500 text-white font-semibold transition active:scale-95" type="submit">Generate</button> <button class="w-full py-2 rounded-lg bg-gradient-to-r from-purple-600 to-orange-500 text-white font-semibold transition active:scale-95" type="submit">Generate</button>
</form> </form>
<footer class="text-xs text-gray-500">Client-side • No servers • Lightning ⚡</footer> <footer class="text-xs text-gray-500">Client-side • No servers • Lightning ⚡</footer>
</div>`; </div>`;
document.getElementById('form').onsubmit = e => { document.getElementById("form").onsubmit = (e) => {
e.preventDefault(); e.preventDefault();
const v = document.getElementById('input').value.trim(); const v = document.getElementById("input").value.trim();
if (!v.startsWith('npub')) { renderLanding(base, 'npub must start with "npub".'); return; } if (!v.startsWith("npub")) {
const baseUrl = window.location.href.split('?')[0].split('#')[0]; renderLanding(base, 'npub must start with "npub".');
log('Redirecting to', `${baseUrl}?n=${v}`); return;
}
const baseUrl = window.location.href.split("?")[0].split("#")[0];
log("Redirecting to", `${baseUrl}?n=${v}`);
location.href = `${baseUrl}?n=${v}`; location.href = `${baseUrl}?n=${v}`;
}; };
} }
async function renderTipJar(npub) { async function renderTipJar(npub) {
const app = document.getElementById('app'); const app = document.getElementById("app");
app.innerHTML = ` app.innerHTML = `
<img id="avatar" class="w-24 h-24 rounded-full mx-auto hidden" alt="Avatar" /> <img id="avatar" class="w-24 h-24 rounded-full mx-auto hidden" alt="Avatar" />
<h1 id="name" class="text-2xl font-extrabold accent-gradient mb-2">Loading…</h1> <h1 id="name" class="text-2xl font-extrabold accent-gradient mb-2">Loading…</h1>
@@ -103,33 +123,40 @@
try { try {
const dec = nip19.decode(npub); const dec = nip19.decode(npub);
hex = dec.data; hex = dec.data;
log('Decoded hex pubkey', hex); log("Decoded hex pubkey", hex);
} catch { } catch {
return fatal(app, 'Bad npub format'); return fatal(app, "Bad npub format");
} }
const fallback = `${npub}@npub.cash`; const fallback = `${npub}@npub.cash`;
let meta = null, ln = fallback; let meta = null,
ln = fallback;
const pool = new SimplePool(); const pool = new SimplePool();
const sub = pool.sub(RELAYS, [{ kinds: [0], authors: [hex], limit: 1 }]); const sub = pool.sub(RELAYS, [{ kinds: [0], authors: [hex], limit: 1 }]);
const timer = setTimeout(() => { warn('Relay timeout'); finalize(); }, TIMEOUT_MS); const timer = setTimeout(() => {
warn("Relay timeout");
finalize();
}, TIMEOUT_MS);
sub.on('event', ev => { sub.on("event", (ev) => {
clearTimeout(timer); clearTimeout(timer);
try { try {
meta = JSON.parse(ev.content || '{}'); meta = JSON.parse(ev.content || "{}");
ln = meta.lud16 || meta.lud06 || fallback; ln = meta.lud16 || meta.lud06 || fallback;
log('Profile metadata', meta); log("Profile metadata", meta);
} catch (e) { } catch (e) {
error('Failed to parse metadata JSON', e); error("Failed to parse metadata JSON", e);
} }
finalize(); finalize();
}); });
sub.on('eose', () => warn('EOSE reached, no event')); sub.on("eose", () => warn("EOSE reached, no event"));
function finalize() { function finalize() {
try { sub.unsub(); pool.close(); } catch {}; try {
sub.unsub();
pool.close();
} catch {}
buildUI(app, meta, ln); buildUI(app, meta, ln);
} }
@@ -139,27 +166,34 @@
} }
function buildUI(app, meta, ln) { function buildUI(app, meta, ln) {
document.getElementById('name').textContent = meta?.name || 'Lightning Tip Jar'; document.getElementById("name").textContent =
if (meta?.about) document.getElementById('about').textContent = meta.about; meta?.name || "Lightning Tip Jar";
if (meta?.about) document.getElementById("about").textContent = meta.about;
if (meta?.picture) { if (meta?.picture) {
const img = document.getElementById('avatar'); const img = document.getElementById("avatar");
img.src = meta.picture; img.src = meta.picture;
img.classList.remove('hidden'); img.classList.remove("hidden");
} }
if (!meta?.lud16 && !meta?.lud06) { if (!meta?.lud16 && !meta?.lud06) {
const about = document.getElementById('about'); const about = document.getElementById("about");
about.textContent += ' (using fallback address)'; about.textContent += " (using fallback address)";
} }
document.getElementById('addr').textContent = ln; document.getElementById("addr").textContent = ln;
const qrEl = document.getElementById('qrcode'); const qrEl = document.getElementById("qrcode");
qrEl.innerHTML = ''; qrEl.innerHTML = "";
new QRCode(qrEl, { text: ln, width: 200, height: 200, colorDark: '#111', colorLight: '#fff' }); new QRCode(qrEl, {
document.getElementById('copy').onclick = () => { text: ln,
width: 200,
height: 200,
colorDark: "#111",
colorLight: "#fff",
});
document.getElementById("copy").onclick = () => {
navigator.clipboard.writeText(ln).then(() => { navigator.clipboard.writeText(ln).then(() => {
const btn = document.getElementById('copy'); const btn = document.getElementById("copy");
const old = btn.textContent; const old = btn.textContent;
btn.textContent = 'Copied!'; btn.textContent = "Copied!";
setTimeout(() => btn.textContent = old, 1200); setTimeout(() => (btn.textContent = old), 1200);
}); });
}; };
} }