Add blog indexes

This commit is contained in:
Ben Visness 2021-08-02 20:52:46 -05:00
parent b9645a6315
commit c3e067fa44
12 changed files with 265 additions and 29 deletions

View File

@ -7669,7 +7669,6 @@ header {
.content {
background-color: #f8f8f8;
background-color: var(--content-background);
text-align: center;
margin: auto; }
.content p {
-moz-text-size-adjust: auto;
@ -7875,7 +7874,9 @@ header {
.background-even:nth-of-type(even) {
background-color: #f8f8f8;
background-color: var(--background-even-background); }
background-color: var(--background-even-background);
--fade-color: #f8f8f8;
--fade-color: var(--background-even-background); }
.userlist {
text-align: center;
@ -8552,6 +8553,14 @@ input[type=submit] {
background-color: #bbb;
background-color: var(--dimmest-color); }
:root {
--fade-color: #f8f8f8;
--fade-color: var(--content-background); }
.excerpt-fade {
background-image: linear-gradient(to top, var(--fade-color), rgba(0, 0, 0, 0));
pointer-events: none; }
.read {
color: #555;
color: var(--forum-thread-read-color); }
@ -9123,10 +9132,6 @@ span.icon-rss::before {
width: 10rem;
height: 10rem; } }
.landing .excerpt-fade {
background-image: linear-gradient(to top, var(--content-background), rgba(0, 0, 0, 0));
pointer-events: none; }
.star-btn {
border-bottom-width: 2px;
background-color: #fff;

View File

@ -0,0 +1,44 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4"
)
func init() {
registerMigration(DefaultNotSticky{})
}
type DefaultNotSticky struct{}
func (m DefaultNotSticky) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 8, 3, 1, 48, 12, 0, time.UTC))
}
func (m DefaultNotSticky) Name() string {
return "DefaultNotSticky"
}
func (m DefaultNotSticky) Description() string {
return "Make sticky default to false"
}
func (m DefaultNotSticky) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
ALTER TABLE handmade_thread
ALTER sticky SET DEFAULT FALSE;
`)
if err != nil {
return oops.New(err, "failed to set default")
}
return nil
}
func (m DefaultNotSticky) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me")
}

View File

@ -581,7 +581,6 @@ footer {
.content {
@include usevar(background-color, content-background);
text-align:center;
margin:auto;
p {
@ -874,6 +873,7 @@ footer {
}
.background-even:nth-of-type(even) {
@include usevar(background-color, background-even-background);
// this is the default, and should be overridden by dynamic colors.
@include usevar(background-color, background-even-background);
@include usevar(--fade-color, background-even-background);
}

View File

@ -59,6 +59,15 @@
@include usevar(background-color, dimmest-color);
}
:root {
@include usevar(--fade-color, 'content-background');
}
.excerpt-fade {
background-image: linear-gradient(to top, var(--fade-color), rgba(0, 0, 0, 0));
pointer-events: none;
}
.read {
@include usevar('color', 'forum-thread-read-color');

View File

@ -83,9 +83,4 @@
height: 10rem;
}
}
.excerpt-fade {
background-image: linear-gradient(to top, var(--content-background) , rgba(0, 0, 0, 0));
pointer-events: none;
}
}

View File

@ -0,0 +1,41 @@
{{ template "base.html" . }}
{{ define "content" }}
<div class="optionbar">
<div class="options">
<a class="button" href="{{ .NewPostUrl }}"><span class="big pr1">+</span> Create Post</a>
</div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div>
{{/* TODO: Breadcrumbs, or some other link back to the blog index */}}
{{ range .Posts }}
<div class="flex items-start ph3 pv3 background-even">
<img class="avatar-icon mr2" src="{{ .Author.AvatarUrl }}">
<div class="flex-grow-1 overflow-hidden">
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details">
<a class="user" href="{{ .Author.ProfileUrl }}">{{ .Author.Name }}</a> &mdash; {{ timehtml (relativedate .Date) .Date }}
</div>
<div class="overflow-hidden mh-5 mt2 relative">
<div>
{{ .Content }}
</div>
<div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div>
</div>
<div class="mt2">
<a href="{{ .Url }}">Read More &rarr;</a>
</div>
</div>
</div>
{{ end }}
<div class="optionbar bottom">
<div class="options">
<a class="button" href="{{ .NewPostUrl }}"><span class="big pr1">+</span> Create Post</a>
</div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div>
{{ end }}

View File

@ -1,4 +1,4 @@
<footer class="pa3 pa4-l">
<footer class="pa3 pa4-l tc">
<h2>
Community by <a href="{{ .Footer.HomepageUrl }}">handmade.network</a>
</h2>

View File

@ -2,16 +2,127 @@ package website
import (
"fmt"
"html/template"
"net/http"
"strconv"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
)
func BlogIndex(c *RequestContext) ResponseData {
type blogIndexEntry struct {
Title string
Url string
Author templates.User
Date time.Time
Content template.HTML
}
type blogIndexData struct {
templates.BaseData
Posts []blogIndexEntry
Pagination templates.Pagination
NewPostUrl string
}
const postsPerPage = 5
c.Perf.StartBlock("SQL", "Fetch count of posts")
numPosts, err := db.QueryInt(c.Context(), c.Conn,
`
SELECT COUNT(*)
FROM
handmade_thread
WHERE
project_id = $1
AND type = $2
AND NOT deleted
`,
c.CurrentProject.ID,
models.ThreadTypeProjectBlogPost,
)
c.Perf.EndBlock()
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts"))
}
numPages := NumPages(numPosts, postsPerPage)
page, ok := ParsePageNumber(c, "page", numPages)
if !ok {
c.Redirect(hmnurl.BuildBlog(c.CurrentProject.Slug, page), http.StatusSeeOther)
}
type blogIndexQuery struct {
Thread models.Thread `db:"thread"`
Post models.Post `db:"post"`
CurrentVersion models.PostVersion `db:"ver"`
Author *models.User `db:"author"`
}
c.Perf.StartBlock("SQL", "Fetch blog posts")
postsResult, err := db.Query(c.Context(), c.Conn, blogIndexQuery{},
`
SELECT $columns
FROM
handmade_thread AS thread
JOIN handmade_post AS post ON thread.first_id = post.id
JOIN handmade_postversion AS ver ON post.current_id = ver.id
LEFT JOIN auth_user AS author ON post.author_id = author.id
WHERE
post.project_id = $1
AND post.thread_type = $2
AND NOT thread.deleted
ORDER BY post.postdate DESC
LIMIT $3 OFFSET $4
`,
c.CurrentProject.ID,
models.ThreadTypeProjectBlogPost,
postsPerPage,
(page-1)*postsPerPage,
)
c.Perf.EndBlock()
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch blog posts for index"))
}
var entries []blogIndexEntry
for _, irow := range postsResult.ToSlice() {
row := irow.(*blogIndexQuery)
entries = append(entries, blogIndexEntry{
Title: row.Thread.Title,
Url: hmnurl.BuildBlogThread(c.CurrentProject.Slug, row.Thread.ID, row.Thread.Title),
Author: templates.UserToTemplate(row.Author, c.Theme),
Date: row.Post.PostDate,
Content: template.HTML(row.CurrentVersion.TextParsed),
})
}
baseData := getBaseData(c)
baseData.Title = fmt.Sprintf("%s Blog", c.CurrentProject.Name)
var res ResponseData
res.MustWriteTemplate("blog_index.html", blogIndexData{
BaseData: baseData,
Posts: entries,
Pagination: templates.Pagination{
Current: page,
Total: numPages,
FirstUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
LastUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, numPages),
PreviousUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, utils.IntClamp(1, page-1, numPages)),
NextUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, utils.IntClamp(1, page+1, numPages)),
},
NewPostUrl: hmnurl.BuildBlogNewThread(c.CurrentProject.Slug),
}, c.Perf)
return res
}
func BlogThread(c *RequestContext) ResponseData {
type blogPostData struct {
templates.BaseData

View File

@ -2,7 +2,6 @@ package website
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
@ -99,21 +98,11 @@ func Forum(c *RequestContext) ResponseData {
}
c.Perf.EndBlock()
numPages := utils.IntMax(int(math.Ceil(float64(numThreads)/threadsPerPage)), 1)
page := 1
pageString, hasPage := c.PathParams["page"]
if hasPage && pageString != "" {
if pageParsed, err := strconv.Atoi(pageString); err == nil {
page = pageParsed
} else {
return c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther)
numPages := NumPages(numThreads, threadsPerPage)
page, ok := ParsePageNumber(c, "page", numPages)
if !ok {
c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, page), http.StatusSeeOther)
}
}
if page < 1 || numPages < page {
return c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
}
howManyThreadsToSkip := (page - 1) * threadsPerPage
var currentUserId *int

View File

@ -0,0 +1,34 @@
package website
import (
"strconv"
"git.handmade.network/hmn/hmn/src/utils"
)
/*
Parses a path param as a page number, and returns the parsed result and
a value indicating whether parsing was successful.
The returned page number is always valid, even when parsing fails. If
parsing fails (ok is false), you should redirect to the returned
page number.
*/
func ParsePageNumber(
c *RequestContext,
paramName string,
numPages int,
) (page int, ok bool) {
if pageString, hasPage := c.PathParams[paramName]; hasPage && pageString != "" {
if pageParsed, err := strconv.Atoi(pageString); err == nil {
page = pageParsed
} else {
return 1, false
}
}
if page < 1 || numPages < page {
return utils.IntClamp(1, page, numPages), false
}
return page, true
}

View File

@ -166,6 +166,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
mainRoutes.GET(hmnurl.RegexBlog, BlogIndex)
mainRoutes.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread))
mainRoutes.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit)))
mainRoutes.GET(hmnurl.RegexBlogThread, BlogThread)

View File

@ -1,10 +1,17 @@
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: