diff --git a/API.md b/API.md index 36cc0c4..de4d6cd 100644 --- a/API.md +++ b/API.md @@ -152,7 +152,7 @@ GET /api/v1/prompts?sortBy=date&ascending=false&limit=50&search=cat Used by Marketplace. Supports sorting, search and category filtering. The marketplace excludes prompts created by the logged-in user. -Response items include prompt title, description, creator id, creator name, creator avatar, example image, price, like/save counts, average rating, review count and access state. +Response items include prompt title, description, creator id, creator name, creator avatar, example image, tier data, like/save counts, average rating, review count and access state. ### Feed @@ -187,6 +187,8 @@ Request: Response: created prompt. The frontend redirects to `/post-detail?id={id}`. +`subscriptionTier` is the creator's tier level. `null` means the prompt is public/free. `price` is kept as `null` because prompt access is handled through monthly creator tiers. + ### Own Prompts ```http @@ -202,7 +204,7 @@ PUT /api/v1/prompts/{id} Content-Type: application/json ``` -Request body uses the same editable fields as prompt creation. Used by the edit prompt flow. +Request body uses the same editable fields as prompt creation, including `subscriptionTier`. Used by the edit prompt flow. ### Prompt Detail @@ -216,13 +218,13 @@ Response includes: - prompt content if accessible - category - creator information -- price or free state +- tier or free state - example output - example image - average rating and review count - like/save state and counts -Paid prompts return no detail content for users without access. +Tier prompts return no detail content for users without a matching creator subscription. ### Likes and Saves @@ -283,10 +285,49 @@ Default categories are created automatically when the backend starts. ```http GET /api/v1/subscriptions/{creatorId} PUT /api/v1/subscriptions/{creatorId} +PUT /api/v1/subscriptions/{creatorId}/{level} DELETE /api/v1/subscriptions/{creatorId} ``` -Used by Community and public profiles to read follow state, follow creators or unfollow creators. +Used by Community, public profiles and locked prompt details to read follow state, follow creators, subscribe to a monthly tier or unfollow creators. + +- `PUT /api/v1/subscriptions/{creatorId}` follows a creator without a paid tier. +- `PUT /api/v1/subscriptions/{creatorId}/{level}` subscribes to one of the creator's tiers. A higher tier gives access to prompts from the same level and lower levels. + +### Subscription Tiers + +```http +GET /api/v1/subscriptions/tiers +GET /api/v1/subscriptions/tiers/{creatorId} +POST /api/v1/subscriptions/tiers +PUT /api/v1/subscriptions/tiers/{tierId} +DELETE /api/v1/subscriptions/tiers/{tierId} +``` + +Used by the Subscription Tiers page, Create Prompt and public creator profiles. + +Create request: + +```json +{ + "name": "Supporter", + "monthlyPrice": 4.99, + "level": 1, + "description": "Access to basic premium prompts." +} +``` + +Response: + +```json +{ + "id": "uuid", + "name": "Supporter", + "level": 1, + "monthlyPrice": 4.99, + "description": "Access to basic premium prompts." +} +``` ## Error Handling diff --git a/OnlyPrompt.Backend/ApiModels/Prompt/Models.cs b/OnlyPrompt.Backend/ApiModels/Prompt/Models.cs index 8187c54..183e743 100644 --- a/OnlyPrompt.Backend/ApiModels/Prompt/Models.cs +++ b/OnlyPrompt.Backend/ApiModels/Prompt/Models.cs @@ -1,7 +1,7 @@ namespace OnlyPrompt.Backend.ApiModels.Prompt { - 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 LikeCount, bool IsLiked, int SaveCount, bool IsSaved, int? TierLevel, string? TierName, double? AverageRating, int ReviewCount, bool CanAccess); - public record ApiMinimalPrompt(Guid Id, string Title, string Description, DateTime TimeStamp, Guid CreatorId, string CreatorName, string CreatorAvatarUrl, string? ExampleImageUrl, decimal? Price, int LikeCount, bool IsLiked, int SaveCount, bool IsSaved, int? TierLevel, string? TierName, double? AverageRating, int ReviewCount, 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 LikeCount, bool IsLiked, int SaveCount, bool IsSaved, int? TierLevel, string? TierName, decimal? TierMonthlyPrice, double? AverageRating, int ReviewCount, bool CanAccess); + public record ApiMinimalPrompt(Guid Id, string Title, string Description, DateTime TimeStamp, Guid CreatorId, string CreatorName, string CreatorAvatarUrl, string? ExampleImageUrl, decimal? Price, int LikeCount, bool IsLiked, int SaveCount, bool IsSaved, int? TierLevel, string? TierName, decimal? TierMonthlyPrice, double? AverageRating, int ReviewCount, bool CanAccess); public record ApiReview(Guid CreatorId, string CreatorName, string? Comment, int Rating); public record ApiLikeState(int LikeCount, bool IsLiked); public record ApiSaveState(int SaveCount, bool IsSaved); diff --git a/OnlyPrompt.Backend/ApiModels/Prompt/Requests.cs b/OnlyPrompt.Backend/ApiModels/Prompt/Requests.cs index 54ef693..1d55543 100644 --- a/OnlyPrompt.Backend/ApiModels/Prompt/Requests.cs +++ b/OnlyPrompt.Backend/ApiModels/Prompt/Requests.cs @@ -1,5 +1,5 @@ namespace OnlyPrompt.Backend.ApiModels.Prompt { public record ApiCreatePromptRequest(string Title, string Description, string Content, string Category, int? SubscriptionTier, string? Slug, string? ExampleOutput, string? ExampleImageUrl, decimal? Price); - public record ApiUpdatePromptRequest(string Title, string Description, string Content, string Category, string? ExampleOutput, string? ExampleImageUrl, decimal? Price); + public record ApiUpdatePromptRequest(string Title, string Description, string Content, string Category, int? SubscriptionTier, string? ExampleOutput, string? ExampleImageUrl, decimal? Price); } diff --git a/OnlyPrompt.Backend/Controllers/FeedController.cs b/OnlyPrompt.Backend/Controllers/FeedController.cs index 19dc043..495fd14 100644 --- a/OnlyPrompt.Backend/Controllers/FeedController.cs +++ b/OnlyPrompt.Backend/Controllers/FeedController.cs @@ -71,9 +71,10 @@ namespace OnlyPrompt.Backend.Controllers x.Saves.Any(s => s.UserId == userId), x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level, x.SubscriptionTier == null ? null : x.SubscriptionTier.Name, + x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice, x.Reviews.Average(r => (double?)r.Rating), x.Reviews.Count, - 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(); return prompts; diff --git a/OnlyPrompt.Backend/Controllers/PromptController.cs b/OnlyPrompt.Backend/Controllers/PromptController.cs index ebef988..8bacfd6 100644 --- a/OnlyPrompt.Backend/Controllers/PromptController.cs +++ b/OnlyPrompt.Backend/Controllers/PromptController.cs @@ -81,9 +81,10 @@ namespace OnlyPrompt.Backend.Controllers x.Saves.Any(s => s.UserId == userId), x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level, x.SubscriptionTier == null ? null : x.SubscriptionTier.Name, + x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice, x.Reviews.Average(r => (double?)r.Rating), x.Reviews.Count, - x.CreatorId == userId || ((x.Price == null || x.Price <= 0) && (x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level))) + x.CreatorId == userId || x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level <= s.SubscriptionTier.Level) )).ToArrayAsync(); return prompts; @@ -112,6 +113,7 @@ namespace OnlyPrompt.Backend.Controllers x.Saves.Any(s => s.UserId == userId), x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level, x.SubscriptionTier == null ? null : x.SubscriptionTier.Name, + x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice, x.Reviews.Average(r => (double?)r.Rating), x.Reviews.Count, true @@ -132,9 +134,6 @@ namespace OnlyPrompt.Backend.Controllers if (prompt is null) return TypedResults.NotFound("Prompt not found"); - if (prompt.CreatorId != userId && prompt.Price.HasValue && prompt.Price.Value > 0) - return TypedResults.NotFound("Prompt not found or requires payment"); - var canAccess = await GetAccessiblePrompts(userId.Value).AnyAsync(p => p.Id == prompt.Id); var apiPrompt = _mapper.Map(prompt) with { @@ -169,7 +168,20 @@ namespace OnlyPrompt.Backend.Controllers prompt.Category = category; prompt.ExampleOutput = request.ExampleOutput; prompt.ExampleImageUrl = request.ExampleImageUrl; - prompt.Price = request.Price; + prompt.Price = null; + prompt.SubscriptionTier = null; + if (request.SubscriptionTier.HasValue) + { + var subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync( + t => t.Level == request.SubscriptionTier.Value + && t.UserId == userId + ); + + if (subscriptionTier is null) + return TypedResults.NotFound("Subscription tier not found"); + + prompt.SubscriptionTier = subscriptionTier; + } await _db.SaveChangesAsync(); var apiPrompt = _mapper.Map(prompt) with { Content = prompt.Prompt, CanAccess = true }; @@ -315,7 +327,7 @@ namespace OnlyPrompt.Backend.Controllers Prompt = request.Content, ExampleOutput = request.ExampleOutput, ExampleImageUrl = request.ExampleImageUrl, - Price = request.Price, + Price = null, CreatorId = userId.Value, SubscriptionTier = subscriptionTier, Category = category, diff --git a/OnlyPrompt.Backend/Controllers/SubscriptionController.cs b/OnlyPrompt.Backend/Controllers/SubscriptionController.cs index ff247bf..e796129 100644 --- a/OnlyPrompt.Backend/Controllers/SubscriptionController.cs +++ b/OnlyPrompt.Backend/Controllers/SubscriptionController.cs @@ -100,6 +100,35 @@ namespace OnlyPrompt.Backend.Controllers return subscription; } + [HttpGet("tiers")] + public async Task GetOwnSubscriptionTiersAsync() + { + var userId = User.GetUserId(); + return await _db.SubscriptionTiers + .Where(t => t.UserId == userId) + .OrderBy(t => t.Level) + .ProjectTo(_mapper.ConfigurationProvider) + .ToArrayAsync(); + } + + [HttpGet("tiers/{userId}")] + public async Task, NotFound>> GetCreatorSubscriptionTiersAsync([FromRoute(Name = "userId")] Identifier creatorId) + { + var creatorExists = await _db.Users.AnyAsync( + user => creatorId.Id.HasValue ? user.Id == creatorId.Id.Value : user.Profile.Slug == creatorId.Slug + ); + if (creatorExists == false) + return TypedResults.NotFound($"No user found with identifier {creatorId}"); + + var tiers = await _db.SubscriptionTiers + .Where(t => creatorId.Id.HasValue ? t.UserId == creatorId.Id.Value : t.User.Profile.Slug == creatorId.Slug) + .OrderBy(t => t.Level) + .ProjectTo(_mapper.ConfigurationProvider) + .ToArrayAsync(); + + return TypedResults.Ok(tiers); + } + [HttpDelete("{userId}")] public async Task>> UnsubscribeAsync([FromRoute(Name = "userId")] Identifier subscribeToId) { diff --git a/OnlyPrompt.Backend/Utils/AutoMapperSetup.cs b/OnlyPrompt.Backend/Utils/AutoMapperSetup.cs index 603e5dc..f5f5719 100644 --- a/OnlyPrompt.Backend/Utils/AutoMapperSetup.cs +++ b/OnlyPrompt.Backend/Utils/AutoMapperSetup.cs @@ -44,6 +44,7 @@ namespace OnlyPrompt.Backend.Utils .MapCtorParamFrom(x => x.IsSaved, x => false) .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.TierMonthlyPrice, x => x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice) .MapCtorParamFrom(x => x.CreatorName, x => x.Creator.Profile.DisplayName) .MapCtorParamFrom(x => x.CreatorId, x => x.CreatorId) .MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating)) @@ -66,6 +67,7 @@ namespace OnlyPrompt.Backend.Utils .MapCtorParamFrom(x => x.IsSaved, x => false) .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.TierMonthlyPrice, x => x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice) .MapCtorParamFrom(x => x.AverageRating, x => x.Reviews.Average(r => (double?)r.Rating)) .MapCtorParamFrom(x => x.ReviewCount, x => x.Reviews.Count) .MapCtorParamFrom(x => x.CanAccess, x => true); diff --git a/OnlyPrompt.Frontend/create.html b/OnlyPrompt.Frontend/create.html index 1938c0e..c1923e0 100644 --- a/OnlyPrompt.Frontend/create.html +++ b/OnlyPrompt.Frontend/create.html @@ -83,15 +83,18 @@
- +
- +
-
- +
+ + Manage tiers
- You can set a price later or keep it free. + Free prompts are public. Tier prompts require a monthly creator subscription.
@@ -109,23 +112,24 @@ diff --git a/OnlyPrompt.Frontend/css/base.css b/OnlyPrompt.Frontend/css/base.css index e929496..8195254 100644 --- a/OnlyPrompt.Frontend/css/base.css +++ b/OnlyPrompt.Frontend/css/base.css @@ -58,6 +58,25 @@ body { overflow-y: auto; } +@media (max-width: 700px) { + .layout { + padding-bottom: 74px; + } + + #sidebar-container { + position: fixed; + top: auto; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + width: 100%; + height: auto; + overflow: visible; + border-top: 1px solid #e2e8f0; + } +} + /* Main content area - flex child that fills remaining space */ .page-body { flex: 1; diff --git a/OnlyPrompt.Frontend/css/create.css b/OnlyPrompt.Frontend/css/create.css index c7a17cb..49e346c 100644 --- a/OnlyPrompt.Frontend/css/create.css +++ b/OnlyPrompt.Frontend/css/create.css @@ -96,9 +96,21 @@ background: var(--gradient); color: white; } -#priceField { - margin-top: 8px; +#tierField { display: none; + gap: 8px; + margin-top: 8px; +} + +.tier-manage-link { + color: #3b82f6; + font-size: 0.85rem; + font-weight: 700; + text-decoration: none; +} + +.tier-manage-link:hover { + text-decoration: underline; } /* Image preview */ diff --git a/OnlyPrompt.Frontend/css/marketplace.css b/OnlyPrompt.Frontend/css/marketplace.css index 77ff255..2d9f98e 100644 --- a/OnlyPrompt.Frontend/css/marketplace.css +++ b/OnlyPrompt.Frontend/css/marketplace.css @@ -115,6 +115,7 @@ display: flex; flex-direction: column; gap: 8px; + flex: 1; } .prompt-title { @@ -157,7 +158,7 @@ font-size: 1.3rem; font-weight: 700; color: #3b82f6; - margin: 8px 0 4px; + margin: auto 0 4px; } .prompt-actions { @@ -220,200 +221,6 @@ } } -/* 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; -} - -/* ── Payment Modal ──────────────────────────────────────────────────── */ -#payment-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - align-items: center; - justify-content: center; -} - -.payment-modal { - background: #fff; - border-radius: 16px; - padding: 32px; - max-width: 460px; - width: 90%; - position: relative; - max-height: 90vh; - overflow-y: auto; -} - -.payment-close-btn { - position: absolute; - top: 14px; - right: 18px; - background: none; - border: none; - font-size: 1.4rem; - cursor: pointer; - color: #64748b; -} - -.payment-title { - margin-bottom: 4px; -} - -#pay-prompt-title { - color: #6366f1; - font-weight: 600; - margin-bottom: 16px; -} - -.payment-intro { - color: #64748b; - margin-bottom: 24px; -} - -.payment-method-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.pay-crypto-icon { - font-size: 1.4rem; -} - -.pay-method-price { - margin-left: auto; - font-size: 0.85rem; - color: #94a3b8; -} - -.payment-disclaimer { - margin-top: 20px; - font-size: 0.8rem; - color: #94a3b8; - text-align: center; -} - -#pay-step-2 { - display: none; -} - -.payment-back-btn { - background: none; - border: none; - color: #6366f1; - cursor: pointer; - margin-bottom: 16px; - font-size: 0.9rem; -} - -#pay-crypto-title { - margin-bottom: 8px; -} - -.payment-amount-text { - color: #64748b; - margin-bottom: 8px; -} - -.payment-address-box { - background: #f1f5f9; - border-radius: 10px; - padding: 14px 16px; - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 8px; -} - -#pay-address { - font-size: 0.85rem; - word-break: break-all; - flex: 1; -} - -.payment-copy-btn { - background: none; - border: none; - cursor: pointer; - color: #6366f1; - font-size: 1.1rem; -} - -.payment-warning-text { - font-size: 0.78rem; - color: #94a3b8; - margin-bottom: 20px; -} - -.payment-info-box { - background: #fef9c3; - border: 1px solid #fde68a; - border-radius: 10px; - padding: 12px 14px; - font-size: 0.82rem; - color: #92400e; - margin-bottom: 20px; -} -.payment-confirm-btn { - width: 100%; - padding: 12px; - background: #6366f1; - color: #fff; - border: none; - border-radius: 10px; - font-weight: 600; - font-size: 1rem; - cursor: pointer; -} -#pay-step-3 { - display: none; - text-align: center; - padding: 20px 0; -} -.payment-success-icon { - font-size: 3.5rem; - color: #10b981; - display: block; - margin-bottom: 16px; -} -.payment-success-title { - margin-bottom: 8px; -} -.payment-success-desc { - color: #64748b; - margin-bottom: 24px; -} -.payment-done-btn { - padding: 12px 28px; - background: #6366f1; - color: #fff; - border: none; - border-radius: 10px; - font-weight: 600; - cursor: pointer; -} .market-card-header { display: flex; align-items: center; diff --git a/OnlyPrompt.Frontend/css/post-detail.css b/OnlyPrompt.Frontend/css/post-detail.css index 8ec8e4c..053cad8 100644 --- a/OnlyPrompt.Frontend/css/post-detail.css +++ b/OnlyPrompt.Frontend/css/post-detail.css @@ -482,14 +482,6 @@ font-size: 0.9rem; color: #94a3b8; } -.tier-badge-paid { - background: #fef3c7; - color: #92400e; - border-radius: 20px; - padding: 4px 14px; - font-size: 0.8rem; - font-weight: 600; -} .tier-badge-tier { background: #f1f5f9; color: #475569; diff --git a/OnlyPrompt.Frontend/css/profile.css b/OnlyPrompt.Frontend/css/profile.css index 87e3bee..1cfbd24 100644 --- a/OnlyPrompt.Frontend/css/profile.css +++ b/OnlyPrompt.Frontend/css/profile.css @@ -59,6 +59,62 @@ display: flex; flex-direction: column; gap: 10px; + min-width: 220px; +} + +#profileActions .login-button { + box-sizing: border-box; + width: 100%; +} + +.profile-tier-list { + background: #ffffff; + border: 1px solid #eef2f7; + border-radius: 16px; + display: grid; + gap: 10px; + margin-top: 6px; + padding: 14px; +} + +.profile-tier-list h3 { + font-size: 0.95rem; + margin: 0 0 4px; +} + +.profile-tier-option { + align-items: center; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + color: #334155; + cursor: pointer; + display: flex; + gap: 12px; + justify-content: space-between; + padding: 10px 12px; + text-align: left; +} + +.profile-tier-option.active { + background: #eef2ff; + border-color: #818cf8; + color: #2563eb; +} + +.profile-tier-option strong, +.profile-tier-option small { + display: block; +} + +.profile-tier-option small { + color: #64748b; + font-size: 0.75rem; + margin-top: 2px; +} + +.profile-tier-option b { + white-space: nowrap; } /* ── Profile tabs ────────────────────────────────────────────────────── */ @@ -162,16 +218,32 @@ } /* Share button: darker background and text */ -.profile-header button:last-child { +#shareProfileButton { background: #cbd5e1 !important; /* darker gray */ color: #1e293b !important; box-shadow: none !important; border: none !important; } +#manageTiersButton { + background: #f3e8ff !important; + color: #7c3aed !important; + box-shadow: none !important; + border: 1px solid #d8b4fe !important; +} + /* Buttons keep rounded corners */ .login-button { + align-items: center; border-radius: 14px !important; + display: flex; + gap: 8px; + justify-content: center; + text-decoration: none; +} + +.login-button i { + font-size: 1rem; } .profile-tab { diff --git a/OnlyPrompt.Frontend/css/sidebar.css b/OnlyPrompt.Frontend/css/sidebar.css index 75bbd9e..2fb5018 100644 --- a/OnlyPrompt.Frontend/css/sidebar.css +++ b/OnlyPrompt.Frontend/css/sidebar.css @@ -172,8 +172,45 @@ } @media (max-width: 700px) { + .sidebar-shell { + height: auto; + padding: 7px 6px; + border-right: none; + border-top: 1px solid #eef2f7; + box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.08); + } + + .sidebar-logo, + .sidebar-bottom { + display: none; + } + + .sidebar ul { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: 1px; + } + + .sidebar li { + margin: 0; + } + + .sidebar li.mobile-hidden { + display: none; + } + .sidebar a, .sidebar-logout { - padding: 10px; + min-height: 46px; + padding: 7px 2px; + } + + .sidebar i { + font-size: 1.05rem; + } + + .sidebar a.active { + border-right: none; + border-top: 3px solid #3b82f6; } } diff --git a/OnlyPrompt.Frontend/css/subscription-tiers.css b/OnlyPrompt.Frontend/css/subscription-tiers.css new file mode 100644 index 0000000..288ff4e --- /dev/null +++ b/OnlyPrompt.Frontend/css/subscription-tiers.css @@ -0,0 +1,271 @@ +/* Subscription tiers page - manage monthly creator access levels */ + +.tiers-main { + flex: 1; + padding: 20px 32px; + width: 100%; + max-width: 1200px; + margin: 0 auto; +} + +.tiers-header { + text-align: center; + margin-bottom: 28px; +} + +.tiers-header h1 { + font-size: 2rem; + font-weight: 800; + margin-bottom: 8px; +} + +.tiers-header p { + color: #64748b; +} + +.tiers-layout { + display: grid; + grid-template-columns: minmax(280px, 380px) 1fr; + gap: 24px; + align-items: start; +} + +.tiers-tabs { + border-bottom: 2px solid #e5e7eb; + display: flex; + gap: 24px; + margin-bottom: 24px; +} + +.tiers-tab { + background: transparent; + border: none; + border-bottom: 3px solid transparent; + color: #64748b; + cursor: pointer; + font: inherit; + font-weight: 800; + padding: 10px 0; +} + +.tiers-tab.active { + border-bottom-color: #3b82f6; + color: #2563eb; +} + +.subscriptions-panel { + display: none; +} + +.tier-panel, +.tier-card { + background: #ffffff; + border: 1px solid #eef2f7; + border-radius: 18px; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06); +} + +.tier-panel { + padding: 24px; +} + +.tier-panel h2, +.tier-list-header h2 { + font-size: 1.25rem; + margin-bottom: 16px; +} + +.tier-form { + display: grid; + gap: 16px; +} + +.tier-form label { + display: grid; + gap: 8px; + color: #334155; + font-weight: 700; +} + +.tier-form input, +.tier-form textarea { + border: 1px solid #dbe2ea; + border-radius: 12px; + font: inherit; + padding: 12px 14px; +} + +.tier-form textarea { + min-height: 88px; + resize: vertical; +} + +.tier-form input:focus, +.tier-form textarea:focus { + border-color: #8b5cf6; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.12); + outline: none; +} + +.tier-form-actions { + display: flex; + gap: 12px; +} + +.tier-primary-btn, +.tier-secondary-btn, +.tier-card button { + border: none; + border-radius: 14px; + cursor: pointer; + font: inherit; + font-weight: 800; + padding: 12px 16px; +} + +.tier-primary-btn { + background: var(--gradient); + color: #ffffff; + flex: 1; +} + +.tier-secondary-btn { + background: #f1f5f9; + color: #334155; +} + +#tier-status { + color: #64748b; + min-height: 20px; +} + +.tier-list-header { + margin-bottom: 16px; +} + +.tier-list-header p { + color: #64748b; +} + +.tiers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; +} + +.subscriptions-grid { + display: grid; + gap: 14px; +} + +.subscription-card { + align-items: center; + background: #ffffff; + border: 1px solid #eef2f7; + border-radius: 18px; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06); + display: flex; + justify-content: space-between; + gap: 16px; + padding: 18px 20px; +} + +.subscription-card h3 { + margin-bottom: 4px; +} + +.subscription-card p { + color: #64748b; +} + +.subscription-price { + color: #3b82f6; + font-weight: 900; + white-space: nowrap; +} + +.tier-card { + padding: 20px; +} + +.tier-card-top { + align-items: start; + display: flex; + justify-content: space-between; + gap: 16px; + margin-bottom: 12px; +} + +.tier-card h3 { + font-size: 1.2rem; + margin-bottom: 4px; +} + +.tier-level { + color: #64748b; + font-size: 0.9rem; + font-weight: 700; +} + +.tier-price { + color: #3b82f6; + font-size: 1.4rem; + font-weight: 900; + white-space: nowrap; +} + +.tier-desc { + color: #475569; + line-height: 1.45; + min-height: 44px; +} + +.tier-card-actions { + display: flex; + gap: 10px; + margin-top: 16px; +} + +.tier-card button { + background: #f1f5f9; + color: #334155; + padding: 9px 12px; +} + +.tier-delete-btn { + color: #dc2626 !important; +} + +.tiers-empty, +.tiers-error { + background: #ffffff; + border-radius: 18px; + color: #64748b; + padding: 32px; + text-align: center; +} + +.tiers-error { + color: #ef4444; +} + +@media (max-width: 900px) { + .tiers-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 700px) { + .tiers-main { + padding: 16px; + } + + .tiers-tabs { + gap: 16px; + } + + .subscription-card { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/OnlyPrompt.Frontend/css/topbar.css b/OnlyPrompt.Frontend/css/topbar.css index 21273e7..8fc808e 100644 --- a/OnlyPrompt.Frontend/css/topbar.css +++ b/OnlyPrompt.Frontend/css/topbar.css @@ -1,128 +1,143 @@ /* Topbar styles for OnlyPrompt - - clean, modern, full-width - - search bar centered (expands on full screen), profile avatar always on the right - - ONLY search bar and avatar have rounded corners + - sticky on all app pages + - search bar fills the available width + - logout, messages and profile avatar stay on the right */ .topbar-shell { - width: 100%; + align-items: center; background: #ffffff; border-bottom: 1px solid #eef2f7; - padding: 16px 32px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 18px; box-sizing: border-box; + display: flex; + gap: 18px; + justify-content: space-between; + padding: 16px 32px; + position: sticky; + top: 0; + width: 100%; + z-index: 90; +} + +.topbar-search { + align-items: center; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 14px; + display: flex; + flex: 1; + gap: 12px; + max-width: none; + padding: 10px 20px; +} + +.topbar-search i { + color: #94a3b8; + font-size: 1.3rem; +} + +.topbar-search input { + background: transparent; + border: none; + color: #334155; + font-size: 0.95rem; + outline: none; + width: 100%; +} + +.topbar-search input::placeholder { + color: #94a3b8; + font-weight: 400; +} + +.topbar-actions { + align-items: center; + display: flex; + gap: 16px; +} + +.topbar-logout-form { + display: flex; + margin: 0; +} + +.topbar-icon-btn { + align-items: center; + background: transparent; + border: none; + border-radius: 50%; + color: #475569; + cursor: pointer; + display: flex; + font-size: 1.4rem; + justify-content: center; + padding: 8px; + transition: background 0.2s, color 0.2s; +} + +.topbar-icon-btn:hover { + background: #f1f5f9; + color: #3b82f6; +} + +.topbar-avatar-btn { + align-items: center; + background: transparent; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + padding: 0; +} + +.topbar-avatar { + border: 1px solid #e2e8f0; + border-radius: 50%; + height: 48px; + object-fit: cover; + width: 48px; +} + +@media (max-width: 768px) { + .topbar-shell { + gap: 12px; + padding: 12px 20px; + } .topbar-search { - flex: 1; /* Takes all available space */ - max-width: none; /* No upper limit, expands freely */ - display: flex; - align-items: center; - gap: 12px; - background: #f8fafc; - border: 1px solid #e2e8f0; - padding: 10px 20px; - border-radius: 14px; /* Rounded like login inputs */ + padding: 8px 16px; } .topbar-search i { - color: #94a3b8; - font-size: 1.3rem; - } - - .topbar-search input { - width: 100%; - border: none; - outline: none; - background: transparent; - font-size: 0.95rem; - color: #334155; - } - - .topbar-search input::placeholder { - color: #94a3b8; - font-weight: 400; - } - - /* Icons and avatar container */ - .topbar-actions { - display: flex; - align-items: center; - gap: 16px; - } - - .topbar-icon-btn { - background: transparent; - border: none; - font-size: 1.4rem; - color: #475569; - cursor: pointer; - padding: 8px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: - background 0.2s, - color 0.2s; - } - - .topbar-icon-btn:hover { - background: #f1f5f9; - color: #3b82f6; - } - - .topbar-avatar-btn { - border: none; - background: transparent; - padding: 0; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; + font-size: 1.1rem; } .topbar-avatar { - width: 48px; - height: 48px; - object-fit: cover; - border: 1px solid #e2e8f0; - border-radius: 50%; /* Avatar round */ + height: 40px; + width: 40px; } - /* Responsive adjustments */ - @media (max-width: 768px) { - .topbar-shell { - padding: 12px 20px; - } - .topbar-search { - padding: 8px 16px; - } - .topbar-search i { - font-size: 1.1rem; - } - .topbar-avatar { - width: 40px; - height: 40px; - } - .topbar-icon-btn { - font-size: 1.2rem; - padding: 6px; - } - .topbar-actions { - gap: 8px; - } + .topbar-icon-btn { + font-size: 1.2rem; + padding: 6px; } - @media (max-width: 480px) { - .topbar-shell { - padding: 10px 16px; - } - .topbar-search { - padding: 6px 12px; - } + .topbar-actions { + gap: 8px; + } +} + +@media (max-width: 480px) { + .topbar-shell { + padding: 10px 12px; + } + + .topbar-search { + padding: 6px 10px; + } + + .topbar-actions { + gap: 4px; } } diff --git a/OnlyPrompt.Frontend/dashboard.html b/OnlyPrompt.Frontend/dashboard.html index 5f770e4..9db5e54 100644 --- a/OnlyPrompt.Frontend/dashboard.html +++ b/OnlyPrompt.Frontend/dashboard.html @@ -148,7 +148,7 @@ ${prompt.exampleImageUrl ? `${prompt.title}` : `${prompt.title}`}

${prompt.title}

${prompt.description || ""}

- ${locked ? `

${prompt.tierName ?? "Paid"} tier required

` : ""} + ${locked ? `

${prompt.tierName ?? "Subscription"} tier required

` : ""} ${renderStars(prompt.averageRating)}
diff --git a/OnlyPrompt.Frontend/marketplace.html b/OnlyPrompt.Frontend/marketplace.html index 6001c08..343c272 100644 --- a/OnlyPrompt.Frontend/marketplace.html +++ b/OnlyPrompt.Frontend/marketplace.html @@ -1,4 +1,4 @@ - + @@ -17,47 +17,6 @@ rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" /> -
@@ -88,8 +47,8 @@ - - + +
@@ -113,90 +72,6 @@
- -
-
- - - -
-

Subscribe to access

-

-

- Choose a payment method to unlock this prompt: -

-
- - - - -
-

- Payments are processed - on-chain. No account needed. -

-
- - -
- -

-

- Send exactly to: -

-
- - -
-

- ⚠️ Only send the exact amount. Payments are non-refundable. -

-
- After sending, click the - button below to confirm. Access will be granted once the transaction - is verified. -
- -
- - -
- -

Payment received!

-

- Your access is being activated. This usually takes 1–2 minutes. -

- -
-
-
- + + + +
+ + +
+
+ +
+
+

Subscription Tiers

+

Create monthly access levels for your paid prompts.

+
+ + + +
+
+

Create Tier

+
+ + + + + + + + +
+ + +
+

+
+
+ +
+
+

Your Tiers

+

Higher levels include access to prompts from lower levels.

+
+
+
Loading tiers...
+
+
+
+ +
+
+

Your Subscriptions

+

Creators you follow or support with a monthly tier.

+
+
+
Loading subscriptions...
+
+
+
+
+
+ + + + diff --git a/OnlyPrompt.Frontend/topbar.html b/OnlyPrompt.Frontend/topbar.html index 7c46160..54a930c 100644 --- a/OnlyPrompt.Frontend/topbar.html +++ b/OnlyPrompt.Frontend/topbar.html @@ -1,7 +1,7 @@ @@ -12,15 +12,17 @@
- - + + -
diff --git a/README.md b/README.md index 5d54099..393113a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ # OnlyPrompt - AI Prompt Marketplace ## Description -OnlyPrompt is a web application where users can create, publish, discover and interact with AI prompts. Users can edit their profiles, follow creators, like and save prompts, write reviews and browse free or paid prompt cards in a marketplace. +OnlyPrompt is a web application where users can create, publish, discover and interact with AI prompts. Users can edit their profiles, follow creators, like and save prompts, write reviews and browse free or subscription-based prompt cards in a marketplace. This project is built with HTML, CSS and JavaScript. ## Special Features - 📝 Create, edit and publish AI prompts -- 🔍 Browse prompts in a marketplace with category, search and price filters +- 🔍 Browse prompts in a marketplace with category, search and tier price filters - 📄 View prompt detail pages with examples, ratings and access states +- 💎 Create creator subscription tiers and assign prompts to tier levels +- 🔐 Subscribe to creator tiers to unlock matching and lower-level prompts - ⭐ Write reviews with star ratings and comments - ❤️ Like and save prompts - 👥 Follow and discover other creators - 👤 Edit user profiles with display name, username, bio and profile picture - 🌐 View own and public creator profiles -- 📱 Responsive layout for desktop and mobile +- 📱 Responsive layout for desktop and mobile, including a bottom icon navigation on smartphones - 🔄 Server communication through a REST API - 💾 Shared data persistence with backend and database @@ -53,12 +55,12 @@ AI tools were used as support during development, mainly for debugging, comparin The project uses authentication with a JWT cookie so that protected pages and API endpoints require a logged-in user. User input is validated on the backend for important operations such as registration, profile updates, prompt creation and reviews. Known limitations: -- Payment and premium access are simulated and are not connected to a real payment provider. +- Subscription access is simulated for the semester project and is not connected to a real payment provider. - User-generated content is displayed in the frontend, so XSS prevention is important. The project avoids intentionally executing user input as code, but further output sanitization would be needed for production. - Authentication is implemented for local project use and would need additional hardening for production. ## Reflection -A main challenge was connecting static frontend pages with dynamic backend data while keeping the application usable and consistent. During development, the project evolved from demo pages into a connected application with real profiles, prompts, reviews, likes, saves and creator interactions. We learned how important clear API structures, consistent data models and regular browser testing are when multiple pages depend on the same shared data. +A main challenge was connecting static frontend pages with dynamic backend data while keeping the application usable and consistent. During development, the project evolved from demo pages into a connected application with real profiles, prompts, reviews, likes, saves, creator interactions and subscription tiers. We learned how important clear API structures, consistent data models, responsive navigation and regular browser testing are when multiple pages depend on the same shared data. ## Group members and their roles | Name | Role |