diff --git a/public/js/tabs.js b/public/js/tabs.js index 0440425d..d9e1721f 100644 --- a/public/js/tabs.js +++ b/public/js/tabs.js @@ -1,106 +1,24 @@ -function TabState(tabbed) { - this.container = tabbed; - this.tabs = tabbed.querySelector(".tab"); +function initTabs(container, initialTab = null) { + const buttons = Array.from(container.querySelectorAll("[data-tab-button]")); + const tabs = Array.from(container.querySelectorAll("[data-tab]")); - this.tabbar = document.createElement("div"); - this.tabbar.classList.add("tab-bar"); - 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"); - 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) { - const slugMatches = tab.getAttribute("data-slug") === slug; - tab.classList.toggle('dn', !slugMatches); - - if (slugMatches) { - didMatch = true; - } + if (!initialTab) { + initialTab = tabs[0].getAttribute("data-tab"); } - const tabButtons = document.querySelectorAll(".tab-button"); - for (const tabButton of tabButtons) { - const buttonSlug = tabButton.getAttribute("data-slug"); - tabButton.classList.toggle('current', slug === buttonSlug); + function switchTo(name) { + for (const tab of tabs) { + tab.hidden = tab.getAttribute("data-tab") !== name; + } + for (const button of buttons) { + button.classList.toggle("tab-button-active", button.getAttribute("data-tab-button") === name); + } } + switchTo(initialTab); - if (!didMatch) { - // switch to first tab as a fallback - tabs[0].classList.remove('dn'); - tabButtons[0].classList.add('current'); + for (const button of buttons) { + button.addEventListener("click", () => { + switchTo(button.getAttribute("data-tab-button")); + }); } - - 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; - } } diff --git a/public/style.css b/public/style.css index de3b9f03..046eab69 100644 --- a/public/style.css +++ b/public/style.css @@ -8844,6 +8844,15 @@ code .ss, 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 */ .avatar { object-fit: cover; diff --git a/src/hmndata/project_helper.go b/src/hmndata/project_helper.go index 340b390b..4f9179d0 100644 --- a/src/hmndata/project_helper.go +++ b/src/hmndata/project_helper.go @@ -23,6 +23,7 @@ type ProjectsQuery struct { // 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. Types ProjectTypeQuery // bitfield + FeaturedOnly bool IncludeHidden bool // Ignored when using FetchProject @@ -133,6 +134,9 @@ func FetchProjects( } qb.Add(`)`) } + if q.FeaturedOnly { + qb.Add(`AND project.featured`) + } if !q.IncludeHidden { qb.Add(`AND NOT project.hidden`) } diff --git a/src/hmndata/snippet_helper.go b/src/hmndata/snippet_helper.go index ba31a946..73397f4c 100644 --- a/src/hmndata/snippet_helper.go +++ b/src/hmndata/snippet_helper.go @@ -16,6 +16,8 @@ type SnippetQuery struct { Tags []int DiscordMessageIDs []string + FeaturedOnly bool + Limit, Offset int // if empty, no pagination } diff --git a/src/rawdata/scss/style.css b/src/rawdata/scss/style.css index ea5e2472..1cbd0681 100644 --- a/src/rawdata/scss/style.css +++ b/src/rawdata/scss/style.css @@ -16,4 +16,5 @@ @import "projects.css"; @import "showcase.css"; @import "syntax.css"; +@import "tabs.css"; @import "timeline.css"; \ No newline at end of file diff --git a/src/rawdata/scss/tabs.css b/src/rawdata/scss/tabs.css new file mode 100644 index 00000000..777ac837 --- /dev/null +++ b/src/rawdata/scss/tabs.css @@ -0,0 +1,8 @@ +.tab-button { + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} + +.tab-button-active { + border-color: var(--link-color); +} \ No newline at end of file diff --git a/src/templates/src/include/timeline_item.html b/src/templates/src/include/timeline_item.html index 55816cce..c7861b68 100644 --- a/src/templates/src/include/timeline_item.html +++ b/src/templates/src/include/timeline_item.html @@ -4,16 +4,16 @@
{{ if .OwnerAvatarUrl }} - + {{ end }} {{ if .ForumLayout }} -
+
{{ with .Breadcrumbs }} {{ template "breadcrumbs.html" . }} {{ end }} {{ if .Title }} -
+
{{ with .TypeTitle }}{{ . }}:{{ end }} {{ .Title }}
diff --git a/src/templates/src/landing.html b/src/templates/src/landing.html index 1420e485..e510551f 100644 --- a/src/templates/src/landing.html +++ b/src/templates/src/landing.html @@ -2,6 +2,7 @@ {{ define "extrahead" }} + {{ end }} {{ define "content" }} @@ -139,11 +140,38 @@
-
- {{ range .RecentItems }} - {{ template "timeline_item.html" . }} - {{ end }} - TODO: READ MORE LINK +
+
+
Following
+
Featured
+
Recent
+
News
+
+
+ {{ if .User }} +
+ {{ range .FollowingItems }} + {{ template "timeline_item.html" . }} + {{ end }} +
+ {{ end }} +
+ {{ range .FeaturedItems }} + {{ template "timeline_item.html" . }} + {{ end }} +
+
+ {{ range .RecentItems }} + {{ template "timeline_item.html" . }} + {{ end }} +
+
+ {{ range .NewsItems }} + {{ template "timeline_item.html" . }} + {{ end }} +
+ TODO: READ MORE LINK +
@@ -160,6 +188,8 @@ content.hidden = hide; chevron.classList.toggle("rot-180", !hide); } + + initTabs(document.querySelector("#landing-tabs")); {{ end }} diff --git a/src/templates/src/project_edit.html b/src/templates/src/project_edit.html index 6c3e55aa..48f7ec32 100644 --- a/src/templates/src/project_edit.html +++ b/src/templates/src/project_edit.html @@ -2,7 +2,6 @@ {{ define "extrahead" }} {{ template "markdown_previews.html" .TextEditor }} - diff --git a/src/templates/src/user_settings.html b/src/templates/src/user_settings.html index f9c073f7..eb49c218 100644 --- a/src/templates/src/user_settings.html +++ b/src/templates/src/user_settings.html @@ -1,7 +1,6 @@ {{ template "base.html" . }} {{ define "extrahead" }} - {{ end }} diff --git a/src/templates/types.go b/src/templates/types.go index 9d6cad70..a05fde4c 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -340,6 +340,7 @@ type TimelineItem struct { Media []TimelineItemMedia ForumLayout bool + AllowTitleWrap bool TruncateDescription bool CanShowcase bool // whether this snippet can be shown in a showcase gallery Editable bool diff --git a/src/website/landing.go b/src/website/landing.go index 0ef94f55..f868df08 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -20,7 +20,6 @@ func Index(c *RequestContext) ResponseData { type LandingTemplateData struct { templates.BaseData - NewsPost *templates.TimelineItem FollowingItems []templates.TimelineItem FeaturedItems []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{ Limit: 100, }) @@ -63,26 +80,24 @@ func Index(c *RequestContext) ResponseData { 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{ - ProjectIDs: []int{models.HMNProjectID}, - ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost}, - Limit: 1, + ProjectIDs: []int{models.HMNProjectID}, + ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost}, + Limit: 100, + OrderByCreated: true, }) 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 - if len(newsThreads) > 0 { - t := newsThreads[0] - item := PostToTimelineItem(hmndata.UrlContextForProject(&t.Project), lineageBuilder, &t.FirstPost, &t.Thread, t.FirstPostAuthor) + for _, t := range newsThreads { + item := PostToTimelineItem(c.UrlContext, lineageBuilder, &t.FirstPost, &t.Thread, t.FirstPostAuthor) item.Breadcrumbs = nil item.TypeTitle = "" item.Description = template.HTML(t.FirstPostCurrentVersion.TextParsed) + item.AllowTitleWrap = true item.TruncateDescription = true - newsPostItem = &item + newsItems = append(newsItems, item) } - c.Perf.EndBlock() var projects []templates.Project if c.CurrentUser != nil { @@ -116,7 +131,6 @@ func Index(c *RequestContext) ResponseData { err = res.WriteTemplate("landing.html", LandingTemplateData{ BaseData: baseData, - NewsPost: newsPostItem, FollowingItems: followingItems, FeaturedItems: featuredItems, RecentItems: recentItems,