feat: community.html dynamisch via API, GET /api/v1/profiles, follow/unfollow fix
This commit is contained in:
parent
cb11acd306
commit
4f7e9bac68
@ -1,4 +1,5 @@
|
|||||||
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
||||||
{
|
{
|
||||||
public record ApiUserProfile(string DisplayName, string Slug, string? Bio, string AvatarUrl, string? Specialities, double AverageRating, int Subscribers);
|
public record ApiUserProfile(string DisplayName, string Slug, string? Bio, string AvatarUrl, string? Specialities, double AverageRating, int Subscribers);
|
||||||
|
public record ApiCreatorCard(Guid UserId, string DisplayName, string Slug, string? Bio, double AverageRating, int Subscribers, int PromptCount, bool IsFollowing);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ using OnlyPrompt.Backend.ApiModels.UserProfile;
|
|||||||
using OnlyPrompt.Backend.Database;
|
using OnlyPrompt.Backend.Database;
|
||||||
using OnlyPrompt.Backend.Database.Models;
|
using OnlyPrompt.Backend.Database.Models;
|
||||||
using OnlyPrompt.Backend.Utils;
|
using OnlyPrompt.Backend.Utils;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.Controllers
|
namespace OnlyPrompt.Backend.Controllers
|
||||||
{
|
{
|
||||||
@ -41,6 +42,38 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
return TypedResults.Ok(profile);
|
return TypedResults.Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ApiCreatorCard[]> GetCreatorsAsync(
|
||||||
|
[Range(0, int.MaxValue)] int offset = 0,
|
||||||
|
[Range(1, 100)] int limit = 20,
|
||||||
|
[FromQuery] string sort = "popular"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var query = _db.UserProfiles.Where(up => up.IsPublic && up.Id != userId);
|
||||||
|
|
||||||
|
var projected = query.Select(up => new ApiCreatorCard(
|
||||||
|
up.Id,
|
||||||
|
up.DisplayName,
|
||||||
|
up.Slug,
|
||||||
|
up.Bio,
|
||||||
|
_db.Reviews.Where(r => r.Prompt.CreatorId == up.Id).Average(r => (double?)r.Rating) ?? 0.0,
|
||||||
|
_db.Subscriptions.Count(s => s.SubscribedToId == up.Id),
|
||||||
|
_db.Prompts.Count(p => p.CreatorId == up.Id),
|
||||||
|
_db.Subscriptions.Any(s => s.SubscribedToId == up.Id && s.SubscriberId == userId)
|
||||||
|
));
|
||||||
|
|
||||||
|
var allCreators = await projected.ToArrayAsync();
|
||||||
|
|
||||||
|
return (sort switch
|
||||||
|
{
|
||||||
|
"rating" => allCreators.OrderByDescending(c => c.AverageRating),
|
||||||
|
"new" => allCreators.OrderByDescending(c => c.UserId),
|
||||||
|
"prompts" => allCreators.OrderByDescending(c => c.PromptCount),
|
||||||
|
_ => allCreators.OrderByDescending(c => c.Subscribers),
|
||||||
|
}).Skip(offset).Take(limit).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<Results<ValidationProblem, NotFound<string>, Ok<ApiUserProfile>>> UpdateProfileAsync([FromBody] ApiUpdateProfileRequest request)
|
public async Task<Results<ValidationProblem, NotFound<string>, Ok<ApiUserProfile>>> UpdateProfileAsync([FromBody] ApiUpdateProfileRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -26,8 +26,8 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{userId}/{level}")]
|
[HttpPut("{userId}/{level?}")]
|
||||||
public async Task<Results<Ok, BadRequest<string>, NotFound<string>>> SubscribeAsync(Identifier subscribeToId, int? level = null)
|
public async Task<Results<Ok, BadRequest<string>, NotFound<string>>> SubscribeAsync([FromRoute(Name = "userId")] Identifier subscribeToId, int? level = null)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var subscribeTo = await _db.Users.Include(x => x.SubscriptionTiers.Where(st => st.Level == level))
|
var subscribeTo = await _db.Users.Include(x => x.SubscriptionTiers.Where(st => st.Level == level))
|
||||||
@ -86,7 +86,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{userId}")]
|
[HttpGet("{userId}")]
|
||||||
public async Task<ApiSubscription?> GetCurrentSubscriptionAsync(Identifier subscribeToId)
|
public async Task<ApiSubscription?> GetCurrentSubscriptionAsync([FromRoute(Name = "userId")] Identifier subscribeToId)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var subscription = await _db.Subscriptions
|
var subscription = await _db.Subscriptions
|
||||||
@ -101,7 +101,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{userId}")]
|
[HttpDelete("{userId}")]
|
||||||
public async Task<Results<Ok, NotFound<string>>> UnsubscribeAsync(Identifier subscribeToId)
|
public async Task<Results<Ok, NotFound<string>>> UnsubscribeAsync([FromRoute(Name = "userId")] Identifier subscribeToId)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var count = await _db.Subscriptions
|
var count = await _db.Subscriptions
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!-- OnlyPrompt - Marketplace page:
|
<!-- OnlyPrompt - Community page:
|
||||||
- Browse and filter AI prompts with buy/view details buttons and pricing -->
|
- Discover creators, follow/unfollow, dynamic via API -->
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -26,136 +26,152 @@
|
|||||||
|
|
||||||
<main class="creators-main">
|
<main class="creators-main">
|
||||||
|
|
||||||
<!-- Header / Titel -->
|
|
||||||
<div class="creators-header">
|
<div class="creators-header">
|
||||||
<h1>Discover Creators</h1>
|
<h1>Discover Creators</h1>
|
||||||
<p>Follow your favorite prompt artists and get inspired.</p>
|
<p>Follow your favorite prompt artists and get inspired.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Buttons -->
|
|
||||||
<div class="filter-buttons">
|
<div class="filter-buttons">
|
||||||
<button class="filter-btn active">Popular</button>
|
<button class="filter-btn active" data-sort="popular">Popular</button>
|
||||||
<button class="filter-btn">Rising</button>
|
<button class="filter-btn" data-sort="prompts">Rising</button>
|
||||||
<button class="filter-btn">New</button>
|
<button class="filter-btn" data-sort="new">New</button>
|
||||||
<button class="filter-btn">Top Rated</button>
|
<button class="filter-btn" data-sort="rating">Top Rated</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Creators Grid -->
|
<div class="creators-grid" id="creators-grid"></div>
|
||||||
<div class="creators-grid">
|
|
||||||
|
|
||||||
<!-- Creator Card 1 -->
|
<div id="creators-empty" style="display:none; text-align:center; padding:60px 20px; color:#64748b;">
|
||||||
<div class="creator-card">
|
<i class="bi bi-people" style="font-size:3rem; display:block; margin-bottom:16px;"></i>
|
||||||
<img src="../images/content/creator1.png" alt="Sarah Jenkins" class="creator-avatar">
|
<h3 style="margin-bottom:8px;">No creators found</h3>
|
||||||
<div class="creator-info">
|
<p>Check back later for new creators to follow.</p>
|
||||||
<h3 class="creator-name">Sarah Jenkins</h3>
|
|
||||||
<div class="creator-handle">@sarahj</div>
|
|
||||||
<p class="creator-bio">AI Explorer | Prompt Curator | Exploring creativity through generative AI.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 42 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 2 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator2.png" alt="Alex Chen" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Alex Chen</h3>
|
|
||||||
<div class="creator-handle">@alexchen</div>
|
|
||||||
<p class="creator-bio">Digital artist & prompt engineer. Creating surreal landscapes.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 87 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 3 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator3.png" alt="Mia Wong" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Mia Wong</h3>
|
|
||||||
<div class="creator-handle">@miawong</div>
|
|
||||||
<p class="creator-bio">Midjourney master | UI/UX prompts | Design systems.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 124 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 5.0</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 4 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator4.png" alt="Tom Rivera" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Tom Rivera</h3>
|
|
||||||
<div class="creator-handle">@tomrivera</div>
|
|
||||||
<p class="creator-bio">3D artist | Character design | Sci-fi & fantasy prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 33 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 5 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator5.png" alt="Emma Watson" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Emma Watson</h3>
|
|
||||||
<div class="creator-handle">@emmawatson</div>
|
|
||||||
<p class="creator-bio">Watercolor & pet portraits | Whimsical art prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 56 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator Card 6 -->
|
|
||||||
<div class="creator-card">
|
|
||||||
<img src="../images/content/creator6.png" alt="Liam O'Brien" class="creator-avatar">
|
|
||||||
<div class="creator-info">
|
|
||||||
<h3 class="creator-name">Liam O'Brien</h3>
|
|
||||||
<div class="creator-handle">@liamob</div>
|
|
||||||
<p class="creator-bio">Minimalist logo designer | Brand identity prompts.</p>
|
|
||||||
<div class="creator-stats">
|
|
||||||
<span><i class="bi bi-puzzle"></i> 28 prompts</span>
|
|
||||||
<span><i class="bi bi-star-fill"></i> 4.6</span>
|
|
||||||
</div>
|
|
||||||
<button class="follow-btn">Follow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="creators-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 creators</h3>
|
||||||
|
<p id="creators-error-msg"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script type="module">
|
||||||
|
// ── Sidebar & Topbar ─────────────────────────────────────────────
|
||||||
fetch('/sidebar.html')
|
fetch('/sidebar.html')
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
document.getElementById('sidebar-container').innerHTML = data;
|
||||||
// Remove 'active' from all sidebar links
|
document.querySelectorAll('#sidebar-container .sidebar a').forEach(l => l.classList.remove('active'));
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
|
||||||
// Set 'active' on the third link (Community)
|
|
||||||
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
|
const thirdLink = document.querySelectorAll('#sidebar-container .sidebar li a')[2];
|
||||||
if (thirdLink) thirdLink.classList.add('active');
|
if (thirdLink) thirdLink.classList.add('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
fetch('/topbar.html')
|
fetch('/topbar.html')
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
function creatorImg(id) {
|
||||||
|
return `/images/content/creator${(parseInt(id.slice(-1), 16) % 6) + 1}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStars(rating) {
|
||||||
|
if (!rating) return '';
|
||||||
|
const stars = Math.round(rating);
|
||||||
|
return `<span style="color:#f59e0b">${'★'.repeat(stars)}${'☆'.repeat(5 - stars)}</span> <span style="color:#64748b;font-size:0.8rem">${rating.toFixed(1)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCard(c) {
|
||||||
|
return `
|
||||||
|
<div class="creator-card">
|
||||||
|
<img class="creator-avatar"
|
||||||
|
src="${creatorImg(c.userId)}"
|
||||||
|
alt="${c.displayName}"
|
||||||
|
style="cursor:pointer"
|
||||||
|
onclick="location.href='/profile?id=${c.userId}'">
|
||||||
|
<div class="creator-info">
|
||||||
|
<h3 class="creator-name"
|
||||||
|
style="cursor:pointer"
|
||||||
|
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 errorEl = document.getElementById('creators-error');
|
||||||
|
const errorMsg = document.getElementById('creators-error-msg');
|
||||||
|
|
||||||
|
async function loadCreators(sort = 'popular') {
|
||||||
|
grid.innerHTML = '';
|
||||||
|
emptyEl.style.display = 'none';
|
||||||
|
errorEl.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/profiles?sort=${sort}&limit=50`);
|
||||||
|
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) { 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadCreators();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -137,6 +137,16 @@
|
|||||||
.follow-btn:hover {
|
.follow-btn:hover {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
.follow-btn.following {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #94a3b8;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.follow-btn.following:hover {
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: #ef4444;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@ -219,6 +219,10 @@
|
|||||||
.post-locked {
|
.post-locked {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
.post-image-locked {
|
||||||
|
filter: blur(6px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.post-locked-msg {
|
.post-locked-msg {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
@ -147,20 +147,28 @@
|
|||||||
</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) {
|
function renderCard(prompt) {
|
||||||
const locked = !prompt.canAccess;
|
const locked = !prompt.canAccess;
|
||||||
return `
|
return `
|
||||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
|
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
|
||||||
<div class="post-header">
|
<div class="post-header">
|
||||||
<div class="post-avatar post-avatar-initials">${prompt.creatorName.charAt(0).toUpperCase()}</div>
|
<img class="post-avatar" src="${creatorImg(prompt.creatorId)}" alt="${prompt.creatorName}">
|
||||||
<div class="post-author">
|
<div class="post-author">
|
||||||
<span class="post-name">${prompt.creatorName}</span>
|
<span class="post-name">${prompt.creatorName}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<h3 class="post-title">${prompt.title}</h3>
|
<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">
|
||||||
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? "Paid"} tier required</p>` : ""}
|
<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)}
|
${renderStars(prompt.averageRating)}
|
||||||
</div>
|
</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user