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 @@