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
|
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 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);
|
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}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> GetPromptAsync(Identifier id)
|
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> GetPromptAsync(Identifier id)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var prompt = await GetAccessiblePrompts(userId.Value)
|
|
||||||
|
var prompt = await _db.Prompts
|
||||||
.OfIdentifer(id)
|
.OfIdentifer(id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (prompt is null)
|
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);
|
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.TierName, x => x.SubscriptionTier == null ? null : x.SubscriptionTier.Name)
|
||||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
||||||
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
.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>()
|
config.CreateMap<PromptModel, ApiMinimalPrompt>()
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
||||||
|
|||||||
@ -99,15 +99,17 @@
|
|||||||
.prompt-card {
|
.prompt-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
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;
|
overflow: hidden;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.prompt-card:hover {
|
.prompt-card:hover {
|
||||||
transform: translateY(-2px);
|
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 {
|
.prompt-img {
|
||||||
@ -171,7 +173,8 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.buy-btn, .details-btn {
|
.buy-btn,
|
||||||
|
.details-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@ -189,7 +192,8 @@
|
|||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
.buy-btn:hover, .details-btn:hover {
|
.buy-btn:hover,
|
||||||
|
.details-btn:hover {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,4 +226,28 @@
|
|||||||
.prompt-actions {
|
.prompt-actions {
|
||||||
flex-direction: column;
|
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:
|
<!-- OnlyPrompt - Marketplace page: dynamic prompts, category filter, sort, crypto payment modal -->
|
||||||
- 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 -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OnlyPrompt - Marketplace</title>
|
<title>OnlyPrompt - Marketplace</title>
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
<link rel="stylesheet" href="../css/variables.css" />
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
<link rel="stylesheet" href="../css/base.css" />
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
<link rel="stylesheet" href="../css/sidebar.css" />
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
<link rel="stylesheet" href="../css/login.css" />
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
<link rel="stylesheet" href="../css/topbar.css" />
|
||||||
<link rel="stylesheet" href="../css/marketplace.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">
|
<link
|
||||||
<style>
|
rel="stylesheet"
|
||||||
/* Additional inline style for sort dropdown – can be moved to marketplace.css */
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||||
.filter-sort-row {
|
/>
|
||||||
display: flex;
|
<style>
|
||||||
justify-content: space-between;
|
/* Additional inline style for sort dropdown – can be moved to marketplace.css */
|
||||||
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) {
|
|
||||||
.filter-sort-row {
|
.filter-sort-row {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
align-items: stretch;
|
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 {
|
.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;
|
||||||
}
|
}
|
||||||
}
|
.sort-dropdown:hover {
|
||||||
</style>
|
border-color: #94a3b8;
|
||||||
</head>
|
}
|
||||||
<body>
|
@media (max-width: 700px) {
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
.filter-sort-row {
|
||||||
|
flex-direction: column;
|
||||||
<div id="sidebar-container"></div>
|
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 style="flex: 1; margin: 40px auto; max-width: 950px">
|
||||||
|
<div id="topbar-container"></div>
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="marketplace-main">
|
<main class="marketplace-main">
|
||||||
|
<!-- Header -->
|
||||||
<!-- Header -->
|
<div class="marketplace-header">
|
||||||
<div class="marketplace-header">
|
<h1>Marketplace</h1>
|
||||||
<h1>Marketplace</h1>
|
<p>Browse and discover high-quality AI prompts</p>
|
||||||
<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>
|
|
||||||
</div>
|
</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 -->
|
<!-- Filter + Sort Row -->
|
||||||
<div class="prompts-grid">
|
<div class="filter-sort-row">
|
||||||
|
<div class="filter-buttons" id="category-filters">
|
||||||
<!-- Prompt Card 1 -->
|
<button class="filter-btn active" data-category="">All</button>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Prompt Card 2 -->
|
<!-- Search -->
|
||||||
<div class="prompt-card">
|
<div style="margin-bottom: 24px">
|
||||||
<img src="../images/content/market2.png" alt="Python Code Assistant" class="prompt-img">
|
<input
|
||||||
<div class="prompt-info">
|
id="search-input"
|
||||||
<h3 class="prompt-title">Python Code Assistant</h3>
|
type="text"
|
||||||
<div class="prompt-author">@JaneDoe</div>
|
placeholder="Search prompts..."
|
||||||
<p class="prompt-description">Efficiently debug and write Python code with AI assistance.</p>
|
style="
|
||||||
<div class="prompt-rating">
|
width: 100%;
|
||||||
<span><i class="bi bi-star-fill"></i> 4.7</span>
|
padding: 10px 16px;
|
||||||
<span>(98 reviews)</span>
|
border: 1px solid #e2e8f0;
|
||||||
</div>
|
border-radius: 30px;
|
||||||
<div class="prompt-price">$19.99</div>
|
font-size: 0.95rem;
|
||||||
<div class="prompt-actions">
|
outline: none;
|
||||||
<button class="buy-btn">Buy Now</button>
|
background: #f8fafc;
|
||||||
<button class="details-btn">View Details</button>
|
"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prompt Card 3 -->
|
<!-- Prompts Grid -->
|
||||||
<div class="prompt-card">
|
<div class="prompts-grid" id="prompts-grid"></div>
|
||||||
<img src="../images/content/market3.png" alt="Digital Art Style Guide" class="prompt-img">
|
|
||||||
<div class="prompt-info">
|
<!-- Empty State -->
|
||||||
<h3 class="prompt-title">Digital Art Style Guide</h3>
|
<div
|
||||||
<div class="prompt-author">@JaneDoe</div>
|
id="market-empty"
|
||||||
<p class="prompt-description">Create stunning digital art styles with this comprehensive guide.</p>
|
style="
|
||||||
<div class="prompt-rating">
|
display: none;
|
||||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
text-align: center;
|
||||||
<span>(156 reviews)</span>
|
padding: 60px 20px;
|
||||||
</div>
|
color: #64748b;
|
||||||
<div class="prompt-price">$19.99</div>
|
"
|
||||||
<div class="prompt-actions">
|
>
|
||||||
<button class="buy-btn">Buy Now</button>
|
<i
|
||||||
<button class="details-btn">View Details</button>
|
class="bi bi-bag-x"
|
||||||
</div>
|
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||||
</div>
|
></i>
|
||||||
|
<h3 style="margin-bottom: 8px">No prompts found</h3>
|
||||||
|
<p>Try a different category or search term.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prompt Card 4 -->
|
<!-- Error State -->
|
||||||
<div class="prompt-card">
|
<div
|
||||||
<img src="../images/content/market4.png" alt="Marketing Copywriter" class="prompt-img">
|
id="market-error"
|
||||||
<div class="prompt-info">
|
style="
|
||||||
<h3 class="prompt-title">Marketing Copywriter</h3>
|
display: none;
|
||||||
<div class="prompt-author">@JaneDoe</div>
|
text-align: center;
|
||||||
<p class="prompt-description">Generate engaging marketing copy in minutes.</p>
|
padding: 60px 20px;
|
||||||
<div class="prompt-rating">
|
color: #ef4444;
|
||||||
<span><i class="bi bi-star-fill"></i> 4.8</span>
|
"
|
||||||
<span>(112 reviews)</span>
|
>
|
||||||
</div>
|
<i
|
||||||
<div class="prompt-price">$19.99</div>
|
class="bi bi-exclamation-circle"
|
||||||
<div class="prompt-actions">
|
style="font-size: 3rem; display: block; margin-bottom: 16px"
|
||||||
<button class="buy-btn">Buy Now</button>
|
></i>
|
||||||
<button class="details-btn">View Details</button>
|
<h3 style="margin-bottom: 8px">Could not load prompts</h3>
|
||||||
</div>
|
<p id="market-error-msg"></p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<!-- Prompt Card 5 -->
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<!-- ── Payment Modal ─────────────────────────────────────────────── -->
|
||||||
fetch('/sidebar.html')
|
<div
|
||||||
.then(r => r.text())
|
id="payment-overlay"
|
||||||
.then(data => {
|
style="
|
||||||
document.getElementById('sidebar-container').innerHTML = data;
|
display: none;
|
||||||
// Remove 'active' from all sidebar links
|
position: fixed;
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
inset: 0;
|
||||||
link.classList.remove('active');
|
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
|
fetch("/topbar.html")
|
||||||
const secondLink = document.querySelectorAll('#sidebar-container .sidebar li a')[1];
|
.then((r) => r.text())
|
||||||
if (secondLink) secondLink.classList.add('active');
|
.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')
|
// Make openPayment global
|
||||||
.then(r => r.text())
|
window.openPayment = openPayment;
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
// ── Payment Modal ──────────────────────────────────────────────────
|
||||||
</body>
|
const CRYPTO_ADDRESSES = {
|
||||||
</html>
|
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:
|
<!-- OnlyPrompt - Post Detail: dynamic prompt loaded via API -->
|
||||||
- User preferences with tabs for profile, security, and notifications -->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OnlyPrompt - Post Detail</title>
|
<title>OnlyPrompt - Post Detail</title>
|
||||||
<link rel="stylesheet" href="../css/variables.css">
|
<link rel="stylesheet" href="../css/variables.css" />
|
||||||
<link rel="stylesheet" href="../css/base.css">
|
<link rel="stylesheet" href="../css/base.css" />
|
||||||
<link rel="stylesheet" href="../css/sidebar.css">
|
<link rel="stylesheet" href="../css/sidebar.css" />
|
||||||
<link rel="stylesheet" href="../css/login.css">
|
<link rel="stylesheet" href="../css/login.css" />
|
||||||
<link rel="stylesheet" href="../css/topbar.css">
|
<link rel="stylesheet" href="../css/topbar.css" />
|
||||||
<link rel="stylesheet" href="../css/post-detail.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">
|
<link
|
||||||
</head>
|
rel="stylesheet"
|
||||||
<body>
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||||
<div class="layout" style="display: flex; min-height: 100vh; background: var(--bg);">
|
/>
|
||||||
|
</head>
|
||||||
<div id="sidebar-container"></div>
|
<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 style="flex: 1; display: flex; flex-direction: column">
|
||||||
|
<div id="topbar-container"></div>
|
||||||
<div id="topbar-container"></div>
|
|
||||||
|
|
||||||
<main class="post-detail-main">
|
<main class="post-detail-main">
|
||||||
<div class="post-detail-container">
|
<div class="post-detail-container" id="detail-content">
|
||||||
<!-- Header -->
|
<!-- Loading -->
|
||||||
<div class="post-header">
|
<div
|
||||||
<h1 class="post-title">Marketing Guru: Campaign Strategist</h1>
|
id="detail-loading"
|
||||||
<div class="post-meta">
|
style="text-align: center; padding: 60px 20px; color: #64748b"
|
||||||
<span class="category">Marketing</span>
|
>
|
||||||
<span class="category">Business</span>
|
<i
|
||||||
<span class="category">Copywriting</span>
|
class="bi bi-hourglass-split"
|
||||||
<span class="updated">Updated: Oct 26, 2023</span>
|
style="font-size: 2.5rem; display: block; margin-bottom: 12px"
|
||||||
|
></i>
|
||||||
|
<p>Loading prompt...</p>
|
||||||
</div>
|
</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 -->
|
<!-- Error -->
|
||||||
<div class="prompt-section">
|
<div
|
||||||
<h2>PROMPT</h2>
|
id="detail-error"
|
||||||
<div class="prompt-content">
|
style="
|
||||||
<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>
|
display: none;
|
||||||
<ul>
|
text-align: center;
|
||||||
<li>Campaign objectives</li>
|
padding: 60px 20px;
|
||||||
<li>Target Audience: [User Input]</li>
|
color: #ef4444;
|
||||||
<li>Expected Output: A detailed marketing plan with channel strategy, messaging, and sample copy</li>
|
"
|
||||||
<li>Budget considerations and KPIs</li>
|
>
|
||||||
</ul>
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rating & Like -->
|
<!-- Content (hidden until loaded) -->
|
||||||
<div class="rating-section">
|
<div id="detail-body" style="display: none">
|
||||||
<div class="rating-stars">
|
<!-- Header -->
|
||||||
<span><i class="bi bi-star-fill"></i> 4.9</span>
|
<div class="post-header">
|
||||||
<span><i class="bi bi-star-fill"></i> 4.8 (241 Ratings)</span>
|
<div
|
||||||
</div>
|
style="
|
||||||
<button class="like-btn"><i class="bi bi-heart"></i> Like (212)</button>
|
display: flex;
|
||||||
</div>
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
<!-- Example Output Section -->
|
margin-bottom: 16px;
|
||||||
<div class="example-section">
|
"
|
||||||
<h2>EXAMPLE OUTPUT</h2>
|
>
|
||||||
<div class="example-content">
|
<div
|
||||||
<h3>Generated Strategy 16px</h3>
|
id="creator-avatar"
|
||||||
<div class="example-output-text">
|
style="
|
||||||
<p><strong>Output: EcoWare Campaign</strong></p>
|
width: 42px;
|
||||||
<p><strong>Campaign Overview</strong><br>Target Persona: Sarah Jenkins, 28, Eco-conscious</p>
|
height: 42px;
|
||||||
<p><strong>Key Messaging</strong><br>Channel Strategy<br>Sample Copy<br>Social<br>Email</p>
|
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>
|
</div>
|
||||||
<!-- Optional example image (if uploaded in create) -->
|
|
||||||
<div class="example-image" style="display: none;">
|
<!-- Description -->
|
||||||
<img src="#" alt="Example Output Image">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<!-- Unlock / Buy Section -->
|
</div>
|
||||||
<div class="unlock-section">
|
|
||||||
<button class="unlock-btn">Unlock Full Prompt • $19.99</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script type="module">
|
||||||
// Fetch sidebar and 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;
|
document
|
||||||
// Remove active class from all sidebar links
|
.querySelectorAll("#sidebar-container .sidebar a")
|
||||||
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
|
.forEach((l) => l.classList.remove("active"));
|
||||||
link.classList.remove('active');
|
|
||||||
});
|
});
|
||||||
// Optionally set active on a relevant link (e.g., Marketplace)
|
fetch("/topbar.html")
|
||||||
const marketplaceLink = document.querySelector('#sidebar-container a[href="marketplace.html"]');
|
.then((r) => r.text())
|
||||||
if (marketplaceLink) marketplaceLink.classList.add('active');
|
.then(
|
||||||
});
|
(data) =>
|
||||||
|
(document.getElementById("topbar-container").innerHTML = data),
|
||||||
|
);
|
||||||
|
|
||||||
fetch('/topbar.html')
|
const params = new URLSearchParams(location.search);
|
||||||
.then(r => r.text())
|
const id = params.get("id");
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
|
||||||
</script>
|
if (!id) {
|
||||||
</body>
|
showError("No prompt ID provided", "Add ?id=... to the URL.");
|
||||||
</html>
|
} 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