Breadcrumbs

This commit is contained in:
Asaf Gartner 2021-09-01 21:25:09 +03:00
parent 1f39b166cb
commit d78a2e8e82
23 changed files with 233 additions and 164 deletions

View File

@ -88,7 +88,7 @@
{{- if gt $i 0 -}} {{- if gt $i 0 -}}
<span class="ph2">&raquo;</span> <span class="ph2">&raquo;</span>
{{- end -}} {{- end -}}
<a class="breadcrumb {{ if .Current }}current{{ end }}" href="{{ .Url }}">{{ .Name }}</a> <a class="breadcrumb" href="{{ .Url }}">{{ .Name }}</a>
{{- end }} {{- end }}
</div> </div>
{{ end }} {{ end }}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"time" "time"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
@ -34,6 +35,10 @@ func Int64Max(a, b int64) int64 {
return b return b
} }
func NumPages(numThings, thingsPerPage int) int {
return IntMax(int(math.Ceil(float64(numThings)/float64(thingsPerPage))), 1)
}
/* /*
Recover a panic and convert it to a returned error. Call it like so: Recover a panic and convert it to a returned error. Call it like so:

View File

@ -33,7 +33,7 @@ func LoginPage(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_login.html", LoginPageData{ res.MustWriteTemplate("auth_login.html", LoginPageData{
BaseData: getBaseData(c), BaseData: getBaseDataAutocrumb(c, "Log in"),
RedirectUrl: c.Req.URL.Query().Get("redirect"), RedirectUrl: c.Req.URL.Query().Get("redirect"),
ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(), ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(),
}, c.Perf) }, c.Perf)
@ -63,7 +63,7 @@ func Login(c *RequestContext) ResponseData {
showLoginWithFailure := func(c *RequestContext, redirect string) ResponseData { showLoginWithFailure := func(c *RequestContext, redirect string) ResponseData {
var res ResponseData var res ResponseData
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Log in")
baseData.AddImmediateNotice("failure", "Incorrect username or password") baseData.AddImmediateNotice("failure", "Incorrect username or password")
res.MustWriteTemplate("auth_login.html", LoginPageData{ res.MustWriteTemplate("auth_login.html", LoginPageData{
BaseData: baseData, BaseData: baseData,
@ -122,7 +122,7 @@ func RegisterNewUser(c *RequestContext) ResponseData {
} }
// TODO(asaf): Do something to prevent bot registration // TODO(asaf): Do something to prevent bot registration
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_register.html", getBaseData(c), c.Perf) res.MustWriteTemplate("auth_register.html", getBaseDataAutocrumb(c, "Register"), c.Perf)
return res return res
} }
@ -275,7 +275,7 @@ func RegisterNewUserSuccess(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_register_success.html", RegisterNewUserSuccessData{ res.MustWriteTemplate("auth_register_success.html", RegisterNewUserSuccessData{
BaseData: getBaseData(c), BaseData: getBaseDataAutocrumb(c, "Register"),
ContactUsUrl: hmnurl.BuildContactPage(), ContactUsUrl: hmnurl.BuildContactPage(),
}, c.Perf) }, c.Perf)
return res return res
@ -321,7 +321,7 @@ func EmailConfirmation(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{
BaseData: getBaseData(c), BaseData: getBaseDataAutocrumb(c, "Register"),
Token: token, Token: token,
Username: username, Username: username,
}, c.Perf) }, c.Perf)
@ -346,11 +346,11 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} else if !success { } else if !success {
var res ResponseData var res ResponseData
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Register")
// NOTE(asaf): We can report that the password is incorrect, because an attacker wouldn't have a valid token to begin with. // NOTE(asaf): We can report that the password is incorrect, because an attacker wouldn't have a valid token to begin with.
baseData.AddImmediateNotice("failure", "Incorrect password. Please try again.") baseData.AddImmediateNotice("failure", "Incorrect password. Please try again.")
res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{
BaseData: getBaseData(c), BaseData: baseData,
Token: token, Token: token,
Username: username, Username: username,
}, c.Perf) }, c.Perf)
@ -424,7 +424,7 @@ func RequestPasswordReset(c *RequestContext) ResponseData {
return c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) return c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther)
} }
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_password_reset.html", getBaseData(c), c.Perf) res.MustWriteTemplate("auth_password_reset.html", getBaseDataAutocrumb(c, "Password Reset"), c.Perf)
return res return res
} }
@ -548,7 +548,7 @@ func PasswordResetSent(c *RequestContext) ResponseData {
} }
var res ResponseData var res ResponseData
res.MustWriteTemplate("auth_password_reset_sent.html", PasswordResetSentData{ res.MustWriteTemplate("auth_password_reset_sent.html", PasswordResetSentData{
BaseData: getBaseData(c), BaseData: getBaseDataAutocrumb(c, "Password Reset"),
ContactUsUrl: hmnurl.BuildContactPage(), ContactUsUrl: hmnurl.BuildContactPage(),
}, c.Perf) }, c.Perf)
return res return res
@ -582,7 +582,7 @@ func DoPasswordReset(c *RequestContext) ResponseData {
} }
res.MustWriteTemplate("auth_do_password_reset.html", DoPasswordResetData{ res.MustWriteTemplate("auth_do_password_reset.html", DoPasswordResetData{
BaseData: getBaseData(c), BaseData: getBaseDataAutocrumb(c, "Password Reset"),
Username: username, Username: username,
Token: token, Token: token,
}, c.Perf) }, c.Perf)

View File

@ -53,7 +53,7 @@ func BlogIndex(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts"))
} }
numPages := NumPages(numPosts, postsPerPage) numPages := utils.NumPages(numPosts, postsPerPage)
page, ok := ParsePageNumber(c, "page", numPages) page, ok := ParsePageNumber(c, "page", numPages)
if !ok { if !ok {
c.Redirect(hmnurl.BuildBlog(c.CurrentProject.Slug, page), http.StatusSeeOther) c.Redirect(hmnurl.BuildBlog(c.CurrentProject.Slug, page), http.StatusSeeOther)
@ -104,8 +104,7 @@ func BlogIndex(c *RequestContext) ResponseData {
}) })
} }
baseData := getBaseData(c) baseData := getBaseData(c, fmt.Sprintf("%s Blog", c.CurrentProject.Name), []templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)})
baseData.Title = fmt.Sprintf("%s Blog", c.CurrentProject.Name)
canCreate := false canCreate := false
if c.CurrentUser != nil { if c.CurrentUser != nil {
@ -181,8 +180,7 @@ func BlogThread(c *RequestContext) ResponseData {
templatePosts = append(templatePosts, post) templatePosts = append(templatePosts, post)
} }
baseData := getBaseData(c) baseData := getBaseData(c, thread.Title, []templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)})
baseData.Title = thread.Title
var res ResponseData var res ResponseData
res.MustWriteTemplate("blog_post.html", blogPostData{ res.MustWriteTemplate("blog_post.html", blogPostData{
@ -209,9 +207,11 @@ func BlogPostRedirectToThread(c *RequestContext) ResponseData {
} }
func BlogNewThread(c *RequestContext) ResponseData { func BlogNewThread(c *RequestContext) ResponseData {
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name) c,
// TODO(ben): Set breadcrumbs fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name),
[]templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)},
)
editData := getEditorDataForNew(baseData, nil) editData := getEditorDataForNew(baseData, nil)
editData.SubmitUrl = hmnurl.BuildBlogNewThread(c.CurrentProject.Slug) editData.SubmitUrl = hmnurl.BuildBlogNewThread(c.CurrentProject.Slug)
@ -284,13 +284,17 @@ func BlogPostEdit(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) title := ""
if postData.Thread.FirstID == postData.Post.ID { if postData.Thread.FirstID == postData.Post.ID {
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name) title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
} else { } else {
baseData.Title = fmt.Sprintf("Editing Post | %s", c.CurrentProject.Name) title = fmt.Sprintf("Editing Post | %s", c.CurrentProject.Name)
} }
// TODO(ben): Set breadcrumbs baseData := getBaseData(
c,
title,
BlogThreadBreadcrumbs(c.CurrentProject.Slug, &postData.Thread),
)
editData := getEditorDataForEdit(baseData, postData) editData := getEditorDataForEdit(baseData, postData)
editData.SubmitUrl = hmnurl.BuildBlogPostEdit(c.CurrentProject.Slug, cd.ThreadID, cd.PostID) editData.SubmitUrl = hmnurl.BuildBlogPostEdit(c.CurrentProject.Slug, cd.ThreadID, cd.PostID)
@ -352,9 +356,11 @@ func BlogPostReply(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = fmt.Sprintf("Replying to comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name) c,
// TODO(ben): Set breadcrumbs fmt.Sprintf("Replying to comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name),
BlogThreadBreadcrumbs(c.CurrentProject.Slug, &postData.Thread),
)
replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme) replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor) replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor)
@ -412,12 +418,17 @@ func BlogPostDelete(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) title := ""
if postData.Thread.FirstID == postData.Post.ID { if postData.Thread.FirstID == postData.Post.ID {
baseData.Title = fmt.Sprintf("Deleting \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name) title = fmt.Sprintf("Deleting \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
} else { } else {
baseData.Title = fmt.Sprintf("Deleting comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name) title = fmt.Sprintf("Deleting comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
} }
baseData := getBaseData(
c,
title,
BlogThreadBreadcrumbs(c.CurrentProject.Slug, &postData.Thread),
)
// TODO(ben): Set breadcrumbs // TODO(ben): Set breadcrumbs
templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme) templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)

View File

@ -0,0 +1,63 @@
package website
import (
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/templates"
)
func ProjectBreadcrumb(project *models.Project) templates.Breadcrumb {
return templates.Breadcrumb{
Name: project.Name,
Url: hmnurl.BuildProjectHomepage(project.Slug),
}
}
func ForumBreadcrumb(projectSlug string) templates.Breadcrumb {
return templates.Breadcrumb{
Name: "Forums",
Url: hmnurl.BuildForum(projectSlug, nil, 1),
}
}
func SubforumBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, subforumID int) []templates.Breadcrumb {
var result []templates.Breadcrumb
result = []templates.Breadcrumb{
ProjectBreadcrumb(project),
ForumBreadcrumb(project.Slug),
}
subforums := lineageBuilder.GetSubforumLineage(subforumID)
slugs := lineageBuilder.GetSubforumLineageSlugs(subforumID)
for i, subforum := range subforums {
result = append(result, templates.Breadcrumb{
Name: subforum.Name,
Url: hmnurl.BuildForum(project.Slug, slugs[0:i+1], 1),
})
}
return result
}
func ForumThreadBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, thread *models.Thread) []templates.Breadcrumb {
result := SubforumBreadcrumbs(lineageBuilder, project, *thread.SubforumID)
result = append(result, templates.Breadcrumb{
Name: thread.Title,
Url: hmnurl.BuildForumThread(project.Slug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), thread.ID, thread.Title, 1),
})
return result
}
func BlogBreadcrumb(projectSlug string) templates.Breadcrumb {
return templates.Breadcrumb{
Name: "Blog",
Url: hmnurl.BuildBlog(projectSlug, 1),
}
}
func BlogThreadBreadcrumbs(projectSlug string, thread *models.Thread) []templates.Breadcrumb {
result := []templates.Breadcrumb{
BlogBreadcrumb(projectSlug),
{Name: thread.Title, Url: hmnurl.BuildBlogThread(projectSlug, thread.ID, thread.Title)},
}
return result
}

View File

@ -88,7 +88,7 @@ func EpisodeList(c *RequestContext) ResponseData {
} }
var res ResponseData var res ResponseData
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, fmt.Sprintf("Episode Guide"))
res.MustWriteTemplate("episode_list.html", EpisodeListData{ res.MustWriteTemplate("episode_list.html", EpisodeListData{
BaseData: baseData, BaseData: baseData,
Content: template.HTML(guide), Content: template.HTML(guide),
@ -147,8 +147,11 @@ func Episode(c *RequestContext) ResponseData {
content := contentMatches[episodeContentRegex.SubexpIndex("content")] content := contentMatches[episodeContentRegex.SubexpIndex("content")]
var res ResponseData var res ResponseData
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = title c,
title,
[]templates.Breadcrumb{{Name: "Episode Guide", Url: hmnurl.BuildEpisodeList(c.CurrentProject.Slug, foundTopic)}},
)
res.MustWriteTemplate("episode.html", EpisodeData{ res.MustWriteTemplate("episode.html", EpisodeData{
BaseData: baseData, BaseData: baseData,
Content: template.HTML(content), Content: template.HTML(content),

View File

@ -90,7 +90,7 @@ func Feed(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
} }
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Feed")
baseData.BodyClasses = append(baseData.BodyClasses, "feed") baseData.BodyClasses = append(baseData.BodyClasses, "feed")
var res ResponseData var res ResponseData

View File

@ -100,7 +100,7 @@ func Forum(c *RequestContext) ResponseData {
} }
c.Perf.EndBlock() c.Perf.EndBlock()
numPages := NumPages(numThreads, threadsPerPage) numPages := utils.NumPages(numThreads, threadsPerPage)
page, ok := ParsePageNumber(c, "page", numPages) page, ok := ParsePageNumber(c, "page", numPages)
if !ok { if !ok {
c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, page), http.StatusSeeOther) c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, page), http.StatusSeeOther)
@ -259,27 +259,11 @@ func Forum(c *RequestContext) ResponseData {
// Template assembly // Template assembly
// --------------------- // ---------------------
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = c.CurrentProject.Name + " Forums" c,
baseData.Breadcrumbs = []templates.Breadcrumb{ fmt.Sprintf("%s Forums", c.CurrentProject.Name),
{ SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID),
Name: c.CurrentProject.Name, )
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
},
{
Name: "Forums",
Url: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
Current: true,
},
}
currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.SubforumID)
for i, subforum := range currentSubforums {
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
Name: subforum.Name,
Url: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
})
}
var res ResponseData var res ResponseData
res.MustWriteTemplate("forum.html", forumData{ res.MustWriteTemplate("forum.html", forumData{
@ -511,9 +495,7 @@ func ForumThread(c *RequestContext) ResponseData {
} }
} }
baseData := getBaseData(c) baseData := getBaseData(c, thread.Title, SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID))
baseData.Title = thread.Title
// TODO(asaf): Set breadcrumbs
var res ResponseData var res ResponseData
res.MustWriteTemplate("forum_thread.html", forumThreadData{ res.MustWriteTemplate("forum_thread.html", forumThreadData{
@ -596,15 +578,12 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
} }
func ForumNewThread(c *RequestContext) ResponseData { func ForumNewThread(c *RequestContext) ResponseData {
baseData := getBaseData(c)
baseData.Title = "Create New Thread"
// TODO(ben): Set breadcrumbs
cd, ok := getCommonForumData(c) cd, ok := getCommonForumData(c)
if !ok { if !ok {
return FourOhFour(c) return FourOhFour(c)
} }
baseData := getBaseData(c, "Create New Thread", SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID))
editData := getEditorDataForNew(baseData, nil) editData := getEditorDataForNew(baseData, nil)
editData.SubmitUrl = hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), true) editData.SubmitUrl = hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), true)
editData.SubmitLabel = "Post New Thread" editData.SubmitLabel = "Post New Thread"
@ -683,9 +662,11 @@ func ForumPostReply(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = fmt.Sprintf("Replying to post | %s", cd.SubforumTree[cd.SubforumID].Name) c,
// TODO(ben): Set breadcrumbs fmt.Sprintf("Replying to post | %s", cd.SubforumTree[cd.SubforumID].Name),
ForumThreadBreadcrumbs(cd.LineageBuilder, c.CurrentProject, &postData.Thread),
)
replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme) replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor) replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor)
@ -743,13 +724,13 @@ func ForumPostEdit(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) title := ""
if postData.Thread.FirstID == postData.Post.ID { if postData.Thread.FirstID == postData.Post.ID {
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
} else { } else {
baseData.Title = fmt.Sprintf("Editing Post | %s", cd.SubforumTree[cd.SubforumID].Name) title = fmt.Sprintf("Editing Post | %s", cd.SubforumTree[cd.SubforumID].Name)
} }
// TODO(ben): Set breadcrumbs baseData := getBaseData(c, title, ForumThreadBreadcrumbs(cd.LineageBuilder, c.CurrentProject, &postData.Thread))
editData := getEditorDataForEdit(baseData, postData) editData := getEditorDataForEdit(baseData, postData)
editData.SubmitUrl = hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID) editData.SubmitUrl = hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID)
@ -806,9 +787,11 @@ func ForumPostDelete(c *RequestContext) ResponseData {
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) c,
// TODO(ben): Set breadcrumbs fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name),
ForumThreadBreadcrumbs(cd.LineageBuilder, c.CurrentProject, &postData.Thread),
)
templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme) templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
templatePost.AddContentVersion(postData.CurrentVersion, postData.Editor) templatePost.AddContentVersion(postData.CurrentVersion, postData.Editor)

View File

@ -13,8 +13,7 @@ func JamIndex(c *RequestContext) ResponseData {
ogimageurl = urljoin(current_site_host(), ogimagepath) ogimageurl = urljoin(current_site_host(), ogimagepath)
*/ */
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam")
baseData.Title = "Wheel Reinvention Jam"
baseData.OpenGraphItems = []templates.OpenGraphItem{ baseData.OpenGraphItems = []templates.OpenGraphItem{
{Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:site_name", Value: "Handmade.Network"},
{Property: "og:type", Value: "website"}, {Property: "og:type", Value: "website"},

View File

@ -316,7 +316,7 @@ func Index(c *RequestContext) ResponseData {
showcaseJson := templates.TimelineItemsToJSON(showcaseItems) showcaseJson := templates.TimelineItemsToJSON(showcaseItems)
c.Perf.EndBlock() c.Perf.EndBlock()
baseData := getBaseData(c) baseData := getBaseData(c, "", nil)
baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more? baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
var res ResponseData var res ResponseData

View File

@ -44,8 +44,7 @@ func PodcastIndex(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} }
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, podcastResult.Podcast.Title)
baseData.Title = podcastResult.Podcast.Title
podcastIndexData := PodcastIndexData{ podcastIndexData := PodcastIndexData{
BaseData: baseData, BaseData: baseData,
@ -89,8 +88,11 @@ func PodcastEdit(c *RequestContext) ResponseData {
} }
podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile) podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile)
baseData := getBaseData(c) baseData := getBaseData(
baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} c,
fmt.Sprintf("Edit %s", podcast.Title),
[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
)
podcastEditData := PodcastEditData{ podcastEditData := PodcastEditData{
BaseData: baseData, BaseData: baseData,
Podcast: podcast, Podcast: podcast,
@ -229,9 +231,11 @@ func PodcastEpisode(c *RequestContext) ResponseData {
podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile) podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile)
episode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, podcastResult.Episodes[0], 0, podcastResult.ImageFile) episode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, podcastResult.Episodes[0], 0, podcastResult.ImageFile)
baseData := getBaseData(c) baseData := getBaseData(
baseData.Title = podcastResult.Podcast.Title c,
baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} fmt.Sprintf("%s | %s", episode.Title, podcast.Title),
[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
)
podcastEpisodeData := PodcastEpisodeData{ podcastEpisodeData := PodcastEpisodeData{
BaseData: baseData, BaseData: baseData,
@ -280,8 +284,11 @@ func PodcastEpisodeNew(c *RequestContext) ResponseData {
podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "") podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "")
var res ResponseData var res ResponseData
baseData := getBaseData(c) baseData := getBaseData(
baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} c,
fmt.Sprintf("New episode | %s", podcast.Title),
[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
)
err = res.WriteTemplate("podcast_episode_edit.html", PodcastEpisodeEditData{ err = res.WriteTemplate("podcast_episode_edit.html", PodcastEpisodeEditData{
BaseData: baseData, BaseData: baseData,
IsEdit: false, IsEdit: false,
@ -321,8 +328,11 @@ func PodcastEpisodeEdit(c *RequestContext) ResponseData {
podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "") podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "")
podcastEpisode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, episode, 0, "") podcastEpisode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, episode, 0, "")
baseData := getBaseData(c) baseData := getBaseData(
baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}, {Name: podcastEpisode.Title, Url: podcastEpisode.Url}} c,
fmt.Sprintf("Edit episode %s | %s", podcastEpisode.Title, podcast.Title),
[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}, {Name: podcastEpisode.Title, Url: podcastEpisode.Url}},
)
podcastEpisodeEditData := PodcastEpisodeEditData{ podcastEpisodeEditData := PodcastEpisodeEditData{
BaseData: baseData, BaseData: baseData,
IsEdit: true, IsEdit: true,

View File

@ -31,30 +31,40 @@ var PostTypePrefix = map[templates.PostType]string{
templates.PostTypeForumReply: "Forum reply", templates.PostTypeForumReply: "Forum reply",
} }
func PostBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, thread *models.Thread) []templates.Breadcrumb { var ThreadTypeDisplayNames = map[models.ThreadType]string{
models.ThreadTypeProjectBlogPost: "Blog",
models.ThreadTypeForumPost: "Forums",
}
func GenericThreadBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, thread *models.Thread) []templates.Breadcrumb {
var result []templates.Breadcrumb var result []templates.Breadcrumb
result = append(result, templates.Breadcrumb{ if thread.Type == models.ThreadTypeForumPost {
result = SubforumBreadcrumbs(lineageBuilder, project, *thread.SubforumID)
} else {
result = []templates.Breadcrumb{
{
Name: project.Name, Name: project.Name,
Url: hmnurl.BuildProjectHomepage(project.Slug), Url: hmnurl.BuildProjectHomepage(project.Slug),
}) },
result = append(result, templates.Breadcrumb{ {
Name: ThreadTypeDisplayNames[thread.Type], Name: ThreadTypeDisplayNames[thread.Type],
Url: BuildProjectRootResourceUrl(project.Slug, thread.Type), Url: BuildProjectRootResourceUrl(project.Slug, thread.Type),
}) },
switch thread.Type {
case models.ThreadTypeForumPost:
subforums := lineageBuilder.GetSubforumLineage(*thread.SubforumID)
slugs := lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID)
for i, subforum := range subforums {
result = append(result, templates.Breadcrumb{
Name: subforum.Name,
Url: hmnurl.BuildForum(project.Slug, slugs[0:i+1], 1),
})
} }
} }
return result return result
} }
func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) string {
switch kind {
case models.ThreadTypeProjectBlogPost:
return hmnurl.BuildBlog(projectSlug, 1)
case models.ThreadTypeForumPost:
return hmnurl.BuildForum(projectSlug, nil, 1)
}
return hmnurl.BuildProjectHomepage(projectSlug)
}
func MakePostListItem( func MakePostListItem(
lineageBuilder *models.SubforumLineageBuilder, lineageBuilder *models.SubforumLineageBuilder,
project *models.Project, project *models.Project,
@ -87,7 +97,7 @@ func MakePostListItem(
result.PostTypePrefix = PostTypePrefix[result.PostType] result.PostTypePrefix = PostTypePrefix[result.PostType]
if includeBreadcrumbs { if includeBreadcrumbs {
result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, thread) result.Breadcrumbs = GenericThreadBreadcrumbs(lineageBuilder, project, thread)
} }
return result return result

View File

@ -181,8 +181,7 @@ func ProjectIndex(c *RequestContext) ResponseData {
} }
c.Perf.EndBlock() c.Perf.EndBlock()
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Project List")
baseData.Title = "Project List"
var res ResponseData var res ResponseData
res.MustWriteTemplate("project_index.html", ProjectTemplateData{ res.MustWriteTemplate("project_index.html", ProjectTemplateData{
BaseData: baseData, BaseData: baseData,
@ -369,7 +368,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
var projectHomepageData ProjectHomepageData var projectHomepageData ProjectHomepageData
projectHomepageData.BaseData = getBaseData(c) projectHomepageData.BaseData = getBaseData(c, project.Name, nil)
if canEdit { if canEdit {
projectHomepageData.BaseData.Header.EditUrl = hmnurl.BuildProjectEdit(project.Slug, "") projectHomepageData.BaseData.Header.EditUrl = hmnurl.BuildProjectEdit(project.Slug, "")
} }

View File

@ -280,7 +280,7 @@ func (c *RequestContext) ErrorResponse(status int, errs ...error) ResponseData {
StatusCode: status, StatusCode: status,
Errors: errs, Errors: errs,
} }
res.MustWriteTemplate("error.html", getBaseData(c), c.Perf) res.MustWriteTemplate("error.html", getBaseData(c, "", nil), c.Perf)
return res return res
} }

View File

@ -252,7 +252,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
return router return router
} }
func getBaseData(c *RequestContext) templates.BaseData { func getBaseDataAutocrumb(c *RequestContext, title string) templates.BaseData {
return getBaseData(c, title, []templates.Breadcrumb{{Name: title, Url: ""}})
}
// NOTE(asaf): If you set breadcrumbs, the breadcrumb for the current project will automatically be prepended when necessary.
// If you pass nil, no breadcrumbs will be created.
func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadcrumb) templates.BaseData {
var templateUser *templates.User var templateUser *templates.User
var templateSession *templates.Session var templateSession *templates.Session
if c.CurrentUser != nil { if c.CurrentUser != nil {
@ -264,6 +270,17 @@ func getBaseData(c *RequestContext) templates.BaseData {
notices := getNoticesFromCookie(c) notices := getNoticesFromCookie(c)
if len(breadcrumbs) > 0 {
projectUrl := hmnurl.BuildProjectHomepage(c.CurrentProject.Slug)
if breadcrumbs[0].Url != projectUrl {
rootBreadcrumb := templates.Breadcrumb{
Name: c.CurrentProject.Name,
Url: projectUrl,
}
breadcrumbs = append([]templates.Breadcrumb{rootBreadcrumb}, breadcrumbs...)
}
}
episodeGuideUrl := "" episodeGuideUrl := ""
defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug] defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug]
if hasAnnotations { if hasAnnotations {
@ -272,6 +289,8 @@ func getBaseData(c *RequestContext) templates.BaseData {
baseData := templates.BaseData{ baseData := templates.BaseData{
Theme: c.Theme, Theme: c.Theme,
Title: title,
Breadcrumbs: breadcrumbs,
CurrentUrl: c.FullUrl(), CurrentUrl: c.FullUrl(),
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()), LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
@ -365,7 +384,7 @@ func ProjectCSS(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n")) return c.ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n"))
} }
baseData := getBaseData(c) baseData := getBaseData(c, "", nil)
bgColor := noire.NewHex(color) bgColor := noire.NewHex(color)
h, s, l := bgColor.HSL() h, s, l := bgColor.HSL()
@ -408,7 +427,7 @@ func FourOhFour(c *RequestContext) ResponseData {
templates.BaseData templates.BaseData
Wanted string Wanted string
}{ }{
BaseData: getBaseData(c), BaseData: getBaseData(c, "Page not found", nil),
Wanted: c.FullUrl(), Wanted: c.FullUrl(),
} }
res.MustWriteTemplate("404.html", templateData, c.Perf) res.MustWriteTemplate("404.html", templateData, c.Perf)
@ -426,7 +445,7 @@ type RejectData struct {
func RejectRequest(c *RequestContext, reason string) ResponseData { func RejectRequest(c *RequestContext, reason string) ResponseData {
var res ResponseData var res ResponseData
err := res.WriteTemplate("reject.html", RejectData{ err := res.WriteTemplate("reject.html", RejectData{
BaseData: getBaseData(c), BaseData: getBaseData(c, "Rejected", nil),
RejectReason: reason, RejectReason: reason,
}, c.Perf) }, c.Perf)
if err != nil { if err != nil {

View File

@ -53,8 +53,7 @@ func Showcase(c *RequestContext) ResponseData {
jsonItems := templates.TimelineItemsToJSON(showcaseItems) jsonItems := templates.TimelineItemsToJSON(showcaseItems)
c.Perf.EndBlock() c.Perf.EndBlock()
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "Community Showcase")
baseData.Title = "Community Showcase"
var res ResponseData var res ResponseData
res.MustWriteTemplate("showcase.html", ShowcaseData{ res.MustWriteTemplate("showcase.html", ShowcaseData{
BaseData: baseData, BaseData: baseData,

View File

@ -103,7 +103,11 @@ func Snippet(c *RequestContext) ResponseData {
opengraph = append(opengraph, opengraphYoutube...) opengraph = append(opengraph, opengraphYoutube...)
} }
baseData := getBaseData(c) baseData := getBaseData(
c,
fmt.Sprintf("Snippet by %s", snippet.OwnerName),
[]templates.Breadcrumb{{Name: snippet.OwnerName, Url: snippet.OwnerUrl}},
)
baseData.OpenGraphItems = opengraph // NOTE(asaf): We're overriding the defaults on purpose. baseData.OpenGraphItems = opengraph // NOTE(asaf): We're overriding the defaults on purpose.
var res ResponseData var res ResponseData
err = res.WriteTemplate("snippet.html", SnippetData{ err = res.WriteTemplate("snippet.html", SnippetData{

View File

@ -2,42 +2,42 @@ package website
func Manifesto(c *RequestContext) ResponseData { func Manifesto(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("manifesto.html", getBaseData(c), c.Perf) res.MustWriteTemplate("manifesto.html", getBaseDataAutocrumb(c, "Manifesto"), c.Perf)
return res return res
} }
func About(c *RequestContext) ResponseData { func About(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("about.html", getBaseData(c), c.Perf) res.MustWriteTemplate("about.html", getBaseDataAutocrumb(c, "About"), c.Perf)
return res return res
} }
func CodeOfConduct(c *RequestContext) ResponseData { func CodeOfConduct(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("code_of_conduct.html", getBaseData(c), c.Perf) res.MustWriteTemplate("code_of_conduct.html", getBaseDataAutocrumb(c, "Code of Conduct"), c.Perf)
return res return res
} }
func CommunicationGuidelines(c *RequestContext) ResponseData { func CommunicationGuidelines(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("communication_guidelines.html", getBaseData(c), c.Perf) res.MustWriteTemplate("communication_guidelines.html", getBaseDataAutocrumb(c, "Communication Guidelines"), c.Perf)
return res return res
} }
func ContactPage(c *RequestContext) ResponseData { func ContactPage(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("contact.html", getBaseData(c), c.Perf) res.MustWriteTemplate("contact.html", getBaseDataAutocrumb(c, "Contact Us"), c.Perf)
return res return res
} }
func MonthlyUpdatePolicy(c *RequestContext) ResponseData { func MonthlyUpdatePolicy(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("monthly_update_policy.html", getBaseData(c), c.Perf) res.MustWriteTemplate("monthly_update_policy.html", getBaseDataAutocrumb(c, "Monthly Update Policy"), c.Perf)
return res return res
} }
func ProjectSubmissionGuidelines(c *RequestContext) ResponseData { func ProjectSubmissionGuidelines(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
res.MustWriteTemplate("project_submission_guidelines.html", getBaseData(c), c.Perf) res.MustWriteTemplate("project_submission_guidelines.html", getBaseDataAutocrumb(c, "Project Submission Guidelines"), c.Perf)
return res return res
} }

View File

@ -1,10 +0,0 @@
package website
import (
"git.handmade.network/hmn/hmn/src/models"
)
var ThreadTypeDisplayNames = map[models.ThreadType]string{
models.ThreadTypeProjectBlogPost: "Blog",
models.ThreadTypeForumPost: "Forums",
}

View File

@ -70,7 +70,7 @@ func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *mod
Description: "", // NOTE(asaf): No description for posts Description: "", // NOTE(asaf): No description for posts
Title: thread.Title, Title: thread.Title,
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, thread), Breadcrumbs: GenericThreadBreadcrumbs(lineageBuilder, project, thread),
} }
} }

View File

@ -1,23 +0,0 @@
package website
import (
"math"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/utils"
)
func NumPages(numThings, thingsPerPage int) int {
return utils.IntMax(int(math.Ceil(float64(numThings)/float64(thingsPerPage))), 1)
}
func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) string {
switch kind {
case models.ThreadTypeProjectBlogPost:
return hmnurl.BuildBlog(projectSlug, 1)
case models.ThreadTypeForumPost:
return hmnurl.BuildForum(projectSlug, nil, 1)
}
return hmnurl.BuildProjectHomepage(projectSlug)
}

View File

@ -224,8 +224,7 @@ func UserProfile(c *RequestContext) ResponseData {
templateUser := templates.UserToTemplate(profileUser, c.Theme) templateUser := templates.UserToTemplate(profileUser, c.Theme)
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, templateUser.Name)
baseData.Title = templateUser.Name
var res ResponseData var res ResponseData
res.MustWriteTemplate("user_profile.html", UserProfileTemplateData{ res.MustWriteTemplate("user_profile.html", UserProfileTemplateData{
@ -322,8 +321,7 @@ func UserSettings(c *RequestContext) ResponseData {
templateUser := templates.UserToTemplate(c.CurrentUser, c.Theme) templateUser := templates.UserToTemplate(c.CurrentUser, c.Theme)
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, templateUser.Name)
baseData.Title = templateUser.Name
res.MustWriteTemplate("user_settings.html", UserSettingsTemplateData{ res.MustWriteTemplate("user_settings.html", UserSettingsTemplateData{
BaseData: baseData, BaseData: baseData,

View File

@ -25,8 +25,7 @@ func WhenIsIt(c *RequestContext) ResponseData {
hasTimestamp = (err == nil) hasTimestamp = (err == nil)
} }
baseData := getBaseData(c) baseData := getBaseDataAutocrumb(c, "When is it?")
baseData.Title = "When is it?"
baseData.OpenGraphItems = append(baseData.OpenGraphItems, templates.OpenGraphItem{ baseData.OpenGraphItems = append(baseData.OpenGraphItems, templates.OpenGraphItem{
Property: "og:title", Property: "og:title",