226 lines
8.4 KiB
HTML
226 lines
8.4 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>
|
|
<div class="layout">
|
|
<div id="sidebar-container"></div>
|
|
|
|
<div class="page-body">
|
|
<div id="topbar-container"></div>
|
|
|
|
<main class="creators-main">
|
|
<div class="creators-header">
|
|
<h1>Discover Creators</h1>
|
|
<p>Follow your favorite prompt artists and get inspired.</p>
|
|
</div>
|
|
|
|
<div class="filter-buttons">
|
|
<button class="filter-btn active" data-sort="popular">
|
|
Popular
|
|
</button>
|
|
<button class="filter-btn" data-sort="prompts">Rising</button>
|
|
<button class="filter-btn" data-sort="new">New</button>
|
|
<button class="filter-btn" data-sort="rating">Top Rated</button>
|
|
</div>
|
|
|
|
<div class="creators-grid" id="creators-grid"></div>
|
|
|
|
<div id="creators-empty" class="state-empty">
|
|
<i class="bi bi-people state-icon"></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">
|
|
<i class="bi bi-exclamation-circle state-icon"></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"));
|
|
const thirdLink = document.querySelectorAll(
|
|
"#sidebar-container .sidebar li a",
|
|
)[2];
|
|
if (thirdLink) thirdLink.classList.add("active");
|
|
});
|
|
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) {
|
|
return `
|
|
<div class="creator-card">
|
|
<img class="creator-avatar"
|
|
src="${c.avatarUrl || "../images/content/cat.png"}"
|
|
alt="${c.displayName}"
|
|
onclick="location.href='/profile?id=${c.userId}'">
|
|
<div class="creator-info">
|
|
<h3 class="creator-name"
|
|
onclick="location.href='/profile?id=${c.userId}'">${c.displayName}</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"></i> ${c.promptCount} prompts</span>
|
|
<span><i class="bi bi-people"></i> ${c.subscribers}</span>
|
|
${c.averageRating > 0 ? `<span>${renderStars(c.averageRating)}</span>` : ""}
|
|
</div>
|
|
<button class="follow-btn ${c.isFollowing ? "following" : ""}"
|
|
data-userid="${c.userId}"
|
|
data-following="${c.isFollowing}">
|
|
${c.isFollowing ? "Following" : "Follow"}
|
|
</button>
|
|
</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.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"));
|
|
btn.classList.add("active");
|
|
loadCreators(btn.dataset.sort);
|
|
});
|
|
});
|
|
|
|
window.applyCreatorSearch = (value) => {
|
|
currentSearch = value.trim();
|
|
loadCreators(activeSort);
|
|
};
|
|
|
|
loadCreators();
|
|
</script>
|
|
</body>
|
|
</html>
|