Add all tabs to home page
This commit is contained in:
parent
fe051080a6
commit
163eba8475
|
@ -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) {
|
function switchTo(name) {
|
||||||
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) {
|
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) {
|
||||||
|
button.classList.toggle("tab-button-active", button.getAttribute("data-tab-button") === name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switchTo(initialTab);
|
||||||
|
|
||||||
if (slugMatches) {
|
for (const button of buttons) {
|
||||||
didMatch = true;
|
button.addEventListener("click", () => {
|
||||||
}
|
switchTo(button.getAttribute("data-tab-button"));
|
||||||
}
|
});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
|
@ -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">
|
<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>
|
||||||
|
|
|
@ -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,15 +140,42 @@
|
||||||
|
|
||||||
<!-- 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>
|
||||||
function collapse(e) {
|
function collapse(e) {
|
||||||
|
@ -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 }}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Reference in New Issue