mirror of
https://github.com/PR0M3TH3AN/NostrTipJar.git
synced 2025-09-08 07:19:09 +00:00
update
This commit is contained in:
168
src/index.html
Normal file
168
src/index.html
Normal file
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nostr Tip Jar</title>
|
||||
|
||||
<!-- TailwindCSS (Play CDN for demo) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- QRCode generator -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root { --accent-purple: #a855f7; --accent-orange: #f97316; }
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.accent-gradient {
|
||||
background: linear-gradient(90deg, var(--accent-purple), var(--accent-orange));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
|
||||
<script type="module">
|
||||
import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.7.5?bundle";
|
||||
|
||||
const RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.snort.social",
|
||||
"wss://relay.primal.net",
|
||||
"wss://relay.nostr.band",
|
||||
];
|
||||
const TIMEOUT_MS = 8000;
|
||||
|
||||
const log = (...a) => console.log("[tipjar]", ...a);
|
||||
const warn = (...a) => console.warn("[tipjar]", ...a);
|
||||
const error = (...a) => console.error("[tipjar]", ...a);
|
||||
window.addEventListener("error", e => error("Uncaught:", e.message));
|
||||
window.addEventListener("unhandledrejection", e => error("Promise rejection:", e.reason));
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
const { npub, base } = parseUrl();
|
||||
if (!npub) return renderLanding(base);
|
||||
if (!npub.startsWith("npub")) return renderLanding(base, "Invalid npub provided.");
|
||||
renderTipJar(npub);
|
||||
}
|
||||
|
||||
function parseUrl() {
|
||||
const parts = location.pathname.split("/").filter(Boolean);
|
||||
let npub = null;
|
||||
if (parts[0] === "n" && parts[1]) npub = parts[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, base };
|
||||
}
|
||||
|
||||
function renderLanding(base, 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">Enter a Nostr <span class="font-mono">npub…</span> to create a tipping page.</p>
|
||||
${msg ? `<p class='text-red-500'>${msg}</p>` : ''}
|
||||
<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" />
|
||||
<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>
|
||||
<footer class="text-xs text-gray-500">Client-side • No servers • Lightning ⚡</footer>
|
||||
</div>`;
|
||||
document.getElementById('form').onsubmit = e => {
|
||||
e.preventDefault();
|
||||
const v = document.getElementById('input').value.trim();
|
||||
if (!v.startsWith('npub')) return renderLanding(base, 'npub must start with "npub".');
|
||||
const isFile = /\.html?$/.test(base);
|
||||
location.href = isFile ? `${base}?n=${v}` : `${base.replace(/\/$/, '')}/n/${v}`;
|
||||
};
|
||||
}
|
||||
|
||||
async function renderTipJar(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 bg-white p-4 rounded-xl inline-block"></div>
|
||||
<p id="addr" class="break-all font-mono text-lg"></p>
|
||||
<button id="copy" 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">Copy Address</button>`;
|
||||
|
||||
let hex;
|
||||
try {
|
||||
const dec = nip19.decode(npub);
|
||||
hex = dec.data;
|
||||
log('Decoded hex pubkey', hex);
|
||||
} catch {
|
||||
return fatal(app, 'Bad npub format');
|
||||
}
|
||||
|
||||
const fallback = `${npub}@npub.cash`;
|
||||
let meta = null, ln = fallback;
|
||||
|
||||
const pool = new SimplePool();
|
||||
const sub = pool.sub(RELAYS, [{ kinds: [0], authors: [hex], limit: 1 }]);
|
||||
const timer = setTimeout(() => { warn('Relay timeout'); finalize(); }, TIMEOUT_MS);
|
||||
|
||||
sub.on('event', ev => {
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
meta = JSON.parse(ev.content || '{}');
|
||||
ln = meta.lud16 || meta.lud06 || fallback;
|
||||
log('Profile metadata', meta);
|
||||
} catch (e) {
|
||||
error('Failed to parse metadata JSON', e);
|
||||
}
|
||||
finalize();
|
||||
});
|
||||
sub.on('eose', () => warn('EOSE reached, no event'));
|
||||
|
||||
function finalize() {
|
||||
try { sub.unsub(); pool.close(); } catch {};
|
||||
buildUI(app, meta, ln);
|
||||
}
|
||||
|
||||
function fatal(container, msg) {
|
||||
container.innerHTML = `<p class='text-red-500'>${msg}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUI(app, 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');
|
||||
}
|
||||
if (!meta?.lud16 && !meta?.lud06) {
|
||||
const about = document.getElementById('about');
|
||||
about.textContent += ' (using fallback address)';
|
||||
}
|
||||
document.getElementById('addr').textContent = ln;
|
||||
const qrEl = document.getElementById('qrcode');
|
||||
qrEl.innerHTML = '';
|
||||
new QRCode(qrEl, { text: ln, width: 200, height: 200, colorDark: '#111', colorLight: '#fff' });
|
||||
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, 1200);
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user