664 lines
24 KiB
HTML
664 lines
24 KiB
HTML
<!-- OnlyPrompt - Marketplace page: dynamic prompts, category filter, sort, crypto payment modal -->
|
||
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>OnlyPrompt - Marketplace</title>
|
||
<link rel="stylesheet" href="../css/variables.css" />
|
||
<link rel="stylesheet" href="../css/base.css" />
|
||
<link rel="stylesheet" href="../css/sidebar.css" />
|
||
<link rel="stylesheet" href="../css/login.css" />
|
||
<link rel="stylesheet" href="../css/topbar.css" />
|
||
<link rel="stylesheet" href="../css/marketplace.css" />
|
||
<script src="../js/profile-shared.js"></script>
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||
/>
|
||
<style>
|
||
/* Additional inline style for sort dropdown – can be moved to marketplace.css */
|
||
.filter-sort-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
margin-bottom: 32px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
padding-bottom: 16px;
|
||
}
|
||
.filter-buttons {
|
||
margin-bottom: 0;
|
||
border-bottom: none;
|
||
padding-bottom: 0;
|
||
}
|
||
.sort-dropdown {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 30px;
|
||
padding: 8px 16px;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: #334155;
|
||
cursor: pointer;
|
||
outline: none;
|
||
}
|
||
.sort-dropdown:hover {
|
||
border-color: #94a3b8;
|
||
}
|
||
@media (max-width: 700px) {
|
||
.filter-sort-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.sort-dropdown {
|
||
align-self: flex-start;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
class="layout"
|
||
style="display: flex; min-height: 100vh; background: var(--bg)"
|
||
>
|
||
<div id="sidebar-container"></div>
|
||
|
||
<div style="flex: 1; display: flex; flex-direction: column">
|
||
<div id="topbar-container"></div>
|
||
|
||
<main class="marketplace-main">
|
||
<!-- Header -->
|
||
<div class="marketplace-header">
|
||
<h1>Marketplace</h1>
|
||
<p>Browse and discover high-quality AI prompts</p>
|
||
</div>
|
||
|
||
<!-- Filter + Sort Row -->
|
||
<div class="filter-sort-row">
|
||
<div class="filter-buttons" id="category-filters">
|
||
<button class="filter-btn active" data-category="">All</button>
|
||
</div>
|
||
<select
|
||
class="sort-dropdown"
|
||
id="sort-select"
|
||
aria-label="Sort prompts"
|
||
>
|
||
<option value="date|false">Newest</option>
|
||
<option value="date|true">Oldest</option>
|
||
<option value="rating|false">Best Rating</option>
|
||
<option value="rating|true">Lowest Rating</option>
|
||
<option value="free|true">Free</option>
|
||
<option value="price|true">Lowest Price</option>
|
||
<option value="price|false">Highest Price</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Prompts Grid -->
|
||
<div class="prompts-grid" id="prompts-grid"></div>
|
||
|
||
<!-- Empty State -->
|
||
<div
|
||
id="market-empty"
|
||
style="
|
||
display: none;
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #64748b;
|
||
"
|
||
>
|
||
<i
|
||
class="bi bi-bag-x"
|
||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||
></i>
|
||
<h3 style="margin-bottom: 8px">No prompts found</h3>
|
||
<p>Try a different category or search term.</p>
|
||
</div>
|
||
|
||
<!-- Error State -->
|
||
<div
|
||
id="market-error"
|
||
style="
|
||
display: none;
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #ef4444;
|
||
"
|
||
>
|
||
<i
|
||
class="bi bi-exclamation-circle"
|
||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||
></i>
|
||
<h3 style="margin-bottom: 8px">Could not load prompts</h3>
|
||
<p id="market-error-msg"></p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
|
||
<div
|
||
id="payment-overlay"
|
||
style="
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 32px;
|
||
max-width: 460px;
|
||
width: 90%;
|
||
position: relative;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<button
|
||
onclick="closePayment()"
|
||
style="
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 18px;
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.4rem;
|
||
cursor: pointer;
|
||
color: #64748b;
|
||
"
|
||
>
|
||
✕
|
||
</button>
|
||
|
||
<!-- Step 1: Choose method -->
|
||
<div id="pay-step-1">
|
||
<h2 style="margin-bottom: 4px">Subscribe to access</h2>
|
||
<p
|
||
id="pay-prompt-title"
|
||
style="color: #6366f1; font-weight: 600; margin-bottom: 16px"
|
||
></p>
|
||
<p style="color: #64748b; margin-bottom: 24px">
|
||
Choose a payment method to unlock this prompt:
|
||
</p>
|
||
<div style="display: flex; flex-direction: column; gap: 12px">
|
||
<button class="pay-method-btn" onclick="selectCrypto('btc')">
|
||
<span style="font-size: 1.4rem">₿</span> Bitcoin (BTC)
|
||
<span
|
||
id="price-btc"
|
||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||
></span>
|
||
</button>
|
||
<button class="pay-method-btn" onclick="selectCrypto('eth')">
|
||
<span style="font-size: 1.4rem">Ξ</span> Ethereum (ETH)
|
||
<span
|
||
id="price-eth"
|
||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||
></span>
|
||
</button>
|
||
<button class="pay-method-btn" onclick="selectCrypto('sol')">
|
||
<span style="font-size: 1.4rem">◎</span> Solana (SOL)
|
||
<span
|
||
id="price-sol"
|
||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||
></span>
|
||
</button>
|
||
<button class="pay-method-btn" onclick="selectCrypto('usdt')">
|
||
<span style="font-size: 1.4rem">₮</span> USDT (TRC-20)
|
||
<span
|
||
id="price-usdt"
|
||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||
></span>
|
||
</button>
|
||
</div>
|
||
<p
|
||
style="
|
||
margin-top: 20px;
|
||
font-size: 0.8rem;
|
||
color: #94a3b8;
|
||
text-align: center;
|
||
"
|
||
>
|
||
<i class="bi bi-shield-lock-fill"></i> Payments are processed
|
||
on-chain. No account needed.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Step 2: Send payment -->
|
||
<div id="pay-step-2" style="display: none">
|
||
<button
|
||
onclick="backToStep1()"
|
||
style="
|
||
background: none;
|
||
border: none;
|
||
color: #6366f1;
|
||
cursor: pointer;
|
||
margin-bottom: 16px;
|
||
font-size: 0.9rem;
|
||
"
|
||
>
|
||
← Back
|
||
</button>
|
||
<h2 id="pay-crypto-title" style="margin-bottom: 8px"></h2>
|
||
<p style="color: #64748b; margin-bottom: 8px">
|
||
Send exactly <strong id="pay-amount"></strong> to:
|
||
</p>
|
||
<div
|
||
style="
|
||
background: #f1f5f9;
|
||
border-radius: 10px;
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
"
|
||
>
|
||
<code
|
||
id="pay-address"
|
||
style="font-size: 0.85rem; word-break: break-all; flex: 1"
|
||
></code>
|
||
<button
|
||
onclick="copyAddress()"
|
||
title="Copy"
|
||
style="
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: #6366f1;
|
||
font-size: 1.1rem;
|
||
"
|
||
>
|
||
<i class="bi bi-clipboard"></i>
|
||
</button>
|
||
</div>
|
||
<p style="font-size: 0.78rem; color: #94a3b8; margin-bottom: 20px">
|
||
⚠️ Only send the exact amount. Payments are non-refundable.
|
||
</p>
|
||
<div
|
||
style="
|
||
background: #fef9c3;
|
||
border: 1px solid #fde68a;
|
||
border-radius: 10px;
|
||
padding: 12px 14px;
|
||
font-size: 0.82rem;
|
||
color: #92400e;
|
||
margin-bottom: 20px;
|
||
"
|
||
>
|
||
<i class="bi bi-info-circle-fill"></i> After sending, click the
|
||
button below to confirm. Access will be granted once the transaction
|
||
is verified.
|
||
</div>
|
||
<button
|
||
onclick="confirmPayment()"
|
||
style="
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: #6366f1;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
I've sent the payment ✓
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Step 3: Success -->
|
||
<div
|
||
id="pay-step-3"
|
||
style="display: none; text-align: center; padding: 20px 0"
|
||
>
|
||
<i
|
||
class="bi bi-check-circle-fill"
|
||
style="
|
||
font-size: 3.5rem;
|
||
color: #10b981;
|
||
display: block;
|
||
margin-bottom: 16px;
|
||
"
|
||
></i>
|
||
<h2 style="margin-bottom: 8px">Payment received!</h2>
|
||
<p style="color: #64748b; margin-bottom: 24px">
|
||
Your access is being activated. This usually takes 1–2 minutes.
|
||
</p>
|
||
<button
|
||
onclick="closePayment()"
|
||
style="
|
||
padding: 12px 28px;
|
||
background: #6366f1;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
Done
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
// ── Sidebar & Topbar ──────────────────────────────────────────────
|
||
fetch("/sidebar.html")
|
||
.then((r) => r.text())
|
||
.then((data) => {
|
||
document.getElementById("sidebar-container").innerHTML = data;
|
||
document
|
||
.querySelectorAll("#sidebar-container .sidebar a")
|
||
.forEach((l) => l.classList.remove("active"));
|
||
const link = document.querySelectorAll(
|
||
"#sidebar-container .sidebar li a",
|
||
)[1];
|
||
if (link) link.classList.add("active");
|
||
});
|
||
fetch("/topbar.html")
|
||
.then((r) => r.text())
|
||
.then(
|
||
(data) =>
|
||
(document.getElementById("topbar-container").innerHTML = data),
|
||
);
|
||
|
||
// ── State ─────────────────────────────────────────────────────────
|
||
let activeCategory = "";
|
||
let searchTimeout;
|
||
|
||
function timeAgo(dateStr) {
|
||
const diff = Date.now() - new Date(dateStr).getTime();
|
||
const m = Math.floor(diff / 60000);
|
||
if (m < 1) return "just now";
|
||
if (m < 60) return `${m}m ago`;
|
||
const h = Math.floor(m / 60);
|
||
if (h < 24) return `${h}h ago`;
|
||
return `${Math.floor(h / 24)}d ago`;
|
||
}
|
||
|
||
function renderStars(rating) {
|
||
if (rating == null)
|
||
return '<span style="color:#94a3b8;font-size:0.8rem;">No reviews yet</span>';
|
||
const stars = Math.round(rating);
|
||
return `<span class="prompt-rating"><span style="color:#f59e0b">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)}</span>`;
|
||
}
|
||
|
||
function promptPrice(prompt) {
|
||
if (prompt.price != null && Number(prompt.price) > 0) {
|
||
return `$${Number(prompt.price).toFixed(2)}`;
|
||
}
|
||
if (prompt.tierLevel) return `$${(prompt.tierLevel * 4.99).toFixed(2)}/mo`;
|
||
if (prompt.canAccess === false) return "Paid";
|
||
return "Free";
|
||
}
|
||
|
||
function getNumericPrice(prompt) {
|
||
if (prompt.price != null && Number(prompt.price) > 0) {
|
||
return Number(prompt.price);
|
||
}
|
||
if (prompt.tierLevel) return prompt.tierLevel * 4.99;
|
||
return 0;
|
||
}
|
||
|
||
function applyMarketplaceSort(prompts, sortBy, ascending) {
|
||
if (sortBy === "free") {
|
||
return prompts.filter((prompt) => getNumericPrice(prompt) === 0);
|
||
}
|
||
|
||
if (sortBy === "price") {
|
||
const direction = ascending === "true" ? 1 : -1;
|
||
return prompts
|
||
.slice()
|
||
.sort((a, b) => (getNumericPrice(a) - getNumericPrice(b)) * direction);
|
||
}
|
||
|
||
return prompts;
|
||
}
|
||
|
||
const MARKET_IMAGES = [
|
||
"/images/content/market1.png",
|
||
"/images/content/market2.png",
|
||
"/images/content/market3.png",
|
||
"/images/content/market4.png",
|
||
"/images/content/market5.png",
|
||
"/images/content/market6.png",
|
||
];
|
||
let cardIndex = 0;
|
||
|
||
function renderCard(p) {
|
||
const paid = p.price != null && Number(p.price) > 0;
|
||
const locked = p.canAccess === false || paid || p.tierLevel != null;
|
||
const img = p.exampleImageUrl || p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
||
return `
|
||
<div class="prompt-card">
|
||
<img src="${img}" alt="${p.title}" class="prompt-img">
|
||
<div class="prompt-info">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||
<div style="width:34px;height:34px;border-radius:50%;background:#6366f1;color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:0.95rem;flex-shrink:0;">${p.creatorName.charAt(0).toUpperCase()}</div>
|
||
<span class="prompt-author">@${p.creatorName}</span>
|
||
<span style="margin-left:auto;font-size:0.75rem;color:#94a3b8;">${timeAgo(p.timeStamp)}</span>
|
||
</div>
|
||
<h3 class="prompt-title">${p.title}</h3>
|
||
<p class="prompt-description">${p.description || 'No description yet.'}</p>
|
||
<div style="margin-bottom:12px;">${renderStars(p.averageRating)}</div>
|
||
<div class="prompt-price">${promptPrice(p)}</div>
|
||
<div class="prompt-actions">
|
||
${
|
||
locked
|
||
? `<button class="buy-btn" style="background:#ef4444;" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
|
||
: `<button class="buy-btn" style="background:#10b981;" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
|
||
}
|
||
${
|
||
locked
|
||
? `<button class="details-btn" disabled style="opacity:.45;cursor:not-allowed;"><i class="bi bi-lock-fill"></i> Details</button>`
|
||
: `<button class="details-btn" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>`
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
async function loadPrompts() {
|
||
const grid = document.getElementById("prompts-grid");
|
||
const emptyEl = document.getElementById("market-empty");
|
||
const errorEl = document.getElementById("market-error");
|
||
const search = document.getElementById("topbarSearchInput")?.value.trim() || "";
|
||
const [sortBy, ascending] = document
|
||
.getElementById("sort-select")
|
||
.value.split("|");
|
||
|
||
grid.innerHTML = "";
|
||
emptyEl.style.display = "none";
|
||
errorEl.style.display = "none";
|
||
cardIndex = 0;
|
||
|
||
try {
|
||
const apiSortBy = sortBy === "price" || sortBy === "free" ? "date" : sortBy;
|
||
const apiAscending = sortBy === "price" || sortBy === "free" ? "false" : ascending;
|
||
let url = `/api/v1/prompts?sortBy=${apiSortBy}&ascending=${apiAscending}&limit=50`;
|
||
if (activeCategory) url += `&category=${activeCategory}`;
|
||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||
|
||
const res = await fetch(url);
|
||
if (res.status === 401) {
|
||
location.href = "/login";
|
||
return;
|
||
}
|
||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||
|
||
let prompts = applyMarketplaceSort(await res.json(), sortBy, ascending);
|
||
|
||
if (prompts.length === 0) {
|
||
emptyEl.style.display = "block";
|
||
return;
|
||
}
|
||
grid.innerHTML = prompts.map(renderCard).join("");
|
||
} catch (e) {
|
||
document.getElementById("market-error-msg").textContent =
|
||
e.message || "Please check if the backend is running.";
|
||
errorEl.style.display = "block";
|
||
}
|
||
}
|
||
|
||
async function loadCategories() {
|
||
try {
|
||
const res = await fetch("/api/v1/categories/minimal");
|
||
if (!res.ok) return;
|
||
const cats = await res.json();
|
||
const container = document.getElementById("category-filters");
|
||
cats.forEach((c) => {
|
||
const btn = document.createElement("button");
|
||
btn.className = "filter-btn";
|
||
btn.dataset.category = c.slug;
|
||
btn.textContent = c.name;
|
||
btn.addEventListener("click", () => {
|
||
document
|
||
.querySelectorAll("#category-filters .filter-btn")
|
||
.forEach((b) => b.classList.remove("active"));
|
||
btn.classList.add("active");
|
||
activeCategory = c.slug;
|
||
loadPrompts();
|
||
});
|
||
container.appendChild(btn);
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
// ── Category filter (All) ──────────────────────────────────────────
|
||
document
|
||
.querySelector("#category-filters .filter-btn")
|
||
.addEventListener("click", function () {
|
||
document
|
||
.querySelectorAll("#category-filters .filter-btn")
|
||
.forEach((b) => b.classList.remove("active"));
|
||
this.classList.add("active");
|
||
activeCategory = "";
|
||
loadPrompts();
|
||
});
|
||
|
||
document
|
||
.getElementById("sort-select")
|
||
.addEventListener("change", loadPrompts);
|
||
function wireMarketplaceTopbarSearch() {
|
||
const input = document.getElementById("topbarSearchInput");
|
||
if (!input || input.dataset.marketplaceSearchReady === "true") return;
|
||
|
||
input.dataset.marketplaceSearchReady = "true";
|
||
input.addEventListener("input", () => {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(loadPrompts, 400);
|
||
});
|
||
|
||
input.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter") return;
|
||
event.preventDefault();
|
||
loadPrompts();
|
||
});
|
||
}
|
||
|
||
const topbarObserver = new MutationObserver(wireMarketplaceTopbarSearch);
|
||
topbarObserver.observe(document.body, { childList: true, subtree: true });
|
||
wireMarketplaceTopbarSearch();
|
||
|
||
// Make openPayment global
|
||
window.openPayment = openPayment;
|
||
|
||
// ── Payment Modal ──────────────────────────────────────────────────
|
||
const CRYPTO_ADDRESSES = {
|
||
btc: "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf1N",
|
||
eth: "0x742d35Cc6634C0532925a3b8D4C9B8E4D8F2b1a",
|
||
sol: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV1",
|
||
usdt: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
|
||
};
|
||
const CRYPTO_NAMES = {
|
||
btc: "Bitcoin (BTC)",
|
||
eth: "Ethereum (ETH)",
|
||
sol: "Solana (SOL)",
|
||
usdt: "USDT TRC-20",
|
||
};
|
||
let currentPrompt = null;
|
||
let currentCrypto = null;
|
||
|
||
function openPayment(prompt) {
|
||
currentPrompt = prompt;
|
||
const usd = prompt.price != null && Number(prompt.price) > 0
|
||
? Number(prompt.price)
|
||
: prompt.tierLevel ? prompt.tierLevel * 4.99 : 0;
|
||
document.getElementById("pay-prompt-title").textContent = prompt.title;
|
||
document.getElementById("price-btc").textContent =
|
||
`≈ ${(usd / 67000).toFixed(6)} BTC`;
|
||
document.getElementById("price-eth").textContent =
|
||
`≈ ${(usd / 3200).toFixed(5)} ETH`;
|
||
document.getElementById("price-sol").textContent =
|
||
`≈ ${(usd / 145).toFixed(3)} SOL`;
|
||
document.getElementById("price-usdt").textContent =
|
||
`${usd.toFixed(2)} USDT`;
|
||
|
||
document.getElementById("pay-step-1").style.display = "block";
|
||
document.getElementById("pay-step-2").style.display = "none";
|
||
document.getElementById("pay-step-3").style.display = "none";
|
||
document.getElementById("payment-overlay").style.display = "flex";
|
||
}
|
||
|
||
window.selectCrypto = function (crypto) {
|
||
currentCrypto = crypto;
|
||
const usd = currentPrompt.tierLevel
|
||
? currentPrompt.tierLevel * 4.99
|
||
: 0;
|
||
const amounts = {
|
||
btc: `${(usd / 67000).toFixed(6)} BTC`,
|
||
eth: `${(usd / 3200).toFixed(5)} ETH`,
|
||
sol: `${(usd / 145).toFixed(3)} SOL`,
|
||
usdt: `${usd.toFixed(2)} USDT`,
|
||
};
|
||
document.getElementById("pay-crypto-title").textContent =
|
||
CRYPTO_NAMES[crypto];
|
||
document.getElementById("pay-amount").textContent = amounts[crypto];
|
||
document.getElementById("pay-address").textContent =
|
||
CRYPTO_ADDRESSES[crypto];
|
||
document.getElementById("pay-step-1").style.display = "none";
|
||
document.getElementById("pay-step-2").style.display = "block";
|
||
};
|
||
|
||
window.backToStep1 = function () {
|
||
document.getElementById("pay-step-1").style.display = "block";
|
||
document.getElementById("pay-step-2").style.display = "none";
|
||
};
|
||
|
||
window.copyAddress = function () {
|
||
navigator.clipboard.writeText(CRYPTO_ADDRESSES[currentCrypto]);
|
||
};
|
||
|
||
window.confirmPayment = function () {
|
||
document.getElementById("pay-step-2").style.display = "none";
|
||
document.getElementById("pay-step-3").style.display = "block";
|
||
};
|
||
|
||
window.closePayment = function () {
|
||
document.getElementById("payment-overlay").style.display = "none";
|
||
};
|
||
|
||
document
|
||
.getElementById("payment-overlay")
|
||
.addEventListener("click", function (e) {
|
||
if (e.target === this) closePayment();
|
||
});
|
||
|
||
// ── Init ───────────────────────────────────────────────────────────
|
||
await loadCategories();
|
||
await loadPrompts();
|
||
</script>
|
||
</body>
|
||
</html>
|