265 lines
10 KiB
HTML
265 lines
10 KiB
HTML
<!-- OnlyPrompt - Feed page:
|
|
- Social media style post feed with likes, comments, saves, and share actions (following/foryou tabs) -->
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>OnlyPrompt - Feed</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/dashboard.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="feed-main">
|
|
<!-- Optional: Feed Header -->
|
|
<div class="feed-header">
|
|
<h1>Feed</h1>
|
|
<p>Latest prompts and inspiration from creators you follow</p>
|
|
</div>
|
|
|
|
<!-- Filter Buttons -->
|
|
<div class="filter-buttons">
|
|
<button
|
|
class="filter-btn active"
|
|
data-sort="date"
|
|
data-ascending="false"
|
|
>
|
|
Recent
|
|
</button>
|
|
<button
|
|
class="filter-btn"
|
|
data-sort="rating"
|
|
data-ascending="false"
|
|
>
|
|
Top Rated
|
|
</button>
|
|
<button class="filter-btn" data-sort="date" data-ascending="true">
|
|
Oldest
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Posts Grid -->
|
|
<div class="posts-grid" id="posts-grid"></div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="feed-empty" class="state-empty">
|
|
<i class="bi bi-inbox state-icon"></i>
|
|
<h3 class="state-title">No posts yet</h3>
|
|
<p>Follow some creators to see their prompts here.</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="feed-error" class="state-error">
|
|
<i class="bi bi-exclamation-circle state-icon"></i>
|
|
<h3 class="state-title">Could not load feed</h3>
|
|
<p id="feed-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((link) => link.classList.remove("active"));
|
|
const firstLink = document.querySelectorAll(
|
|
"#sidebar-container .sidebar li a",
|
|
)[0];
|
|
if (firstLink) firstLink.classList.add("active");
|
|
});
|
|
|
|
fetch("/topbar.html")
|
|
.then((r) => r.text())
|
|
.then(
|
|
(data) =>
|
|
(document.getElementById("topbar-container").innerHTML = data),
|
|
);
|
|
|
|
// ── Feed ──────────────────────────────────────────────────────────
|
|
const grid = document.getElementById("posts-grid");
|
|
const emptyEl = document.getElementById("feed-empty");
|
|
const errorEl = document.getElementById("feed-error");
|
|
const errorMsg = document.getElementById("feed-error-msg");
|
|
|
|
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`;
|
|
const d = Math.floor(h / 24);
|
|
if (d < 7) return `${d}d ago`;
|
|
return new Date(dateStr).toLocaleDateString();
|
|
}
|
|
|
|
function renderStars(rating) {
|
|
if (rating == null) return "";
|
|
const stars = Math.round(rating);
|
|
return `<span class="post-rating" title="${rating.toFixed(1)} / 5">
|
|
${'<i class="bi bi-star-fill"></i>'.repeat(stars)}${'<i class="bi bi-star"></i>'.repeat(5 - stars)}
|
|
<span>${rating.toFixed(1)}</span>
|
|
</span>`;
|
|
}
|
|
|
|
function feedImg(id) {
|
|
return `/images/content/feed${(parseInt(id.slice(-1), 16) % 4) + 1}.png`;
|
|
}
|
|
|
|
function profileUrl(userId) {
|
|
return `/profile.html?id=${encodeURIComponent(userId)}`;
|
|
}
|
|
|
|
function renderCard(prompt) {
|
|
const locked = !prompt.canAccess;
|
|
const liked = prompt.isLiked;
|
|
const saved = prompt.isSaved;
|
|
return `
|
|
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='${profileUrl(prompt.creatorId)}'">
|
|
<div class="post-header">
|
|
<img class="post-avatar" src="${prompt.creatorAvatarUrl || "../images/content/cat.png"}" alt="${prompt.creatorName}">
|
|
<div class="post-author">
|
|
<span class="post-name">${prompt.creatorName}</span>
|
|
</div>
|
|
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
|
</div>
|
|
<div class="post-content">
|
|
${prompt.exampleImageUrl ? `<img class="post-image${locked ? " post-image-locked" : ""}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? " post-image-locked" : ""}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
|
|
<h3 class="post-title">${prompt.title}</h3>
|
|
<p class="post-description">${prompt.description || ""}</p>
|
|
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? "Paid"} tier required</p>` : ""}
|
|
${renderStars(prompt.averageRating)}
|
|
</div>
|
|
<div class="post-actions">
|
|
<button class="action-btn like-btn ${liked ? "active" : ""}" onclick="toggleLike(event, '${prompt.id}', ${liked})"><i class="bi ${liked ? "bi-heart-fill" : "bi-heart"}"></i> <span>Like (${prompt.likeCount || 0})</span></button>
|
|
<button class="action-btn comment-btn" onclick="event.stopPropagation(); location.href='/post-detail?id=${prompt.id}#rating-section'"><i class="bi bi-chat"></i> <span>Review</span></button>
|
|
<button class="action-btn share-btn" onclick="sharePrompt(event, '${prompt.id}')"><i class="bi bi-share"></i> <span>Share</span></button>
|
|
<button class="action-btn save-btn ${saved ? "active" : ""}" onclick="toggleSave(event, '${prompt.id}', ${saved})"><i class="bi ${saved ? "bi-bookmark-fill" : "bi-bookmark"}"></i> <span>Save (${prompt.saveCount || 0})</span></button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
window.toggleLike = async function (event, id, isLiked) {
|
|
event.stopPropagation();
|
|
const response = await fetch(`/api/v1/prompts/${id}/likes`, {
|
|
method: isLiked ? "DELETE" : "PUT",
|
|
credentials: "same-origin",
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
location.href = "/login";
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) return;
|
|
loadFeed(
|
|
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
|
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
|
"true",
|
|
);
|
|
};
|
|
|
|
window.toggleFeedState = function (event, type, id) {
|
|
event.stopPropagation();
|
|
const key = `prompt-${type}-${id}`;
|
|
const next = localStorage.getItem(key) !== "true";
|
|
localStorage.setItem(key, next);
|
|
loadFeed(
|
|
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
|
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
|
"true",
|
|
);
|
|
};
|
|
|
|
window.toggleSave = async function (event, id, isSaved) {
|
|
event.stopPropagation();
|
|
const response = await fetch(`/api/v1/prompts/${id}/saves`, {
|
|
method: isSaved ? "DELETE" : "PUT",
|
|
credentials: "same-origin",
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
location.href = "/login";
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) return;
|
|
loadFeed(
|
|
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
|
document.querySelector(".filter-btn.active")?.dataset.ascending ===
|
|
"true",
|
|
);
|
|
};
|
|
|
|
window.sharePrompt = function (event, id) {
|
|
event.stopPropagation();
|
|
navigator.clipboard.writeText(
|
|
`${location.origin}/post-detail?id=${id}`,
|
|
);
|
|
};
|
|
|
|
async function loadFeed(sortBy = "date", ascending = false) {
|
|
grid.innerHTML = "";
|
|
emptyEl.style.display = "none";
|
|
errorEl.style.display = "none";
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`/api/v1/feed?sortBy=${sortBy}&ascending=${ascending}&limit=20`,
|
|
);
|
|
if (res.status === 401) {
|
|
location.href = "/login";
|
|
return;
|
|
}
|
|
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
|
|
|
const prompts = await res.json();
|
|
if (prompts.length === 0) {
|
|
emptyEl.style.display = "block";
|
|
return;
|
|
}
|
|
grid.innerHTML = prompts.map(renderCard).join("");
|
|
} 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");
|
|
loadFeed(btn.dataset.sort, btn.dataset.ascending === "true");
|
|
});
|
|
});
|
|
|
|
// Initial load
|
|
loadFeed();
|
|
</script>
|
|
</body>
|
|
</html>
|