Files
seedPass/landing/tip-jar.html
thePR0M3TH3AN 159df3fe29 update
2025-06-29 19:17:33 -04:00

162 lines
7.3 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="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightning Tip Jar</title>
<!-- TailwindCSS (Play CDN) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- QRCode generator -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-Rdym6z1nIh/x6O+4QJBk/w++Q5e7oapbPo8EjS2X3tq6MUJBbiuKxVhQ5BaRPHUl2j3Ky4gGB4vJfIQDw4RGeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style>
:root {
--accent-purple: #a855f7;
--accent-orange: #f97316;
}
.accent-gradient {
background: linear-gradient(90deg, var(--accent-purple), var(--accent-orange));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn-primary {
@apply w-full py-2 rounded-lg bg-gradient-to-r from-purple-600 to-orange-500 text-white font-semibold transition active:scale-95;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center bg-gray-900 text-gray-200">
<main id="app" class="w-full max-w-sm p-6 space-y-6 text-center"></main>
<script type="module">
/******************** Imports ********************/
import { nip19 } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/+esm";
import { SimplePool } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/pool/+esm";
/******************** Config ********************/
const RELAYS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://relay.primal.net",
"wss://relay.nostr.band",
];
const TIMEOUT_MS = 7000;
/******************** Logger ********************/
const log = (...a) => console.log("[tipjar]", ...a);
const warn = (...a) => console.warn("[tipjar]", ...a);
const err = (...a) => console.error("[tipjar]", ...a);
window.addEventListener("error", e => err("Uncaught:", e.message, e.error||e));
window.addEventListener("unhandledrejection", e => err("Promise rejection:", e.reason));
/******************** Entry ********************/
init();
function init() {
const { npub, basePath } = parseURL();
if (!npub) return showLanding(basePath);
if (!npub.startsWith("npub")) return showLanding(basePath, "Please enter a valid npub.");
loadTipJar(npub);
}
/******** URL helpers ********/
function parseURL() {
const pathParts = location.pathname.split("/").filter(Boolean);
let npub = null;
if (pathParts.length >= 2 && pathParts[0] === "n") npub = pathParts[1];
else npub = new URLSearchParams(location.search).get("n");
const idx = location.pathname.indexOf("/n/");
const base = idx >= 0 ? location.pathname.slice(0, idx) : location.pathname;
return { npub, basePath: base };
}
/******** LANDING PAGE ********/
function showLanding(basePath, msg = "") {
document.getElementById("app").innerHTML = `
<div class="space-y-6">
<h1 class="text-3xl font-extrabold accent-gradient">Lightning Tip Jar</h1>
<p class="text-gray-400">Generate a oneclick tipping page from any Nostr <span class="font-mono">npub</span>.</p>
${msg ? `<p class="text-red-500">${msg}</p>` : ""}
<form id="lookup" class="space-y-4">
<input id="npubIn" 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 type="submit" class="btn-primary">Create Tip Page</button>
</form>
<footer class="text-xs text-gray-500 pt-4">Works entirely clientside • No servers • 100% Lightning ⚡</footer>
</div>`;
document.getElementById("lookup").onsubmit = (e) => {
e.preventDefault();
const val = document.getElementById("npubIn").value.trim();
if (!val.startsWith("npub")) return showLanding(basePath, "npub should start with npub. Try again.");
const isFile = /\.html?$/.test(basePath);
const dest = isFile ? `${basePath}?n=${val}` : `${basePath.replace(/\/$/, "")}/n/${val}`;
location.href = dest;
};
}
/******** TIPJAR FLOW ********/
async function loadTipJar(npub) {
const app = document.getElementById("app");
app.innerHTML = `
<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>
<p id="about" class="text-sm text-gray-400"></p>
<div id="qrcode" class="mx-auto"></div>
<p id="address" class="break-all font-mono text-lg"></p>
<button id="copy" class="btn-primary">Copy Address</button>`;
// decode npub
let hex;
try { hex = nip19.decode(npub).data; log("Hex pubkey", hex); }
catch { return fail("Bad npub format"); }
const fallback = `${npub}@npub.cash`;
let meta = null, addr = fallback;
const pool = new SimplePool();
const sub = pool.sub(RELAYS, [{ kinds:[0], authors:[hex], limit:1 }]);
let finished = false;
const timer = setTimeout(() => { warn("Timeout: no profile"); wrapUp(); }, TIMEOUT_MS);
sub.on("event", (ev, relay) => {
log("Profile", ev.id, "from", relay);
clearTimeout(timer); finished = true;
try {
meta = JSON.parse(ev.content || "{}");
addr = meta.lud16 || meta.lud06 || fallback;
} catch(e) { err("Meta parse", e); }
wrapUp();
});
sub.on("notice", (n) => warn("Relay notice", n));
sub.on("eose", () => { if (!finished) warn("EOSE no profile"); });
function wrapUp() {
try { sub.unsub(); pool.close(RELAYS); } catch {}
render(meta, addr);
}
function fail(msg){ app.innerHTML = `<p class='text-red-500'>${msg}</p>`; }
}
/******** RENDER TIP CARD ********/
function render(meta, ln) {
document.getElementById("name").textContent = meta?.name || "Lightning Tip Jar";
document.getElementById("about").textContent = meta?.about || "";
if (meta?.picture){ const img = document.getElementById("avatar"); img.src = meta.picture; img.classList.remove("hidden"); }
if (!meta?.lud16 && !meta?.lud06) document.getElementById("about").textContent += "\n(No lightning address on profile; using fallback)";
document.getElementById("address").textContent = ln;
const qr = document.getElementById("qrcode"); qr.innerHTML = "";
new QRCode(qr, { text: ln, width: 200, height: 200, colorDark: "#fff", colorLight: "#000", correctLevel: QRCode.CorrectLevel.H });
document.getElementById("copy").onclick = () => {
navigator.clipboard.writeText(ln).then(() => {
const btn = document.getElementById("copy"); const old = btn.textContent;
btn.textContent = "Copied!"; setTimeout(()=>btn.textContent=old, 1500);
});
};
}
</script>
</body>
</html>