feat: dashboard feed dynamisch via API
This commit is contained in:
parent
63ee71a19b
commit
ba282613ac
@ -71,16 +71,18 @@
|
||||
.post-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 8px rgba(59,130,246,0.06);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59,130,246,0.12);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
/* Post Header */
|
||||
@ -198,6 +200,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Avatar initials fallback */
|
||||
.post-avatar-initials {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary, #6366f1);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Locked post */
|
||||
.post-locked {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.post-locked-msg {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
/* Star rating */
|
||||
.post-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #f59e0b;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.post-rating span {
|
||||
color: #64748b;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.feed-main {
|
||||
padding: 12px !important;
|
||||
@ -214,4 +255,4 @@
|
||||
.post-actions {
|
||||
padding: 8px 10px 10px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,166 +1,214 @@
|
||||
<!-- OnlyPrompt - Feed page:
|
||||
- Social media style post feed with likes, comments, saves, and share actions (following/foryou tabs) -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
<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>
|
||||
<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 (optional) -->
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active">For You</button>
|
||||
<button class="filter-btn">Following</button>
|
||||
<button class="filter-btn">Trending</button>
|
||||
<button class="filter-btn">Recent</button>
|
||||
</div>
|
||||
|
||||
<!-- Posts Grid (einfach als Liste / Grid – hier als Grid wie Marketplace) -->
|
||||
<div class="posts-grid">
|
||||
|
||||
<!-- Post 1 -->
|
||||
<div class="post-card" onclick="location.href='post-detail.html?id=1'">
|
||||
<div class="post-header">
|
||||
<img src="../images/content/creator1.png" alt="Sarah Jenkins" class="post-avatar">
|
||||
<div class="post-author">
|
||||
<span class="post-name">Sarah Jenkins</span>
|
||||
<span class="post-handle">@sarahj</span>
|
||||
</div>
|
||||
<span class="post-date">2 hours ago</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">Conceptual Landscape Art</h3>
|
||||
<p class="post-description">Enchanting, vintage, antique vibes. A journey through surreal landscapes.</p>
|
||||
<img src="../images/content/feed1.png" alt="Conceptual Landscape" class="post-image">
|
||||
</div>
|
||||
<!-- Like, Comment, Share, Save Buttons -->
|
||||
<div class="post-actions">
|
||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
||||
</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>
|
||||
|
||||
<!-- Post 2 -->
|
||||
<div class="post-card" onclick="location.href='post-detail.html?id=2'">
|
||||
<div class="post-header">
|
||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="post-avatar">
|
||||
<div class="post-author">
|
||||
<span class="post-name">Alex Chen</span>
|
||||
<span class="post-handle">@alexchen</span>
|
||||
</div>
|
||||
<span class="post-date">Yesterday</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">Minimalist Logo Design</h3>
|
||||
<p class="post-description">Clean, modern, minimalist logo for tech startups.</p>
|
||||
<img src="../images/content/feed2.png" alt="Minimalist Logo" class="post-image">
|
||||
</div>
|
||||
<!-- Like, Comment, Share, Save Buttons -->
|
||||
<div class="post-actions">
|
||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
||||
</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>
|
||||
|
||||
<!-- Post 3 -->
|
||||
<div class="post-card" onclick="location.href='post-detail.html?id=3'">
|
||||
<div class="post-header">
|
||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="post-avatar">
|
||||
<div class="post-author">
|
||||
<span class="post-name">Mia Wong</span>
|
||||
<span class="post-handle">@miawong</span>
|
||||
</div>
|
||||
<span class="post-date">3 days ago</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">Futuristic Cityscape</h3>
|
||||
<p class="post-description">Cyberpunk neon city with flying cars and rain.</p>
|
||||
<img src="../images/content/feed3.png" alt="Cityscape" class="post-image">
|
||||
</div>
|
||||
<!-- Like, Comment, Share, Save Buttons -->
|
||||
<div class="post-actions">
|
||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></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>
|
||||
|
||||
<!-- Post 4 -->
|
||||
<div class="post-card" onclick="location.href='post-detail.html?id=4'">
|
||||
<div class="post-header">
|
||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="post-avatar">
|
||||
<div class="post-author">
|
||||
<span class="post-name">Tom Rivera</span>
|
||||
<span class="post-handle">@tomrivera</span>
|
||||
</div>
|
||||
<span class="post-date">5 days ago</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">Watercolor Pet Portrait</h3>
|
||||
<p class="post-description">Soft watercolor style, cute pet portrait.</p>
|
||||
<img src="../images/content/feed4.png" alt="Watercolor Pet" class="post-image">
|
||||
</div>
|
||||
<!-- Like, Comment, Share, Save Buttons -->
|
||||
<div class="post-actions">
|
||||
<button class="action-btn like-btn"><i class="bi bi-heart"></i> <span>128</span></button>
|
||||
<button class="action-btn comment-btn"><i class="bi bi-chat"></i> <span>15</span></button>
|
||||
<button class="action-btn share-btn"><i class="bi bi-share"></i></button>
|
||||
<button class="action-btn save-btn"><i class="bi bi-bookmark"></i></button>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove 'active' from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
<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 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">
|
||||
<div class="post-avatar post-avatar-initials">${prompt.creatorName.charAt(0).toUpperCase()}</div>
|
||||
<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">
|
||||
<h3 class="post-title">${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");
|
||||
});
|
||||
// Set 'active' on the first link (Dashboard) - index 0
|
||||
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);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
// Initial load
|
||||
loadFeed();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user