Add all tabs to home page
This commit is contained in:
parent
fe051080a6
commit
163eba8475
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ type SnippetQuery struct {
|
|||
Tags []int
|
||||
DiscordMessageIDs []string
|
||||
|
||||
FeaturedOnly bool
|
||||
|
||||
Limit, Offset int // if empty, no pagination
|
||||
}
|
||||
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
@import "projects.css";
|
||||
@import "showcase.css";
|
||||
@import "syntax.css";
|
||||
@import "tabs.css";
|
||||
@import "timeline.css";
|
|
@ -0,0 +1,8 @@
|
|||
.tab-button {
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tab-button-active {
|
||||
border-color: var(--link-color);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
{{ define "extrahead" }}
|
||||
<script src="{{ static "js/templates.js" }}"></script>
|
||||
<script src="{{ static "js/tabs.js" }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
@ -139,11 +140,38 @@
|
|||
|
||||
<!-- Feed -->
|
||||
<div class="flex flex-column flex-grow-1 overflow-hidden">
|
||||
<div class="timeline flex flex-column g3">
|
||||
{{ range .RecentItems }}
|
||||
{{ template "timeline_item.html" . }}
|
||||
{{ end }}
|
||||
TODO: READ MORE LINK
|
||||
<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>
|
||||
|
@ -160,6 +188,8 @@
|
|||
content.hidden = hide;
|
||||
chevron.classList.toggle("rot-180", !hide);
|
||||
}
|
||||
|
||||
initTabs(document.querySelector("#landing-tabs"));
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Reference in New Issue