Make progress on the landing page
This commit is contained in:
parent
6ed2bd0c02
commit
d7c512f1c8
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> — <span class="datetime">{{ relativedate .Date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="goto">
|
||||||
|
<a href="{{ .Url }}">»</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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 %}
|
||||||
|
|
|
@ -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}})
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -39,10 +50,11 @@ type User struct {
|
||||||
IsSuperuser bool
|
IsSuperuser bool
|
||||||
IsStaff bool
|
IsStaff bool
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -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 ""
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
@ -64,9 +59,12 @@ func Index(c *RequestContext) ResponseData {
|
||||||
proj := projRow.(*models.Project)
|
proj := projRow.(*models.Project)
|
||||||
|
|
||||||
type ProjectPost struct {
|
type ProjectPost struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
Thread models.Thread `db:"thread"`
|
||||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
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{},
|
projectPostIter, err := db.Query(c.Context(), c.Conn, ProjectPost{},
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -246,5 +246,8 @@ func doRequest(rw http.ResponseWriter, c *RequestContext, h HMNHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rw.WriteHeader(res.StatusCode)
|
rw.WriteHeader(res.StatusCode)
|
||||||
io.Copy(rw, res.Body)
|
|
||||||
|
if res.Body != nil {
|
||||||
|
io.Copy(rw, res.Body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue