Add all tabs to home page

This commit is contained in:
Ben Visness 2024-06-24 13:36:50 -05:00
parent fe051080a6
commit 163eba8475
12 changed files with 107 additions and 122 deletions

View File

@ -1,106 +1,24 @@
function TabState(tabbed) { function initTabs(container, initialTab = null) {
this.container = tabbed; const buttons = Array.from(container.querySelectorAll("[data-tab-button]"));
this.tabs = tabbed.querySelector(".tab"); const tabs = Array.from(container.querySelectorAll("[data-tab]"));
this.tabbar = document.createElement("div"); if (!initialTab) {
this.tabbar.classList.add("tab-bar"); initialTab = tabs[0].getAttribute("data-tab");
this.container.insertBefore(this.tabbar, this.container.firstChild);
this.current_i = -1;
this.tab_buttons = [];
}
function switch_tab_old(state, tab_i) {
return function() {
if (state.current_i >= 0) {
state.tabs[state.current_i].classList.add("hidden");
state.tab_buttons[state.current_i].classList.remove("current");
} }
state.tabs[tab_i].classList.remove("hidden"); function switchTo(name) {
state.tab_buttons[tab_i].classList.add("current");
var hash = "";
if (state.tabs[tab_i].hasAttribute("data-url-hash")) {
hash = state.tabs[tab_i].getAttribute("data-url-hash");
}
window.location.hash = hash;
state.current_i = tab_i;
};
}
document.addEventListener("DOMContentLoaded", function() {
const tabContainers = document.getElementsByClassName("tabbed");
for (const container of tabContainers) {
const tabBar = document.createElement("div");
tabBar.classList.add("tab-bar");
container.insertAdjacentElement('afterbegin', tabBar);
const tabs = container.querySelectorAll(".tab");
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.classList.toggle('dn', i > 0);
const slug = tab.getAttribute("data-slug");
// TODO: Should this element be a link?
const tabButton = document.createElement("div");
tabButton.classList.add("tab-button");
tabButton.classList.toggle("current", i === 0);
tabButton.innerText = tab.getAttribute("data-name");
tabButton.setAttribute("data-slug", slug);
tabButton.addEventListener("click", () => {
switchTab(container, slug);
});
tabBar.appendChild(tabButton);
}
const initialSlug = window.location.hash;
if (initialSlug) {
switchTab(container, initialSlug.substring(1));
}
}
});
function switchTab(container, slug) {
const tabs = container.querySelectorAll('.tab');
let didMatch = false;
for (const tab of tabs) { for (const tab of tabs) {
const slugMatches = tab.getAttribute("data-slug") === slug; tab.hidden = tab.getAttribute("data-tab") !== name;
tab.classList.toggle('dn', !slugMatches); }
for (const button of buttons) {
if (slugMatches) { button.classList.toggle("tab-button-active", button.getAttribute("data-tab-button") === name);
didMatch = true;
} }
} }
switchTo(initialTab);
const tabButtons = document.querySelectorAll(".tab-button"); for (const button of buttons) {
for (const tabButton of tabButtons) { button.addEventListener("click", () => {
const buttonSlug = tabButton.getAttribute("data-slug"); switchTo(button.getAttribute("data-tab-button"));
tabButton.classList.toggle('current', slug === buttonSlug); });
}
if (!didMatch) {
// switch to first tab as a fallback
tabs[0].classList.remove('dn');
tabButtons[0].classList.add('current');
}
window.location.hash = slug;
}
function switchToTabOfElement(container, el) {
const tabs = Array.from(container.querySelectorAll('.tab'));
let target = el.parentElement;
while (target) {
if (tabs.includes(target)) {
switchTab(container, target.getAttribute("data-slug"));
return;
}
target = target.parentElement;
} }
} }

View File

@ -8844,6 +8844,15 @@ code .ss,
color: #a31515; color: #a31515;
} }
/* src/rawdata/scss/tabs.css */
.tab-button {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab-button-active {
border-color: var(--link-color);
}
/* src/rawdata/scss/timeline.css */ /* src/rawdata/scss/timeline.css */
.avatar { .avatar {
object-fit: cover; object-fit: cover;

View File

@ -23,6 +23,7 @@ type ProjectsQuery struct {
// are generally visible to all users. // are generally visible to all users.
Lifecycles []models.ProjectLifecycle // If empty, defaults to visible lifecycles. Do not conflate this with permissions; those are checked separately. Lifecycles []models.ProjectLifecycle // If empty, defaults to visible lifecycles. Do not conflate this with permissions; those are checked separately.
Types ProjectTypeQuery // bitfield Types ProjectTypeQuery // bitfield
FeaturedOnly bool
IncludeHidden bool IncludeHidden bool
// Ignored when using FetchProject // Ignored when using FetchProject
@ -133,6 +134,9 @@ func FetchProjects(
} }
qb.Add(`)`) qb.Add(`)`)
} }
if q.FeaturedOnly {
qb.Add(`AND project.featured`)
}
if !q.IncludeHidden { if !q.IncludeHidden {
qb.Add(`AND NOT project.hidden`) qb.Add(`AND NOT project.hidden`)
} }

View File

@ -16,6 +16,8 @@ type SnippetQuery struct {
Tags []int Tags []int
DiscordMessageIDs []string DiscordMessageIDs []string
FeaturedOnly bool
Limit, Offset int // if empty, no pagination Limit, Offset int // if empty, no pagination
} }

