415 lines
15 KiB
HTML
415 lines
15 KiB
HTML
<!-- OnlyPrompt - Post Detail: dynamic prompt loaded via API -->
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>OnlyPrompt - Post Detail</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/post-detail.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"
|
|
/>
|
|
</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="post-detail-main">
|
|
<div class="post-detail-container" id="detail-content">
|
|
<!-- Loading -->
|
|
<div
|
|
id="detail-loading"
|
|
style="text-align: center; padding: 60px 20px; color: #64748b"
|
|
>
|
|
<i
|
|
class="bi bi-hourglass-split"
|
|
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
|
></i>
|
|
<p>Loading prompt...</p>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div
|
|
id="detail-error"
|
|
style="
|
|
display: none;
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #ef4444;
|
|
"
|
|
>
|
|
<i
|
|
class="bi bi-exclamation-circle"
|
|
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
|
></i>
|
|
<h3 id="detail-error-title">Prompt not found</h3>
|
|
<p
|
|
id="detail-error-msg"
|
|
style="color: #64748b; margin-top: 8px"
|
|
></p>
|
|
<button
|
|
onclick="history.back()"
|
|
style="
|
|
margin-top: 20px;
|
|
padding: 10px 24px;
|
|
background: #6366f1;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
"
|
|
>
|
|
Go Back
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content (hidden until loaded) -->
|
|
<div id="detail-body" style="display: none">
|
|
<!-- Header -->
|
|
<div class="post-header">
|
|
<div
|
|
style="
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 16px;
|
|
"
|
|
>
|
|
<div
|
|
id="creator-avatar"
|
|
style="
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 50%;
|
|
background: #6366f1;
|
|
color: #fff;
|
|
font-weight: 700;
|
|
font-size: 1.1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
"
|
|
></div>
|
|
<div>
|
|
<span
|
|
id="creator-name"
|
|
style="font-weight: 600; font-size: 0.95rem"
|
|
></span>
|
|
<span
|
|
id="prompt-date"
|
|
style="display: block; font-size: 0.8rem; color: #94a3b8"
|
|
></span>
|
|
</div>
|
|
<div style="margin-left: auto">
|
|
<span id="tier-badge"></span>
|
|
<button
|
|
id="edit-prompt-btn"
|
|
style="
|
|
display: none;
|
|
margin-left: 10px;
|
|
padding: 6px 14px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: #f1f5f9;
|
|
color: #334155;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
"
|
|
>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<h1 class="post-title" id="prompt-title"></h1>
|
|
<div class="post-meta">
|
|
<span class="category" id="prompt-category"></span>
|
|
<span class="updated" id="prompt-updated"></span>
|
|
</div>
|
|
<div class="post-stats">
|
|
<span id="prompt-rating-stat"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="prompt-section" id="desc-section">
|
|
<h2>DESCRIPTION</h2>
|
|
<div class="prompt-content">
|
|
<p id="prompt-description"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prompt Content (only if accessible) -->
|
|
<div class="prompt-section" id="prompt-content-section">
|
|
<h2>PROMPT</h2>
|
|
<div
|
|
class="prompt-content"
|
|
id="prompt-body"
|
|
style="
|
|
white-space: pre-wrap;
|
|
font-family: monospace;
|
|
background: #f8fafc;
|
|
border-radius: 10px;
|
|
padding: 16px;
|
|
font-size: 0.9rem;
|
|
line-height: 1.7;
|
|
"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Example Output -->
|
|
<div class="example-section" id="example-section" style="display: none">
|
|
<h2>EXAMPLE OUTPUT</h2>
|
|
<div class="example-content">
|
|
<div id="example-output-text" class="example-output-text" style="white-space: pre-wrap"></div>
|
|
<div id="example-image" class="example-image" style="display: none">
|
|
<img id="example-image-img" src="" alt="Example output image">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Locked section (shown instead of prompt if no access) -->
|
|
<div
|
|
id="locked-section"
|
|
style="
|
|
display: none;
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
background: #f8fafc;
|
|
border-radius: 12px;
|
|
margin-bottom: 28px;
|
|
"
|
|
>
|
|
<i
|
|
class="bi bi-lock-fill"
|
|
style="
|
|
font-size: 2.5rem;
|
|
color: #94a3b8;
|
|
display: block;
|
|
margin-bottom: 12px;
|
|
"
|
|
></i>
|
|
<h3 style="margin-bottom: 8px">
|
|
This prompt requires a subscription
|
|
</h3>
|
|
<p style="color: #64748b; margin-bottom: 20px">
|
|
Subscribe to <strong id="locked-creator"></strong> to access
|
|
this prompt.
|
|
</p>
|
|
<button
|
|
id="locked-subscribe-btn"
|
|
style="
|
|
padding: 12px 28px;
|
|
background: #6366f1;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
"
|
|
>
|
|
Subscribe <span id="locked-tier-name"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Rating -->
|
|
<div class="rating-section" id="rating-section">
|
|
<div class="rating-stars" id="rating-display"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
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"));
|
|
});
|
|
fetch("/topbar.html")
|
|
.then((r) => r.text())
|
|
.then(
|
|
(data) =>
|
|
(document.getElementById("topbar-container").innerHTML = data),
|
|
);
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
const id = params.get("id");
|
|
|
|
if (!id) {
|
|
showError("No prompt ID provided", "Add ?id=... to the URL.");
|
|
} else {
|
|
loadPrompt(id);
|
|
}
|
|
|
|
async function loadPrompt(id) {
|
|
try {
|
|
const res = await fetch(`/api/v1/prompts/${id}`);
|
|
if (res.status === 401) {
|
|
location.href = "/login";
|
|
return;
|
|
}
|
|
if (res.status === 404) {
|
|
showError(
|
|
"Prompt not found",
|
|
"This prompt does not exist or you do not have access.",
|
|
);
|
|
return;
|
|
}
|
|
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
|
|
|
const p = await res.json();
|
|
renderPrompt(p);
|
|
} catch (e) {
|
|
showError("Could not load prompt", e.message);
|
|
}
|
|
}
|
|
|
|
function renderPrompt(p) {
|
|
document.title = `${p.title} — OnlyPrompt`;
|
|
|
|
document.getElementById("creator-avatar").textContent = p.creatorName
|
|
.charAt(0)
|
|
.toUpperCase();
|
|
document.getElementById("creator-name").textContent = p.creatorName;
|
|
document.getElementById("prompt-date").textContent = new Date(
|
|
p.timeStamp,
|
|
).toLocaleDateString("de-CH", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
document.getElementById("prompt-title").textContent = p.title;
|
|
document.getElementById("prompt-category").textContent =
|
|
p.categoryName || "Uncategorized";
|
|
document.getElementById("prompt-updated").textContent =
|
|
`Updated: ${new Date(p.timeStamp).toLocaleDateString()}`;
|
|
document.getElementById("prompt-description").textContent =
|
|
p.description;
|
|
document.getElementById("prompt-body").textContent = p.content;
|
|
document.getElementById("prompt-rating-stat").innerHTML =
|
|
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
|
|
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>`;
|
|
renderExamples(p);
|
|
renderOwnerActions(p);
|
|
|
|
// Tier badge
|
|
const badge = document.getElementById("tier-badge");
|
|
if (p.price != null && Number(p.price) > 0) {
|
|
badge.innerHTML = `<span style="background:#fef3c7;color:#92400e;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">$${Number(p.price).toFixed(2)}</span>`;
|
|
} else if (p.tierName) {
|
|
badge.innerHTML = `<span style="background:#f1f5f9;color:#475569;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
|
|
} else {
|
|
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
|
|
}
|
|
|
|
// Rating
|
|
if (p.averageRating != null) {
|
|
const stars = Math.round(p.averageRating);
|
|
document.getElementById("rating-display").innerHTML =
|
|
`<span style="color:#f59e0b;font-size:1.1rem;">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
|
|
<span style="margin-left:8px;font-weight:600;">${p.averageRating.toFixed(1)}</span>
|
|
<span style="color:#94a3b8;font-size:0.85rem;margin-left:4px;">/ 5.0</span>`;
|
|
document.getElementById("prompt-rating-stat").innerHTML =
|
|
`<i class="bi ${p.isLiked ? 'bi-heart-fill' : 'bi-heart'}" style="color:#ef4444;"></i> ${p.likeCount || 0} likes
|
|
<span style="margin-left:12px;"><i class="bi ${p.isSaved ? 'bi-bookmark-fill' : 'bi-bookmark'}" style="color:#f59e0b;"></i> ${p.saveCount || 0} saves</span>
|
|
<span style="margin-left:12px;"><i class="bi bi-star-fill" style="color:#f59e0b;"></i> ${p.averageRating.toFixed(1)} rating</span>`;
|
|
} else {
|
|
document.getElementById("rating-display").innerHTML =
|
|
'<span style="color:#94a3b8;font-size:0.9rem;">No ratings yet</span>';
|
|
}
|
|
|
|
// Content visibility
|
|
if (p.content) {
|
|
document.getElementById("prompt-content-section").style.display =
|
|
"block";
|
|
document.getElementById("locked-section").style.display = "none";
|
|
} else {
|
|
document.getElementById("prompt-content-section").style.display =
|
|
"none";
|
|
document.getElementById("locked-section").style.display = "block";
|
|
document.getElementById("locked-creator").textContent = p.creatorName;
|
|
document.getElementById("locked-tier-name").textContent = p.tierName
|
|
? `— ${p.tierName}`
|
|
: "";
|
|
}
|
|
|
|
document.getElementById("detail-loading").style.display = "none";
|
|
document.getElementById("detail-body").style.display = "block";
|
|
}
|
|
|
|
async function renderOwnerActions(p) {
|
|
try {
|
|
const response = await fetch("/api/v1/auth/me", {
|
|
credentials: "same-origin",
|
|
});
|
|
if (!response.ok) return;
|
|
|
|
const user = await response.json();
|
|
if (user.id !== p.creatorId) return;
|
|
|
|
const editBtn = document.getElementById("edit-prompt-btn");
|
|
editBtn.style.display = "inline-block";
|
|
editBtn.onclick = () => {
|
|
location.href = `/create?id=${p.id}`;
|
|
};
|
|
} catch {
|
|
// Keep edit hidden if the current user cannot be loaded.
|
|
}
|
|
}
|
|
|
|
function renderExamples(p) {
|
|
const section = document.getElementById("example-section");
|
|
const text = document.getElementById("example-output-text");
|
|
const imageWrap = document.getElementById("example-image");
|
|
const image = document.getElementById("example-image-img");
|
|
|
|
if (!p.exampleOutput && !p.exampleImageUrl) {
|
|
section.style.display = "none";
|
|
return;
|
|
}
|
|
|
|
section.style.display = "block";
|
|
text.textContent = p.exampleOutput || "";
|
|
text.style.display = p.exampleOutput ? "block" : "none";
|
|
|
|
if (p.exampleImageUrl) {
|
|
image.src = p.exampleImageUrl;
|
|
imageWrap.style.display = "block";
|
|
} else {
|
|
image.removeAttribute("src");
|
|
imageWrap.style.display = "none";
|
|
}
|
|
}
|
|
|
|
function showError(title, msg) {
|
|
document.getElementById("detail-loading").style.display = "none";
|
|
document.getElementById("detail-error").style.display = "block";
|
|
document.getElementById("detail-error-title").textContent = title;
|
|
document.getElementById("detail-error-msg").textContent = msg;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|