feat(blog): add subscription-only content gating
Adds SubscriptionOnly flag to blog posts to restrict full content access to active subscribers. Shows teaser/preview for non-subscribers with subscribe CTA. Integrates with repository subscription system when monetization is enabled. Updates v2 API structs and editor UI with subscription toggle. Admins and repo writers bypass the gate.
This commit is contained in:
@@ -43,21 +43,22 @@ func (s BlogPostStatus) String() string {
|
||||
|
||||
// BlogPost represents a blog article belonging to a repository.
|
||||
type BlogPost struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
AuthorID int64 `xorm:"INDEX NOT NULL"`
|
||||
Title string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Subtitle string `xorm:"VARCHAR(500)"`
|
||||
Content string `xorm:"LONGTEXT NOT NULL"`
|
||||
RenderedContent string `xorm:"-"`
|
||||
Tags string `xorm:"TEXT"`
|
||||
Series string `xorm:"VARCHAR(255)"`
|
||||
FeaturedImageID int64 `xorm:"DEFAULT 0"`
|
||||
Status BlogPostStatus `xorm:"SMALLINT NOT NULL DEFAULT 0"`
|
||||
AllowComments bool `xorm:"NOT NULL DEFAULT true"`
|
||||
PublishedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
AuthorID int64 `xorm:"INDEX NOT NULL"`
|
||||
Title string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Subtitle string `xorm:"VARCHAR(500)"`
|
||||
Content string `xorm:"LONGTEXT NOT NULL"`
|
||||
RenderedContent string `xorm:"-"`
|
||||
Tags string `xorm:"TEXT"`
|
||||
Series string `xorm:"VARCHAR(255)"`
|
||||
FeaturedImageID int64 `xorm:"DEFAULT 0"`
|
||||
Status BlogPostStatus `xorm:"SMALLINT NOT NULL DEFAULT 0"`
|
||||
AllowComments bool `xorm:"NOT NULL DEFAULT true"`
|
||||
SubscriptionOnly bool `xorm:"NOT NULL DEFAULT false"`
|
||||
PublishedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
|
||||
Author *user_model.User `xorm:"-"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
|
||||
@@ -16,6 +16,7 @@ type BlogPostV2 struct {
|
||||
Tags []string `json:"tags"`
|
||||
Status string `json:"status"`
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
SubscriptionOnly bool `json:"subscription_only"`
|
||||
FeaturedImageURL string `json:"featured_image_url,omitempty"`
|
||||
Author *BlogAuthorV2 `json:"author,omitempty"`
|
||||
Repo *BlogRepoRefV2 `json:"repo,omitempty"`
|
||||
@@ -56,6 +57,7 @@ type CreateBlogPostV2Option struct {
|
||||
Tags []string `json:"tags"`
|
||||
Status string `json:"status"` // draft, public, published
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
SubscriptionOnly bool `json:"subscription_only"`
|
||||
FeaturedImageUUID string `json:"featured_image_uuid"`
|
||||
}
|
||||
|
||||
@@ -68,5 +70,6 @@ type UpdateBlogPostV2Option struct {
|
||||
Tags []string `json:"tags"`
|
||||
Status *string `json:"status"` // draft, public, published
|
||||
AllowComments *bool `json:"allow_comments"`
|
||||
SubscriptionOnly *bool `json:"subscription_only"`
|
||||
FeaturedImageUUID *string `json:"featured_image_uuid"`
|
||||
}
|
||||
|
||||
@@ -2027,6 +2027,11 @@
|
||||
"repo.blog.link_copied": "Link Copied!",
|
||||
"repo.blog.allow_comments": "Allow Comments",
|
||||
"repo.blog.allow_comments_help": "When enabled, visitors can leave comments on this post.",
|
||||
"repo.blog.subscription_only": "Subscription Only",
|
||||
"repo.blog.subscription_only_help": "Only subscribers with an active subscription can read the full content of this post.",
|
||||
"repo.blog.subscription_required": "Subscription Required",
|
||||
"repo.blog.subscription_required_desc": "This post is available exclusively to subscribers. Subscribe to unlock the full content.",
|
||||
"repo.blog.subscribe_to_read": "Subscribe to Read",
|
||||
"repo.blog.reactions.admin_hint": "Thumbs down counts are only visible to repo admins.",
|
||||
"repo.blog.comments": "Comments",
|
||||
"repo.blog.comments.empty": "No comments yet. Be the first to share your thoughts!",
|
||||
|
||||
@@ -34,16 +34,17 @@ func blogPostStatusFromString(s string) (blog_model.BlogPostStatus, bool) {
|
||||
|
||||
func toBlogPostV2(ctx *context.APIContext, post *blog_model.BlogPost) *api.BlogPostV2 {
|
||||
result := &api.BlogPostV2{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Subtitle: post.Subtitle,
|
||||
Series: post.Series,
|
||||
Content: post.Content,
|
||||
Tags: []string{},
|
||||
Status: post.Status.String(),
|
||||
AllowComments: post.AllowComments,
|
||||
CreatedAt: post.CreatedUnix.AsTime(),
|
||||
UpdatedAt: post.UpdatedUnix.AsTime(),
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Subtitle: post.Subtitle,
|
||||
Series: post.Series,
|
||||
Content: post.Content,
|
||||
Tags: []string{},
|
||||
Status: post.Status.String(),
|
||||
AllowComments: post.AllowComments,
|
||||
SubscriptionOnly: post.SubscriptionOnly,
|
||||
CreatedAt: post.CreatedUnix.AsTime(),
|
||||
UpdatedAt: post.UpdatedUnix.AsTime(),
|
||||
}
|
||||
|
||||
if post.Tags != "" {
|
||||
@@ -195,15 +196,16 @@ func CreateBlogPostV2(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
post := &blog_model.BlogPost{
|
||||
RepoID: repo.ID,
|
||||
AuthorID: ctx.Doer.ID,
|
||||
Title: form.Title,
|
||||
Subtitle: form.Subtitle,
|
||||
Series: strings.TrimSpace(form.Series),
|
||||
Content: form.Content,
|
||||
Tags: strings.Join(form.Tags, ","),
|
||||
Status: status,
|
||||
AllowComments: form.AllowComments,
|
||||
RepoID: repo.ID,
|
||||
AuthorID: ctx.Doer.ID,
|
||||
Title: form.Title,
|
||||
Subtitle: form.Subtitle,
|
||||
Series: strings.TrimSpace(form.Series),
|
||||
Content: form.Content,
|
||||
Tags: strings.Join(form.Tags, ","),
|
||||
Status: status,
|
||||
AllowComments: form.AllowComments,
|
||||
SubscriptionOnly: form.SubscriptionOnly,
|
||||
}
|
||||
|
||||
if status == blog_model.BlogPostPublished {
|
||||
@@ -276,6 +278,9 @@ func UpdateBlogPostV2(ctx *context.APIContext) {
|
||||
if form.AllowComments != nil {
|
||||
post.AllowComments = *form.AllowComments
|
||||
}
|
||||
if form.SubscriptionOnly != nil {
|
||||
post.SubscriptionOnly = *form.SubscriptionOnly
|
||||
}
|
||||
if form.Status != nil {
|
||||
status, ok := blogPostStatusFromString(*form.Status)
|
||||
if !ok {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
blog_model "code.gitcaddy.com/server/v3/models/blog"
|
||||
monetize_model "code.gitcaddy.com/server/v3/models/monetize"
|
||||
access_model "code.gitcaddy.com/server/v3/models/perm/access"
|
||||
"code.gitcaddy.com/server/v3/models/renderhelper"
|
||||
"code.gitcaddy.com/server/v3/models/unit"
|
||||
@@ -254,6 +255,20 @@ func StandaloneBlogView(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription-only gate
|
||||
subscriptionGated := false
|
||||
if post.SubscriptionOnly && !isWriter && setting.Monetize.Enabled && post.Repo.SubscriptionsEnabled {
|
||||
if ctx.Doer == nil || !ctx.Doer.IsAdmin {
|
||||
hasAccess := false
|
||||
if ctx.Doer != nil {
|
||||
hasAccess, _ = monetize_model.HasActiveSubscription(ctx, ctx.Doer.ID, post.Repo.ID)
|
||||
}
|
||||
if !hasAccess {
|
||||
subscriptionGated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RepoLink is needed for form actions (reactions, comments)
|
||||
repoLink := post.Repo.Link()
|
||||
|
||||
@@ -261,6 +276,7 @@ func StandaloneBlogView(ctx *context.Context) {
|
||||
ctx.Data["BlogPost"] = post
|
||||
ctx.Data["IsWriter"] = isWriter
|
||||
ctx.Data["IsSigned"] = ctx.Doer != nil
|
||||
ctx.Data["SubscriptionGated"] = subscriptionGated
|
||||
ctx.Data["RepoLink"] = repoLink
|
||||
if ctx.Doer != nil {
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
blog_model "code.gitcaddy.com/server/v3/models/blog"
|
||||
monetize_model "code.gitcaddy.com/server/v3/models/monetize"
|
||||
"code.gitcaddy.com/server/v3/models/renderhelper"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
"code.gitcaddy.com/server/v3/models/unit"
|
||||
@@ -178,6 +179,20 @@ func BlogView(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Subscription-only gate
|
||||
subscriptionGated := false
|
||||
if post.SubscriptionOnly && !isWriter && setting.Monetize.Enabled && ctx.Repo.Repository.SubscriptionsEnabled {
|
||||
if ctx.Doer == nil || !ctx.Doer.IsAdmin {
|
||||
hasAccess := false
|
||||
if ctx.Doer != nil {
|
||||
hasAccess, _ = monetize_model.HasActiveSubscription(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
}
|
||||
if !hasAccess {
|
||||
subscriptionGated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := post.LoadAuthor(ctx); err != nil {
|
||||
ctx.ServerError("LoadAuthor", err)
|
||||
return
|
||||
@@ -258,6 +273,7 @@ func BlogView(ctx *context.Context) {
|
||||
ctx.Data["BlogPost"] = post
|
||||
ctx.Data["IsWriter"] = isWriter
|
||||
ctx.Data["IsSigned"] = ctx.Doer != nil
|
||||
ctx.Data["SubscriptionGated"] = subscriptionGated
|
||||
if ctx.Doer != nil {
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
}
|
||||
@@ -281,6 +297,7 @@ func BlogNew(ctx *context.Context) {
|
||||
ctx.Data["PageIsRepoBlog"] = true
|
||||
ctx.Data["IsNewPost"] = true
|
||||
ctx.Data["UnsplashEnabled"] = setting.Unsplash.Enabled
|
||||
ctx.Data["SubscriptionsEnabled"] = setting.Monetize.Enabled && ctx.Repo.Repository.SubscriptionsEnabled
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBlogEditor)
|
||||
}
|
||||
@@ -295,15 +312,16 @@ func BlogNewPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.BlogPostForm)
|
||||
|
||||
post := &blog_model.BlogPost{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
AuthorID: ctx.Doer.ID,
|
||||
Title: form.Title,
|
||||
Subtitle: form.Subtitle,
|
||||
Content: form.Content,
|
||||
Tags: strings.TrimSpace(form.Tags),
|
||||
Series: strings.TrimSpace(form.Series),
|
||||
Status: blog_model.BlogPostStatus(form.Status),
|
||||
AllowComments: form.AllowComments,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
AuthorID: ctx.Doer.ID,
|
||||
Title: form.Title,
|
||||
Subtitle: form.Subtitle,
|
||||
Content: form.Content,
|
||||
Tags: strings.TrimSpace(form.Tags),
|
||||
Series: strings.TrimSpace(form.Series),
|
||||
Status: blog_model.BlogPostStatus(form.Status),
|
||||
AllowComments: form.AllowComments,
|
||||
SubscriptionOnly: form.SubscriptionOnly,
|
||||
}
|
||||
|
||||
// Link featured image if provided
|
||||
@@ -361,6 +379,7 @@ func BlogEdit(ctx *context.Context) {
|
||||
ctx.Data["BlogPost"] = post
|
||||
ctx.Data["IsNewPost"] = false
|
||||
ctx.Data["UnsplashEnabled"] = setting.Unsplash.Enabled
|
||||
ctx.Data["SubscriptionsEnabled"] = setting.Monetize.Enabled && ctx.Repo.Repository.SubscriptionsEnabled
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBlogEditor)
|
||||
}
|
||||
@@ -393,6 +412,7 @@ func BlogEditPost(ctx *context.Context) {
|
||||
post.Series = strings.TrimSpace(form.Series)
|
||||
post.Status = blog_model.BlogPostStatus(form.Status)
|
||||
post.AllowComments = form.AllowComments
|
||||
post.SubscriptionOnly = form.SubscriptionOnly
|
||||
|
||||
// Link featured image if provided
|
||||
if form.FeaturedImage != "" {
|
||||
|
||||
@@ -14,14 +14,15 @@ import (
|
||||
|
||||
// BlogPostForm is the form for creating/editing blog posts.
|
||||
type BlogPostForm struct {
|
||||
Title string `binding:"Required;MaxSize(255)"`
|
||||
Subtitle string `binding:"MaxSize(500)"`
|
||||
Content string `binding:"Required"`
|
||||
Tags string `binding:"MaxSize(1000)"`
|
||||
Series string `binding:"MaxSize(255)"`
|
||||
FeaturedImage string // attachment UUID
|
||||
Status int `binding:"Range(0,2)"` // 0=draft, 1=public, 2=published
|
||||
AllowComments bool
|
||||
Title string `binding:"Required;MaxSize(255)"`
|
||||
Subtitle string `binding:"MaxSize(500)"`
|
||||
Content string `binding:"Required"`
|
||||
Tags string `binding:"MaxSize(1000)"`
|
||||
Series string `binding:"MaxSize(255)"`
|
||||
FeaturedImage string // attachment UUID
|
||||
Status int `binding:"Range(0,2)"` // 0=draft, 1=public, 2=published
|
||||
AllowComments bool
|
||||
SubscriptionOnly bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
{{if .BlogPost.Series}}
|
||||
<a class="blog-view-series" href="{{AppSubUrl}}/blogs?series={{.BlogPost.Series}}">{{.BlogPost.Series}}</a>
|
||||
{{end}}
|
||||
<h1 class="blog-view-title">{{.BlogPost.Title}}</h1>
|
||||
<h1 class="blog-view-title">
|
||||
{{.BlogPost.Title}}
|
||||
{{if .BlogPost.SubscriptionOnly}}<span class="blog-sub-badge" data-tooltip-content="{{ctx.Locale.Tr "repo.blog.subscription_only"}}">{{svg "octicon-lock" 16}}</span>{{end}}
|
||||
</h1>
|
||||
{{if .BlogPost.Subtitle}}
|
||||
<p class="blog-view-subtitle">{{.BlogPost.Subtitle}}</p>
|
||||
{{end}}
|
||||
@@ -62,9 +65,20 @@
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
{{if .SubscriptionGated}}
|
||||
<div class="blog-subscription-gate">
|
||||
<div class="blog-subscription-gate-icon">{{svg "octicon-lock" 32}}</div>
|
||||
<h3>{{ctx.Locale.Tr "repo.blog.subscription_required"}}</h3>
|
||||
<p>{{ctx.Locale.Tr "repo.blog.subscription_required_desc"}}</p>
|
||||
<a href="{{.RepoLink}}/subscribe" class="ui primary button">
|
||||
{{svg "octicon-unlock" 16}} {{ctx.Locale.Tr "repo.blog.subscribe_to_read"}}
|
||||
</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="blog-view-body markup markdown">
|
||||
{{.BlogPost.RenderedContent | SafeHTML}}
|
||||
</div>
|
||||
{{end}}
|
||||
</article>
|
||||
|
||||
{{if .IsWriter}}
|
||||
@@ -81,6 +95,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .SubscriptionGated}}
|
||||
<!-- Reaction Bar -->
|
||||
<div class="blog-reactions" id="blog-reactions">
|
||||
<button class="blog-reaction-btn{{if and .UserReaction .UserReaction.IsLike}} active{{end}}" id="btn-like"
|
||||
@@ -280,6 +295,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}{{/* end if not SubscriptionGated */}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -643,6 +659,35 @@
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--color-secondary-alpha-40);
|
||||
}
|
||||
/* Subscription gate */
|
||||
.blog-subscription-gate {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
border: 2px dashed var(--color-secondary-alpha-40);
|
||||
border-radius: 12px;
|
||||
background: var(--color-secondary-alpha-10);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.blog-subscription-gate-icon {
|
||||
color: var(--color-text-light-3);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.blog-subscription-gate h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.blog-subscription-gate p {
|
||||
color: var(--color-text-light);
|
||||
margin: 0 0 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.blog-sub-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-light-3);
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="blog-featured-content">
|
||||
<h2 class="blog-featured-title">{{.FeaturedPost.Title}}</h2>
|
||||
<h2 class="blog-featured-title">{{.FeaturedPost.Title}}{{if .FeaturedPost.SubscriptionOnly}} {{svg "octicon-lock" 16}}{{end}}</h2>
|
||||
{{if .FeaturedPost.Subtitle}}
|
||||
<p class="blog-featured-subtitle">{{.FeaturedPost.Subtitle}}</p>
|
||||
{{end}}
|
||||
@@ -81,7 +81,7 @@
|
||||
{{if .Series}}
|
||||
<span class="blog-list-item-series">{{.Series}}</span>
|
||||
{{end}}
|
||||
<h3 class="blog-list-item-title">{{.Title}}</h3>
|
||||
<h3 class="blog-list-item-title">{{.Title}}{{if .SubscriptionOnly}} {{svg "octicon-lock" 14}}{{end}}</h3>
|
||||
{{if .Subtitle}}
|
||||
<p class="blog-list-item-subtitle">{{.Subtitle}}</p>
|
||||
{{end}}
|
||||
|
||||
@@ -140,6 +140,18 @@
|
||||
<div class="help">{{ctx.Locale.Tr "repo.blog.allow_comments_help"}}</div>
|
||||
</div>
|
||||
|
||||
{{if .SubscriptionsEnabled}}
|
||||
<!-- Subscription Only -->
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="subscription_only"
|
||||
{{if and .BlogPost .BlogPost.SubscriptionOnly}}checked{{end}}>
|
||||
<label><b>{{svg "octicon-lock" 14}} {{ctx.Locale.Tr "repo.blog.subscription_only"}}</b></label>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "repo.blog.subscription_only_help"}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Status indicator (edit only) -->
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{{if and (eq .FeaturedPost.Status 0) $.IsWriter}}
|
||||
<span class="ui mini label yellow">{{ctx.Locale.Tr "repo.blog.draft"}}</span>
|
||||
{{end}}
|
||||
<h2 class="blog-featured-title">{{.FeaturedPost.Title}}</h2>
|
||||
<h2 class="blog-featured-title">{{.FeaturedPost.Title}}{{if .FeaturedPost.SubscriptionOnly}} {{svg "octicon-lock" 16}}{{end}}</h2>
|
||||
{{if .FeaturedPost.Subtitle}}
|
||||
<p class="blog-featured-subtitle">{{.FeaturedPost.Subtitle}}</p>
|
||||
{{end}}
|
||||
@@ -53,7 +53,7 @@
|
||||
{{if and (eq .Status 0) $.IsWriter}}
|
||||
<span class="ui mini label yellow">{{ctx.Locale.Tr "repo.blog.draft"}}</span>
|
||||
{{end}}
|
||||
<h3 class="blog-post-tile-title">{{.Title}}</h3>
|
||||
<h3 class="blog-post-tile-title">{{.Title}}{{if .SubscriptionOnly}} {{svg "octicon-lock" 14}}{{end}}</h3>
|
||||
</div>
|
||||
{{if .Subtitle}}
|
||||
<p class="blog-post-tile-subtitle">{{.Subtitle}}</p>
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
{{if .BlogPost.Series}}
|
||||
<a class="blog-view-series" href="{{AppSubUrl}}/blogs?series={{.BlogPost.Series}}">{{.BlogPost.Series}}</a>
|
||||
{{end}}
|
||||
<h1 class="blog-view-title">{{.BlogPost.Title}}</h1>
|
||||
<h1 class="blog-view-title">
|
||||
{{.BlogPost.Title}}
|
||||
{{if .BlogPost.SubscriptionOnly}}<span class="blog-sub-badge" data-tooltip-content="{{ctx.Locale.Tr "repo.blog.subscription_only"}}">{{svg "octicon-lock" 16}}</span>{{end}}
|
||||
</h1>
|
||||
{{if .BlogPost.Subtitle}}
|
||||
<p class="blog-view-subtitle">{{.BlogPost.Subtitle}}</p>
|
||||
{{end}}
|
||||
@@ -53,9 +56,20 @@
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
{{if .SubscriptionGated}}
|
||||
<div class="blog-subscription-gate">
|
||||
<div class="blog-subscription-gate-icon">{{svg "octicon-lock" 32}}</div>
|
||||
<h3>{{ctx.Locale.Tr "repo.blog.subscription_required"}}</h3>
|
||||
<p>{{ctx.Locale.Tr "repo.blog.subscription_required_desc"}}</p>
|
||||
<a href="{{.RepoLink}}/subscribe" class="ui primary button">
|
||||
{{svg "octicon-unlock" 16}} {{ctx.Locale.Tr "repo.blog.subscribe_to_read"}}
|
||||
</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="blog-view-body markup markdown">
|
||||
{{.BlogPost.RenderedContent | SafeHTML}}
|
||||
</div>
|
||||
{{end}}
|
||||
</article>
|
||||
|
||||
{{if .IsWriter}}
|
||||
@@ -72,6 +86,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .SubscriptionGated}}
|
||||
<!-- Reaction Bar -->
|
||||
<div class="blog-reactions" id="blog-reactions">
|
||||
<button class="blog-reaction-btn{{if and .UserReaction .UserReaction.IsLike}} active{{end}}" id="btn-like"
|
||||
@@ -266,6 +281,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}{{/* end if not SubscriptionGated */}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -608,6 +624,35 @@
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--color-secondary-alpha-40);
|
||||
}
|
||||
/* Subscription gate */
|
||||
.blog-subscription-gate {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
border: 2px dashed var(--color-secondary-alpha-40);
|
||||
border-radius: 12px;
|
||||
background: var(--color-secondary-alpha-10);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.blog-subscription-gate-icon {
|
||||
color: var(--color-text-light-3);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.blog-subscription-gate h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.blog-subscription-gate p {
|
||||
color: var(--color-text-light);
|
||||
margin: 0 0 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.blog-sub-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-light-3);
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
Reference in New Issue
Block a user