feat: marketplace + post-detail dynamisch via API, syntax fix
This commit is contained in:
parent
ba282613ac
commit
cb11acd306
@ -1,6 +1,6 @@
|
||||
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
||||
{
|
||||
public record ApiPrompt(Guid Id, string Title, string Description, string Content, DateTime TimeStamp, Guid CreatorId, string CreatorName, int? TierLevel, string? TierName, double? AverageRating);
|
||||
public record ApiPrompt(Guid Id, string Title, string Description, string? Content, DateTime TimeStamp, Guid CreatorId, string CreatorName, int? TierLevel, string? TierName, double? AverageRating, bool CanAccess);
|
||||
public record ApiMinimalPrompt(Guid Id, string Title, DateTime TimeStamp, Guid CreatorId, string CreatorName, int? TierLevel, string? TierName, double? AverageRating, bool CanAccess);
|
||||
public record ApiReview(Guid CreatorId, string CreatorName, string? Comment, int Rating);
|
||||
}
|
||||
|
||||
@ -33,18 +33,64 @@ namespace OnlyPrompt.Backend.Controllers
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ApiMinimalPrompt[]> GetPromptsAsync(
|
||||
[Range(0, int.MaxValue)][FromQuery] int offset = 0,
|
||||
[Range(1, 100)][FromQuery] int limit = 20,
|
||||
[FromQuery] FeedSortType sortBy = FeedSortType.Date,
|
||||
[FromQuery] bool ascending = false,
|
||||
[FromQuery] Identifier? category = null,
|
||||
[FromQuery] string? search = null
|
||||
)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var query = _db.Prompts.AsQueryable();
|
||||
|
||||
if (category.HasValue)
|
||||
query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
query = query.Where(x => x.Title.Contains(search) || x.Description.Contains(search));
|
||||
|
||||
query = sortBy switch
|
||||
{
|
||||
FeedSortType.Date => query.OrderBy(x => x.UpdatedAt, ascending),
|
||||
FeedSortType.Rating => query.OrderBy(x => x.Reviews.Average(r => (double?)r.Rating) ?? 2.5, ascending),
|
||||
_ => query
|
||||
};
|
||||
|
||||
var prompts = await query
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.Select(x => new ApiMinimalPrompt(
|
||||
x.Id,
|
||||
x.Title,
|
||||
x.UpdatedAt,
|
||||
x.CreatorId,
|
||||
x.Creator.Profile.DisplayName,
|
||||
x.SubscriptionTier.Level,
|
||||
x.SubscriptionTier.Name,
|
||||
x.Reviews.Average(r => (double?)r.Rating),
|
||||
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
||||
)).ToArrayAsync();
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> GetPromptAsync(Identifier id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var prompt = await GetAccessiblePrompts(userId.Value)
|
||||
|
||||
var prompt = await _db.Prompts
|
||||
.OfIdentifer(id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (prompt is null)
|
||||
return TypedResults.NotFound("Prompt not found or no permission");
|
||||
return TypedResults.NotFound("Prompt not found");
|
||||
|
||||
var apiPrompt = _mapper.Map<ApiPrompt>(prompt);
|
||||
var canAccess = await GetAccessiblePrompts(userId.Value).AnyAsync(p => p.Id == prompt.Id);
|
||||
var apiPrompt = _mapper.Map<ApiPrompt>(prompt) with { Content = canAccess ? prompt.Prompt : null, CanAccess = canAccess };
|
||||
return TypedResults.Ok(apiPrompt);
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,8 @@ namespace OnlyPrompt.Backend.Utils
|
||||
.MapCtorParamFrom(x => x.TierName, x => x.SubscriptionTier == null ? null : x.SubscriptionTier.Name)
|
||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
||||
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
||||
.MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating));
|
||||
.MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating))
|
||||
.MapCtorParamFrom(x => x.CanAccess, x => false);
|
||||
|
||||
config.CreateMap<PromptModel, ApiMinimalPrompt>()
|
||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
||||
|
||||
@ -99,15 +99,17 @@
|
||||
.prompt-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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.prompt-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);
|
||||
}
|
||||
|
||||
.prompt-img {
|
||||
@ -171,7 +173,8 @@
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.buy-btn, .details-btn {
|
||||
.buy-btn,
|
||||
.details-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
@ -189,7 +192,8 @@
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.buy-btn:hover, .details-btn:hover {
|
||||
.buy-btn:hover,
|
||||
.details-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@ -222,4 +226,28 @@
|
||||
.prompt-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Payment method buttons */
|
||||
.pay-method-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
.pay-method-btn:hover {
|
||||
border-color: #6366f1;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
@ -1,237 +1,699 @@
|
||||
<!-- OnlyPrompt - Marketplace page:
|
||||
- Browse and filter AI prompts with buy/view details buttons and pricing
|
||||
- Added sort dropdown (Best Rating, Price Low to High, Price High to Low) - UI only -->
|
||||
<!-- OnlyPrompt - Marketplace page: dynamic prompts, category filter, sort, crypto payment modal -->
|
||||
|
||||
<!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 - Marketplace</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/marketplace.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
/* Additional inline style for sort dropdown – can be moved to marketplace.css */
|
||||
.filter-sort-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.filter-buttons {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.sort-dropdown {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 30px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.sort-dropdown:hover {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OnlyPrompt - Marketplace</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/marketplace.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
/>
|
||||
<style>
|
||||
/* Additional inline style for sort dropdown – can be moved to marketplace.css */
|
||||
.filter-sort-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.filter-buttons {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.sort-dropdown {
|
||||
align-self: flex-start;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 30px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
||||
|
||||
<div id="sidebar-container"></div>
|
||||
.sort-dropdown:hover {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.filter-sort-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.sort-dropdown {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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="marketplace-main">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="marketplace-header">
|
||||
<h1>Marketplace</h1>
|
||||
<p>Browse and discover high-quality AI prompts</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter + Sort Row -->
|
||||
<div class="filter-sort-row">
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active">All</button>
|
||||
<button class="filter-btn">Writing</button>
|
||||
<button class="filter-btn">Coding</button>
|
||||
<button class="filter-btn">Art</button>
|
||||
<button class="filter-btn">Marketing</button>
|
||||
<button class="filter-btn">Video</button>
|
||||
<button class="filter-btn">Data</button>
|
||||
<main class="marketplace-main">
|
||||
<!-- Header -->
|
||||
<div class="marketplace-header">
|
||||
<h1>Marketplace</h1>
|
||||
<p>Browse and discover high-quality AI prompts</p>
|
||||
</div>
|
||||
<select class="sort-dropdown" aria-label="Sort prompts">
|
||||
<option value="best">Best Rating</option>
|
||||
<option value="price_low">Price: Low to High</option>
|
||||
<option value="price_high">Price: High to Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Prompts Grid -->
|
||||
<div class="prompts-grid">
|
||||
|
||||
<!-- Prompt Card 1 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market1.png" alt="Creative Blog Post Generator" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Creative Blog Post Generator</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate engaging blog posts in minutes to captivate your audience.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(124 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
<!-- Filter + Sort Row -->
|
||||
<div class="filter-sort-row">
|
||||
<div class="filter-buttons" id="category-filters">
|
||||
<button class="filter-btn active" data-category="">All</button>
|
||||
</div>
|
||||
<select
|
||||
class="sort-dropdown"
|
||||
id="sort-select"
|
||||
aria-label="Sort prompts"
|
||||
>
|
||||
<option value="date|false">Newest</option>
|
||||
<option value="date|true">Oldest</option>
|
||||
<option value="rating|false">Best Rating</option>
|
||||
<option value="rating|true">Lowest Rating</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 2 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market2.png" alt="Python Code Assistant" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Python Code Assistant</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Efficiently debug and write Python code with AI assistance.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
||||
<span>(98 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Search -->
|
||||
<div style="margin-bottom: 24px">
|
||||
<input
|
||||
id="search-input"
|
||||
type="text"
|
||||
placeholder="Search prompts..."
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 30px;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
background: #f8fafc;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 3 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market3.png" alt="Digital Art Style Guide" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Digital Art Style Guide</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Create stunning digital art styles with this comprehensive guide.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(156 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Prompts Grid -->
|
||||
<div class="prompts-grid" id="prompts-grid"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
id="market-empty"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-bag-x"
|
||||
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">No prompts found</h3>
|
||||
<p>Try a different category or search term.</p>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 4 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market4.png" alt="Marketing Copywriter" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Marketing Copywriter</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate engaging marketing copy in minutes.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
||||
<span>(112 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Error State -->
|
||||
<div
|
||||
id="market-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 prompts</h3>
|
||||
<p id="market-error-msg"></p>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 5 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market5.png" alt="Midjourney Image Prompts" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">Midjourney Image Prompts</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Curated prompts for stunning Midjourney images.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
||||
<span>(203 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Card 6 -->
|
||||
<div class="prompt-card">
|
||||
<img src="../images/content/market6.png" alt="UI Design Assistant" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<h3 class="prompt-title">UI Design Assistant</h3>
|
||||
<div class="prompt-author">@JaneDoe</div>
|
||||
<p class="prompt-description">Generate UI components and design systems with AI.</p>
|
||||
<div class="prompt-rating">
|
||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
||||
<span>(87 reviews)</span>
|
||||
</div>
|
||||
<div class="prompt-price">$19.99</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="buy-btn">Buy Now</button>
|
||||
<button class="details-btn">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</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');
|
||||
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
|
||||
<div
|
||||
id="payment-overlay"
|
||||
style="
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
max-width: 460px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>
|
||||
<button
|
||||
onclick="closePayment()"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<!-- Step 1: Choose method -->
|
||||
<div id="pay-step-1">
|
||||
<h2 style="margin-bottom: 4px">Subscribe to access</h2>
|
||||
<p
|
||||
id="pay-prompt-title"
|
||||
style="color: #6366f1; font-weight: 600; margin-bottom: 16px"
|
||||
></p>
|
||||
<p style="color: #64748b; margin-bottom: 24px">
|
||||
Choose a payment method to unlock this prompt:
|
||||
</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px">
|
||||
<button class="pay-method-btn" onclick="selectCrypto('btc')">
|
||||
<span style="font-size: 1.4rem">₿</span> Bitcoin (BTC)
|
||||
<span
|
||||
id="price-btc"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('eth')">
|
||||
<span style="font-size: 1.4rem">Ξ</span> Ethereum (ETH)
|
||||
<span
|
||||
id="price-eth"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('sol')">
|
||||
<span style="font-size: 1.4rem">◎</span> Solana (SOL)
|
||||
<span
|
||||
id="price-sol"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
</button>
|
||||
<button class="pay-method-btn" onclick="selectCrypto('usdt')">
|
||||
<span style="font-size: 1.4rem">₮</span> USDT (TRC-20)
|
||||
<span
|
||||
id="price-usdt"
|
||||
style="margin-left: auto; font-size: 0.85rem; color: #94a3b8"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
style="
|
||||
margin-top: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<i class="bi bi-shield-lock-fill"></i> Payments are processed
|
||||
on-chain. No account needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Send payment -->
|
||||
<div id="pay-step-2" style="display: none">
|
||||
<button
|
||||
onclick="backToStep1()"
|
||||
style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6366f1;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<h2 id="pay-crypto-title" style="margin-bottom: 8px"></h2>
|
||||
<p style="color: #64748b; margin-bottom: 8px">
|
||||
Send exactly <strong id="pay-amount"></strong> to:
|
||||
</p>
|
||||
<div
|
||||
style="
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
"
|
||||
>
|
||||
<code
|
||||
id="pay-address"
|
||||
style="font-size: 0.85rem; word-break: break-all; flex: 1"
|
||||
></code>
|
||||
<button
|
||||
onclick="copyAddress()"
|
||||
title="Copy"
|
||||
style="
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6366f1;
|
||||
font-size: 1.1rem;
|
||||
"
|
||||
>
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size: 0.78rem; color: #94a3b8; margin-bottom: 20px">
|
||||
⚠️ Only send the exact amount. Payments are non-refundable.
|
||||
</p>
|
||||
<div
|
||||
style="
|
||||
background: #fef9c3;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 0.82rem;
|
||||
color: #92400e;
|
||||
margin-bottom: 20px;
|
||||
"
|
||||
>
|
||||
<i class="bi bi-info-circle-fill"></i> After sending, click the
|
||||
button below to confirm. Access will be granted once the transaction
|
||||
is verified.
|
||||
</div>
|
||||
<button
|
||||
onclick="confirmPayment()"
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
I've sent the payment ✓
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<div
|
||||
id="pay-step-3"
|
||||
style="display: none; text-align: center; padding: 20px 0"
|
||||
>
|
||||
<i
|
||||
class="bi bi-check-circle-fill"
|
||||
style="
|
||||
font-size: 3.5rem;
|
||||
color: #10b981;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
></i>
|
||||
<h2 style="margin-bottom: 8px">Payment received!</h2>
|
||||
<p style="color: #64748b; margin-bottom: 24px">
|
||||
Your access is being activated. This usually takes 1–2 minutes.
|
||||
</p>
|
||||
<button
|
||||
onclick="closePayment()"
|
||||
style="
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</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 link = document.querySelectorAll(
|
||||
"#sidebar-container .sidebar li a",
|
||||
)[1];
|
||||
if (link) link.classList.add("active");
|
||||
});
|
||||
// Set 'active' on the second link (Marketplace) - index 1
|
||||
const secondLink = document.querySelectorAll('#sidebar-container .sidebar li a')[1];
|
||||
if (secondLink) secondLink.classList.add('active');
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
.then(
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────
|
||||
let activeCategory = "";
|
||||
let searchTimeout;
|
||||
|
||||
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`;
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
}
|
||||
|
||||
function renderStars(rating) {
|
||||
if (rating == null)
|
||||
return '<span style="color:#94a3b8;font-size:0.8rem;">No reviews yet</span>';
|
||||
const stars = Math.round(rating);
|
||||
return `<span class="prompt-rating"><span style="color:#f59e0b">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span> ${rating.toFixed(1)}</span>`;
|
||||
}
|
||||
|
||||
function tierPrice(level) {
|
||||
if (!level) return "Free";
|
||||
return `$${(level * 4.99).toFixed(2)}/mo`;
|
||||
}
|
||||
|
||||
const MARKET_IMAGES = [
|
||||
"/images/content/market1.png",
|
||||
"/images/content/market2.png",
|
||||
"/images/content/market3.png",
|
||||
"/images/content/market4.png",
|
||||
"/images/content/market5.png",
|
||||
"/images/content/market6.png",
|
||||
];
|
||||
let cardIndex = 0;
|
||||
|
||||
function renderCard(p) {
|
||||
const locked = !p.canAccess && p.tierLevel != null;
|
||||
const img = p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
||||
return `
|
||||
<div class="prompt-card">
|
||||
<img src="${img}" alt="${p.title}" class="prompt-img">
|
||||
<div class="prompt-info">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<div style="width:34px;height:34px;border-radius:50%;background:#6366f1;color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:0.95rem;flex-shrink:0;">${p.creatorName.charAt(0).toUpperCase()}</div>
|
||||
<span class="prompt-author">@${p.creatorName}</span>
|
||||
<span style="margin-left:auto;font-size:0.75rem;color:#94a3b8;">${timeAgo(p.timeStamp)}</span>
|
||||
</div>
|
||||
<h3 class="prompt-title">${p.title}</h3>
|
||||
${p.tierName ? `<span style="display:inline-block;background:#f1f5f9;color:#475569;border-radius:20px;padding:2px 10px;font-size:0.75rem;margin-bottom:8px;"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>` : '<span style="display:inline-block;background:#dcfce7;color:#166534;border-radius:20px;padding:2px 10px;font-size:0.75rem;margin-bottom:8px;">Free</span>'}
|
||||
<div style="margin-bottom:12px;">${renderStars(p.averageRating)}</div>
|
||||
<div class="prompt-price">${tierPrice(p.tierLevel)}</div>
|
||||
<div class="prompt-actions">
|
||||
${
|
||||
locked
|
||||
? `<button class="buy-btn" onclick='openPayment(${JSON.stringify(p)})'>Subscribe</button>`
|
||||
: `<button class="buy-btn" style="background:#10b981;" onclick="location.href='/post-detail?id=${p.id}'">Access <i class="bi bi-unlock-fill"></i></button>`
|
||||
}
|
||||
<button class="details-btn" onclick="location.href='/post-detail?id=${p.id}'">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadPrompts() {
|
||||
const grid = document.getElementById("prompts-grid");
|
||||
const emptyEl = document.getElementById("market-empty");
|
||||
const errorEl = document.getElementById("market-error");
|
||||
const search = document.getElementById("search-input").value.trim();
|
||||
const [sortBy, ascending] = document
|
||||
.getElementById("sort-select")
|
||||
.value.split("|");
|
||||
|
||||
grid.innerHTML = "";
|
||||
emptyEl.style.display = "none";
|
||||
errorEl.style.display = "none";
|
||||
cardIndex = 0;
|
||||
const DEMO_PROMPTS = [
|
||||
{
|
||||
id: "demo-1",
|
||||
title: "Creative Blog Post Generator",
|
||||
creatorName: "JaneDoe",
|
||||
timeStamp: new Date(Date.now() - 7200000).toISOString(),
|
||||
tierLevel: null,
|
||||
tierName: null,
|
||||
averageRating: 4.8,
|
||||
canAccess: true,
|
||||
},
|
||||
{
|
||||
id: "demo-2",
|
||||
title: "Python Code Assistant",
|
||||
creatorName: "AlexDev",
|
||||
timeStamp: new Date(Date.now() - 86400000).toISOString(),
|
||||
tierLevel: 1,
|
||||
tierName: "Basic",
|
||||
averageRating: 4.7,
|
||||
canAccess: false,
|
||||
},
|
||||
{
|
||||
id: "demo-3",
|
||||
title: "Digital Art Style Guide",
|
||||
creatorName: "MiaArt",
|
||||
timeStamp: new Date(Date.now() - 172800000).toISOString(),
|
||||
tierLevel: 2,
|
||||
tierName: "Pro",
|
||||
averageRating: 4.9,
|
||||
canAccess: false,
|
||||
},
|
||||
{
|
||||
id: "demo-4",
|
||||
title: "Marketing Copywriter",
|
||||
creatorName: "TomCopy",
|
||||
timeStamp: new Date(Date.now() - 259200000).toISOString(),
|
||||
tierLevel: null,
|
||||
tierName: null,
|
||||
averageRating: 4.6,
|
||||
canAccess: true,
|
||||
},
|
||||
{
|
||||
id: "demo-5",
|
||||
title: "Midjourney Image Prompts",
|
||||
creatorName: "SarahViz",
|
||||
timeStamp: new Date(Date.now() - 432000000).toISOString(),
|
||||
tierLevel: 1,
|
||||
tierName: "Basic",
|
||||
averageRating: 4.7,
|
||||
canAccess: false,
|
||||
},
|
||||
{
|
||||
id: "demo-6",
|
||||
title: "UI Design Assistant",
|
||||
creatorName: "LilyUI",
|
||||
timeStamp: new Date(Date.now() - 604800000).toISOString(),
|
||||
tierLevel: 3,
|
||||
tierName: "Premium",
|
||||
averageRating: 4.9,
|
||||
canAccess: false,
|
||||
_img: "/images/content/market6.png",
|
||||
},
|
||||
];
|
||||
// pre-assign images to demo prompts
|
||||
DEMO_PROMPTS.forEach((p, i) => (p._img = MARKET_IMAGES[i]));
|
||||
|
||||
try {
|
||||
let url = `/api/v1/prompts?sortBy=${sortBy}&ascending=${ascending}&limit=50`;
|
||||
if (activeCategory) url += `&category=${activeCategory}`;
|
||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
if (res.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||||
|
||||
let prompts = await res.json();
|
||||
|
||||
// If API is empty and no filters active, show demo content
|
||||
if (prompts.length === 0 && !activeCategory && !search) {
|
||||
grid.innerHTML =
|
||||
`<div style="grid-column:1/-1;text-align:center;padding:8px 0 20px;color:#94a3b8;font-size:0.85rem;"><i class="bi bi-info-circle"></i> Showing example prompts — no real prompts yet.</div>` +
|
||||
DEMO_PROMPTS.map(renderCard).join("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompts.length === 0) {
|
||||
emptyEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = prompts.map(renderCard).join("");
|
||||
} catch (e) {
|
||||
// On error, show demo content as fallback
|
||||
grid.innerHTML =
|
||||
`<div style="grid-column:1/-1;text-align:center;padding:8px 0 20px;color:#94a3b8;font-size:0.85rem;"><i class="bi bi-wifi-off"></i> Could not connect to server — showing demo content.</div>` +
|
||||
DEMO_PROMPTS.map(renderCard).join("");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await fetch("/api/v1/categories/minimal");
|
||||
if (!res.ok) return;
|
||||
const cats = await res.json();
|
||||
const container = document.getElementById("category-filters");
|
||||
cats.forEach((c) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "filter-btn";
|
||||
btn.dataset.category = c.slug;
|
||||
btn.textContent = c.name;
|
||||
btn.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll("#category-filters .filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
activeCategory = c.slug;
|
||||
loadPrompts();
|
||||
});
|
||||
container.appendChild(btn);
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ── Category filter (All) ──────────────────────────────────────────
|
||||
document
|
||||
.querySelector("#category-filters .filter-btn")
|
||||
.addEventListener("click", function () {
|
||||
document
|
||||
.querySelectorAll("#category-filters .filter-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
activeCategory = "";
|
||||
loadPrompts();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("sort-select")
|
||||
.addEventListener("change", loadPrompts);
|
||||
document.getElementById("search-input").addEventListener("input", () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(loadPrompts, 400);
|
||||
});
|
||||
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
// Make openPayment global
|
||||
window.openPayment = openPayment;
|
||||
|
||||
// ── Payment Modal ──────────────────────────────────────────────────
|
||||
const CRYPTO_ADDRESSES = {
|
||||
btc: "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf1N",
|
||||
eth: "0x742d35Cc6634C0532925a3b8D4C9B8E4D8F2b1a",
|
||||
sol: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV1",
|
||||
usdt: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
|
||||
};
|
||||
const CRYPTO_NAMES = {
|
||||
btc: "Bitcoin (BTC)",
|
||||
eth: "Ethereum (ETH)",
|
||||
sol: "Solana (SOL)",
|
||||
usdt: "USDT TRC-20",
|
||||
};
|
||||
let currentPrompt = null;
|
||||
let currentCrypto = null;
|
||||
|
||||
function openPayment(prompt) {
|
||||
currentPrompt = prompt;
|
||||
const usd = prompt.tierLevel ? prompt.tierLevel * 4.99 : 0;
|
||||
document.getElementById("pay-prompt-title").textContent = prompt.title;
|
||||
document.getElementById("price-btc").textContent =
|
||||
`≈ ${(usd / 67000).toFixed(6)} BTC`;
|
||||
document.getElementById("price-eth").textContent =
|
||||
`≈ ${(usd / 3200).toFixed(5)} ETH`;
|
||||
document.getElementById("price-sol").textContent =
|
||||
`≈ ${(usd / 145).toFixed(3)} SOL`;
|
||||
document.getElementById("price-usdt").textContent =
|
||||
`${usd.toFixed(2)} USDT`;
|
||||
|
||||
document.getElementById("pay-step-1").style.display = "block";
|
||||
document.getElementById("pay-step-2").style.display = "none";
|
||||
document.getElementById("pay-step-3").style.display = "none";
|
||||
document.getElementById("payment-overlay").style.display = "flex";
|
||||
}
|
||||
|
||||
window.selectCrypto = function (crypto) {
|
||||
currentCrypto = crypto;
|
||||
const usd = currentPrompt.tierLevel
|
||||
? currentPrompt.tierLevel * 4.99
|
||||
: 0;
|
||||
const amounts = {
|
||||
btc: `${(usd / 67000).toFixed(6)} BTC`,
|
||||
eth: `${(usd / 3200).toFixed(5)} ETH`,
|
||||
sol: `${(usd / 145).toFixed(3)} SOL`,
|
||||
usdt: `${usd.toFixed(2)} USDT`,
|
||||
};
|
||||
document.getElementById("pay-crypto-title").textContent =
|
||||
CRYPTO_NAMES[crypto];
|
||||
document.getElementById("pay-amount").textContent = amounts[crypto];
|
||||
document.getElementById("pay-address").textContent =
|
||||
CRYPTO_ADDRESSES[crypto];
|
||||
document.getElementById("pay-step-1").style.display = "none";
|
||||
document.getElementById("pay-step-2").style.display = "block";
|
||||
};
|
||||
|
||||
window.backToStep1 = function () {
|
||||
document.getElementById("pay-step-1").style.display = "block";
|
||||
document.getElementById("pay-step-2").style.display = "none";
|
||||
};
|
||||
|
||||
window.copyAddress = function () {
|
||||
navigator.clipboard.writeText(CRYPTO_ADDRESSES[currentCrypto]);
|
||||
};
|
||||
|
||||
window.confirmPayment = function () {
|
||||
document.getElementById("pay-step-2").style.display = "none";
|
||||
document.getElementById("pay-step-3").style.display = "block";
|
||||
};
|
||||
|
||||
window.closePayment = function () {
|
||||
document.getElementById("payment-overlay").style.display = "none";
|
||||
};
|
||||
|
||||
document
|
||||
.getElementById("payment-overlay")
|
||||
.addEventListener("click", function (e) {
|
||||
if (e.target === this) closePayment();
|
||||
});
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────
|
||||
await loadCategories();
|
||||
await loadPrompts();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,114 +1,331 @@
|
||||
<!-- OnlyPrompt - Settings page:
|
||||
- User preferences with tabs for profile, security, and notifications -->
|
||||
<!-- OnlyPrompt - Post Detail: dynamic prompt loaded via API -->
|
||||
|
||||
<!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 - Post Detail</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/post-detail.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 - Post Detail</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/post-detail.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; display: flex; flex-direction: column;">
|
||||
|
||||
<div id="topbar-container"></div>
|
||||
<div style="flex: 1; display: flex; flex-direction: column">
|
||||
<div id="topbar-container"></div>
|
||||
|
||||
<main class="post-detail-main">
|
||||
<div class="post-detail-container">
|
||||
<!-- Header -->
|
||||
<div class="post-header">
|
||||
<h1 class="post-title">Marketing Guru: Campaign Strategist</h1>
|
||||
<div class="post-meta">
|
||||
<span class="category">Marketing</span>
|
||||
<span class="category">Business</span>
|
||||
<span class="category">Copywriting</span>
|
||||
<span class="updated">Updated: Oct 26, 2023</span>
|
||||
<main class="post-detail-main">
|
||||
<div class="post-detail-container" id="detail-content">
|
||||
<!-- Loading -->
|
||||
<div
|
||||
id="detail-loading"
|
||||
style="text-align: center; padding: 60px 20px; color: #64748b"
|
||||
>
|
||||
<i
|
||||
class="bi bi-hourglass-split"
|
||||
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
||||
></i>
|
||||
<p>Loading prompt...</p>
|
||||
</div>
|
||||
<div class="post-stats">
|
||||
<span><i class="bi bi-bookmark-star"></i> 2,109 Saves</span>
|
||||
<span><i class="bi bi-eye"></i> 14.5k Views</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Content Section -->
|
||||
<div class="prompt-section">
|
||||
<h2>PROMPT</h2>
|
||||
<div class="prompt-content">
|
||||
<p>You are an expert marketing strategist and copywriter. I will provide a product name, target audience, and key benefits. Your goal is to create a comprehensive marketing campaign plan, including:</p>
|
||||
<ul>
|
||||
<li>Campaign objectives</li>
|
||||
<li>Target Audience: [User Input]</li>
|
||||
<li>Expected Output: A detailed marketing plan with channel strategy, messaging, and sample copy</li>
|
||||
<li>Budget considerations and KPIs</li>
|
||||
</ul>
|
||||
<!-- Error -->
|
||||
<div
|
||||
id="detail-error"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #ef4444;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-exclamation-circle"
|
||||
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
||||
></i>
|
||||
<h3 id="detail-error-title">Prompt not found</h3>
|
||||
<p
|
||||
id="detail-error-msg"
|
||||
style="color: #64748b; margin-top: 8px"
|
||||
></p>
|
||||
<button
|
||||
onclick="history.back()"
|
||||
style="
|
||||
margin-top: 20px;
|
||||
padding: 10px 24px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating & Like -->
|
||||
<div class="rating-section">
|
||||
<div class="rating-stars">
|
||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
||||
<span><i class="bi bi-star-fill"></i> 4.8 (241 Ratings)</span>
|
||||
</div>
|
||||
<button class="like-btn"><i class="bi bi-heart"></i> Like (212)</button>
|
||||
</div>
|
||||
|
||||
<!-- Example Output Section -->
|
||||
<div class="example-section">
|
||||
<h2>EXAMPLE OUTPUT</h2>
|
||||
<div class="example-content">
|
||||
<h3>Generated Strategy 16px</h3>
|
||||
<div class="example-output-text">
|
||||
<p><strong>Output: EcoWare Campaign</strong></p>
|
||||
<p><strong>Campaign Overview</strong><br>Target Persona: Sarah Jenkins, 28, Eco-conscious</p>
|
||||
<p><strong>Key Messaging</strong><br>Channel Strategy<br>Sample Copy<br>Social<br>Email</p>
|
||||
<!-- Content (hidden until loaded) -->
|
||||
<div id="detail-body" style="display: none">
|
||||
<!-- Header -->
|
||||
<div class="post-header">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
id="creator-avatar"
|
||||
style="
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
"
|
||||
></div>
|
||||
<div>
|
||||
<span
|
||||
id="creator-name"
|
||||
style="font-weight: 600; font-size: 0.95rem"
|
||||
></span>
|
||||
<span
|
||||
id="prompt-date"
|
||||
style="display: block; font-size: 0.8rem; color: #94a3b8"
|
||||
></span>
|
||||
</div>
|
||||
<div style="margin-left: auto">
|
||||
<span id="tier-badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="post-title" id="prompt-title"></h1>
|
||||
<div class="post-meta">
|
||||
<span class="category" id="prompt-category"></span>
|
||||
<span class="updated" id="prompt-updated"></span>
|
||||
</div>
|
||||
<div class="post-stats">
|
||||
<span id="prompt-rating-stat"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Optional example image (if uploaded in create) -->
|
||||
<div class="example-image" style="display: none;">
|
||||
<img src="#" alt="Example Output Image">
|
||||
|
||||
<!-- Description -->
|
||||
<div class="prompt-section" id="desc-section">
|
||||
<h2>DESCRIPTION</h2>
|
||||
<div class="prompt-content">
|
||||
<p id="prompt-description"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Content (only if accessible) -->
|
||||
<div class="prompt-section" id="prompt-content-section">
|
||||
<h2>PROMPT</h2>
|
||||
<div
|
||||
class="prompt-content"
|
||||
id="prompt-body"
|
||||
style="
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Locked section (shown instead of prompt if no access) -->
|
||||
<div
|
||||
id="locked-section"
|
||||
style="
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 28px;
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-lock-fill"
|
||||
style="
|
||||
font-size: 2.5rem;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
"
|
||||
></i>
|
||||
<h3 style="margin-bottom: 8px">
|
||||
This prompt requires a subscription
|
||||
</h3>
|
||||
<p style="color: #64748b; margin-bottom: 20px">
|
||||
Subscribe to <strong id="locked-creator"></strong> to access
|
||||
this prompt.
|
||||
</p>
|
||||
<button
|
||||
id="locked-subscribe-btn"
|
||||
style="
|
||||
padding: 12px 28px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
"
|
||||
>
|
||||
Subscribe <span id="locked-tier-name"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="rating-section" id="rating-section">
|
||||
<div class="rating-stars" id="rating-display"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unlock / Buy Section -->
|
||||
<div class="unlock-section">
|
||||
<button class="unlock-btn">Unlock Full Prompt • $19.99</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Fetch sidebar and topbar
|
||||
fetch('/sidebar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
document.getElementById('sidebar-container').innerHTML = data;
|
||||
// Remove active class from all sidebar links
|
||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
<script type="module">
|
||||
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"));
|
||||
});
|
||||
// Optionally set active on a relevant link (e.g., Marketplace)
|
||||
const marketplaceLink = document.querySelector('#sidebar-container a[href="marketplace.html"]');
|
||||
if (marketplaceLink) marketplaceLink.classList.add('active');
|
||||
});
|
||||
fetch("/topbar.html")
|
||||
.then((r) => r.text())
|
||||
.then(
|
||||
(data) =>
|
||||
(document.getElementById("topbar-container").innerHTML = data),
|
||||
);
|
||||
|
||||
fetch('/topbar.html')
|
||||
.then(r => r.text())
|
||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get("id");
|
||||
|
||||
if (!id) {
|
||||
showError("No prompt ID provided", "Add ?id=... to the URL.");
|
||||
} else {
|
||||
loadPrompt(id);
|
||||
}
|
||||
|
||||
async function loadPrompt(id) {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/prompts/${id}`);
|
||||
if (res.status === 401) {
|
||||
location.href = "/login";
|
||||
return;
|
||||
}
|
||||
if (res.status === 404) {
|
||||
showError(
|
||||
"Prompt not found",
|
||||
"This prompt does not exist or you do not have access.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Server error ${res.status}`);
|
||||
|
||||
const p = await res.json();
|
||||
renderPrompt(p);
|
||||
} catch (e) {
|
||||
showError("Could not load prompt", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrompt(p) {
|
||||
document.title = `${p.title} — OnlyPrompt`;
|
||||
|
||||
document.getElementById("creator-avatar").textContent = p.creatorName
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
document.getElementById("creator-name").textContent = p.creatorName;
|
||||
document.getElementById("prompt-date").textContent = new Date(
|
||||
p.timeStamp,
|
||||
).toLocaleDateString("de-CH", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
document.getElementById("prompt-title").textContent = p.title;
|
||||
document.getElementById("prompt-updated").textContent =
|
||||
`Updated: ${new Date(p.timeStamp).toLocaleDateString()}`;
|
||||
document.getElementById("prompt-description").textContent =
|
||||
p.description;
|
||||
document.getElementById("prompt-body").textContent = p.content;
|
||||
|
||||
// Tier badge
|
||||
const badge = document.getElementById("tier-badge");
|
||||
if (p.tierName) {
|
||||
badge.innerHTML = `<span style="background:#f1f5f9;color:#475569;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;"><i class="bi bi-lock-fill"></i> ${p.tierName}</span>`;
|
||||
} else {
|
||||
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
|
||||
}
|
||||
|
||||
// Rating
|
||||
if (p.averageRating != null) {
|
||||
const stars = Math.round(p.averageRating);
|
||||
document.getElementById("rating-display").innerHTML =
|
||||
`<span style="color:#f59e0b;font-size:1.1rem;">${"★".repeat(stars)}${"☆".repeat(5 - stars)}</span>
|
||||
<span style="margin-left:8px;font-weight:600;">${p.averageRating.toFixed(1)}</span>
|
||||
<span style="color:#94a3b8;font-size:0.85rem;margin-left:4px;">/ 5.0</span>`;
|
||||
document.getElementById("prompt-rating-stat").innerHTML =
|
||||
`<i class="bi bi-star-fill" style="color:#f59e0b;"></i> ${p.averageRating.toFixed(1)} rating`;
|
||||
} else {
|
||||
document.getElementById("rating-display").innerHTML =
|
||||
'<span style="color:#94a3b8;font-size:0.9rem;">No ratings yet</span>';
|
||||
}
|
||||
|
||||
// Content visibility
|
||||
if (p.content) {
|
||||
document.getElementById("prompt-content-section").style.display =
|
||||
"block";
|
||||
document.getElementById("locked-section").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("prompt-content-section").style.display =
|
||||
"none";
|
||||
document.getElementById("locked-section").style.display = "block";
|
||||
document.getElementById("locked-creator").textContent = p.creatorName;
|
||||
document.getElementById("locked-tier-name").textContent = p.tierName
|
||||
? `— ${p.tierName}`
|
||||
: "";
|
||||
}
|
||||
|
||||
document.getElementById("detail-loading").style.display = "none";
|
||||
document.getElementById("detail-body").style.display = "block";
|
||||
}
|
||||
|
||||
function showError(title, msg) {
|
||||
document.getElementById("detail-loading").style.display = "none";
|
||||
document.getElementById("detail-error").style.display = "block";
|
||||
document.getElementById("detail-error-title").textContent = title;
|
||||
document.getElementById("detail-error-msg").textContent = msg;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user