diff --git a/public/style.css b/public/style.css index ba65c40..5b691bb 100644 --- a/public/style.css +++ b/public/style.css @@ -8587,16 +8587,6 @@ input[type=submit] { color: var(--forum-thread-read-link-color); } .thread.read .title { font-weight: 500; } - .thread .goto { - font-size: 200%; - width: 30px; } - .thread .goto a { - display: block; - padding: 0px 10px; - box-sizing: border-box; - position: relative; - line-height: 100%; - background-color: transparent; } .forum .thread .info th { width: 50px; } @@ -8621,6 +8611,17 @@ input[type=submit] { left: 0px; bottom: -10px; } +.goto { + font-size: 200%; + width: 30px; } + .goto a { + display: block; + padding: 0px 10px; + box-sizing: border-box; + position: relative; + line-height: 100%; + background-color: transparent; } + .badge { display: inline-block; border-radius: 1000em; diff --git a/src/db/db.go b/src/db/db.go index 3407b1f..c79a7e8 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -169,7 +169,7 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin fieldType = destType.Elem() } - _, isRecognizedByPgtype := connInfo.DataTypeForValue(reflect.New(fieldType)) // if pgtype recognizes it, we don't need to dig in further for more `db` tags + _, isRecognizedByPgtype := connInfo.DataTypeForValue(reflect.New(fieldType).Elem().Interface()) // if pgtype recognizes it, we don't need to dig in further for more `db` tags // NOTE: boy it would be nice if we didn't have to do reflect.New here, considering that pgtype is just doing reflection on the value anyway if fieldType.Kind() == reflect.Struct && !isRecognizedByPgtype { diff --git a/src/hmnurl/hmnurl.go b/src/hmnurl/hmnurl.go index 166c268..634baf9 100644 --- a/src/hmnurl/hmnurl.go +++ b/src/hmnurl/hmnurl.go @@ -55,7 +55,7 @@ func StaticThemeUrl(path string, theme string, query []Q) string { } func trim(path string) string { - if path[0] == '/' { + if len(path) > 0 && path[0] == '/' { return path[1:] } return path diff --git a/src/models/post.go b/src/models/post.go index c88d403..660507a 100644 --- a/src/models/post.go +++ b/src/models/post.go @@ -17,7 +17,7 @@ type Post struct { Depth int `db:"depth"` Slug string `db:"slug"` - AuthorName string `db:"author_name"` + AuthorName string `db:"author_name"` // TODO: Drop this. PostDate time.Time `db:"postdate"` IP net.IPNet `db:"ip"` Sticky bool `db:"sticky"` diff --git a/src/models/project.go b/src/models/project.go index f6d4a6f..63bb075 100644 --- a/src/models/project.go +++ b/src/models/project.go @@ -26,3 +26,11 @@ type Project struct { func (p *Project) IsHMN() bool { return p.ID == HMNProjectID } + +func (p *Project) Subdomain() string { + if p.IsHMN() { + return "" + } + + return *p.Slug +} diff --git a/src/rawdata/scss/_forum.scss b/src/rawdata/scss/_forum.scss index 2bfbd83..899daf9 100644 --- a/src/rawdata/scss/_forum.scss +++ b/src/rawdata/scss/_forum.scss @@ -63,20 +63,6 @@ } } - .goto { - font-size: 200%; - width: 30px; - - a { - display: block; - padding: 0px 10px; - box-sizing: border-box; - position: relative; - line-height: 100%; - background-color: transparent; - } - } - .forum & .info th { width: 50px; } @@ -113,6 +99,20 @@ } } +.goto { + font-size: 200%; + width: 30px; + + a { + display: block; + padding: 0px 10px; + box-sizing: border-box; + position: relative; + line-height: 100%; + background-color: transparent; + } +} + .badge { display: inline-block; border-radius: 1000em; diff --git a/src/templates/mapping.go b/src/templates/mapping.go index e13d398..d5e9596 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -1,6 +1,9 @@ package templates -import "git.handmade.network/hmn/hmn/src/models" +import ( + "git.handmade.network/hmn/hmn/src/hmnurl" + "git.handmade.network/hmn/hmn/src/models" +) func PostToTemplate(p *models.Post) Post { return Post{ @@ -12,7 +15,7 @@ func PostToTemplate(p *models.Post) Post { func ProjectToTemplate(p *models.Project) Project { return Project{ Name: maybeString(p.Name), - Subdomain: maybeString(p.Slug), + Subdomain: p.Subdomain(), Color1: p.Color1, Color2: p.Color2, @@ -26,15 +29,27 @@ func ProjectToTemplate(p *models.Project) Project { } func UserToTemplate(u *models.User) User { + avatar := "" + if u.Avatar != nil { + avatar = hmnurl.StaticUrl(*u.Avatar, nil) + } + + name := u.Name + if u.Name == "" { + name = u.Username + } + return User{ Username: u.Username, Email: u.Email, IsSuperuser: u.IsSuperuser, IsStaff: u.IsStaff, - Name: u.Name, - Blurb: u.Blurb, - Signature: u.Signature, + Name: name, + Blurb: u.Blurb, + Signature: u.Signature, + AvatarUrl: avatar, // TODO + ProfileUrl: hmnurl.Url("m/"+u.Username, nil), DarkTheme: u.DarkTheme, Timezone: u.Timezone, diff --git a/src/templates/src/include/post_list_item.html b/src/templates/src/include/post_list_item.html new file mode 100644 index 0000000..cde112b --- /dev/null +++ b/src/templates/src/include/post_list_item.html @@ -0,0 +1,24 @@ +{{/* +This template is intended to display a single post or thread in the context of a forum, the feed, or a similar layout. + +It should be called with PostListItemData. +*/}} + +
+ +
+ + +
+ {{ .User.Name }}{{ relativedate .Date }} +
+
+
+ » +
+
\ No newline at end of file diff --git a/src/templates/src/index.html b/src/templates/src/index.html index 2cb4e0a..475021f 100644 --- a/src/templates/src/index.html +++ b/src/templates/src/index.html @@ -66,15 +66,8 @@ {% endif %} */}} - {{ range $posts }} -
- {{ .Post.Preview }} -
- {{/* - {% if forloop.counter0 < max_posts %} - {% include "thread_list_entry.html" with thread=post.thread %} - {% endif %} - */}} + {{ range $post := $posts }} + {{ template "post_list_item.html" $post }} {{ end }} {{/* {% with more=posts|length|add:-5|clamp_lower:0 %} diff --git a/src/templates/templates.go b/src/templates/templates.go index 5017a2c..f5e6885 100644 --- a/src/templates/templates.go +++ b/src/templates/templates.go @@ -14,6 +14,13 @@ import ( "github.com/teacat/noire" ) +const ( + Dayish = time.Hour * 24 + Weekish = Dayish * 7 + Monthish = Dayish * 30 + Yearish = Dayish * 365 +) + //go:embed src var templateFs embed.FS var Templates map[string]*template.Template @@ -96,6 +103,44 @@ var HMNTemplateFuncs = template.FuncMap{ } return query.Encode() }, + "relativedate": func(t time.Time) string { + // TODO: Support relative future dates + + // NOTE(asaf): Months and years aren't exactly accurate, but good enough for now I guess. + str := func(primary int, primaryName string, secondary int, secondaryName string) string { + result := fmt.Sprintf("%d %s", primary, primaryName) + if primary != 1 { + result += "s" + } + if secondary > 0 { + result += fmt.Sprintf(", %d %s", secondary, secondaryName) + + if secondary != 1 { + result += "s" + } + } + + return result + " ago" + } + + delta := time.Now().Sub(t) + + if delta < time.Minute { + return "Less than a minute ago" + } else if delta < time.Hour { + return str(int(delta.Minutes()), "minute", 0, "") + } else if delta < Dayish { + return str(int(delta/time.Hour), "hour", int((delta%time.Hour)/time.Minute), "minute") + } else if delta < Weekish { + return str(int(delta/Dayish), "day", int((delta%Dayish)/time.Hour), "hour") + } else if delta < Monthish { + return str(int(delta/Weekish), "week", int((delta%Weekish)/Dayish), "day") + } else if delta < Yearish { + return str(int(delta/Monthish), "month", int((delta%Monthish)/Weekish), "week") + } else { + return str(int(delta/Yearish), "year", int((delta%Yearish)/Monthish), "month") + } + }, "static": func(filepath string) string { return hmnurl.StaticUrl(filepath, []hmnurl.Q{{"v", cachebust}}) }, diff --git a/src/templates/types.go b/src/templates/types.go index 735ff1b..7ce2544 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -1,5 +1,7 @@ package templates +import "time" + type BaseData struct { Title string CanonicalLink string @@ -12,7 +14,16 @@ type BaseData struct { User *User } +type Thread struct { + Title string + + Locked bool + Sticky bool + Moderated bool +} + type Post struct { + Author User Preview string ReadOnly bool @@ -39,10 +50,11 @@ type User struct { IsSuperuser bool IsStaff bool - Name string - Blurb string - Signature string - // TODO: Avatar?? + Name string + Blurb string + Signature string + AvatarUrl string + ProfileUrl string DarkTheme bool Timezone string @@ -64,3 +76,17 @@ type BackgroundImage struct { Url string Size string // A valid CSS background-size value } + +// Data from post_list_item.html +type PostListItem struct { + Title string + Url string + Breadcrumbs []Breadcrumb + User User + Date time.Time + Unread bool +} + +type Breadcrumb struct { + Name, Url string +} diff --git a/src/templates/urls.go b/src/templates/urls.go new file mode 100644 index 0000000..2e252bf --- /dev/null +++ b/src/templates/urls.go @@ -0,0 +1,20 @@ +package templates + +import ( + "fmt" + + "git.handmade.network/hmn/hmn/src/hmnurl" + "git.handmade.network/hmn/hmn/src/models" +) + +func PostUrl(post models.Post, catType models.CategoryType, subdomain string) string { + switch catType { + // TODO: All the relevant post types. Maybe it doesn't make sense to lump them all together here. + case models.CatTypeBlog: + return hmnurl.ProjectUrl(fmt.Sprintf("blogs/p/%d/e/%d", *post.ThreadID, post.ID), nil, subdomain) + case models.CatTypeForum: + return hmnurl.ProjectUrl(fmt.Sprintf("forums/t/%d/p/%d", *post.ThreadID, post.ID), nil, subdomain) + } + + return "" +} diff --git a/src/website/landing.go b/src/website/landing.go index 6a948ad..42dd170 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -19,13 +19,8 @@ type LandingTemplateData struct { type LandingPageProject struct { Project templates.Project - FeaturedPost *LandingPagePost - Posts []LandingPagePost -} - -type LandingPagePost struct { - Post templates.Post - HasRead bool + FeaturedPost *templates.PostListItem + Posts []templates.PostListItem } func Index(c *RequestContext) ResponseData { @@ -64,9 +59,12 @@ func Index(c *RequestContext) ResponseData { proj := projRow.(*models.Project) type ProjectPost struct { - Post models.Post `db:"post"` - ThreadLastReadTime *time.Time `db:"tlri.lastread"` - CatLastReadTime *time.Time `db:"clri.lastread"` + Post models.Post `db:"post"` + Thread models.Thread `db:"thread"` + Cat models.Category `db:"cat"` + User models.User `db:"auth_user"` + ThreadLastReadTime *time.Time `db:"tlri.lastread"` + CatLastReadTime *time.Time `db:"clri.lastread"` } projectPostIter, err := db.Query(c.Context(), c.Conn, ProjectPost{}, @@ -84,6 +82,7 @@ func Index(c *RequestContext) ResponseData { clri.category_id = cat.id AND clri.user_id = $1 ) + LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id WHERE cat.project_id = $2 AND cat.kind IN ($3, $4, $5, $6) @@ -117,9 +116,14 @@ func Index(c *RequestContext) ResponseData { hasRead = true } - landingPageProject.Posts = append(landingPageProject.Posts, LandingPagePost{ - Post: templates.PostToTemplate(&projectPost.Post), - HasRead: hasRead, + c.Logger.Debug().Time("post date", projectPost.Post.PostDate).Msg("") + + landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{ + Title: projectPost.Thread.Title, + Url: templates.PostUrl(projectPost.Post, projectPost.Cat.Kind, proj.Subdomain()), // TODO + User: templates.UserToTemplate(&projectPost.User), + Date: projectPost.Post.PostDate, + Unread: !hasRead, }) } diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index 1f48b7d..b0028a3 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -246,5 +246,8 @@ func doRequest(rw http.ResponseWriter, c *RequestContext, h HMNHandler) { } } rw.WriteHeader(res.StatusCode) - io.Copy(rw, res.Body) + + if res.Body != nil { + io.Copy(rw, res.Body) + } } diff --git a/src/website/routes.go b/src/website/routes.go index 1adf725..e3c05e0 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -42,7 +42,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { ) mainRoutes.GET("/", func(c *RequestContext) ResponseData { - if c.CurrentProject.ID == models.HMNProjectID { + if c.CurrentProject.IsHMN() { return Index(c) } else { // TODO: Return the project landing page