2026-06-14 12:07:01 +02:00

248 lines
10 KiB
HTML

<!-- OnlyPrompt - Community page:
- Discover creators, follow/unfollow, dynamic 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 - Discover Creators</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/community.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="creators-main" id="main-content" tabindex="-1">
<div class="creators-header">
<h1>Discover Creators</h1>
<p>Follow your favorite prompt artists and get inspired.</p>
</div>
<div class="filter-buttons" role="group" aria-label="Sort creators">
<button type="button" class="filter-btn active" data-sort="popular" aria-pressed="true">
Popular
</button>
<button type="button" class="filter-btn" data-sort="prompts" aria-pressed="false">Rising</button>
<button type="button" class="filter-btn" data-sort="new" aria-pressed="false">New</button>
<button type="button" class="filter-btn" data-sort="rating" aria-pressed="false">Top Rated</button>
</div>
<div class="creators-grid" id="creators-grid" aria-live="polite"></div>
<div id="creators-empty" class="state-empty" role="status" aria-live="polite">
<i class="bi bi-people state-icon" aria-hidden="true"></i>
<h3 id="creators-empty-title" class="state-title">
No creators found
</h3>
<p id="creators-empty-text">
Check back later for new creators to follow.
</p>
</div>
<div id="creators-error" class="state-error" role="alert" aria-live="assertive">
<i class="bi bi-exclamation-circle state-icon" aria-hidden="true"></i>
<h3 class="state-title">Could not load creators</h3>
<p id="creators-error-msg"></p>
</div>
</main>
</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");
l.removeAttribute("aria-current");
});
const thirdLink = document.querySelectorAll(
"#sidebar-container .sidebar li a",
)[2];
if (thirdLink) {
thirdLink.classList.add("active");
thirdLink.setAttribute("aria-current", "page");
}
});
fetch("/topbar.html")
.then((r) => r.text())
.then(
(data) =>
(document.getElementById("topbar-container").innerHTML = data),
);
// ── Helpers ──────────────────────────────────────────────────────
function renderStars(rating) {
if (!rating) return "";
const stars = Math.round(rating);
return `<span class="creator-stars">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> <span class="creator-stars-value">${rating.toFixed(1)}</span>`;
}
function renderCard(c) {
const profileHref = `/profile?id=${encodeURIComponent(c.userId)}`;
const chatHref = `/chats.html?userId=${encodeURIComponent(c.userId)}&name=${encodeURIComponent(c.displayName)}&avatar=${encodeURIComponent(c.avatarUrl || "../images/content/cat.png")}`;
return `
<div class="creator-card">
<a class="creator-avatar-link" href="${profileHref}" aria-label="Open profile for ${c.displayName}">
<img class="creator-avatar"
src="${c.avatarUrl || "../images/content/cat.png"}"
alt="${c.displayName}">
</a>
<div class="creator-info">
<h3 class="creator-name"><a href="${profileHref}">${c.displayName}</a></h3>
<div class="creator-handle">@${c.slug}</div>
<p class="creator-bio">${c.bio ?? "No bio yet."}</p>
<div class="creator-stats">
<span><i class="bi bi-puzzle" aria-hidden="true"></i> ${c.promptCount} prompts</span>
<span><i class="bi bi-people" aria-hidden="true"></i> ${c.subscribers} subscribers</span>
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ""}
</div>
<div class="creator-actions">
<button type="button" class="follow-btn ${c.isFollowing ? "following" : ""}"
data-userid="${c.userId}"
data-following="${c.isFollowing}"
aria-pressed="${c.isFollowing}"
aria-label="${c.isFollowing ? "Unfollow" : "Follow"} ${c.displayName}">
${c.isFollowing ? "Following" : "Follow"}
</button>
<a class="creator-chat-btn" href="${chatHref}" aria-label="Chat with ${c.displayName}">
<i class="bi bi-chat-dots" aria-hidden="true"></i>
Chat
</a>
</div>
</div>
</div>`;
}
// ── Follow / Unfollow ────────────────────────────────────────────
async function toggleFollow(btn) {
const userId = btn.dataset.userid;
const isFollowing = btn.dataset.following === "true";
btn.disabled = true;
const res = await fetch(`/api/v1/subscriptions/${userId}`, {
method: isFollowing ? "DELETE" : "PUT",
credentials: "same-origin",
});
if (res.status === 401) {
location.href = "/login";
return;
}
if (res.ok) {
const nowFollowing = !isFollowing;
btn.dataset.following = nowFollowing;
btn.textContent = nowFollowing ? "Following" : "Follow";
btn.setAttribute("aria-pressed", String(nowFollowing));
btn.classList.toggle("following", nowFollowing);
}
btn.disabled = false;
}
// ── Load Creators ────────────────────────────────────────────────
const grid = document.getElementById("creators-grid");
const emptyEl = document.getElementById("creators-empty");
const emptyTitle = document.getElementById("creators-empty-title");
const emptyText = document.getElementById("creators-empty-text");
const errorEl = document.getElementById("creators-error");
const errorMsg = document.getElementById("creators-error-msg");
let activeSort = "popular";
let currentSearch =
new URLSearchParams(location.search).get("search") || "";
function getSearchTerm() {
return currentSearch.trim();
}
async function loadCreators(sort = activeSort) {
activeSort = sort;
grid.innerHTML = "";
emptyEl.style.display = "none";
errorEl.style.display = "none";
try {
const params = new URLSearchParams({
sort,
limit: "50",
});
const search = getSearchTerm();
if (search) params.set("search", search);
const res = await fetch(`/api/v1/profiles?${params}`);
if (res.status === 401) {
location.href = "/login";
return;
}
if (!res.ok) throw new Error(`Server error ${res.status}`);
const creators = await res.json();
if (creators.length === 0) {
const search = getSearchTerm();
emptyTitle.textContent = search
? "No matching creators"
: "No creators found";
emptyText.textContent = search
? `No creator matches "${search}". Try another name or clear the search.`
: "Create another local user to see creators here.";
emptyEl.style.display = "block";
return;
}
grid.innerHTML = creators.map(renderCard).join("");
grid.querySelectorAll(".follow-btn").forEach((btn) => {
btn.addEventListener("click", () => toggleFollow(btn));
});
} catch (e) {
errorEl.style.display = "block";
errorMsg.textContent = e.message;
}
}
// ── Filter buttons ───────────────────────────────────────────────
document.querySelectorAll(".filter-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document
.querySelectorAll(".filter-btn")
.forEach((b) => {
b.classList.remove("active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("active");
btn.setAttribute("aria-pressed", "true");
loadCreators(btn.dataset.sort);
});
});
window.applyCreatorSearch = (value) => {
currentSearch = value.trim();
loadCreators(activeSort);
};
loadCreators();
</script>
</body>
</html>