Files
seedPass/landing/tip-jar.html
thePR0M3TH3AN 495080ade3 update
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/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 = 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 doesnt 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;
});
}
// -------- TipJar 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>