All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m20s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m23s
Build and Release / Lint (push) Successful in 5m40s
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 8h4m50s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m58s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 8m36s
Build and Release / Build Binary (linux/arm64) (push) Successful in 10m51s
Implements search by keyword (title/subtitle), tag filtering, and sort by newest/popular on explore blogs page. Adds GetExploreTopTags to show popular tags with usage counts. Enforces repository access permissions using AccessibleRepositoryCondition. Fixes secret lookup to skip scope conditions when querying by ID. Updates UI with tag cloud, search box, and sort dropdown.
422 lines
11 KiB
Handlebars
422 lines
11 KiB
Handlebars
{{template "base/head" .}}
|
|
<div role="main" aria-label="{{.Title}}" class="page-content explore blogs">
|
|
{{template "explore/navbar" .}}
|
|
<div class="ui container">
|
|
{{if .FeaturedPost}}
|
|
<div class="blog-featured">
|
|
<a href="{{.FeaturedPost.Repo.Link}}/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}}</h2>
|
|
{{if .FeaturedPost.Subtitle}}
|
|
<p class="blog-featured-subtitle">{{.FeaturedPost.Subtitle}}</p>
|
|
{{end}}
|
|
<div class="blog-featured-meta">
|
|
{{if .FeaturedPost.Author}}
|
|
<img class="blog-avatar" src="{{.FeaturedPost.Author.AvatarLink ctx}}" alt="{{.FeaturedPost.Author.Name}}">
|
|
{{end}}
|
|
{{if .FeaturedPost.Repo}}
|
|
<span class="blog-featured-repo">{{if .FeaturedPost.Repo.DisplayTitle}}{{.FeaturedPost.Repo.DisplayTitle}}{{else}}{{.FeaturedPost.Repo.Name}}{{end}}</span>
|
|
<span class="blog-meta-sep">·</span>
|
|
{{end}}
|
|
{{if .FeaturedPost.Author}}
|
|
<span>{{.FeaturedPost.Author.DisplayName}}</span>
|
|
<span class="blog-meta-sep">·</span>
|
|
{{end}}
|
|
{{if .FeaturedPost.PublishedUnix}}
|
|
<span class="blog-date">{{DateUtils.TimeSince .FeaturedPost.PublishedUnix}}</span>
|
|
{{end}}
|
|
{{if .FeaturedLikes}}
|
|
<span class="blog-meta-sep">·</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 .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}}">
|
|
{{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}}">
|
|
{{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="{{.Repo.Link}}/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">
|
|
<h3 class="blog-list-item-title">{{.Title}}</h3>
|
|
{{if .Subtitle}}
|
|
<p class="blog-list-item-subtitle">{{.Subtitle}}</p>
|
|
{{end}}
|
|
</div>
|
|
</a>
|
|
<div class="blog-list-item-footer">
|
|
{{if .Author}}
|
|
<a href="{{.Author.HomeLink}}" class="blog-list-item-author">
|
|
<img class="blog-avatar-sm" src="{{.Author.AvatarLink ctx}}" alt="{{.Author.Name}}">
|
|
<span>{{.Author.DisplayName}}</span>
|
|
</a>
|
|
{{end}}
|
|
{{if .Repo}}
|
|
<span class="blog-meta-sep">·</span>
|
|
<a href="{{.Repo.Link}}" class="blog-list-item-repo">{{if .Repo.DisplayTitle}}{{.Repo.DisplayTitle}}{{else}}{{.Repo.FullName}}{{end}}</a>
|
|
{{end}}
|
|
<span class="blog-meta-sep">·</span>
|
|
{{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}}
|
|
<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-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" .}}
|