Implements Timmy as an economic peer with sovereign Nostr presence: - nostr.js: New module handling keypair lifecycle (localStorage), NIP-01 signed notes, NIP-57 zap requests, NIP-58 badge awards, relay broadcast, and an economic activity feed - app.js: Imports nostr module; adds 3D holographic "ECONOMIC" panel on the left side of the Nexus showing live npub + activity feed; wires /zap, /vouch, /note, /identity, /relays chat commands; animates economic panel scanline in game loop - index.html: Adds Nostr identity HUD widget (top-right) showing npub with Zap, Vouch, and ID buttons; Zap modal and Vouch modal for point-and-click economic actions; inline module script for modal UX - style.css: Full design system for .nostr-panel, .nostr-modal, form inputs, action buttons, status indicators, and zapFlash keyframe animation on incoming zaps Keypair persists in localStorage (timmy_nostr_privkey_v1). Broadcasts to damus.io, relay.nostr.band, nos.lol, relay.snort.social. Fixes #13 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
269 lines
11 KiB
HTML
269 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<!--
|
|
______ __
|
|
/ ____/___ ____ ___ ____ __ __/ /____ _____
|
|
/ / / __ \/ __ `__ \/ __ \/ / / / __/ _ \/ ___/
|
|
/ /___/ /_/ / / / / / / /_/ / /_/ / /_/ __/ /
|
|
\____/\____/_/ /_/ /_/ .___/\__,_/\__/\___/_/
|
|
/_/
|
|
Created with Perplexity Computer
|
|
https://www.perplexity.ai/computer
|
|
-->
|
|
<meta name="generator" content="Perplexity Computer">
|
|
<meta name="author" content="Perplexity Computer">
|
|
<meta property="og:see_also" content="https://www.perplexity.ai/computer">
|
|
<link rel="author" href="https://www.perplexity.ai/computer">
|
|
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>The Nexus — Timmy's Sovereign Home</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="./style.css">
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<!-- Loading Screen -->
|
|
<div id="loading-screen">
|
|
<div class="loader-content">
|
|
<div class="loader-sigil">
|
|
<svg viewBox="0 0 120 120" width="120" height="120">
|
|
<defs>
|
|
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#4af0c0"/>
|
|
<stop offset="100%" stop-color="#7b5cff"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<circle cx="60" cy="60" r="55" fill="none" stroke="url(#sigil-grad)" stroke-width="1.5" opacity="0.4"/>
|
|
<circle cx="60" cy="60" r="45" fill="none" stroke="url(#sigil-grad)" stroke-width="1" opacity="0.3">
|
|
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="8s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<polygon points="60,15 95,80 25,80" fill="none" stroke="#4af0c0" stroke-width="1.5" opacity="0.6">
|
|
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="-360 60 60" dur="12s" repeatCount="indefinite"/>
|
|
</polygon>
|
|
<circle cx="60" cy="60" r="8" fill="#4af0c0" opacity="0.8">
|
|
<animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
|
|
</circle>
|
|
</svg>
|
|
</div>
|
|
<h1 class="loader-title">THE NEXUS</h1>
|
|
<p class="loader-subtitle">Initializing Sovereign Space...</p>
|
|
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HUD Overlay -->
|
|
<div id="hud" class="game-ui" style="display:none;">
|
|
<!-- Top Left: Debug -->
|
|
<div id="debug-overlay" class="hud-debug"></div>
|
|
|
|
<!-- Top Center: Location -->
|
|
<div class="hud-location">
|
|
<span class="hud-location-icon">◈</span>
|
|
<span id="hud-location-text">The Nexus</span>
|
|
</div>
|
|
|
|
<!-- Bottom: Chat Interface -->
|
|
<div id="chat-panel" class="chat-panel">
|
|
<div class="chat-header">
|
|
<span class="chat-status-dot"></span>
|
|
<span>Timmy Terminal</span>
|
|
<button id="chat-toggle" class="chat-toggle-btn" aria-label="Toggle chat">▼</button>
|
|
</div>
|
|
<div id="chat-messages" class="chat-messages">
|
|
<div class="chat-msg chat-msg-system">
|
|
<span class="chat-msg-prefix">[NEXUS]</span> Sovereign space initialized. Timmy is observing.
|
|
</div>
|
|
<div class="chat-msg chat-msg-timmy">
|
|
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus, Alexander. All systems nominal.
|
|
</div>
|
|
</div>
|
|
<div class="chat-input-row">
|
|
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
|
|
<button id="chat-send" class="chat-send-btn" aria-label="Send message">→</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimap / Controls hint -->
|
|
<div class="hud-controls">
|
|
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
|
</div>
|
|
|
|
<!-- Nostr Identity Panel (top-right) -->
|
|
<div id="nostr-panel" class="nostr-panel">
|
|
<div class="nostr-panel-header">
|
|
<span class="nostr-sigil">⚡</span>
|
|
<span class="nostr-title">NOSTR IDENTITY</span>
|
|
</div>
|
|
<div class="nostr-panel-body">
|
|
<div class="nostr-row">
|
|
<span class="nostr-label">npub</span>
|
|
<span id="nostr-npub" class="nostr-value">initializing…</span>
|
|
</div>
|
|
<div class="nostr-actions">
|
|
<button class="nostr-btn nostr-btn-zap" id="btn-open-zap" title="Send a zap">⚡ Zap</button>
|
|
<button class="nostr-btn nostr-btn-vouch" id="btn-open-vouch" title="Vouch for someone">🏅 Vouch</button>
|
|
<button class="nostr-btn nostr-btn-identity" id="btn-show-identity" title="Show identity">🔑 ID</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zap Modal -->
|
|
<div id="zap-modal" class="nostr-modal" style="display:none;">
|
|
<div class="nostr-modal-box">
|
|
<div class="nostr-modal-header">
|
|
<span>⚡ Send Zap</span>
|
|
<button class="nostr-modal-close" data-modal="zap-modal">✕</button>
|
|
</div>
|
|
<div class="nostr-modal-body">
|
|
<label class="nostr-field-label">Recipient pubkey (hex or npub)</label>
|
|
<input type="text" id="zap-pubkey" class="nostr-input" placeholder="npub1… or hex pubkey">
|
|
<label class="nostr-field-label">Amount (sats)</label>
|
|
<input type="number" id="zap-amount" class="nostr-input" value="21" min="1">
|
|
<label class="nostr-field-label">Comment (optional)</label>
|
|
<input type="text" id="zap-comment" class="nostr-input" placeholder="Great work!">
|
|
<button class="nostr-btn nostr-btn-zap nostr-btn-full" id="btn-send-zap">⚡ Send Zap Request</button>
|
|
<div id="zap-status" class="nostr-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vouch Modal -->
|
|
<div id="vouch-modal" class="nostr-modal" style="display:none;">
|
|
<div class="nostr-modal-box">
|
|
<div class="nostr-modal-header">
|
|
<span>🏅 Vouch for Contributor</span>
|
|
<button class="nostr-modal-close" data-modal="vouch-modal">✕</button>
|
|
</div>
|
|
<div class="nostr-modal-body">
|
|
<label class="nostr-field-label">Recipient pubkey (hex or npub)</label>
|
|
<input type="text" id="vouch-pubkey" class="nostr-input" placeholder="npub1… or hex pubkey">
|
|
<label class="nostr-field-label">Badge name</label>
|
|
<input type="text" id="vouch-badge" class="nostr-input" value="Trusted Builder" placeholder="Trusted Builder">
|
|
<label class="nostr-field-label">Reason (optional)</label>
|
|
<input type="text" id="vouch-reason" class="nostr-input" placeholder="Vouched by Timmy from the Nexus">
|
|
<button class="nostr-btn nostr-btn-vouch nostr-btn-full" id="btn-send-vouch">🏅 Issue Vouch</button>
|
|
<div id="vouch-status" class="nostr-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Click to Enter -->
|
|
<div id="enter-prompt" style="display:none;">
|
|
<div class="enter-content">
|
|
<h2>Enter The Nexus</h2>
|
|
<p>Click anywhere to begin</p>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id="nexus-canvas"></canvas>
|
|
|
|
<footer class="nexus-footer">
|
|
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
|
|
Created with Perplexity Computer
|
|
</a>
|
|
</footer>
|
|
|
|
<script type="module" src="./app.js"></script>
|
|
<script type="module">
|
|
// Nostr modal interaction — wired after app.js imports nostr.js
|
|
import {
|
|
createZapRequest, createVouch, broadcastEvent, getIdentity,
|
|
} from './nostr.js';
|
|
|
|
// Helper: resolve pubkey (handle npub or hex)
|
|
async function resolvePubkey(input) {
|
|
const trimmed = input.trim();
|
|
if (trimmed.startsWith('npub1')) {
|
|
// Use nostr-tools nip19 decode
|
|
const { nip19 } = await import('https://esm.sh/nostr-tools@2.3.1?bundle');
|
|
const decoded = nip19.decode(trimmed);
|
|
return decoded.data;
|
|
}
|
|
return trimmed; // assume hex
|
|
}
|
|
|
|
// Modal helpers
|
|
function openModal(id) {
|
|
document.getElementById(id).style.display = 'flex';
|
|
}
|
|
function closeModal(id) {
|
|
document.getElementById(id).style.display = 'none';
|
|
}
|
|
function setStatus(id, msg, isError = false) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.textContent = msg;
|
|
el.className = 'nostr-status ' + (isError ? 'nostr-status-err' : 'nostr-status-ok');
|
|
}
|
|
}
|
|
|
|
// Open/close buttons
|
|
document.getElementById('btn-open-zap').addEventListener('click', () => openModal('zap-modal'));
|
|
document.getElementById('btn-open-vouch').addEventListener('click', () => openModal('vouch-modal'));
|
|
document.getElementById('btn-show-identity').addEventListener('click', () => {
|
|
const id = getIdentity();
|
|
if (id?.npub) alert(`Timmy's Nostr Identity\n\nnpub: ${id.npub}\npubkey: ${id.pubkey}`);
|
|
else alert('Nostr identity not yet loaded.');
|
|
});
|
|
|
|
document.querySelectorAll('.nostr-modal-close').forEach(btn => {
|
|
btn.addEventListener('click', () => closeModal(btn.dataset.modal));
|
|
});
|
|
document.querySelectorAll('.nostr-modal').forEach(modal => {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) closeModal(modal.id);
|
|
});
|
|
});
|
|
|
|
// Zap send
|
|
document.getElementById('btn-send-zap').addEventListener('click', async () => {
|
|
const pubkeyInput = document.getElementById('zap-pubkey').value;
|
|
const sats = parseInt(document.getElementById('zap-amount').value) || 21;
|
|
const comment = document.getElementById('zap-comment').value;
|
|
if (!pubkeyInput) { setStatus('zap-status', 'Enter a recipient pubkey', true); return; }
|
|
setStatus('zap-status', 'Signing & broadcasting…');
|
|
try {
|
|
const pubkey = await resolvePubkey(pubkeyInput);
|
|
const event = createZapRequest(pubkey, sats * 1000, comment);
|
|
const results = await broadcastEvent(event);
|
|
const ok = results.filter(r => r.ok).length;
|
|
setStatus('zap-status', `✓ Sent to ${ok}/${results.length} relays`);
|
|
} catch (err) {
|
|
setStatus('zap-status', `✗ ${err.message}`, true);
|
|
}
|
|
});
|
|
|
|
// Vouch send
|
|
document.getElementById('btn-send-vouch').addEventListener('click', async () => {
|
|
const pubkeyInput = document.getElementById('vouch-pubkey').value;
|
|
const badge = document.getElementById('vouch-badge').value || 'Trusted Builder';
|
|
const reason = document.getElementById('vouch-reason').value || 'Vouched by Timmy from the Nexus';
|
|
if (!pubkeyInput) { setStatus('vouch-status', 'Enter a recipient pubkey', true); return; }
|
|
setStatus('vouch-status', 'Signing & broadcasting…');
|
|
try {
|
|
const pubkey = await resolvePubkey(pubkeyInput);
|
|
const event = createVouch(pubkey, badge, reason);
|
|
const results = await broadcastEvent(event);
|
|
const ok = results.filter(r => r.ok).length;
|
|
setStatus('vouch-status', `✓ Badge awarded on ${ok}/${results.length} relays`);
|
|
} catch (err) {
|
|
setStatus('vouch-status', `✗ ${err.message}`, true);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|