Files
SeedPass/landing/tip-jar.html
T
2025-06-29 19:08:25 -04:00

155 lines
7.2 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>
<!-- Accent palette -->
<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/[email protected]/+esm";
import { SimplePool } from "https://cdn.jsdelivr.net/npm/[email protected]/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 = 6000;
// ===== Logger helpers =====
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 } = extractNpub();
if (!npub) return renderLanding(basePath);
if (!npub.startsWith("npub")) return renderLanding(basePath, "That doesn’t look like a valid npub.");
bootTipJar(npub);
}
// -------- Extract npub & basePath (keep filename when present) --------
function extractNpub() {
const parts = location.pathname.split("/").filter(Boolean);
let npub = null;
if (parts.length >= 2 && parts[0] === "n") npub = parts[1];
else npub = new URLSearchParams(location.search).get("n");
const idx = location.pathname.indexOf("/n/");
const basePath = idx >= 0 ? location.pathname.slice(0, idx) : location.pathname; // keep .html if any
return { npub, basePath };
}
// -------- Landing page --------
function renderLanding(basePath, errorMsg = "") {
log("Landing", { basePath });
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">Enter a <span class="font-mono">npub…</span> to generate a shareable tipping link from their Nostr profile.</p>
${errorMsg ? `<p class='text-red-500'>${errorMsg}</p>` : ""}
<form id="lookupForm" class="space-y-4">
<input id="npubInput" type="text" required placeholder="npub1…" 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="btn-primary" type="submit">Generate Tip Page</button>
</form>
<footer class="text-xs text-gray-500 pt-4">Built with nostr-tools and Lightning ❤</footer>
</div>`;
document.getElementById("lookupForm").addEventListener("submit", (e) => {
e.preventDefault();
const value = (document.getElementById("npubInput").value || "").trim();
if (!value.startsWith("npub")) return renderLanding(basePath, "Please enter a valid npub starting with ‘npub’." );
// Determine redirect style: if current path ends with .html use query param, else use /n/<npub>
const isFile = /\.html?$/.test(basePath);
const target = isFile ? `${basePath}?n=${value}` : `${basePath.replace(/\/$/, "")}/n/${value}`;
log("Redirect →", target);
location.href = target;
});
}
// -------- Tip‑Jar flow --------
async function bootTipJar(npub) {
const app = document.getElementById("app");
app.innerHTML = `
<img id="avatar" class="w-24 h-24 rounded-full mx-auto hidden" alt="User avatar" />
<h1 id="name" class="text-2xl font-extrabold accent-gradient leading-tight 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="copyBtn" class="btn-primary">Copy Address</button>`;
let pubHex;
try { pubHex = nip19.decode(npub).data; } catch { return fatal("Invalid npub format"); }
const fallbackLn = `${npub}@npub.cash`;
let lnAddr = fallbackLn;
let meta = null;
const pool = new SimplePool();
const sub = pool.sub(RELAYS, [{ kinds:[0], authors:[pubHex], limit:1 }]);
let done = false;
const timer = setTimeout(() => { if(!done){warn("Profile timeout"); finish(); }}, TIMEOUT_MS);
sub.on("event", evt => { done=true; clearTimeout(timer); try{ meta=JSON.parse(evt.content||"{}"); lnAddr = meta.lud16||meta.lud06||fallbackLn;}catch(e){err(e);} finish(); });
sub.on("eose", () => { if(!done){warn("EOSE without meta"); finish(); }});
function finish(){ try{sub.unsub(); pool.close(RELAYS);}catch(_){} renderTip(meta, lnAddr); }
function fatal(msg){ app.innerHTML = `<p class='text-red-500'>${msg}</p>`; }
}
// -------- Render final UI --------
function renderTip(meta, ln) {
document.getElementById("name").textContent = meta?.name || "Lightning Tip Jar";
if (meta?.about) document.getElementById("about").textContent = meta.about;
if (meta?.picture) { const img = document.getElementById("avatar"); img.src = meta.picture; img.classList.remove("hidden"); }
document.getElementById("address").textContent = ln;
const qrWrap = document.getElementById("qrcode"); qrWrap.innerHTML = "";
new QRCode(qrWrap, { text: ln, width: 200, height: 200, colorDark: "#fff", colorLight: "#000", correctLevel: QRCode.CorrectLevel.H });
document.getElementById("copyBtn").onclick = () => {
navigator.clipboard.writeText(ln).then(() => {
const btn = document.getElementById("copyBtn");
const old = btn.textContent; btn.textContent = "Copied!"; setTimeout(() => btn.textContent = old, 1500);
});
};
}
</script>
</body>
</html>