2
0
Files
gitcaddy-server/templates/explore/blogs.tmpl
logikonline 0f9728006a
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
feat(ci): add blog search/filtering and package privacy
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.
2026-02-03 09:47:08 -05:00

442 lines
12 KiB
Handlebars

{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content explore blogs">
{{if not .BlogsInTopNav}}{{template "explore/navbar" .}}{{end}}
<div class="ui container">
{{if .FeaturedPost}}
<div class="blog-featured">
<a href="{{AppSubUrl}}/blog/{{.FeaturedPost.ID}}" class="blog-featured-link">
{{if .FeaturedPost.FeaturedImage}}
<div class="blog-featured-image">
<img src="{{.FeaturedPost.FeaturedImage.DownloadURL}}" alt="{{.FeaturedPost.Title}}">
</div>
{{end}}
<div class="blog-featured-content">
<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}}
<div class="blog-featured-meta">
{{if .FeaturedPost.Repo}}
<img class="blog-avatar-sm" src="{{if .FeaturedPost.Repo.RelAvatarLink ctx}}{{.FeaturedPost.Repo.RelAvatarLink ctx}}{{else if .FeaturedPost.Repo.Owner}}{{.FeaturedPost.Repo.Owner.AvatarLink ctx}}{{end}}" alt="">
<span class="blog-featured-repo">{{if .FeaturedPost.Repo.DisplayTitle}}{{.FeaturedPost.Repo.DisplayTitle}}{{else}}{{.FeaturedPost.Repo.Name}}{{end}}</span>
<span class="blog-meta-sep">&middot;</span>
{{end}}
{{if .FeaturedPost.Author}}
<span>{{.FeaturedPost.Author.DisplayName}}</span>
<span class="blog-meta-sep">&middot;</span>
{{end}}
{{if .FeaturedPost.PublishedUnix}}
<span class="blog-date">{{DateUtils.TimeSince .FeaturedPost.PublishedUnix}}</span>
{{end}}
{{if .FeaturedLikes}}
<span class="blog-meta-sep">&middot;</span>
<span>{{svg "octicon-thumbsup" 14}} {{.FeaturedLikes}}</span>
{{end}}
</div>
</div>
</a>
</div>
{{end}}
<!-- Split pane: articles left, sidebar right -->
<div class="blog-explore-split">
<!-- Main content pane -->
<div class="blog-explore-main">
<!-- Sort controls -->
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
<h3 class="tw-m-0">
{{if .Series}}
{{ctx.Locale.Tr "explore.blogs.filtered_by_series" .Series}}
<a href="?sort={{.SortType}}{{if .Keyword}}&q={{.Keyword}}{{end}}" class="ui small basic button tw-ml-2">{{ctx.Locale.Tr "explore.blogs.clear_filter"}}</a>
{{else if .Tag}}
{{ctx.Locale.Tr "explore.blogs.filtered_by_tag" .Tag}}
<a href="?sort={{.SortType}}{{if .Keyword}}&q={{.Keyword}}{{end}}" class="ui small basic button tw-ml-2">{{ctx.Locale.Tr "explore.blogs.clear_filter"}}</a>
{{else if .Keyword}}
{{ctx.Locale.Tr "explore.blogs.search_results" .Keyword}}
{{else}}
{{ctx.Locale.Tr "explore.blogs.all_posts"}}
{{end}}
</h3>
<div class="ui small compact menu">
<a class="item{{if eq .SortType "newest"}} active{{end}}" href="?sort=newest{{if .Keyword}}&q={{.Keyword}}{{end}}{{if .Tag}}&tag={{.Tag}}{{end}}{{if .Series}}&series={{.Series}}{{end}}">
{{svg "octicon-clock" 14}} {{ctx.Locale.Tr "explore.blogs.sort_newest"}}
</a>
<a class="item{{if eq .SortType "popular"}} active{{end}}" href="?sort=popular{{if .Keyword}}&q={{.Keyword}}{{end}}{{if .Tag}}&tag={{.Tag}}{{end}}{{if .Series}}&series={{.Series}}{{end}}">
{{svg "octicon-thumbsup" 14}} {{ctx.Locale.Tr "explore.blogs.sort_popular"}}
</a>
</div>
</div>
{{if .Posts}}
<div class="blog-post-list">
{{range .Posts}}
<div class="blog-list-item">
<a href="{{AppSubUrl}}/blog/{{.ID}}" class="blog-list-item-link">
{{if .FeaturedImage}}
<div class="blog-list-item-image">
<img src="{{.FeaturedImage.DownloadURL}}" alt="{{.Title}}" loading="lazy">
</div>
{{end}}
<div class="blog-list-item-content">
{{if .Series}}
<span class="blog-list-item-series">{{.Series}}</span>
{{end}}
<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}}
</div>
</a>
<div class="blog-list-item-footer">
{{if .Repo}}
<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">&middot;</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">&middot;</span>
{{end}}
{{if .PublishedUnix}}
<span>{{DateUtils.TimeSince .PublishedUnix}}</span>
{{else}}
<span>{{DateUtils.TimeSince .CreatedUnix}}</span>
{{end}}
</div>
</div>
{{end}}
</div>
{{else}}
<div class="empty-placeholder">
{{svg "octicon-note" 48}}
<h2>{{ctx.Locale.Tr "repo.blog.no_posts"}}</h2>
</div>
{{end}}
{{template "base/paginate" .}}
</div>
<!-- Right sidebar -->
<div class="blog-explore-sidebar">
<!-- Search box -->
<form method="get" action="" class="blog-sidebar-search">
<div class="ui small fluid action input">
<input type="text" name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "explore.blogs.search_placeholder"}}">
<input type="hidden" name="sort" value="{{.SortType}}">
{{if .Tag}}<input type="hidden" name="tag" value="{{.Tag}}">{{end}}
{{if .Series}}<input type="hidden" name="series" value="{{.Series}}">{{end}}
<button class="ui small icon button" type="submit">{{svg "octicon-search" 16}}</button>
</div>
</form>
<!-- Top tags -->
{{if .TopTags}}
<h4 class="blog-sidebar-heading">{{ctx.Locale.Tr "explore.blogs.top_tags"}}</h4>
<div class="blog-tag-list">
{{range .TopTags}}
<a href="?tag={{.Tag}}&sort={{$.SortType}}{{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>
{{end}}
</div>
</div>
</div>
</div>
<style>
.blog-featured {
margin-bottom: 24px;
}
.blog-featured-link {
display: flex;
gap: 24px;
padding: 24px;
border-radius: 12px;
border: 1px solid var(--color-secondary-alpha-40);
background: var(--color-box-body);
color: var(--color-text);
text-decoration: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.blog-featured-link:hover {
border-color: var(--color-primary-alpha-60);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
color: var(--color-text);
text-decoration: none;
}
.blog-featured-image {
flex-shrink: 0;
width: 360px;
height: 220px;
border-radius: 8px;
overflow: hidden;
background: var(--color-secondary-alpha-20);
}
.blog-featured-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-featured-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.blog-featured-title {
font-size: 28px;
font-weight: 700;
line-height: 1.3;
margin: 0 0 8px;
}
.blog-featured-subtitle {
font-size: 16px;
color: var(--color-text-light);
margin: 0 0 16px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-featured-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: var(--color-text-light);
}
.blog-featured-repo {
font-weight: 600;
color: var(--color-text);
}
.blog-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.blog-avatar-sm {
width: 18px;
height: 18px;
border-radius: 50%;
}
.blog-meta-sep {
color: var(--color-text-light-3);
}
.blog-date {
color: var(--color-text-light);
}
/* Split pane layout */
.blog-explore-split {
display: flex;
gap: 24px;
align-items: flex-start;
}
.blog-explore-main {
flex: 1;
min-width: 0;
}
.blog-explore-sidebar {
width: 280px;
flex-shrink: 0;
position: sticky;
top: 16px;
}
/* Article list */
.blog-post-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.blog-list-item {
border-radius: 10px;
border: 1px solid var(--color-secondary-alpha-40);
background: var(--color-box-body);
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
}
.blog-list-item:hover {
border-color: var(--color-primary-alpha-60);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.blog-list-item-link {
display: flex;
gap: 16px;
padding: 16px;
color: var(--color-text);
text-decoration: none;
}
.blog-list-item-link:hover {
color: var(--color-text);
text-decoration: none;
}
.blog-list-item-image {
flex-shrink: 0;
width: 140px;
height: 90px;
border-radius: 6px;
overflow: hidden;
background: var(--color-secondary-alpha-20);
}
.blog-list-item-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-list-item-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.blog-list-item-series {
display: block;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-primary);
margin-bottom: 2px;
}
.blog-list-item-title {
font-size: 16px;
font-weight: 600;
line-height: 1.3;
margin: 0 0 4px;
}
.blog-list-item-subtitle {
font-size: 13px;
color: var(--color-text-light);
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-list-item-footer {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--color-text-light);
padding: 0 16px 12px;
flex-wrap: wrap;
}
.blog-list-item-author {
display: flex;
align-items: center;
gap: 4px;
color: var(--color-text-light);
text-decoration: none;
}
.blog-list-item-author:hover {
color: var(--color-primary);
}
.blog-list-item-repo {
color: var(--color-text-light);
text-decoration: none;
font-weight: 500;
}
.blog-list-item-repo:hover {
color: var(--color-primary);
}
/* Sidebar */
.blog-sidebar-search {
margin-bottom: 20px;
}
.blog-sidebar-heading {
font-size: 14px;
font-weight: 600;
margin: 0 0 10px;
color: var(--color-text);
}
.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;
}
.empty-placeholder {
text-align: center;
padding: 60px 20px;
color: var(--color-text-light);
}
@media (max-width: 768px) {
.blog-featured-link {
flex-direction: column;
}
.blog-featured-image {
width: 100%;
height: 180px;
}
.blog-explore-split {
flex-direction: column-reverse;
}
.blog-explore-sidebar {
width: 100%;
position: static;
}
.blog-list-item-image {
width: 100px;
height: 70px;
}
}
</style>
{{template "base/footer" .}}