View File

@ -16,4 +16,5 @@
@import "projects.css"; @import "projects.css";
@import "showcase.css"; @import "showcase.css";
@import "syntax.css"; @import "syntax.css";
@import "tabs.css";
@import "timeline.css"; @import "timeline.css";

View File

@ -0,0 +1,8 @@
.tab-button {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab-button-active {
border-color: var(--link-color);
}

View File

@ -4,16 +4,16 @@
<div class="flex items-center"> <div class="flex items-center">
{{ if .OwnerAvatarUrl }} {{ if .OwnerAvatarUrl }}
<a class="flex flex-shrink-0" href="{{ .OwnerUrl }}"> <a class="flex flex-shrink-0" href="{{ .OwnerUrl }}">
<img class="avatar avatar-user mr2" src="{{ .OwnerAvatarUrl }}" /> <img class="avatar avatar-user {{ if .ForumLayout }}mr3{{ else }}mr2{{ end }}" src="{{ .OwnerAvatarUrl }}" />
</a> </a>
{{ end }} {{ end }}
{{ if .ForumLayout }} {{ if .ForumLayout }}
<div class="ml2 overflow-hidden flex-grow-1 flex flex-column g1 justify-center"> <div class="overflow-hidden flex-grow-1 flex flex-column g1 justify-center">
{{ with .Breadcrumbs }} {{ with .Breadcrumbs }}
{{ template "breadcrumbs.html" . }} {{ template "breadcrumbs.html" . }}
{{ end }} {{ end }}
{{ if .Title }} {{ if .Title }}
<div class="f5 nowrap truncate"> <div class="f5 lh-title {{ if not .AllowTitleWrap }}nowrap truncate{{ end }}">
{{ with .TypeTitle }}<b class="dn di-ns">{{ . }}:</b>{{ end }} {{ with .TypeTitle }}<b class="dn di-ns">{{ . }}:</b>{{ end }}
<a href="{{ .Url }}">{{ .Title }}</a> <a href="{{ .Url }}">{{ .Title }}</a>
</div> </div>

View File

@ -2,6 +2,7 @@
{{ define "extrahead" }} {{ define "extrahead" }}
<script src="{{ static "js/templates.js" }}"></script> <script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/tabs.js" }}"></script>
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
@ -139,14 +140,41 @@
<!-- Feed --> <!-- Feed -->
<div class="flex flex-column flex-grow-1 overflow-hidden"> <div class="flex flex-column flex-grow-1 overflow-hidden">
<div class="timeline flex flex-column g3"> <div id="landing-tabs">
<div class="bb mb2 flex f6">
<div data-tab-button="following" class="tab-button ph3 pv1 pointer">Following</div>
<div data-tab-button="featured" class="tab-button ph3 pv1 pointer">Featured</div>
<div data-tab-button="recent" class="tab-button ph3 pv1 pointer">Recent</div>
<div data-tab-button="news" class="tab-button ph3 pv1 pointer">News</div>
</div>
<div>
{{ if .User }}
<div data-tab="following" class="timeline flex flex-column g3">
{{ range .FollowingItems }}
{{ template "timeline_item.html" . }}
{{ end }}
</div>
{{ end }}
<div data-tab="featured" class="timeline flex flex-column g3">
{{ range .FeaturedItems }}
{{ template "timeline_item.html" . }}
{{ end }}
</div>
<div data-tab="recent" class="timeline flex flex-column g3">
{{ range .RecentItems }} {{ range .RecentItems }}
{{ template "timeline_item.html" . }} {{ template "timeline_item.html" . }}
{{ end }} {{ end }}
</div>
<div data-tab="news" class="timeline flex flex-column g3">
{{ range .NewsItems }}
{{ template "timeline_item.html" . }}
{{ end }}
</div>
TODO: READ MORE LINK TODO: READ MORE LINK
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<script> <script>
@ -160,6 +188,8 @@
content.hidden = hide; content.hidden = hide;
chevron.classList.toggle("rot-180", !hide); chevron.classList.toggle("rot-180", !hide);
} }
initTabs(document.querySelector("#landing-tabs"));
</script> </script>
{{ end }} {{ end }}

View File

