mirror of
https://github.com/PR0M3TH3AN/NostrTipJar.git
synced 2025-09-08 23:39:24 +00:00
update
This commit is contained in:
112
src/index.html
112
src/index.html
@@ -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?
|
||||||
|
<a
|
||||||
|
href="https://nostrtipjar.netlify.app/?n=npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx"
|
||||||
|
class="underline hover:text-purple-400"
|
||||||
|
>Leave a 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,30 +166,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Reference in New Issue
Block a user