Connect prompt creation flow and document API
This commit is contained in:
parent
863cf3e0ef
commit
691bc72681
247
API.md
Normal file
247
API.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# OnlyPrompt API
|
||||||
|
|
||||||
|
This file documents the backend endpoints used by the frontend. The backend is a helper service for server communication, authentication and shared data persistence.
|
||||||
|
|
||||||
|
Base URL in local Docker setup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:1801
|
||||||
|
```
|
||||||
|
|
||||||
|
Authentication uses a `jwt` cookie set by the login endpoint. Protected endpoints require a logged-in user.
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
### Register
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"displayName": "Isabelle",
|
||||||
|
"userName": "its_isabelle",
|
||||||
|
"email": "isabelle@test.com",
|
||||||
|
"password": "1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: created user data or validation errors.
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userNameOrEmail": "isabelle@test.com",
|
||||||
|
"password": "1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: sets the auth cookie.
|
||||||
|
|
||||||
|
### Current User
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"userName": "its_isabelle",
|
||||||
|
"email": "isabelle@test.com",
|
||||||
|
"roles": ["User"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/auth/logout
|
||||||
|
```
|
||||||
|
|
||||||
|
Deletes the auth cookie and redirects to login.
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
### Current Profile
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/profiles/self
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"displayName": "Isabelle",
|
||||||
|
"slug": "its_isabelle",
|
||||||
|
"bio": "AI creator",
|
||||||
|
"avatarUrl": "data:image/png;base64,...",
|
||||||
|
"specialities": null,
|
||||||
|
"averageRating": 0,
|
||||||
|
"subscribers": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creator List
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/profiles?sort=popular&limit=50&search=belle
|
||||||
|
```
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
|
||||||
|
- `sort`: `popular`, `prompts`, `new`, `rating`
|
||||||
|
- `limit`: number of creators
|
||||||
|
- `search`: optional search term
|
||||||
|
|
||||||
|
Response: list of creator cards including follow state and avatar URL.
|
||||||
|
|
||||||
|
### Update Profile
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/v1/profiles
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"displayName": "Belle",
|
||||||
|
"userName": "lady_belle",
|
||||||
|
"slug": "lady_belle",
|
||||||
|
"bio": "Prompt creator",
|
||||||
|
"avatarUrl": "data:image/png;base64,...",
|
||||||
|
"specialities": null,
|
||||||
|
"isPublic": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates profile data used on My Profile, Community and the topbar.
|
||||||
|
|
||||||
|
## Prompts
|
||||||
|
|
||||||
|
### List Prompts
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/prompts?sortBy=date&ascending=false&limit=50&search=cat
|
||||||
|
```
|
||||||
|
|
||||||
|
Used by Marketplace. Supports sorting, search and category filtering.
|
||||||
|
|
||||||
|
### Feed
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/feed?sortBy=date&ascending=false&limit=20
|
||||||
|
```
|
||||||
|
|
||||||
|
Used by Dashboard. Returns prompt cards with title, description, creator info, avatar and example image.
|
||||||
|
|
||||||
|
### Create Prompt
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/prompts
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Luxury Cat Portrait",
|
||||||
|
"description": "Create a premium cat portrait prompt.",
|
||||||
|
"content": "Write the full prompt instructions here.",
|
||||||
|
"category": "art",
|
||||||
|
"subscriptionTier": null,
|
||||||
|
"slug": null,
|
||||||
|
"exampleOutput": "Example output text",
|
||||||
|
"exampleImageUrl": "data:image/png;base64,...",
|
||||||
|
"price": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: created prompt. The frontend redirects to `/post-detail?id={id}`.
|
||||||
|
|
||||||
|
### Prompt Detail
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/prompts/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes:
|
||||||
|
|
||||||
|
- title and description
|
||||||
|
- prompt content if accessible
|
||||||
|
- category
|
||||||
|
- creator information
|
||||||
|
- price or free state
|
||||||
|
- example output
|
||||||
|
- example image
|
||||||
|
- rating data
|
||||||
|
|
||||||
|
### Reviews
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/prompts/{id}/reviews
|
||||||
|
PUT /api/v1/prompts/{id}/reviews
|
||||||
|
```
|
||||||
|
|
||||||
|
`PUT` request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"comment": "Helpful prompt",
|
||||||
|
"rating": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Used for user feedback on prompts.
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
### Minimal Categories
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/categories/minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Used by Create New and Marketplace filters.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Art",
|
||||||
|
"slug": "art"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Default categories are created automatically when the backend starts.
|
||||||
|
|
||||||
|
## Subscriptions
|
||||||
|
|
||||||
|
### Follow or Subscribe to Creator
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/v1/subscriptions/{creatorId}
|
||||||
|
DELETE /api/v1/subscriptions/{creatorId}
|
||||||
|
```
|
||||||
|
|
||||||
|
Used by Community to follow or unfollow creators.
|
||||||
@ -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, bool CanAccess);
|
public record ApiPrompt(Guid Id, string Title, string Description, string? Content, DateTime TimeStamp, Guid CreatorId, string CreatorName, string CategoryName, string CategorySlug, string? ExampleOutput, string? ExampleImageUrl, decimal? Price, 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, string Description, DateTime TimeStamp, Guid CreatorId, string CreatorName, string CreatorAvatarUrl, string? ExampleImageUrl, 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using OnlyPrompt.Backend.Utils;
|
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
||||||
|
|
||||||
namespace OnlyPrompt.Backend.ApiModels.Prompt
|
|
||||||
{
|
{
|
||||||
public record ApiCreatePromptRequest(string Title, string Description, string Content, Identifier Category, int? SubscriptionTier, string Slug);
|
public record ApiCreatePromptRequest(string Title, string Description, string Content, string Category, int? SubscriptionTier, string? Slug, string? ExampleOutput, string? ExampleImageUrl, decimal? Price);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/categories")]
|
[Route("api/v1/categories")]
|
||||||
[Authorize(Roles = ModelConstants.AdminRole, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
public class CategoryController : BaseController
|
public class CategoryController : BaseController
|
||||||
{
|
{
|
||||||
private static ValidationProblem SlugExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
private static ValidationProblem SlugExistsProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]>
|
||||||
@ -53,6 +53,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Authorize(Roles = ModelConstants.AdminRole)]
|
||||||
public async Task<Results<Ok<ApiCategory>, ValidationProblem>> CreateCategoryAsync([FromBody] ApiCreateCategoryRequest request)
|
public async Task<Results<Ok<ApiCategory>, ValidationProblem>> CreateCategoryAsync([FromBody] ApiCreateCategoryRequest request)
|
||||||
{
|
{
|
||||||
var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug);
|
var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug);
|
||||||
@ -69,6 +70,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = ModelConstants.AdminRole)]
|
||||||
public async Task<Results<Ok<ApiCategory>, NotFound<string>, ValidationProblem>> UpdateCategoryAsync(Identifier id, [FromBody] ApiUpdateCategoryRequest request)
|
public async Task<Results<Ok<ApiCategory>, NotFound<string>, ValidationProblem>> UpdateCategoryAsync(Identifier id, [FromBody] ApiUpdateCategoryRequest request)
|
||||||
{
|
{
|
||||||
var category = await _db.Categories.FindByIdentifierAsync(id);
|
var category = await _db.Categories.FindByIdentifierAsync(id);
|
||||||
@ -95,6 +97,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = ModelConstants.AdminRole)]
|
||||||
public async Task<Results<NoContent, BadRequest<string>, NotFound<string>>> DeleteCategoryAsync(Identifier id, [FromQuery] Identifier? replaceWith = null)
|
public async Task<Results<NoContent, BadRequest<string>, NotFound<string>>> DeleteCategoryAsync(Identifier id, [FromQuery] Identifier? replaceWith = null)
|
||||||
{
|
{
|
||||||
var hasPrompts = await _db.Prompts.AnyAsync(p => p.CategoryId == id.Id);
|
var hasPrompts = await _db.Prompts.AnyAsync(p => p.CategoryId == id.Id);
|
||||||
|
|||||||
@ -31,11 +31,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
var query = _db.Prompts
|
var query = _db.Prompts.AsQueryable();
|
||||||
.Where(
|
|
||||||
x => x.Creator.Subscribers.Any(s => s.SubscriberId == userId)
|
|
||||||
&& x.CreatorId != userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (category.HasValue)
|
if (category.HasValue)
|
||||||
query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug);
|
query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug);
|
||||||
@ -58,11 +54,14 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
.Select(x => new ApiMinimalPrompt(
|
.Select(x => new ApiMinimalPrompt(
|
||||||
x.Id,
|
x.Id,
|
||||||
x.Title,
|
x.Title,
|
||||||
|
x.Description,
|
||||||
x.UpdatedAt,
|
x.UpdatedAt,
|
||||||
x.CreatorId,
|
x.CreatorId,
|
||||||
x.Creator.Profile.DisplayName,
|
x.Creator.Profile.DisplayName,
|
||||||
x.SubscriptionTier.Level,
|
x.Creator.Profile.AvatarUrl,
|
||||||
x.SubscriptionTier.Name,
|
x.ExampleImageUrl,
|
||||||
|
x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level,
|
||||||
|
x.SubscriptionTier == null ? null : x.SubscriptionTier.Name,
|
||||||
x.Reviews.Average(r => (double?)r.Rating),
|
x.Reviews.Average(r => (double?)r.Rating),
|
||||||
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
||||||
)).ToArrayAsync();
|
)).ToArrayAsync();
|
||||||
|
|||||||
@ -65,11 +65,14 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
.Select(x => new ApiMinimalPrompt(
|
.Select(x => new ApiMinimalPrompt(
|
||||||
x.Id,
|
x.Id,
|
||||||
x.Title,
|
x.Title,
|
||||||
|
x.Description,
|
||||||
x.UpdatedAt,
|
x.UpdatedAt,
|
||||||
x.CreatorId,
|
x.CreatorId,
|
||||||
x.Creator.Profile.DisplayName,
|
x.Creator.Profile.DisplayName,
|
||||||
x.SubscriptionTier.Level,
|
x.Creator.Profile.AvatarUrl,
|
||||||
x.SubscriptionTier.Name,
|
x.ExampleImageUrl,
|
||||||
|
x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level,
|
||||||
|
x.SubscriptionTier == null ? null : x.SubscriptionTier.Name,
|
||||||
x.Reviews.Average(r => (double?)r.Rating),
|
x.Reviews.Average(r => (double?)r.Rating),
|
||||||
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level)
|
||||||
)).ToArrayAsync();
|
)).ToArrayAsync();
|
||||||
@ -115,7 +118,7 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
|
|
||||||
var category = await _db.Categories.FindByIdentifierAsync(request.Category);
|
var category = await _db.Categories.FindByIdentifierAsync(new Identifier(request.Category));
|
||||||
if (category is null)
|
if (category is null)
|
||||||
return TypedResults.NotFound("Category not found");
|
return TypedResults.NotFound("Category not found");
|
||||||
|
|
||||||
@ -141,6 +144,9 @@ namespace OnlyPrompt.Backend.Controllers
|
|||||||
Title = request.Title,
|
Title = request.Title,
|
||||||
Description = request.Description,
|
Description = request.Description,
|
||||||
Prompt = request.Content,
|
Prompt = request.Content,
|
||||||
|
ExampleOutput = request.ExampleOutput,
|
||||||
|
ExampleImageUrl = request.ExampleImageUrl,
|
||||||
|
Price = request.Price,
|
||||||
CreatorId = userId.Value,
|
CreatorId = userId.Value,
|
||||||
SubscriptionTier = subscriptionTier,
|
SubscriptionTier = subscriptionTier,
|
||||||
Category = category,
|
Category = category,
|
||||||
|
|||||||
@ -31,6 +31,13 @@ namespace OnlyPrompt.Backend.Database.Models
|
|||||||
[MaxLength(1000)]
|
[MaxLength(1000)]
|
||||||
public required string Description { get; set; }
|
public required string Description { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(4000)]
|
||||||
|
public string? ExampleOutput { get; set; }
|
||||||
|
|
||||||
|
public string? ExampleImageUrl { get; set; }
|
||||||
|
|
||||||
|
public decimal? Price { get; set; }
|
||||||
|
|
||||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
[MaxLength(ModelConstants.MaxSlugLength)]
|
||||||
[Column(TypeName = "citext")]
|
[Column(TypeName = "citext")]
|
||||||
public required string Slug { get; set; }
|
public required string Slug { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace OnlyPrompt.Backend.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PromptCreateDetails : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ExampleImageUrl",
|
||||||
|
table: "Prompts",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ExampleOutput",
|
||||||
|
table: "Prompts",
|
||||||
|
type: "character varying(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Price",
|
||||||
|
table: "Prompts",
|
||||||
|
type: "numeric",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExampleImageUrl",
|
||||||
|
table: "Prompts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExampleOutput",
|
||||||
|
table: "Prompts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Price",
|
||||||
|
table: "Prompts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -79,6 +79,16 @@ namespace OnlyPrompt.Backend.Migrations
|
|||||||
.HasMaxLength(1000)
|
.HasMaxLength(1000)
|
||||||
.HasColumnType("character varying(1000)");
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ExampleImageUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ExampleOutput")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Price")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
b.Property<string>("Prompt")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(4000)
|
.HasMaxLength(4000)
|
||||||
|
|||||||
@ -109,5 +109,54 @@ app.MapFallbackToFile("/login.html");
|
|||||||
using var scope = app.Services.CreateScope();
|
using var scope = app.Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<OnlyPromptContext>();
|
var db = scope.ServiceProvider.GetRequiredService<OnlyPromptContext>();
|
||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
|
await EnsurePromptDetailColumnsAsync(db);
|
||||||
|
await SeedDefaultCategoriesAsync(db);
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
static async Task EnsurePromptDetailColumnsAsync(OnlyPromptContext db)
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync("""
|
||||||
|
ALTER TABLE "Prompts"
|
||||||
|
ADD COLUMN IF NOT EXISTS "ExampleImageUrl" text;
|
||||||
|
""");
|
||||||
|
|
||||||
|
await db.Database.ExecuteSqlRawAsync("""
|
||||||
|
ALTER TABLE "Prompts"
|
||||||
|
ADD COLUMN IF NOT EXISTS "ExampleOutput" character varying(4000);
|
||||||
|
""");
|
||||||
|
|
||||||
|
await db.Database.ExecuteSqlRawAsync("""
|
||||||
|
ALTER TABLE "Prompts"
|
||||||
|
ADD COLUMN IF NOT EXISTS "Price" numeric;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SeedDefaultCategoriesAsync(OnlyPromptContext db)
|
||||||
|
{
|
||||||
|
var defaults = new[]
|
||||||
|
{
|
||||||
|
("creative-writing", "Creative Writing"),
|
||||||
|
("coding", "Coding"),
|
||||||
|
("art", "Art"),
|
||||||
|
("marketing", "Marketing"),
|
||||||
|
("video", "Video"),
|
||||||
|
("data", "Data")
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (slug, name) in defaults)
|
||||||
|
{
|
||||||
|
if (await db.Categories.AnyAsync(c => c.Slug == slug))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
db.Categories.Add(new CategoryModel
|
||||||
|
{
|
||||||
|
Id = Guid.CreateVersion7(),
|
||||||
|
Slug = slug,
|
||||||
|
Name = name,
|
||||||
|
Description = $"{name} prompts"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|||||||
@ -33,6 +33,11 @@ namespace OnlyPrompt.Backend.Utils
|
|||||||
.MapCtorParamFrom(x => x.Description, x => x.Description)
|
.MapCtorParamFrom(x => x.Description, x => x.Description)
|
||||||
.MapCtorParamFrom(x => x.Content, x => x.Prompt)
|
.MapCtorParamFrom(x => x.Content, x => x.Prompt)
|
||||||
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
||||||
|
.MapCtorParamFrom(x => x.CategoryName, x => x.Category.Name)
|
||||||
|
.MapCtorParamFrom(x => x.CategorySlug, x => x.Category.Slug)
|
||||||
|
.MapCtorParamFrom(x => x.ExampleOutput, x => x.ExampleOutput)
|
||||||
|
.MapCtorParamFrom(x => x.ExampleImageUrl, x => x.ExampleImageUrl)
|
||||||
|
.MapCtorParamFrom(x => x.Price, x => x.Price)
|
||||||
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
||||||
.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)
|
||||||
@ -43,9 +48,12 @@ namespace OnlyPrompt.Backend.Utils
|
|||||||
config.CreateMap<PromptModel, ApiMinimalPrompt>()
|
config.CreateMap<PromptModel, ApiMinimalPrompt>()
|
||||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
||||||
.MapCtorParamFrom(x => x.Title, x => x.Title)
|
.MapCtorParamFrom(x => x.Title, x => x.Title)
|
||||||
|
.MapCtorParamFrom(x => x.Description, x => x.Description)
|
||||||
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
.MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName)
|
||||||
|
.MapCtorParamFrom(x => x.CreatorAvatarUrl, x => x.Creator.Profile.AvatarUrl)
|
||||||
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
.MapCtorParamFrom(x => x.TimeStamp, x => x.UpdatedAt)
|
||||||
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
.MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId)
|
||||||
|
.MapCtorParamFrom(x => x.ExampleImageUrl, x => x.ExampleImageUrl)
|
||||||
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
.MapCtorParamFrom(x => x.TierLevel, x => x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level)
|
||||||
.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.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating))
|
.MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating))
|
||||||
|
|||||||
@ -99,6 +99,7 @@
|
|||||||
<button type="submit" class="submit-btn">Publish Prompt</button>
|
<button type="submit" class="submit-btn">Publish Prompt</button>
|
||||||
<button type="button" class="cancel-btn">Cancel</button>
|
<button type="button" class="cancel-btn">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="create-status" style="text-align:center;color:#64748b;margin:0;"></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@ -129,6 +130,7 @@
|
|||||||
const imageInput = document.getElementById('exampleImage');
|
const imageInput = document.getElementById('exampleImage');
|
||||||
const imagePreview = document.getElementById('imagePreview');
|
const imagePreview = document.getElementById('imagePreview');
|
||||||
const previewImg = document.getElementById('previewImg');
|
const previewImg = document.getElementById('previewImg');
|
||||||
|
let exampleImageUrl = '';
|
||||||
|
|
||||||
if (imageInput) {
|
if (imageInput) {
|
||||||
imageInput.addEventListener('change', function(event) {
|
imageInput.addEventListener('change', function(event) {
|
||||||
@ -136,25 +138,99 @@
|
|||||||
if (file && (file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg')) {
|
if (file && (file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg')) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
previewImg.src = e.target.result;
|
exampleImageUrl = e.target.result;
|
||||||
|
previewImg.src = exampleImageUrl;
|
||||||
imagePreview.style.display = 'block';
|
imagePreview.style.display = 'block';
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
} else {
|
} else {
|
||||||
imagePreview.style.display = 'none';
|
imagePreview.style.display = 'none';
|
||||||
previewImg.src = '#';
|
previewImg.src = '#';
|
||||||
|
exampleImageUrl = '';
|
||||||
if (file) alert('Please upload a PNG or JPG image.');
|
if (file) alert('Please upload a PNG or JPG image.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle form submission (demo only)
|
async function loadCategories() {
|
||||||
document.getElementById('createPromptForm').addEventListener('submit', (e) => {
|
const categorySelect = document.getElementById('category');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/categories/minimal');
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const categories = await response.json();
|
||||||
|
if (!categories.length) return;
|
||||||
|
|
||||||
|
categorySelect.innerHTML = categories
|
||||||
|
.map((category) => `<option value="${category.slug}">${category.name}</option>`)
|
||||||
|
.join('');
|
||||||
|
} catch {
|
||||||
|
// Keep the static fallback categories.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.getElementById('createPromptForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Prompt published! (Demo)');
|
const status = document.getElementById('create-status');
|
||||||
// Here you would normally send data to a backend (including the image file)
|
const submitBtn = document.querySelector('.submit-btn');
|
||||||
|
status.textContent = 'Publishing...';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isPaid = paidBtn.classList.contains('active');
|
||||||
|
const price = isPaid ? Number(priceInput.value || 0) : null;
|
||||||
|
const payload = {
|
||||||
|
title: document.getElementById('title').value.trim(),
|
||||||
|
description: document.getElementById('description').value.trim(),
|
||||||
|
category: document.getElementById('category').value,
|
||||||
|
content: document.getElementById('promptContent').value.trim(),
|
||||||
|
exampleOutput: document.getElementById('exampleOutput').value.trim() || null,
|
||||||
|
exampleImageUrl: exampleImageUrl || null,
|
||||||
|
price,
|
||||||
|
subscriptionTier: null,
|
||||||
|
slug: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/prompts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(getCreateErrorMessage(error, response.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await response.json();
|
||||||
|
location.href = `/post-detail?id=${prompt.id}`;
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = error.message;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCreateErrorMessage(errorText, status) {
|
||||||
|
if (!errorText) return `Server error ${status}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(errorText);
|
||||||
|
const messages = error.errors
|
||||||
|
? Object.values(error.errors).flat()
|
||||||
|
: [error.title || errorText];
|
||||||
|
|
||||||
|
return messages.join(' ');
|
||||||
|
} catch {
|
||||||
|
return errorText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel button (go back)
|
// Cancel button (go back)
|
||||||
document.querySelector('.cancel-btn').addEventListener('click', () => {
|
document.querySelector('.cancel-btn').addEventListener('click', () => {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
@ -177,6 +253,8 @@
|
|||||||
fetch('/topbar.html')
|
fetch('/topbar.html')
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
.then(data => document.getElementById('topbar-container').innerHTML = data);
|
||||||
|
|
||||||
|
loadCategories();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -167,6 +167,15 @@
|
|||||||
.action-btn i {
|
.action-btn i {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
.action-btn.active {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.like-btn.active {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.save-btn.active {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
.like-btn:hover {
|
.like-btn:hover {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,33 +151,51 @@
|
|||||||
function feedImg(id) {
|
function feedImg(id) {
|
||||||
return `/images/content/feed${(parseInt(id.slice(-1), 16) % 4) + 1}.png`;
|
return `/images/content/feed${(parseInt(id.slice(-1), 16) % 4) + 1}.png`;
|
||||||
}
|
}
|
||||||
function creatorImg(id) {
|
|
||||||
return `/images/content/creator${(parseInt(id.slice(-1), 16) % 6) + 1}.png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCard(prompt) {
|
function renderCard(prompt) {
|
||||||
const locked = !prompt.canAccess;
|
const locked = !prompt.canAccess;
|
||||||
|
const liked = localStorage.getItem(`prompt-liked-${prompt.id}`) === "true";
|
||||||
|
const saved = localStorage.getItem(`prompt-saved-${prompt.id}`) === "true";
|
||||||
return `
|
return `
|
||||||
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
|
<div class="post-card${locked ? " post-locked" : ""}" onclick="location.href='/post-detail?id=${prompt.id}'">
|
||||||
<div class="post-header">
|
<div class="post-header">
|
||||||
<img class="post-avatar" src="${creatorImg(prompt.creatorId)}" alt="${prompt.creatorName}">
|
<img class="post-avatar" src="${prompt.creatorAvatarUrl || '../images/content/cat.png'}" alt="${prompt.creatorName}">
|
||||||
<div class="post-author">
|
<div class="post-author">
|
||||||
<span class="post-name">${prompt.creatorName}</span>
|
<span class="post-name">${prompt.creatorName}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
<span class="post-date">${timeAgo(prompt.timeStamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">
|
${prompt.exampleImageUrl ? `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${prompt.exampleImageUrl}" alt="${prompt.title}">` : `<img class="post-image${locked ? ' post-image-locked' : ''}" src="${feedImg(prompt.id)}" alt="${prompt.title}">`}
|
||||||
<h3 class="post-title" style="margin-top:10px">${prompt.title}</h3>
|
<h3 class="post-title" style="margin-top:10px">${prompt.title}</h3>
|
||||||
|
<p class="post-description">${prompt.description || ''}</p>
|
||||||
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? 'Paid'} tier required</p>` : ''}
|
${locked ? `<p class="post-locked-msg"><i class="bi bi-lock-fill"></i> ${prompt.tierName ?? 'Paid'} tier required</p>` : ''}
|
||||||
${renderStars(prompt.averageRating)}
|
${renderStars(prompt.averageRating)}
|
||||||
</div>
|
</div>
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<button class="action-btn share-btn" onclick="event.stopPropagation(); navigator.clipboard.writeText(location.origin+'/post-detail?id=${prompt.id}')"><i class="bi bi-share"></i></button>
|
<button class="action-btn like-btn ${liked ? 'active' : ''}" onclick="toggleFeedState(event, 'liked', '${prompt.id}')"><i class="bi ${liked ? 'bi-heart-fill' : 'bi-heart'}"></i> <span>${liked ? 'Liked' : 'Like'}</span></button>
|
||||||
|
<button class="action-btn comment-btn" onclick="event.stopPropagation(); location.href='/post-detail?id=${prompt.id}#rating-section'"><i class="bi bi-chat"></i> <span>Review</span></button>
|
||||||
|
<button class="action-btn share-btn" onclick="sharePrompt(event, '${prompt.id}')"><i class="bi bi-share"></i> <span>Share</span></button>
|
||||||
|
<button class="action-btn save-btn ${saved ? 'active' : ''}" onclick="toggleFeedState(event, 'saved', '${prompt.id}')"><i class="bi ${saved ? 'bi-bookmark-fill' : 'bi-bookmark'}"></i> <span>${saved ? 'Saved' : 'Save'}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.toggleFeedState = function(event, type, id) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const key = `prompt-${type}-${id}`;
|
||||||
|
const next = localStorage.getItem(key) !== "true";
|
||||||
|
localStorage.setItem(key, next);
|
||||||
|
loadFeed(
|
||||||
|
document.querySelector(".filter-btn.active")?.dataset.sort || "date",
|
||||||
|
document.querySelector(".filter-btn.active")?.dataset.ascending === "true"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.sharePrompt = function(event, id) {
|
||||||
|
event.stopPropagation();
|
||||||
|
navigator.clipboard.writeText(`${location.origin}/post-detail?id=${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
async function loadFeed(sortBy = "date", ascending = false) {
|
async function loadFeed(sortBy = "date", ascending = false) {
|
||||||
grid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
emptyEl.style.display = "none";
|
emptyEl.style.display = "none";
|
||||||
|
|||||||
@ -93,24 +93,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Prompts Grid -->
|
<!-- Prompts Grid -->
|
||||||
<div class="prompts-grid" id="prompts-grid"></div>
|
<div class="prompts-grid" id="prompts-grid"></div>
|
||||||
|
|
||||||
@ -426,7 +408,7 @@
|
|||||||
|
|
||||||
function renderCard(p) {
|
function renderCard(p) {
|
||||||
const locked = !p.canAccess && p.tierLevel != null;
|
const locked = !p.canAccess && p.tierLevel != null;
|
||||||
const img = p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
const img = p.exampleImageUrl || p._img || MARKET_IMAGES[cardIndex++ % MARKET_IMAGES.length];
|
||||||
return `
|
return `
|
||||||
<div class="prompt-card">
|
<div class="prompt-card">
|
||||||
<img src="${img}" alt="${p.title}" class="prompt-img">
|
<img src="${img}" alt="${p.title}" class="prompt-img">
|
||||||
@ -456,7 +438,7 @@
|
|||||||
const grid = document.getElementById("prompts-grid");
|
const grid = document.getElementById("prompts-grid");
|
||||||
const emptyEl = document.getElementById("market-empty");
|
const emptyEl = document.getElementById("market-empty");
|
||||||
const errorEl = document.getElementById("market-error");
|
const errorEl = document.getElementById("market-error");
|
||||||
const search = document.getElementById("search-input").value.trim();
|
const search = document.getElementById("topbarSearchInput")?.value.trim() || "";
|
||||||
const [sortBy, ascending] = document
|
const [sortBy, ascending] = document
|
||||||
.getElementById("sort-select")
|
.getElementById("sort-select")
|
||||||
.value.split("|");
|
.value.split("|");
|
||||||
@ -605,10 +587,20 @@
|
|||||||
document
|
document
|
||||||
.getElementById("sort-select")
|
.getElementById("sort-select")
|
||||||
.addEventListener("change", loadPrompts);
|
.addEventListener("change", loadPrompts);
|
||||||
document.getElementById("search-input").addEventListener("input", () => {
|
function wireMarketplaceTopbarSearch() {
|
||||||
|
const input = document.getElementById("topbarSearchInput");
|
||||||
|
if (!input || input.dataset.marketplaceSearchReady === "true") return;
|
||||||
|
|
||||||
|
input.dataset.marketplaceSearchReady = "true";
|
||||||
|
input.addEventListener("input", () => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(loadPrompts, 400);
|
searchTimeout = setTimeout(loadPrompts, 400);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const topbarObserver = new MutationObserver(wireMarketplaceTopbarSearch);
|
||||||
|
topbarObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
wireMarketplaceTopbarSearch();
|
||||||
|
|
||||||
// Make openPayment global
|
// Make openPayment global
|
||||||
window.openPayment = openPayment;
|
window.openPayment = openPayment;
|
||||||
|
|||||||
@ -156,6 +156,17 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Example Output -->
|
||||||
|
<div class="example-section" id="example-section" style="display: none">
|
||||||
|
<h2>EXAMPLE OUTPUT</h2>
|
||||||
|
<div class="example-content">
|
||||||
|
<div id="example-output-text" class="example-output-text" style="white-space: pre-wrap"></div>
|
||||||
|
<div id="example-image" class="example-image" style="display: none">
|
||||||
|
<img id="example-image-img" src="" alt="Example output image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Locked section (shown instead of prompt if no access) -->
|
<!-- Locked section (shown instead of prompt if no access) -->
|
||||||
<div
|
<div
|
||||||
id="locked-section"
|
id="locked-section"
|
||||||
@ -274,15 +285,20 @@
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
document.getElementById("prompt-title").textContent = p.title;
|
document.getElementById("prompt-title").textContent = p.title;
|
||||||
|
document.getElementById("prompt-category").textContent =
|
||||||
|
p.categoryName || "Uncategorized";
|
||||||
document.getElementById("prompt-updated").textContent =
|
document.getElementById("prompt-updated").textContent =
|
||||||
`Updated: ${new Date(p.timeStamp).toLocaleDateString()}`;
|
`Updated: ${new Date(p.timeStamp).toLocaleDateString()}`;
|
||||||
document.getElementById("prompt-description").textContent =
|
document.getElementById("prompt-description").textContent =
|
||||||
p.description;
|
p.description;
|
||||||
document.getElementById("prompt-body").textContent = p.content;
|
document.getElementById("prompt-body").textContent = p.content;
|
||||||
|
renderExamples(p);
|
||||||
|
|
||||||
// Tier badge
|
// Tier badge
|
||||||
const badge = document.getElementById("tier-badge");
|
const badge = document.getElementById("tier-badge");
|
||||||
if (p.tierName) {
|
if (p.price != null && Number(p.price) > 0) {
|
||||||
|
badge.innerHTML = `<span style="background:#fef3c7;color:#92400e;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">$${Number(p.price).toFixed(2)}</span>`;
|
||||||
|
} else 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>`;
|
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 {
|
} else {
|
||||||
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
|
badge.innerHTML = `<span style="background:#dcfce7;color:#166534;border-radius:20px;padding:4px 14px;font-size:0.8rem;font-weight:600;">Free</span>`;
|
||||||
@ -321,6 +337,30 @@
|
|||||||
document.getElementById("detail-body").style.display = "block";
|
document.getElementById("detail-body").style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderExamples(p) {
|
||||||
|
const section = document.getElementById("example-section");
|
||||||
|
const text = document.getElementById("example-output-text");
|
||||||
|
const imageWrap = document.getElementById("example-image");
|
||||||
|
const image = document.getElementById("example-image-img");
|
||||||
|
|
||||||
|
if (!p.exampleOutput && !p.exampleImageUrl) {
|
||||||
|
section.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.style.display = "block";
|
||||||
|
text.textContent = p.exampleOutput || "";
|
||||||
|
text.style.display = p.exampleOutput ? "block" : "none";
|
||||||
|
|
||||||
|
if (p.exampleImageUrl) {
|
||||||
|
image.src = p.exampleImageUrl;
|
||||||
|
imageWrap.style.display = "block";
|
||||||
|
} else {
|
||||||
|
image.removeAttribute("src");
|
||||||
|
imageWrap.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showError(title, msg) {
|
function showError(title, msg) {
|
||||||
document.getElementById("detail-loading").style.display = "none";
|
document.getElementById("detail-loading").style.display = "none";
|
||||||
document.getElementById("detail-error").style.display = "block";
|
document.getElementById("detail-error").style.display = "block";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user