From d78a2e8e8234344a40936759865076d2b102f28d Mon Sep 17 00:00:00 2001 From: Asaf Gartner Date: Wed, 1 Sep 2021 21:25:09 +0300 Subject: [PATCH] Breadcrumbs --- src/templates/src/layouts/base.html | 2 +- src/utils/utils.go | 5 +++ src/website/auth.go | 20 ++++----- src/website/blogs.go | 47 ++++++++++++--------- src/website/breadcrumb_helper.go | 63 +++++++++++++++++++++++++++++ src/website/episode_guide.go | 9 +++-- src/website/feed.go | 2 +- src/website/forums.go | 61 ++++++++++------------------ src/website/jam.go | 3 +- src/website/landing.go | 2 +- src/website/podcast.go | 32 ++++++++++----- src/website/post_helper.go | 48 +++++++++++++--------- src/website/projects.go | 5 +-- src/website/requesthandling.go | 2 +- src/website/routes.go | 29 ++++++++++--- src/website/showcase.go | 3 +- src/website/snippet.go | 6 ++- src/website/staticpages.go | 14 +++---- src/website/subforum_helper.go | 10 ----- src/website/timeline_helper.go | 2 +- src/website/urls.go | 23 ----------- src/website/user.go | 6 +-- src/website/whenisit.go | 3 +- 23 files changed, 233 insertions(+), 164 deletions(-) create mode 100644 src/website/breadcrumb_helper.go delete mode 100644 src/website/subforum_helper.go delete mode 100644 src/website/urls.go diff --git a/src/templates/src/layouts/base.html b/src/templates/src/layouts/base.html index 2a6d224e..5bf931e5 100644 --- a/src/templates/src/layouts/base.html +++ b/src/templates/src/layouts/base.html @@ -88,7 +88,7 @@ {{- if gt $i 0 -}} » {{- end -}} - {{ .Name }} + {{ .Name }} {{- end }} {{ end }} diff --git a/src/utils/utils.go b/src/utils/utils.go index 7920a40f..f739e0c4 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "time" "git.handmade.network/hmn/hmn/src/oops" @@ -34,6 +35,10 @@ func Int64Max(a, b int64) int64 { 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: diff --git a/src/website/auth.go b/src/website/auth.go index e21ba862..7f86f1eb 100644 --- a/src/website/auth.go +++ b/src/website/auth.go @@ -33,7 +33,7 @@ func LoginPage(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("auth_login.html", LoginPageData{ - BaseData: getBaseData(c), + BaseData: getBaseDataAutocrumb(c, "Log in"), RedirectUrl: c.Req.URL.Query().Get("redirect"), ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(), }, c.Perf) @@ -63,7 +63,7 @@ func Login(c *RequestContext) ResponseData { showLoginWithFailure := func(c *RequestContext, redirect string) ResponseData { var res ResponseData - baseData := getBaseData(c) + baseData := getBaseDataAutocrumb(c, "Log in") baseData.AddImmediateNotice("failure", "Incorrect username or password") res.MustWriteTemplate("auth_login.html", LoginPageData{ BaseData: baseData, @@ -122,7 +122,7 @@ func RegisterNewUser(c *RequestContext) ResponseData { } // TODO(asaf): Do something to prevent bot registration var res ResponseData - res.MustWriteTemplate("auth_register.html", getBaseData(c), c.Perf) + res.MustWriteTemplate("auth_register.html", getBaseDataAutocrumb(c, "Register"), c.Perf) return res } @@ -275,7 +275,7 @@ func RegisterNewUserSuccess(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("auth_register_success.html", RegisterNewUserSuccessData{ - BaseData: getBaseData(c), + BaseData: getBaseDataAutocrumb(c, "Register"), ContactUsUrl: hmnurl.BuildContactPage(), }, c.Perf) return res @@ -321,7 +321,7 @@ func EmailConfirmation(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ - BaseData: getBaseData(c), + BaseData: getBaseDataAutocrumb(c, "Register"), Token: token, Username: username, }, c.Perf) @@ -346,11 +346,11 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { return c.ErrorResponse(http.StatusInternalServerError, err) } else if !success { 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. baseData.AddImmediateNotice("failure", "Incorrect password. Please try again.") res.MustWriteTemplate("auth_email_validation.html", EmailValidationData{ - BaseData: getBaseData(c), + BaseData: baseData, Token: token, Username: username, }, c.Perf) @@ -424,7 +424,7 @@ func RequestPasswordReset(c *RequestContext) ResponseData { return c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) } 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 } @@ -548,7 +548,7 @@ func PasswordResetSent(c *RequestContext) ResponseData { } var res ResponseData res.MustWriteTemplate("auth_password_reset_sent.html", PasswordResetSentData{ - BaseData: getBaseData(c), + BaseData: getBaseDataAutocrumb(c, "Password Reset"), ContactUsUrl: hmnurl.BuildContactPage(), }, c.Perf) return res @@ -582,7 +582,7 @@ func DoPasswordReset(c *RequestContext) ResponseData { } res.MustWriteTemplate("auth_do_password_reset.html", DoPasswordResetData{ - BaseData: getBaseData(c), + BaseData: getBaseDataAutocrumb(c, "Password Reset"), Username: username, Token: token, }, c.Perf) diff --git a/src/website/blogs.go b/src/website/blogs.go index 6555d8ba..17f449c8 100644 --- a/src/website/blogs.go +++ b/src/website/blogs.go @@ -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")) } - numPages := NumPages(numPosts, postsPerPage) + numPages := utils.NumPages(numPosts, postsPerPage) page, ok := ParsePageNumber(c, "page", numPages) if !ok { c.Redirect(hmnurl.BuildBlog(c.CurrentProject.Slug, page), http.StatusSeeOther) @@ -104,8 +104,7 @@ func BlogIndex(c *RequestContext) ResponseData { }) } - baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("%s Blog", c.CurrentProject.Name) + baseData := getBaseData(c, fmt.Sprintf("%s Blog", c.CurrentProject.Name), []templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)}) canCreate := false if c.CurrentUser != nil { @@ -181,8 +180,7 @@ func BlogThread(c *RequestContext) ResponseData { templatePosts = append(templatePosts, post) } - baseData := getBaseData(c) - baseData.Title = thread.Title + baseData := getBaseData(c, thread.Title, []templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)}) var res ResponseData res.MustWriteTemplate("blog_post.html", blogPostData{ @@ -209,9 +207,11 @@ func BlogPostRedirectToThread(c *RequestContext) ResponseData { } func BlogNewThread(c *RequestContext) ResponseData { - baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name) - // TODO(ben): Set breadcrumbs + baseData := getBaseData( + c, + fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name), + []templates.Breadcrumb{BlogBreadcrumb(c.CurrentProject.Slug)}, + ) editData := getEditorDataForNew(baseData, nil) 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) - baseData := getBaseData(c) + title := "" 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 { - 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.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) - baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Replying to comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name) - // TODO(ben): Set breadcrumbs + baseData := getBaseData( + c, + 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.AddContentVersion(postData.CurrentVersion, postData.Editor) @@ -412,12 +418,17 @@ func BlogPostDelete(c *RequestContext) ResponseData { postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) - baseData := getBaseData(c) + title := "" 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 { - 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 templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme) diff --git a/src/website/breadcrumb_helper.go b/src/website/breadcrumb_helper.go new file mode 100644 index 00000000..0f69794a --- /dev/null +++ b/src/website/breadcrumb_helper.go @@ -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 +} diff --git a/src/website/episode_guide.go b/src/website/episode_guide.go index 4585b6b4..6be4069f 100644 --- a/src/website/episode_guide.go +++ b/src/website/episode_guide.go @@ -88,7 +88,7 @@ func EpisodeList(c *RequestContext) ResponseData { } var res ResponseData - baseData := getBaseData(c) + baseData := getBaseDataAutocrumb(c, fmt.Sprintf("Episode Guide")) res.MustWriteTemplate("episode_list.html", EpisodeListData{ BaseData: baseData, Content: template.HTML(guide), @@ -147,8 +147,11 @@ func Episode(c *RequestContext) ResponseData { content := contentMatches[episodeContentRegex.SubexpIndex("content")] var res ResponseData - baseData := getBaseData(c) - baseData.Title = title + baseData := getBaseData( + c, + title, + []templates.Breadcrumb{{Name: "Episode Guide", Url: hmnurl.BuildEpisodeList(c.CurrentProject.Slug, foundTopic)}}, + ) res.MustWriteTemplate("episode.html", EpisodeData{ BaseData: baseData, Content: template.HTML(content), diff --git a/src/website/feed.go b/src/website/feed.go index 6ec3ae05..db8831cd 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -90,7 +90,7 @@ func Feed(c *RequestContext) ResponseData { 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") var res ResponseData diff --git a/src/website/forums.go b/src/website/forums.go index aecf82af..d2dc4ce6 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -100,7 +100,7 @@ func Forum(c *RequestContext) ResponseData { } c.Perf.EndBlock() - numPages := NumPages(numThreads, threadsPerPage) + numPages := utils.NumPages(numThreads, threadsPerPage) page, ok := ParsePageNumber(c, "page", numPages) if !ok { c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, page), http.StatusSeeOther) @@ -259,27 +259,11 @@ func Forum(c *RequestContext) ResponseData { // Template assembly // --------------------- - baseData := getBaseData(c) - baseData.Title = c.CurrentProject.Name + " Forums" - baseData.Breadcrumbs = []templates.Breadcrumb{ - { - 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), - }) - } + baseData := getBaseData( + c, + fmt.Sprintf("%s Forums", c.CurrentProject.Name), + SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID), + ) var res ResponseData res.MustWriteTemplate("forum.html", forumData{ @@ -511,9 +495,7 @@ func ForumThread(c *RequestContext) ResponseData { } } - baseData := getBaseData(c) - baseData.Title = thread.Title - // TODO(asaf): Set breadcrumbs + baseData := getBaseData(c, thread.Title, SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID)) var res ResponseData res.MustWriteTemplate("forum_thread.html", forumThreadData{ @@ -596,15 +578,12 @@ func ForumPostRedirect(c *RequestContext) ResponseData { } func ForumNewThread(c *RequestContext) ResponseData { - baseData := getBaseData(c) - baseData.Title = "Create New Thread" - // TODO(ben): Set breadcrumbs - cd, ok := getCommonForumData(c) if !ok { return FourOhFour(c) } + baseData := getBaseData(c, "Create New Thread", SubforumBreadcrumbs(cd.LineageBuilder, c.CurrentProject, cd.SubforumID)) editData := getEditorDataForNew(baseData, nil) editData.SubmitUrl = hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), true) editData.SubmitLabel = "Post New Thread" @@ -683,9 +662,11 @@ func ForumPostReply(c *RequestContext) ResponseData { postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) - baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Replying to post | %s", cd.SubforumTree[cd.SubforumID].Name) - // TODO(ben): Set breadcrumbs + baseData := getBaseData( + c, + 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.AddContentVersion(postData.CurrentVersion, postData.Editor) @@ -743,13 +724,13 @@ func ForumPostEdit(c *RequestContext) ResponseData { postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID) - baseData := getBaseData(c) + title := "" 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 { - 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.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) - baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) - // TODO(ben): Set breadcrumbs + baseData := getBaseData( + c, + 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.AddContentVersion(postData.CurrentVersion, postData.Editor) diff --git a/src/website/jam.go b/src/website/jam.go index a8998e9f..6a715c55 100644 --- a/src/website/jam.go +++ b/src/website/jam.go @@ -13,8 +13,7 @@ func JamIndex(c *RequestContext) ResponseData { ogimageurl = urljoin(current_site_host(), ogimagepath) */ - baseData := getBaseData(c) - baseData.Title = "Wheel Reinvention Jam" + baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam") baseData.OpenGraphItems = []templates.OpenGraphItem{ {Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:type", Value: "website"}, diff --git a/src/website/landing.go b/src/website/landing.go index 0c02aae4..ff82df32 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -316,7 +316,7 @@ func Index(c *RequestContext) ResponseData { showcaseJson := templates.TimelineItemsToJSON(showcaseItems) c.Perf.EndBlock() - baseData := getBaseData(c) + baseData := getBaseData(c, "", nil) baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more? var res ResponseData diff --git a/src/website/podcast.go b/src/website/podcast.go index af280622..dc2c7fb2 100644 --- a/src/website/podcast.go +++ b/src/website/podcast.go @@ -44,8 +44,7 @@ func PodcastIndex(c *RequestContext) ResponseData { return c.ErrorResponse(http.StatusInternalServerError, err) } - baseData := getBaseData(c) - baseData.Title = podcastResult.Podcast.Title + baseData := getBaseDataAutocrumb(c, podcastResult.Podcast.Title) podcastIndexData := PodcastIndexData{ BaseData: baseData, @@ -89,8 +88,11 @@ func PodcastEdit(c *RequestContext) ResponseData { } podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile) - baseData := getBaseData(c) - baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} + baseData := getBaseData( + c, + fmt.Sprintf("Edit %s", podcast.Title), + []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}}, + ) podcastEditData := PodcastEditData{ BaseData: baseData, Podcast: podcast, @@ -229,9 +231,11 @@ func PodcastEpisode(c *RequestContext) ResponseData { podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, podcastResult.ImageFile) episode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, podcastResult.Episodes[0], 0, podcastResult.ImageFile) - baseData := getBaseData(c) - baseData.Title = podcastResult.Podcast.Title - baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} + baseData := getBaseData( + c, + fmt.Sprintf("%s | %s", episode.Title, podcast.Title), + []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}}, + ) podcastEpisodeData := PodcastEpisodeData{ BaseData: baseData, @@ -280,8 +284,11 @@ func PodcastEpisodeNew(c *RequestContext) ResponseData { podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "") var res ResponseData - baseData := getBaseData(c) - baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}} + baseData := getBaseData( + c, + fmt.Sprintf("New episode | %s", podcast.Title), + []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}}, + ) err = res.WriteTemplate("podcast_episode_edit.html", PodcastEpisodeEditData{ BaseData: baseData, IsEdit: false, @@ -321,8 +328,11 @@ func PodcastEpisodeEdit(c *RequestContext) ResponseData { podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "") podcastEpisode := templates.PodcastEpisodeToTemplate(c.CurrentProject.Slug, episode, 0, "") - baseData := getBaseData(c) - baseData.Breadcrumbs = []templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}, {Name: podcastEpisode.Title, Url: podcastEpisode.Url}} + baseData := getBaseData( + 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{ BaseData: baseData, IsEdit: true, diff --git a/src/website/post_helper.go b/src/website/post_helper.go index ce368d26..767c9f82 100644 --- a/src/website/post_helper.go +++ b/src/website/post_helper.go @@ -31,30 +31,40 @@ var PostTypePrefix = map[templates.PostType]string{ 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 - result = append(result, templates.Breadcrumb{ - Name: project.Name, - Url: hmnurl.BuildProjectHomepage(project.Slug), - }) - result = append(result, templates.Breadcrumb{ - Name: ThreadTypeDisplayNames[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), - }) + if thread.Type == models.ThreadTypeForumPost { + result = SubforumBreadcrumbs(lineageBuilder, project, *thread.SubforumID) + } else { + result = []templates.Breadcrumb{ + { + Name: project.Name, + Url: hmnurl.BuildProjectHomepage(project.Slug), + }, + { + Name: ThreadTypeDisplayNames[thread.Type], + Url: BuildProjectRootResourceUrl(project.Slug, thread.Type), + }, } } 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( lineageBuilder *models.SubforumLineageBuilder, project *models.Project, @@ -87,7 +97,7 @@ func MakePostListItem( result.PostTypePrefix = PostTypePrefix[result.PostType] if includeBreadcrumbs { - result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, thread) + result.Breadcrumbs = GenericThreadBreadcrumbs(lineageBuilder, project, thread) } return result diff --git a/src/website/projects.go b/src/website/projects.go index 60157559..f1facd86 100644 --- a/src/website/projects.go +++ b/src/website/projects.go @@ -181,8 +181,7 @@ func ProjectIndex(c *RequestContext) ResponseData { } c.Perf.EndBlock() - baseData := getBaseData(c) - baseData.Title = "Project List" + baseData := getBaseDataAutocrumb(c, "Project List") var res ResponseData res.MustWriteTemplate("project_index.html", ProjectTemplateData{ BaseData: baseData, @@ -369,7 +368,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { var projectHomepageData ProjectHomepageData - projectHomepageData.BaseData = getBaseData(c) + projectHomepageData.BaseData = getBaseData(c, project.Name, nil) if canEdit { projectHomepageData.BaseData.Header.EditUrl = hmnurl.BuildProjectEdit(project.Slug, "") } diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index daeebd8d..ec71dff5 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -280,7 +280,7 @@ func (c *RequestContext) ErrorResponse(status int, errs ...error) ResponseData { StatusCode: status, Errors: errs, } - res.MustWriteTemplate("error.html", getBaseData(c), c.Perf) + res.MustWriteTemplate("error.html", getBaseData(c, "", nil), c.Perf) return res } diff --git a/src/website/routes.go b/src/website/routes.go index dfaa10e8..3d48dc9d 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -252,7 +252,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe 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 templateSession *templates.Session if c.CurrentUser != nil { @@ -264,6 +270,17 @@ func getBaseData(c *RequestContext) templates.BaseData { 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 := "" defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug] if hasAnnotations { @@ -271,7 +288,9 @@ func getBaseData(c *RequestContext) templates.BaseData { } baseData := templates.BaseData{ - Theme: c.Theme, + Theme: c.Theme, + Title: title, + Breadcrumbs: breadcrumbs, CurrentUrl: 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")) } - baseData := getBaseData(c) + baseData := getBaseData(c, "", nil) bgColor := noire.NewHex(color) h, s, l := bgColor.HSL() @@ -408,7 +427,7 @@ func FourOhFour(c *RequestContext) ResponseData { templates.BaseData Wanted string }{ - BaseData: getBaseData(c), + BaseData: getBaseData(c, "Page not found", nil), Wanted: c.FullUrl(), } res.MustWriteTemplate("404.html", templateData, c.Perf) @@ -426,7 +445,7 @@ type RejectData struct { func RejectRequest(c *RequestContext, reason string) ResponseData { var res ResponseData err := res.WriteTemplate("reject.html", RejectData{ - BaseData: getBaseData(c), + BaseData: getBaseData(c, "Rejected", nil), RejectReason: reason, }, c.Perf) if err != nil { diff --git a/src/website/showcase.go b/src/website/showcase.go index 3577fa15..52c328a6 100644 --- a/src/website/showcase.go +++ b/src/website/showcase.go @@ -53,8 +53,7 @@ func Showcase(c *RequestContext) ResponseData { jsonItems := templates.TimelineItemsToJSON(showcaseItems) c.Perf.EndBlock() - baseData := getBaseData(c) - baseData.Title = "Community Showcase" + baseData := getBaseDataAutocrumb(c, "Community Showcase") var res ResponseData res.MustWriteTemplate("showcase.html", ShowcaseData{ BaseData: baseData, diff --git a/src/website/snippet.go b/src/website/snippet.go index 08d5c86d..a5397238 100644 --- a/src/website/snippet.go +++ b/src/website/snippet.go @@ -103,7 +103,11 @@ func Snippet(c *RequestContext) ResponseData { 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. var res ResponseData err = res.WriteTemplate("snippet.html", SnippetData{ diff --git a/src/website/staticpages.go b/src/website/staticpages.go index 9eb5cedf..16ee2168 100644 --- a/src/website/staticpages.go +++ b/src/website/staticpages.go @@ -2,42 +2,42 @@ package website func Manifesto(c *RequestContext) ResponseData { var res ResponseData - res.MustWriteTemplate("manifesto.html", getBaseData(c), c.Perf) + res.MustWriteTemplate("manifesto.html", getBaseDataAutocrumb(c, "Manifesto"), c.Perf) return res } func About(c *RequestContext) ResponseData { var res ResponseData - res.MustWriteTemplate("about.html", getBaseData(c), c.Perf) + res.MustWriteTemplate("about.html", getBaseDataAutocrumb(c, "About"), c.Perf) return res } func CodeOfConduct(c *RequestContext) 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 } func CommunicationGuidelines(c *RequestContext) 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 } func ContactPage(c *RequestContext) ResponseData { var res ResponseData - res.MustWriteTemplate("contact.html", getBaseData(c), c.Perf) + res.MustWriteTemplate("contact.html", getBaseDataAutocrumb(c, "Contact Us"), c.Perf) return res } func MonthlyUpdatePolicy(c *RequestContext) 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 } func ProjectSubmissionGuidelines(c *RequestContext) 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 } diff --git a/src/website/subforum_helper.go b/src/website/subforum_helper.go deleted file mode 100644 index f15def2e..00000000 --- a/src/website/subforum_helper.go +++ /dev/null @@ -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", -} diff --git a/src/website/timeline_helper.go b/src/website/timeline_helper.go index 4dac16ce..3add832a 100644 --- a/src/website/timeline_helper.go +++ b/src/website/timeline_helper.go @@ -70,7 +70,7 @@ func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *mod Description: "", // NOTE(asaf): No description for posts Title: thread.Title, - Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, thread), + Breadcrumbs: GenericThreadBreadcrumbs(lineageBuilder, project, thread), } } diff --git a/src/website/urls.go b/src/website/urls.go deleted file mode 100644 index 69bb4b15..00000000 --- a/src/website/urls.go +++ /dev/null @@ -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) -} diff --git a/src/website/user.go b/src/website/user.go index 2ec35d1a..099fbb03 100644 --- a/src/website/user.go +++ b/src/website/user.go @@ -224,8 +224,7 @@ func UserProfile(c *RequestContext) ResponseData { templateUser := templates.UserToTemplate(profileUser, c.Theme) - baseData := getBaseData(c) - baseData.Title = templateUser.Name + baseData := getBaseDataAutocrumb(c, templateUser.Name) var res ResponseData res.MustWriteTemplate("user_profile.html", UserProfileTemplateData{ @@ -322,8 +321,7 @@ func UserSettings(c *RequestContext) ResponseData { templateUser := templates.UserToTemplate(c.CurrentUser, c.Theme) - baseData := getBaseData(c) - baseData.Title = templateUser.Name + baseData := getBaseDataAutocrumb(c, templateUser.Name) res.MustWriteTemplate("user_settings.html", UserSettingsTemplateData{ BaseData: baseData, diff --git a/src/website/whenisit.go b/src/website/whenisit.go index 871d6912..eadf590c 100644 --- a/src/website/whenisit.go +++ b/src/website/whenisit.go @@ -25,8 +25,7 @@ func WhenIsIt(c *RequestContext) ResponseData { hasTimestamp = (err == nil) } - baseData := getBaseData(c) - baseData.Title = "When is it?" + baseData := getBaseDataAutocrumb(c, "When is it?") baseData.OpenGraphItems = append(baseData.OpenGraphItems, templates.OpenGraphItem{ Property: "og:title",