Make progress on the landing page

This commit is contained in:
Ben Visness 2021-04-22 18:02:50 -05:00
parent 6ed2bd0c02
commit d7c512f1c8
15 changed files with 199 additions and 60 deletions

View File

@ -8587,16 +8587,6 @@ input[type=submit] {
color: var(--forum-thread-read-link-color); } color: var(--forum-thread-read-link-color); }
.thread.read .title { .thread.read .title {
font-weight: 500; } 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 { .forum .thread .info th {
width: 50px; } width: 50px; }
@ -8621,6 +8611,17 @@ input[type=submit] {
left: 0px; left: 0px;
bottom: -10px; } 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 { .badge {
display: inline-block; display: inline-block;
border-radius: 1000em; border-radius: 1000em;

View File

@ -169,7 +169,7 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin
fieldType = destType.Elem() 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 // 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 { if fieldType.Kind() == reflect.Struct && !isRecognizedByPgtype {

View File

@ -55,7 +55,7 @@ func StaticThemeUrl(path string, theme string, query []Q) string {
} }
func trim(path string) string { func trim(path string) string {
if path[0] == '/' { if len(path) > 0 && path[0] == '/' {
return path[1:] return path[1:]
} }
return path return path

View File

@ -17,7 +17,7 @@ type Post struct {
Depth int `db:"depth"` Depth int `db:"depth"`
Slug string `db:"slug"` Slug string `db:"slug"`
AuthorName string `db:"author_name"` AuthorName string `db:"author_name"` // TODO: Drop this.
PostDate time.Time `db:"postdate"` PostDate time.Time `db:"postdate"`
IP net.IPNet `db:"ip"` IP net.IPNet `db:"ip"`
Sticky bool `db:"sticky"` Sticky bool `db:"sticky"`

View File

@ -26,3 +26,11 @@ type Project struct {
func (p *Project) IsHMN() bool { func (p *Project) IsHMN() bool {
return p.ID == HMNProjectID return p.ID == HMNProjectID
} }
func (p *Project) Subdomain() string {
if p.IsHMN() {
return ""
}
return *p.Slug
}

View File

@ -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 { .forum & .info th {
width: 50px; 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 { .badge {
display: inline-block; display: inline-block;
border-radius: 1000em; border-radius: 1000em;

View File

@ -1,6 +1,9 @@
package templates 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 { func PostToTemplate(p *models.Post) Post {
return Post{ return Post{
@ -12,7 +15,7 @@ func PostToTemplate(p *models.Post) Post {
func ProjectToTemplate(p *models.Project) Project { func ProjectToTemplate(p *models.Project) Project {
return Project{ return Project{
Name: maybeString(p.Name), Name: maybeString(p.Name),
Subdomain: maybeString(p.Slug), Subdomain: p.Subdomain(),
Color1: p.Color1, Color1: p.Color1,
Color2: p.Color2, Color2: p.Color2,
@ -26,15 +29,27 @@ func ProjectToTemplate(p *models.Project) Project {
} }
func UserToTemplate(u *models.User) User { 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{ return User{
Username: u.Username, Username: u.Username,
Email: u.Email, Email: u.Email,
IsSuperuser: u.IsSuperuser, IsSuperuser: u.IsSuperuser,
IsStaff: u.IsStaff, IsStaff: u.IsStaff,
Name: u.Name, Name: name,
Blurb: u.Blurb, Blurb: u.Blurb,
Signature: u.Signature, Signature: u.Signature,
AvatarUrl: avatar, // TODO
ProfileUrl: hmnurl.Url("m/"+u.Username, nil),
DarkTheme: u.DarkTheme, DarkTheme: u.DarkTheme,
Timezone: u.Timezone, Timezone: u.Timezone,

View File

@ -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.
*/}}
<div data-tmpl="container" class="flex items-center ph3 pv2">
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
<div class="flex-grow-1 overflow-hidden">
<div class="breadcrumbs">
{{ range $i, $breadcrumb := .Breadcrumbs }}
{{ if gt $i 0 }} » {{ end }}
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
{{ end }}
</div>
<div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details">
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; <span class="datetime">{{ relativedate .Date }}</span>
</div>
</div>
<div class="goto">
<a href="{{ .Url }}">&raquo;</a>
</div>
</div>

View File

@ -66,15 +66,8 @@
{% endif %} {% endif %}
*/}} */}}
{{ range $posts }} {{ range $post := $posts }}
<div> {{ template "post_list_item.html" $post }}
{{ .Post.Preview }}
</div>
{{/*
{% if forloop.counter0 < max_posts %}
{% include "thread_list_entry.html" with thread=post.thread %}
{% endif %}
*/}}
{{ end }} {{ end }}
{{/* {{/*
{% with more=posts|length|add:-5|clamp_lower:0 %} {% with more=posts|length|add:-5|clamp_lower:0 %}

View File

@ -14,6 +14,13 @@ import (
"github.com/teacat/noire" "github.com/teacat/noire"
) )
const (
Dayish = time.Hour * 24
Weekish = Dayish * 7
Monthish = Dayish * 30
Yearish = Dayish * 365
)
//go:embed src //go:embed src
var templateFs embed.FS var templateFs embed.FS
var Templates map[string]*template.Template var Templates map[string]*template.Template
@ -96,6 +103,44 @@ var HMNTemplateFuncs = template.FuncMap{
} }
return query.Encode() 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 { "static": func(filepath string) string {
return hmnurl.StaticUrl(filepath, []hmnurl.Q{{"v", cachebust}}) return hmnurl.StaticUrl(filepath, []hmnurl.Q{{"v", cachebust}})
}, },

View File

@ -1,5 +1,7 @@
package templates package templates
import "time"
type BaseData struct { type BaseData struct {
Title string Title string
CanonicalLink string CanonicalLink string
@ -12,7 +14,16 @@ type BaseData struct {
User *User User *User
} }
type Thread struct {
Title string
Locked bool
Sticky bool
Moderated bool
}
type Post struct { type Post struct {
Author User
Preview string Preview string
ReadOnly bool ReadOnly bool
@ -42,7 +53,8 @@ type User struct {
Name string Name string
Blurb string Blurb string
Signature string Signature string
// TODO: Avatar?? AvatarUrl string
ProfileUrl string
DarkTheme bool DarkTheme bool
Timezone string Timezone string
@ -64,3 +76,17 @@ type BackgroundImage struct {
Url string Url string
Size string // A valid CSS background-size value 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
}

20
src/templates/urls.go Normal file
View File

@ -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 ""
}

View File

@ -19,13 +19,8 @@ type LandingTemplateData struct {
type LandingPageProject struct { type LandingPageProject struct {
Project templates.Project Project templates.Project
FeaturedPost *LandingPagePost FeaturedPost *templates.PostListItem
Posts []LandingPagePost Posts []templates.PostListItem
}
type LandingPagePost struct {
Post templates.Post
HasRead bool
} }
func Index(c *RequestContext) ResponseData { func Index(c *RequestContext) ResponseData {
@ -65,6 +60,9 @@ func Index(c *RequestContext) ResponseData {
type ProjectPost struct { type ProjectPost struct {
Post models.Post `db:"post"` 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"` ThreadLastReadTime *time.Time `db:"tlri.lastread"`
CatLastReadTime *time.Time `db:"clri.lastread"` CatLastReadTime *time.Time `db:"clri.lastread"`
} }
@ -84,6 +82,7 @@ func Index(c *RequestContext) ResponseData {
clri.category_id = cat.id clri.category_id = cat.id
AND clri.user_id = $1 AND clri.user_id = $1
) )
LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
WHERE WHERE
cat.project_id = $2 cat.project_id = $2
AND cat.kind IN ($3, $4, $5, $6) AND cat.kind IN ($3, $4, $5, $6)
@ -117,9 +116,14 @@ func Index(c *RequestContext) ResponseData {
hasRead = true hasRead = true
} }
landingPageProject.Posts = append(landingPageProject.Posts, LandingPagePost{ c.Logger.Debug().Time("post date", projectPost.Post.PostDate).Msg("")
Post: templates.PostToTemplate(&projectPost.Post),
HasRead: hasRead, 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,
}) })
} }

View File

@ -246,5 +246,8 @@ func doRequest(rw http.ResponseWriter, c *RequestContext, h HMNHandler) {
} }
} }
rw.WriteHeader(res.StatusCode) rw.WriteHeader(res.StatusCode)
if res.Body != nil {
io.Copy(rw, res.Body) io.Copy(rw, res.Body)
} }
}

View File

@ -42,7 +42,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
) )
mainRoutes.GET("/", func(c *RequestContext) ResponseData { mainRoutes.GET("/", func(c *RequestContext) ResponseData {
if c.CurrentProject.ID == models.HMNProjectID { if c.CurrentProject.IsHMN() {
return Index(c) return Index(c)
} else { } else {
// TODO: Return the project landing page // TODO: Return the project landing page