frontend_projekt/OnlyPrompt.Frontend/subscription-tiers.html
2026-06-14 11:58:04 +02:00

390 lines
14 KiB
HTML

<!-- OnlyPrompt - Subscription tiers page: create and manage monthly creator tiers -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnlyPrompt - Subscription Tiers</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/topbar.css" />
<link rel="stylesheet" href="../css/subscription-tiers.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>
<a class="skip-link" href="#main-content">Skip to main content</a>
<div class="layout">
<div id="sidebar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<main class="tiers-main" id="main-content" tabindex="-1">
<header class="tiers-header">
<h1>Subscription Tiers</h1>
<p>Create monthly access levels for your paid prompts.</p>
</header>
<nav class="tiers-tabs" role="tablist" aria-label="Subscription tier sections">
<button type="button" class="tiers-tab active" data-tab="manage" id="manageTiersTab" role="tab" aria-selected="true" aria-controls="manage-tab-panel">
My Tiers
</button>
<button type="button" class="tiers-tab" data-tab="subscriptions" id="subscriptionsTiersTab" role="tab" aria-selected="false" aria-controls="subscriptions-tab-panel">
My Subscriptions
</button>
</nav>
<section class="tiers-layout" id="manage-tab-panel" role="tabpanel" aria-labelledby="manageTiersTab">
<article class="tier-panel">
<h2 id="tier-form-title">Create Tier</h2>
<form id="tier-form" class="tier-form">
<label>
Tier Name
<input
id="tier-name"
type="text"
placeholder="Supporter"
required
/>
</label>
<label>
Level
<input id="tier-level" type="number" min="1" step="1" value="1" required />
</label>
<label>
Monthly Price
<input
id="tier-price"
type="number"
min="0"
step="0.01"
placeholder="4.99"
required
/>
</label>
<label>
Description
<textarea
id="tier-description"
placeholder="Access to basic premium prompts."
></textarea>
</label>
<div class="tier-form-actions">
<button type="submit" class="tier-primary-btn" id="tier-submit-btn">
Save Tier
</button>
<button type="button" class="tier-secondary-btn" id="tier-reset-btn">
Clear
</button>
</div>
<p id="tier-status" role="status" aria-live="polite"></p>
</form>
</article>
<section>
<div class="tier-list-header">
<h2>Your Tiers</h2>
<p>Higher levels include access to prompts from lower levels.</p>
</div>
<div class="tiers-grid" id="tiers-grid" aria-live="polite">
<div class="tiers-empty">Loading tiers...</div>
</div>
</section>
</section>
<section class="subscriptions-panel" id="subscriptions-tab-panel" role="tabpanel" aria-labelledby="subscriptionsTiersTab" hidden>
<div class="tier-list-header">
<h2>Your Subscriptions</h2>
<p>Creators you follow or support with a monthly tier.</p>
</div>
<div class="subscriptions-grid" id="subscriptions-grid" aria-live="polite">
<div class="tiers-empty">Loading subscriptions...</div>
</div>
</section>
</main>
</div>
</div>
<script>
fetch("/sidebar.html")
.then((r) => r.text())
.then((data) => {
document.getElementById("sidebar-container").innerHTML = data;
document
.querySelectorAll("#sidebar-container .sidebar a")
.forEach((link) => {
link.classList.remove("active");
link.removeAttribute("aria-current");
});
const tiersLink = document.querySelector(
'#sidebar-container a[href="subscription-tiers.html"]',
);
if (tiersLink) {
tiersLink.classList.add("active");
tiersLink.setAttribute("aria-current", "page");
}
});
fetch("/topbar.html")
.then((r) => r.text())
.then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
const tierForm = document.getElementById("tier-form");
const tierFormTitle = document.getElementById("tier-form-title");
const tierName = document.getElementById("tier-name");
const tierLevel = document.getElementById("tier-level");
const tierPrice = document.getElementById("tier-price");
const tierDescription = document.getElementById("tier-description");
const tierStatus = document.getElementById("tier-status");
const tiersGrid = document.getElementById("tiers-grid");
const subscriptionsGrid = document.getElementById("subscriptions-grid");
const manageTabPanel = document.getElementById("manage-tab-panel");
const subscriptionsTabPanel = document.getElementById(
"subscriptions-tab-panel",
);
const resetBtn = document.getElementById("tier-reset-btn");
let editingTierId = null;
let tiers = [];
let subscriptions = [];
function setActiveTab(tabName) {
document.querySelectorAll(".tiers-tab").forEach((tab) => {
tab.classList.toggle("active", tab.dataset.tab === tabName);
tab.setAttribute("aria-selected", String(tab.dataset.tab === tabName));
});
manageTabPanel.style.display = tabName === "manage" ? "grid" : "none";
manageTabPanel.hidden = tabName !== "manage";
subscriptionsTabPanel.style.display =
tabName === "subscriptions" ? "block" : "none";
subscriptionsTabPanel.hidden = tabName !== "subscriptions";
if (tabName === "subscriptions") loadSubscriptions();
}
function resetForm() {
editingTierId = null;
tierFormTitle.textContent = "Create Tier";
tierForm.reset();
tierLevel.value = tiers.length ? Math.max(...tiers.map((t) => t.level)) + 1 : 1;
tierStatus.textContent = "";
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function getFriendlyTierError(response) {
const fallback = `Server error ${response.status}`;
const text = await response.text();
if (!text) return fallback;
try {
const error = JSON.parse(text);
const messages = error.errors
? Object.values(error.errors).flat()
: [error.title || fallback];
return messages
.map((message) =>
message === "Tier with this level already exists."
? "A tier with this level already exists. Please choose another level."
: message,
)
.join(" ");
} catch {
return text;
}
}
async function loadTiers() {
try {
const response = await fetch("/api/v1/subscriptions/tiers", {
credentials: "same-origin",
});
if (response.status === 401) {
location.href = "/login";
return;
}
if (!response.ok) throw new Error(`Server error ${response.status}`);
tiers = await response.json();
renderTiers();
if (!editingTierId) resetForm();
} catch (error) {
tiersGrid.innerHTML = `<div class="tiers-error">${escapeHtml(error.message)}</div>`;
}
}
async function loadSubscriptions() {
try {
const response = await fetch("/api/v1/subscriptions", {
credentials: "same-origin",
});
if (response.status === 401) {
location.href = "/login";
return;
}
if (!response.ok) throw new Error(`Server error ${response.status}`);
subscriptions = await response.json();
renderSubscriptions();
} catch (error) {
subscriptionsGrid.innerHTML = `<div class="tiers-error">${escapeHtml(error.message)}</div>`;
}
}
function renderSubscriptions() {
if (!subscriptions.length) {
subscriptionsGrid.innerHTML =
'<div class="tiers-empty">No subscriptions yet.</div>';
return;
}
subscriptionsGrid.innerHTML = subscriptions
.map((subscription) => {
const tier = subscription.currentTier;
return `
<article class="subscription-card">
<div>
<h3>${escapeHtml(subscription.subscribedToName)}</h3>
<p>${tier ? `${escapeHtml(tier.name)} - Level ${tier.level}` : "Following without tier"}</p>
</div>
<div class="subscription-price">
${tier ? `$${Number(tier.monthlyPrice || 0).toFixed(2)}/mo` : "Free"}
</div>
</article>`;
})
.join("");
}
function renderTiers() {
if (!tiers.length) {
tiersGrid.innerHTML = '<div class="tiers-empty">No tiers yet.</div>';
return;
}
tiersGrid.innerHTML = tiers
.map(
(tier) => `
<article class="tier-card">
<div class="tier-card-top">
<div>
<h3>${escapeHtml(tier.name)}</h3>
<div class="tier-level">Level ${tier.level}</div>
</div>
<div class="tier-price">$${Number(tier.monthlyPrice || 0).toFixed(2)}/mo</div>
</div>
<p class="tier-desc">${escapeHtml(tier.description || "No description yet.")}</p>
<div class="tier-card-actions">
<button type="button" data-edit="${tier.id}" aria-label="Edit ${escapeHtml(tier.name)} tier">Edit</button>
<button type="button" data-delete="${tier.id}" class="tier-delete-btn" aria-label="Delete ${escapeHtml(tier.name)} tier">Delete</button>
</div>
</article>`,
)
.join("");
tiersGrid.querySelectorAll("[data-edit]").forEach((button) => {
button.addEventListener("click", () => editTier(button.dataset.edit));
});
tiersGrid.querySelectorAll("[data-delete]").forEach((button) => {
button.addEventListener("click", () => deleteTier(button.dataset.delete));
});
}
function editTier(id) {
const tier = tiers.find((item) => item.id === id);
if (!tier) return;
editingTierId = id;
tierFormTitle.textContent = "Edit Tier";
tierName.value = tier.name || "";
tierLevel.value = tier.level || 1;
tierPrice.value = tier.monthlyPrice || 0;
tierDescription.value = tier.description || "";
tierStatus.textContent = "";
}
async function deleteTier(id) {
const tier = tiers.find((item) => item.id === id);
if (!tier || !confirm(`Delete ${tier.name}?`)) return;
const response = await fetch(`/api/v1/subscriptions/tiers/${id}`, {
method: "DELETE",
credentials: "same-origin",
});
if (response.status === 401) {
location.href = "/login";
return;
}
if (!response.ok) {
tierStatus.textContent = await getFriendlyTierError(response);
return;
}
resetForm();
loadTiers();
}
tierForm.addEventListener("submit", async (event) => {
event.preventDefault();
tierStatus.textContent = "Saving...";
const payload = {
name: tierName.value.trim(),
level: Number(tierLevel.value),
monthlyPrice: Number(tierPrice.value),
description: tierDescription.value.trim() || null,
};
const response = await fetch(
editingTierId
? `/api/v1/subscriptions/tiers/${editingTierId}`
: "/api/v1/subscriptions/tiers",
{
method: editingTierId ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(payload),
},
);
if (response.status === 401) {
location.href = "/login";
return;
}
if (!response.ok) {
tierStatus.textContent = await getFriendlyTierError(response);
return;
}
tierStatus.textContent = "Tier saved.";
editingTierId = null;
await loadTiers();
});
resetBtn.addEventListener("click", resetForm);
document.querySelectorAll(".tiers-tab").forEach((tab) => {
tab.addEventListener("click", () => setActiveTab(tab.dataset.tab));
});
setActiveTab("manage");
loadTiers();
</script>
</body>
</html>