223 lines
8.0 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" />
<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; margin: 40px auto; max-width: 950px">
<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"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #64748b;
"
>
<i
class="bi bi-inbox"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">No posts yet</h3>
<p>Follow some creators to see their prompts here.</p>
</div>
<!-- Error State -->
<div
id="feed-error"
style="
display: none;
text-align: center;
padding: 60px 20px;
color: #ef4444;
"
>
<i
class="bi bi-exclamation-circle"
style="font-size: 3rem; display: block; margin-bottom: 16px"
></i>
<h3 style="margin-bottom: 8px">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 creatorImg(id) {
return `/images/content/creator${(parseInt(id.slice(-1), 16) % 6) + 1}.png`;
}
function renderCard(prompt) {
const locked = !prompt.canAccess;
return `
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
<div class="post-header">
<img class="post-avatar" src="${creatorImg(prompt.creatorId)}" 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">
<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">
<h3 class="post-title" style="margin-top:10px">${prompt.title}</h3>
${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 share-btn" onclick="event.stopPropagation(); navigator.clipboard.writeText(location.origin+'/post-detail?id=${prompt.id}')"><i class="bi bi-share"></i></button>
</div>
</div>`;
}
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>