@ -2,7 +2,6 @@
{{ define "extrahead" }} {{ define "extrahead" }}
{{ template "markdown_previews.html" .TextEditor }} {{ template "markdown_previews.html" .TextEditor }}
<script src="{{ static "js/tabs.js" }}"></script>
<script src="{{ static "js/image_selector.js" }}"></script> <script src="{{ static "js/image_selector.js" }}"></script>
<script src="{{ static "js/templates.js" }}"></script> <script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/base64.js" }}"></script> <script src="{{ static "js/base64.js" }}"></script>

View File

@ -1,7 +1,6 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "extrahead" }} {{ define "extrahead" }}
<script src="{{ static "js/tabs.js" }}"></script>
<script src="{{ static "js/image_selector.js" }}"></script> <script src="{{ static "js/image_selector.js" }}"></script>
{{ end }} {{ end }}

View File

@ -340,6 +340,7 @@ type TimelineItem struct {
Media []TimelineItemMedia Media []TimelineItemMedia
ForumLayout bool ForumLayout bool
AllowTitleWrap bool
TruncateDescription bool TruncateDescription bool
CanShowcase bool // whether this snippet can be shown in a showcase gallery CanShowcase bool // whether this snippet can be shown in a showcase gallery
Editable bool Editable bool

View File

@ -20,7 +20,6 @@ func Index(c *RequestContext) ResponseData {
type LandingTemplateData struct { type LandingTemplateData struct {
templates.BaseData templates.BaseData
NewsPost *templates.TimelineItem
FollowingItems []templates.TimelineItem FollowingItems []templates.TimelineItem
FeaturedItems []templates.TimelineItem FeaturedItems []templates.TimelineItem
RecentItems []templates.TimelineItem RecentItems []templates.TimelineItem
@ -56,6 +55,24 @@ func Index(c *RequestContext) ResponseData {
} }
} }
featuredProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
FeaturedOnly: true,
})
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to fetch featured projects")
}
var featuredProjectIDs []int
for _, p := range featuredProjects {
featuredProjectIDs = append(featuredProjectIDs, p.Project.ID)
}
featuredItems, err = FetchTimeline(c, c.Conn, c.CurrentUser, TimelineQuery{
ProjectIDs: featuredProjectIDs,
Limit: 100,
})
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to fetch featured feed")
}
recentItems, err = FetchTimeline(c, c.Conn, c.CurrentUser, TimelineQuery{ recentItems, err = FetchTimeline(c, c.Conn, c.CurrentUser, TimelineQuery{
Limit: 100, Limit: 100,
}) })
@ -63,26 +80,24 @@ func Index(c *RequestContext) ResponseData {
c.Logger.Warn().Err(err).Msg("failed to fetch recent feed") c.Logger.Warn().Err(err).Msg("failed to fetch recent feed")
} }
c.Perf.StartBlock("SQL", "Get news")
newsThreads, err := hmndata.FetchThreads(c, c.Conn, c.CurrentUser, hmndata.ThreadsQuery{ newsThreads, err := hmndata.FetchThreads(c, c.Conn, c.CurrentUser, hmndata.ThreadsQuery{
ProjectIDs: []int{models.HMNProjectID}, ProjectIDs: []int{models.HMNProjectID},
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost}, ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
Limit: 1, Limit: 100,
OrderByCreated: true,
}) })
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news threads"))
} }
var newsPostItem *templates.TimelineItem for _, t := range newsThreads {
if len(newsThreads) > 0 { item := PostToTimelineItem(c.UrlContext, lineageBuilder, &t.FirstPost, &t.Thread, t.FirstPostAuthor)
t := newsThreads[0]
item := PostToTimelineItem(hmndata.UrlContextForProject(&t.Project), lineageBuilder, &t.FirstPost, &t.Thread, t.FirstPostAuthor)
item.Breadcrumbs = nil item.Breadcrumbs = nil
item.TypeTitle = "" item.TypeTitle = ""
item.Description = template.HTML(t.FirstPostCurrentVersion.TextParsed) item.Description = template.HTML(t.FirstPostCurrentVersion.TextParsed)
item.AllowTitleWrap = true
item.TruncateDescription = true item.TruncateDescription = true
newsPostItem = &item newsItems = append(newsItems, item)
} }
c.Perf.EndBlock()
var projects []templates.Project var projects []templates.Project
if c.CurrentUser != nil { if c.CurrentUser != nil {
@ -116,7 +131,6 @@ func Index(c *RequestContext) ResponseData {
err = res.WriteTemplate("landing.html", LandingTemplateData{ err = res.WriteTemplate("landing.html", LandingTemplateData{
BaseData: baseData, BaseData: baseData,
NewsPost: newsPostItem,
FollowingItems: followingItems, FollowingItems: followingItems,
FeaturedItems: featuredItems, FeaturedItems: featuredItems,
RecentItems: recentItems, RecentItems: recentItems,