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) {
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 = [];
if (!initialTab) {
initialTab = tabs[0].getAttribute("data-tab");
}
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;
function switchTo(name) {
for (const tab of tabs) {
const slugMatches = tab.getAttribute("data-slug") === slug;
tab.classList.toggle('dn', !slugMatches);
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 (slugMatches) {
didMatch = true;
}
}
const tabButtons = document.querySelectorAll(".tab-button");
for (const tabButton of tabButtons) {
const buttonSlug = tabButton.getAttribute("data-slug");
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;
for (const button of buttons) {
button.addEventListener("click", () => {
switchTo(button.getAttribute("data-tab-button"));
});
}
}

View File

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

View File

@ -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`)
}

View File

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

View File

@ -16,4 +16,5 @@
@import "projects.css";
@import "showcase.css";
@import "syntax.css";
@import "tabs.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">
{{ if .OwnerAvatarUrl }}
<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>
{{ end }}
{{ 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 }}
{{ template "breadcrumbs.html" . }}
{{ end }}
{{ 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 }}
<a href="{{ .Url }}">{{ .Title }}</a>
</div>

View File

@ -2,6 +2,7 @@
{{ define "extrahead" }}
<script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/tabs.js" }}"></script>
{{ end }}
{{ define "content" }}
@ -139,15 +140,42 @@
<!-- Feed -->
<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 }}
{{ template "timeline_item.html" . }}
{{ end }}
</div>
<div data-tab="news" class="timeline flex flex-column g3">
{{ range .NewsItems }}
{{ template "timeline_item.html" . }}
{{ end }}
</div>
TODO: READ MORE LINK
</div>
</div>
</div>
</div>
</div>
<script>
function collapse(e) {
@ -160,6 +188,8 @@
content.hidden = hide;
chevron.classList.toggle("rot-180", !hide);
}
initTabs(document.querySelector("#landing-tabs"));
</script>
{{ end }}

View File

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

View File

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

View File

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

View File

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