2026-06-02 10:36:02 +02:00

535 lines
20 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.

<!-- 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">
<div id="sidebar-container"></div>
<div class="page-body">
<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" class="state-empty">
<i class="bi bi-bag-x state-icon"></i>
<h3 class="state-title">No prompts found</h3>
<p>Try a different category or search term.</p>
</div>
<!-- Error State -->
<div id="market-error" class="state-error">
<i class="bi bi-exclamation-circle state-icon"></i>
<h3 class="state-title">Could not load prompts</h3>
<p id="market-error-msg"></p>
</div>
</main>
</div>
</div>
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
<div id="payment-overlay">
<div class="payment-modal">
<button onclick="closePayment()" class="payment-close-btn">
&#x2715;
</button>
<!-- Step 1: Choose method -->
<div id="pay-step-1">
<h2 class="payment-title">Subscribe to access</h2>
<p id="pay-prompt-title"></p>
<p class="payment-intro">
Choose a payment method to unlock this prompt:
</p>
<div class="payment-method-list">
<button class="pay-method-btn" onclick="selectCrypto('btc')">
<span class="pay-crypto-icon"></span> Bitcoin (BTC)
<span id="price-btc" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('eth')">
<span class="pay-crypto-icon">Ξ</span> Ethereum (ETH)
<span id="price-eth" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('sol')">
<span class="pay-crypto-icon"></span> Solana (SOL)
<span id="price-sol" class="pay-method-price"></span>
</button>
<button class="pay-method-btn" onclick="selectCrypto('usdt')">
<span class="pay-crypto-icon"></span> USDT (TRC-20)
<span id="price-usdt" class="pay-method-price"></span>
</button>
</div>
<p class="payment-disclaimer">
<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">
<button onclick="backToStep1()" class="payment-back-btn">
&#8592; Back
</button>
<h2 id="pay-crypto-title"></h2>
<p class="payment-amount-text">
Send exactly <strong id="pay-amount"></strong> to:
</p>
<div class="payment-address-box">
<code id="pay-address"></code>
<button
onclick="copyAddress()"
title="Copy"
class="payment-copy-btn"
>
<i class="bi bi-clipboard"></i>
</button>
</div>
<p class="payment-warning-text">
⚠️ Only send the exact amount. Payments are non-refundable.
</p>
<div class="payment-info-box">
<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()" class="payment-confirm-btn">
I've sent the payment ✓
</button>
</div>
<!-- Step 3: Success -->
<div id="pay-step-3">
<i class="bi bi-check-circle-fill payment-success-icon"></i>
<h2 class="payment-success-title">Payment received!</h2>
<p class="payment-success-desc">
Your access is being activated. This usually takes 12 minutes.
</p>
<button onclick="closePayment()" class="payment-done-btn">
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,
reviewCount = 0,
promptId = null,
locked = false,
) {
const target =
promptId && !locked
? ` onclick="location.href='/post-detail?id=${promptId}#rating-section'" title="View reviews" class="market-rating-clickable"`
: "";
if (rating == null)
return `<span${target} class="market-rating-none">No reviews yet</span>`;
const stars = Math.round(rating);
const label = reviewCount === 1 ? "review" : "reviews";
return `<span class="prompt-rating"${target}><span class="market-rating-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)} (${reviewCount} ${label})</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 class="market-card-header">
<div class="market-card-avatar">${p.creatorName.charAt(0).toUpperCase()}</div>
<span class="prompt-author">@${p.creatorName}</span>
<span class="market-card-time">${timeAgo(p.timeStamp)}</span>
</div>
<h3 class="prompt-title">${p.title}</h3>
<p class="prompt-description">${p.description || "No description yet."}</p>
<div class="market-card-rating">${renderStars(p.averageRating, p.reviewCount || 0, p.id, locked)}</div>
<div class="prompt-price">${promptPrice(p)}</div>
<div class="prompt-actions">
${
locked
? `<button class="buy-btn buy-btn-locked" onclick='openPayment(${JSON.stringify(p)})'><i class="bi bi-lock-fill"></i> Pay</button>`
: `<button class="buy-btn buy-btn-unlocked" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
}
${
locked
? `<button class="details-btn" disabled><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>