feat(ci): add blog search/filtering and package privacy
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m21s
Build and Release / Lint (push) Successful in 5m38s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m59s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m19s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m10s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m21s
Build and Release / Lint (push) Successful in 5m38s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 2m59s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m19s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m10s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Adds keyword search and tag filtering to repository blog list with GetRepoTopTags for popular tags display. Implements user-level package privacy setting (KeepPackagesPrivate) to hide packages from profile page. Updates blog UI with search box, tag cloud, and clear filters button. Adds subscription CTA buttons and active subscription indicators.
This commit is contained in:
@@ -114,6 +114,8 @@ type BlogPostSearchOptions struct { //revive:disable-line:exported
|
||||
AuthorID int64
|
||||
Status BlogPostStatus
|
||||
AnyPublicStatus bool // if true, matches Public OR Published
|
||||
Keyword string
|
||||
Tag string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
@@ -141,6 +143,18 @@ func GetBlogPostsByRepoID(ctx context.Context, opts *BlogPostSearchOptions) ([]*
|
||||
sess = sess.And("status = ?", opts.Status)
|
||||
}
|
||||
|
||||
if opts.Keyword != "" {
|
||||
sess = sess.And("(LOWER(title) LIKE ? OR LOWER(subtitle) LIKE ?)",
|
||||
"%"+strings.ToLower(opts.Keyword)+"%",
|
||||
"%"+strings.ToLower(opts.Keyword)+"%")
|
||||
}
|
||||
|
||||
if opts.Tag != "" {
|
||||
// Match tag in comma-separated list
|
||||
sess = sess.And("(tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ?)",
|
||||
opts.Tag+",%", "%,"+opts.Tag+",%", "%,"+opts.Tag, opts.Tag)
|
||||
}
|
||||
|
||||
count, err := sess.Count(new(BlogPost))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -154,6 +168,17 @@ func GetBlogPostsByRepoID(ctx context.Context, opts *BlogPostSearchOptions) ([]*
|
||||
sess = sess.And("status = ?", opts.Status)
|
||||
}
|
||||
|
||||
if opts.Keyword != "" {
|
||||
sess = sess.And("(LOWER(title) LIKE ? OR LOWER(subtitle) LIKE ?)",
|
||||
"%"+strings.ToLower(opts.Keyword)+"%",
|
||||
"%"+strings.ToLower(opts.Keyword)+"%")
|
||||
}
|
||||
|
||||
if opts.Tag != "" {
|
||||
sess = sess.And("(tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ?)",
|
||||
opts.Tag+",%", "%,"+opts.Tag+",%", "%,"+opts.Tag, opts.Tag)
|
||||
}
|
||||
|
||||
pageSize := opts.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
@@ -316,6 +341,49 @@ func GetExploreTopTags(ctx context.Context, actor *user_model.User, limit int) (
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRepoTopTags returns the top N tags for a specific repo's published blog posts.
|
||||
func GetRepoTopTags(ctx context.Context, repoID int64, limit int) ([]*TagCount, error) {
|
||||
type tagRow struct {
|
||||
Tags string `xorm:"tags"`
|
||||
}
|
||||
var rows []tagRow
|
||||
err := db.GetEngine(ctx).Table("blog_post").
|
||||
Cols("tags").
|
||||
Where("repo_id = ? AND status >= ? AND tags != ''", repoID, BlogPostPublic).
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Aggregate tag counts
|
||||
counts := make(map[string]int)
|
||||
for _, r := range rows {
|
||||
for t := range strings.SplitSeq(r.Tags, ",") {
|
||||
t = strings.TrimSpace(t)
|
||||
if t != "" {
|
||||
counts[t]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
result := make([]*TagCount, 0, len(counts))
|
||||
for tag, c := range counts {
|
||||
result = append(result, &TagCount{Tag: tag, Count: c})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Count != result[j].Count {
|
||||
return result[i].Count > result[j].Count
|
||||
}
|
||||
return result[i].Tag < result[j].Tag
|
||||
})
|
||||
|
||||
if limit > 0 && len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CountPublicBlogPosts returns the total number of public/published blog posts across all repos.
|
||||
func CountPublicBlogPosts(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("status >= ?", BlogPostPublic).Count(new(BlogPost))
|
||||
|
||||
@@ -437,6 +437,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(360, "Create wishlist_importance table", v1_26.CreateWishlistImportanceTable),
|
||||
newMigration(361, "Create wishlist_comment table", v1_26.CreateWishlistCommentTable),
|
||||
newMigration(362, "Create wishlist_comment_reaction table", v1_26.CreateWishlistCommentReactionTable),
|
||||
newMigration(363, "Add keep_packages_private to user", v1_26.AddKeepPackagesPrivateToUser),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
13
models/migrations/v1_26/v363.go
Normal file
13
models/migrations/v1_26/v363.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddKeepPackagesPrivateToUser(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
KeepPackagesPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
@@ -153,6 +153,7 @@ type User struct {
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
KeepPackagesPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ShowHeatmapOnProfile bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
|
||||
@@ -636,6 +636,8 @@
|
||||
"user.unfollow": "Unfollow",
|
||||
"user.user_bio": "Biography",
|
||||
"user.disabled_public_activity": "This user has disabled the public visibility of the activity.",
|
||||
"user.packages_private": "Packages are Private",
|
||||
"user.packages_private_desc": "This user has disabled the public visibility of their packages.",
|
||||
"user.email_visibility.limited": "Your email address is visible to all authenticated users",
|
||||
"user.email_visibility.private": "Your email address is only visible to you and administrators",
|
||||
"user.show_on_map": "Show this place on a map",
|
||||
@@ -727,6 +729,8 @@
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.keep_activity_private": "Hide Activity from profile page",
|
||||
"settings.keep_activity_private_popup": "Makes the activity visible only for you and the admins",
|
||||
"settings.keep_packages_private": "Hide Packages from profile page",
|
||||
"settings.keep_packages_private_popup": "Makes the packages visible only for you and the admins",
|
||||
"settings.lookup_avatar_by_mail": "Look Up Avatar by Email Address",
|
||||
"settings.federated_avatar_lookup": "Federated Avatar Lookup",
|
||||
"settings.enable_custom_avatar": "Use Custom Avatar",
|
||||
@@ -2006,6 +2010,9 @@
|
||||
"repo.blog.subscribe": "Subscribe to Blog",
|
||||
"repo.blog.unsubscribe": "Unsubscribe",
|
||||
"repo.blog.subscribed": "Subscribed",
|
||||
"repo.blog.search_placeholder": "Search posts...",
|
||||
"repo.blog.popular_tags": "Popular Tags",
|
||||
"repo.blog.clear_filters": "Clear Filters",
|
||||
"repo.blog.no_posts": "No blog posts yet.",
|
||||
"repo.blog.no_posts_member": "No blog posts yet. Create the first one!",
|
||||
"repo.blog.featured": "Featured",
|
||||
@@ -4288,6 +4295,8 @@
|
||||
"repo.subscribe.description": "This repository requires a subscription to access the source code.",
|
||||
"repo.subscribe.buy": "Subscribe",
|
||||
"repo.subscribe.payment_required": "A subscription is required to view this repository's source code.",
|
||||
"repo.subscribe.button": "Subscribe for Access",
|
||||
"repo.subscribe.active": "You have an active subscription",
|
||||
"repo.cross_promoted": "Also Check Out",
|
||||
"repo.settings.license": "License",
|
||||
"repo.settings.license_type": "License Type",
|
||||
|
||||
@@ -67,11 +67,19 @@ func BlogList(ctx *context.Context) {
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
pageSize := setting.UI.IssuePagingNum
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
tag := ctx.FormTrim("tag")
|
||||
ctx.Data["Tag"] = tag
|
||||
|
||||
opts := &blog_model.BlogPostSearchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: -1, // all statuses for members
|
||||
Keyword: keyword,
|
||||
Tag: tag,
|
||||
}
|
||||
|
||||
// Non-members only see public + published
|
||||
@@ -146,6 +154,34 @@ func BlogList(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["CrossPromotedRepos"] = crossPromoted
|
||||
|
||||
// Load top tags for sidebar
|
||||
topTags, err := blog_model.GetRepoTopTags(ctx, ctx.Repo.Repository.ID, 10)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoTopTags", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["TopTags"] = topTags
|
||||
|
||||
// Paid subscription button (if monetization enabled and repo has subscriptions)
|
||||
if setting.Monetize.Enabled && ctx.Repo.Repository.SubscriptionsEnabled {
|
||||
ctx.Data["ShowSubscribeButton"] = true
|
||||
if ctx.Doer != nil {
|
||||
hasAccess, err := monetize_model.HasActiveSubscription(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
log.Error("HasActiveSubscription: %v", err)
|
||||
} else {
|
||||
ctx.Data["HasActiveSubscription"] = hasAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only show featured post on page 1 with no search/tag filter
|
||||
if keyword != "" || tag != "" {
|
||||
// When filtering, show all posts without featuring
|
||||
ctx.Data["FeaturedPost"] = nil
|
||||
ctx.Data["Posts"] = posts
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
@@ -332,8 +368,8 @@ func BlogNewPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set published timestamp on first publish
|
||||
if post.Status == blog_model.BlogPostPublished {
|
||||
// Set published timestamp when post first becomes visible (public or published)
|
||||
if post.Status >= blog_model.BlogPostPublic {
|
||||
post.PublishedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
@@ -342,7 +378,7 @@ func BlogNewPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger notifications on publish
|
||||
// Trigger notifications on publish (only for BlogPostPublished status)
|
||||
if post.Status == blog_model.BlogPostPublished {
|
||||
go notifyBlogPublished(ctx, post)
|
||||
}
|
||||
@@ -422,8 +458,8 @@ func BlogEditPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set published timestamp on first publish
|
||||
if post.Status == blog_model.BlogPostPublished && post.PublishedUnix == 0 {
|
||||
// Set published timestamp when post first becomes visible (public or published)
|
||||
if post.Status >= blog_model.BlogPostPublic && post.PublishedUnix == 0 {
|
||||
post.PublishedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
git_model "code.gitcaddy.com/server/v3/models/git"
|
||||
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"
|
||||
unit_model "code.gitcaddy.com/server/v3/models/unit"
|
||||
@@ -411,6 +412,25 @@ func prepareHomeSidebarCrossPromotedRepos(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareHomeSidebarSubscription(ctx *context.Context) {
|
||||
// Only show if monetization is enabled and repo has subscriptions enabled
|
||||
if !setting.Monetize.Enabled || !ctx.Repo.Repository.SubscriptionsEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["ShowSubscribeButton"] = true
|
||||
|
||||
// Check if user already has an active subscription
|
||||
if ctx.Doer != nil {
|
||||
hasAccess, err := monetize_model.HasActiveSubscription(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
log.Error("HasActiveSubscription: %v", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["HasActiveSubscription"] = hasAccess
|
||||
}
|
||||
}
|
||||
|
||||
func prepareUpstreamDivergingInfo(ctx *context.Context) {
|
||||
if !ctx.Repo.Repository.IsFork || !ctx.Repo.RefFullName.IsBranch() || ctx.Repo.TreePath != "" {
|
||||
return
|
||||
@@ -699,6 +719,7 @@ func Home(ctx *context.Context) {
|
||||
prepareHomeSidebarCitationFile(entry),
|
||||
prepareHomeSidebarLanguageStats,
|
||||
prepareHomeSidebarCrossPromotedRepos,
|
||||
prepareHomeSidebarSubscription,
|
||||
prepareHomeSidebarLatestRelease,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,17 @@ func ListPackages(ctx *context.Context) {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if packages are private for this user
|
||||
if ctx.ContextUser.KeepPackagesPrivate {
|
||||
isOwnerOrAdmin := ctx.Doer != nil && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
if !isOwnerOrAdmin {
|
||||
ctx.Data["PackagesPrivate"] = true
|
||||
ctx.HTML(200, tplPackagesList)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
query := ctx.FormTrim("q")
|
||||
packageType := ctx.FormTrim("type")
|
||||
|
||||
@@ -103,6 +103,7 @@ func ProfilePost(ctx *context.Context) {
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
||||
KeepPackagesPrivate: optional.Some(form.KeepPackagesPrivate),
|
||||
ShowHeatmapOnProfile: optional.Some(form.ShowHeatmapOnProfile),
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ type UpdateProfileForm struct {
|
||||
Description string `binding:"MaxSize(255)"`
|
||||
Visibility structs.VisibleType
|
||||
KeepActivityPrivate bool
|
||||
KeepPackagesPrivate bool
|
||||
ShowHeatmapOnProfile bool
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ type UpdateOptions struct {
|
||||
IsRestricted optional.Option[bool]
|
||||
Visibility optional.Option[structs.VisibleType]
|
||||
KeepActivityPrivate optional.Option[bool]
|
||||
KeepPackagesPrivate optional.Option[bool]
|
||||
ShowHeatmapOnProfile optional.Option[bool]
|
||||
Language optional.Option[string]
|
||||
Theme optional.Option[string]
|
||||
@@ -165,6 +166,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
|
||||
cols = append(cols, "keep_activity_private")
|
||||
}
|
||||
if opts.KeepPackagesPrivate.Has() {
|
||||
u.KeepPackagesPrivate = opts.KeepPackagesPrivate.Value()
|
||||
|
||||
cols = append(cols, "keep_packages_private")
|
||||
}
|
||||
if opts.ShowHeatmapOnProfile.Has() {
|
||||
u.ShowHeatmapOnProfile = opts.ShowHeatmapOnProfile.Value()
|
||||
|
||||
|
||||
@@ -37,10 +37,15 @@
|
||||
<div class="blog-view-meta">
|
||||
{{if .BlogPost.Author}}
|
||||
<img class="blog-avatar" src="{{.BlogPost.Author.AvatarLink ctx}}" alt="{{.BlogPost.Author.Name}}">
|
||||
{{if not .BlogPost.Author.KeepEmailPrivate}}
|
||||
<a href="mailto:{{.BlogPost.Author.Email}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{/* Only link to author if viewer can access their profile */}}
|
||||
{{if or .BlogPost.Author.Visibility.IsPublic (and .IsSigned .BlogPost.Author.Visibility.IsLimited)}}
|
||||
{{if not .BlogPost.Author.KeepEmailPrivate}}
|
||||
<a href="mailto:{{.BlogPost.Author.Email}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{else}}
|
||||
<a href="{{.BlogPost.Author.HomeLink}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.BlogPost.Author.HomeLink}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
<span class="blog-author-name">{{.BlogPost.Author.DisplayName}}</span>
|
||||
{{end}}
|
||||
<span class="blog-meta-sep">·</span>
|
||||
{{end}}
|
||||
|
||||
@@ -88,19 +88,23 @@
|
||||
</div>
|
||||
</a>
|
||||
<div class="blog-list-item-footer">
|
||||
{{if .Author}}
|
||||
<a href="{{.Author.HomeLink}}" class="blog-list-item-author">
|
||||
<span>{{.Author.DisplayName}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Repo}}
|
||||
<span class="blog-meta-sep">·</span>
|
||||
<a href="{{.Repo.Link}}" class="blog-list-item-repo">
|
||||
<img class="blog-avatar-sm" src="{{if .Repo.RelAvatarLink ctx}}{{.Repo.RelAvatarLink ctx}}{{else if .Repo.Owner}}{{.Repo.Owner.AvatarLink ctx}}{{end}}" alt="">
|
||||
{{if .Repo.DisplayTitle}}{{.Repo.DisplayTitle}}{{else}}{{.Repo.FullName}}{{end}}
|
||||
</a>
|
||||
<span class="blog-meta-sep">·</span>
|
||||
{{end}}
|
||||
{{if .Author}}
|
||||
{{if or .Author.Visibility.IsPublic (and $.IsSigned .Author.Visibility.IsLimited)}}
|
||||
<a href="{{.Author.HomeLink}}" class="blog-list-item-author">
|
||||
<span>{{.Author.DisplayName}}</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="blog-list-item-author">{{.Author.DisplayName}}</span>
|
||||
{{end}}
|
||||
<span class="blog-meta-sep">·</span>
|
||||
{{end}}
|
||||
{{if .PublishedUnix}}
|
||||
<span>{{DateUtils.TimeSince .PublishedUnix}}</span>
|
||||
{{else}}
|
||||
|
||||
@@ -109,6 +109,48 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ShowSubscribeButton}}
|
||||
<div class="tw-mb-4">
|
||||
{{if .HasActiveSubscription}}
|
||||
<div class="ui small success message tw-mb-0">
|
||||
{{svg "octicon-check" 16}} {{ctx.Locale.Tr "repo.subscribe.active"}}
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="{{.RepoLink}}/subscribe" class="ui primary fluid button">
|
||||
{{svg "octicon-unlock" 16}} {{ctx.Locale.Tr "repo.subscribe.button"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Search box -->
|
||||
<form method="get" action="" class="blog-sidebar-search tw-mb-4">
|
||||
<div class="ui small fluid action input">
|
||||
<input type="text" name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.blog.search_placeholder"}}">
|
||||
{{if .Tag}}<input type="hidden" name="tag" value="{{.Tag}}">{{end}}
|
||||
<button class="ui small icon button" type="submit">{{svg "octicon-search" 16}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Top tags -->
|
||||
{{if .TopTags}}
|
||||
<div class="blog-sidebar-section">
|
||||
<h4 class="blog-sidebar-title">{{ctx.Locale.Tr "repo.blog.popular_tags"}}</h4>
|
||||
<div class="blog-tag-list">
|
||||
{{range .TopTags}}
|
||||
<a href="?tag={{.Tag}}{{if $.Keyword}}&q={{$.Keyword}}{{end}}" class="blog-tag-tile{{if eq .Tag $.Tag}} active{{end}}">
|
||||
<span class="blog-tag-name">{{.Tag}}</span>
|
||||
<span class="blog-tag-count">{{.Count}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Keyword .Tag}}
|
||||
<a href="{{.RepoLink}}/blog" class="ui small basic fluid button tw-mb-4">{{ctx.Locale.Tr "repo.blog.clear_filters"}}</a>
|
||||
{{end}}
|
||||
|
||||
{{if .CrossPromotedRepos}}
|
||||
<div class="blog-sidebar-section">
|
||||
<h4 class="blog-sidebar-title">{{ctx.Locale.Tr "repo.cross_promoted"}}</h4>
|
||||
@@ -387,6 +429,51 @@
|
||||
.cross-promote-tile:hover .cross-promote-tile-arrow {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.blog-sidebar-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.blog-tag-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.blog-tag-tile {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-secondary-alpha-40);
|
||||
background: var(--color-box-body);
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.blog-tag-tile:hover {
|
||||
border-color: var(--color-primary-alpha-60);
|
||||
background: var(--color-primary-alpha-10);
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.blog-tag-tile.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-alpha-10);
|
||||
font-weight: 600;
|
||||
}
|
||||
.blog-tag-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.blog-tag-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-light);
|
||||
background: var(--color-secondary-alpha-20);
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.blog-split-pane {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -28,10 +28,15 @@
|
||||
<div class="blog-view-meta">
|
||||
{{if .BlogPost.Author}}
|
||||
<img class="blog-avatar" src="{{.BlogPost.Author.AvatarLink ctx}}" alt="{{.BlogPost.Author.Name}}">
|
||||
{{if not .BlogPost.Author.KeepEmailPrivate}}
|
||||
<a href="mailto:{{.BlogPost.Author.Email}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{/* Only link to author if viewer can access their profile */}}
|
||||
{{if or .BlogPost.Author.Visibility.IsPublic (and .IsSigned .BlogPost.Author.Visibility.IsLimited)}}
|
||||
{{if not .BlogPost.Author.KeepEmailPrivate}}
|
||||
<a href="mailto:{{.BlogPost.Author.Email}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{else}}
|
||||
<a href="{{.BlogPost.Author.HomeLink}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{.BlogPost.Author.HomeLink}}" class="blog-author-link">{{.BlogPost.Author.DisplayName}}</a>
|
||||
<span class="blog-author-name">{{.BlogPost.Author.DisplayName}}</span>
|
||||
{{end}}
|
||||
<span class="blog-meta-sep">·</span>
|
||||
{{end}}
|
||||
|
||||
@@ -86,6 +86,22 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ShowSubscribeButton}}
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
{{if .HasActiveSubscription}}
|
||||
<div class="ui small success message tw-mb-0">
|
||||
{{svg "octicon-check" 16}} {{ctx.Locale.Tr "repo.subscribe.active"}}
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="{{.RepoLink}}/subscribe" class="ui primary fluid button">
|
||||
{{svg "octicon-unlock" 16}} {{ctx.Locale.Tr "repo.subscribe.button"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
</div>
|
||||
<div class="ui twelve wide column tw-mb-4">
|
||||
{{template "user/overview/header" .}}
|
||||
{{template "package/shared/list" .}}
|
||||
{{if .PackagesPrivate}}
|
||||
<div class="ui segment center aligned">
|
||||
{{svg "octicon-lock" 48}}
|
||||
<h4>{{ctx.Locale.Tr "user.packages_private"}}</h4>
|
||||
<p class="text grey">{{ctx.Locale.Tr "user.packages_private_desc"}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{template "package/shared/list" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="ui checkbox" id="keep-packages-private">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "settings.keep_packages_private_popup"}}"><strong>{{ctx.Locale.Tr "settings.keep_packages_private"}}</strong></label>
|
||||
<input name="keep_packages_private" type="checkbox" {{if .SignedUser.KeepPackagesPrivate}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="ui checkbox" id="show-heatmap-on-profile">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "settings.show_heatmap_on_profile_popup"}}"><strong>{{ctx.Locale.Tr "settings.show_heatmap_on_profile"}}</strong></label>
|
||||
|
||||
Reference in New Issue
Block a user