Update project index a bit, track snippet updates for project recency

This commit is contained in:
Ben Visness 2024-06-28 22:37:05 -05:00
parent 06b270514c
commit 9093f38b47
16 changed files with 214 additions and 435 deletions

View File

@ -7174,8 +7174,8 @@ code {
--notice-success-color: #43a52f; --notice-success-color: #43a52f;
--notice-warn-color: #aa7d30; --notice-warn-color: #aa7d30;
--notice-failure-color: #b42222; --notice-failure-color: #b42222;
--site-width: 54rem; --site-width: 58rem;
--site-width-narrow: 40rem; --site-width-narrow: 44rem;
--avatar-size-small: 1.5rem; --avatar-size-small: 1.5rem;
--avatar-size-normal: 2.5rem; --avatar-size-normal: 2.5rem;
--theme-color: #b1b1b1; --theme-color: #b1b1b1;

View File

@ -20,6 +20,7 @@ import (
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/email" "git.handmade.network/hmn/hmn/src/email"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging" "git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
@ -514,6 +515,8 @@ func init() {
} }
} }
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, conn)
fmt.Printf("Done!\n") fmt.Printf("Done!\n")
}, },
} }

View File

@ -995,6 +995,8 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
} }
} }
} }
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, tx)
} }
err = tx.Commit(ctx) err = tx.Commit(ctx)

View File

@ -526,3 +526,20 @@ func SetProjectTag(
return resultTag, nil return resultTag, nil
} }
func UpdateSnippetLastPostedForAllProjects(ctx context.Context, dbConn db.ConnOrTx) error {
_, err := dbConn.Exec(ctx,
`
UPDATE project p SET (snippet_last_posted, all_last_updated) = (
SELECT
COALESCE(MAX(s."when"), 'epoch'),
GREATEST(p.forum_last_updated, p.blog_last_updated, p.annotation_last_updated, MAX(s."when"))
FROM
snippet s
JOIN snippet_project sp ON s.id = sp.snippet_id
WHERE sp.project_id = p.id
)
`,
)
return err
}

View File

@ -132,10 +132,7 @@ func TestFeed(t *testing.T) {
} }
func TestProjectIndex(t *testing.T) { func TestProjectIndex(t *testing.T) {
AssertRegexMatch(t, BuildProjectIndex(1, ""), RegexProjectIndex, nil) AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
AssertRegexMatch(t, BuildProjectIndex(1, "test"), RegexProjectIndex, map[string]string{"category": "test"})
AssertRegexMatch(t, BuildProjectIndex(2, "test"), RegexProjectIndex, map[string]string{"page": "2", "category": "test"})
assert.Panics(t, func() { BuildProjectIndex(0, "") })
} }
func TestProjectNew(t *testing.T) { func TestProjectNew(t *testing.T) {

View File

@ -453,22 +453,11 @@ func BuildAtomFeedForShowcase() string {
* Projects * Projects
*/ */
var RegexProjectIndex = regexp.MustCompile(`^/projects(/(?P<category>[a-z0-9-]+)(/(?P<page>\d+))?)?$`) var RegexProjectIndex = regexp.MustCompile(`^/projects$`)
func BuildProjectIndex(page int, category string) string { func BuildProjectIndex() string {
defer CatchPanic() defer CatchPanic()
if page < 1 { return Url("/projects", nil)
panic(oops.New(nil, "page must be >= 1"))
}
catpath := ""
if category != "" {
catpath = "/" + category
}
if page == 1 {
return Url(fmt.Sprintf("/projects%s", catpath), nil)
} else {
return Url(fmt.Sprintf("/projects%s/%d", catpath, page), nil)
}
} }
var RegexProjectNew = regexp.MustCompile("^/p/new$") var RegexProjectNew = regexp.MustCompile("^/p/new$")

View File

@ -365,24 +365,20 @@ func ResetDB() {
ctx := context.Background() ctx := context.Background()
// Create the HMN database user // Create the HMN database user
{
credentials := append( credentials := append(
[]pgCredentials{ []pgCredentials{
{config.Config.Postgres.User, config.Config.Postgres.Password, false}, // Existing HMN user
{getSystemUsername(), "", true}, // Postgres.app on Mac {getSystemUsername(), "", true}, // Postgres.app on Mac
}, },
guessCredentials()..., guessCredentials()...,
) )
var workingCred pgCredentials var superuserConn *pgconn.PgConn
var createUserConn *pgconn.PgConn
var connErrors []error var connErrors []error
for _, cred := range credentials { for _, cred := range credentials {
// NOTE(asaf): We have to use the low-level API of pgconn, because the pgx Exec always wraps the query in a transaction. // NOTE(asaf): We have to use the low-level API of pgconn, because the pgx Exec always wraps the query in a transaction.
var err error var err error
createUserConn, err = connectLowLevel(ctx, cred.User, cred.Password) superuserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
if err == nil { if err == nil {
workingCred = cred
if cred.SafeToPrint { if cred.SafeToPrint {
fmt.Printf("Connected by guessing username \"%s\" and password \"%s\".\n", cred.User, cred.Password) fmt.Printf("Connected by guessing username \"%s\" and password \"%s\".\n", cred.User, cred.Password)
} }
@ -391,7 +387,7 @@ func ResetDB() {
connErrors = append(connErrors, err) connErrors = append(connErrors, err)
} }
} }
if createUserConn == nil { if superuserConn == nil {
fmt.Println("Failed to connect to the db to reset it.") fmt.Println("Failed to connect to the db to reset it.")
fmt.Println("The following errors occurred for each attempted set of credentials:") fmt.Println("The following errors occurred for each attempted set of credentials:")
for _, err := range connErrors { for _, err := range connErrors {
@ -412,34 +408,27 @@ func ResetDB() {
fmt.Println("and add the username and password to your config.") fmt.Println("and add the username and password to your config.")
os.Exit(1) os.Exit(1)
} }
defer createUserConn.Close(ctx) defer superuserConn.Close(ctx)
// Create the HMN user // Create the HMN user
{ {
userExists := workingCred.User == config.Config.Postgres.User && workingCred.Password == config.Config.Postgres.Password result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
if !userExists {
result := createUserConn.ExecParams(ctx, fmt.Sprintf(`
CREATE USER %s WITH CREATE USER %s WITH
ENCRYPTED PASSWORD '%s' ENCRYPTED PASSWORD '%s'
CREATEDB CREATEDB
`, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil) `, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil)
_, err := result.Close() _, err := result.Close()
pgErr, isPgError := err.(*pgconn.PgError)
if err != nil { if err != nil {
if !(isPgError && pgErr.SQLState() == "42710") { // NOTE(ben): 42710 means "duplicate object", i.e. already exists
panic(fmt.Errorf("failed to create HMN user: %w", err)) panic(fmt.Errorf("failed to create HMN user: %w", err))
} }
} }
} }
}
// Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err))
}
// Disconnect all other users // Disconnect all other users
{ {
result := conn.ExecParams(ctx, fmt.Sprintf(` result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
SELECT pg_terminate_backend(pid) SELECT pg_terminate_backend(pid)
FROM pg_stat_activity FROM pg_stat_activity
WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid() WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid()
@ -449,6 +438,14 @@ func ResetDB() {
panic(fmt.Errorf("failed to disconnect other users: %w", err)) panic(fmt.Errorf("failed to disconnect other users: %w", err))
} }
} }
superuserConn.Close(ctx)
// Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err))
}
defer conn.Close(ctx)
// Drop the database // Drop the database
{ {

View File

@ -0,0 +1,50 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/migration/types"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddSnippetUpdatedField{})
}
type AddSnippetUpdatedField struct{}
func (m AddSnippetUpdatedField) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 6, 29, 1, 43, 20, 0, time.UTC))
}
func (m AddSnippetUpdatedField) Name() string {
return "AddSnippetUpdatedField"
}
func (m AddSnippetUpdatedField) Description() string {
return "Add field to track most recent snippets on projects"
}
func (m AddSnippetUpdatedField) Up(ctx context.Context, tx pgx.Tx) error {
utils.Must1(tx.Exec(ctx,
`
ALTER TABLE project
ADD COLUMN snippet_last_posted TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'epoch';
`,
))
utils.Must(hmndata.UpdateSnippetLastPostedForAllProjects(ctx, tx))
return nil
}
func (m AddSnippetUpdatedField) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE project
DROP COLUMN snippet_last_posted;
`,
)
return err
}

View File

@ -53,8 +53,6 @@ func (lc ProjectLifecycle) In(lcs []ProjectLifecycle) bool {
return false return false
} }
const RecentProjectUpdateTimespanSec = 60 * 60 * 24 * 28 // NOTE(asaf): Four weeks
type Project struct { type Project struct {
ID int `db:"id"` ID int `db:"id"`
@ -81,6 +79,7 @@ type Project struct {
ForumLastUpdated time.Time `db:"forum_last_updated"` ForumLastUpdated time.Time `db:"forum_last_updated"`
BlogLastUpdated time.Time `db:"blog_last_updated"` BlogLastUpdated time.Time `db:"blog_last_updated"`
AnnotationLastUpdated time.Time `db:"annotation_last_updated"` AnnotationLastUpdated time.Time `db:"annotation_last_updated"`
SnippetLastPosted time.Time `db:"snippet_last_posted"`
ForumEnabled bool `db:"forum_enabled"` ForumEnabled bool `db:"forum_enabled"`
BlogEnabled bool `db:"blog_enabled"` BlogEnabled bool `db:"blog_enabled"`

View File

@ -54,8 +54,8 @@ $breakpoint-large: screen and (min-width: 60em)
--notice-warn-color: #aa7d30; --notice-warn-color: #aa7d30;
--notice-failure-color: #b42222; --notice-failure-color: #b42222;
--site-width: 54rem; --site-width: 58rem;
--site-width-narrow: 40rem; --site-width-narrow: 44rem;
--avatar-size-small: 1.5rem; --avatar-size-small: 1.5rem;
--avatar-size-normal: 2.5rem; --avatar-size-normal: 2.5rem;

View File

@ -2,12 +2,11 @@
{{ define "content" }} {{ define "content" }}
<div class="mw6 ph3 pv3 center post-content"> <div class="mw6 ph3 pv3 center post-content">
<h2> <div style="font-size: 4rem; margin-left: -0.08em">
Hi there, {{ if .User }}{{ .User.Name }}{{ else }}visitor{{ end }}! :(
</h2> </div>
<div> <div class="pt3">
We have encountered an error while processing your request. We have encountered an error while processing your request.<br>
<br />
If this keeps happening, please <a href="mailto:{{ .ReportIssueEmail }}">let us know</a>. If this keeps happening, please <a href="mailto:{{ .ReportIssueEmail }}">let us know</a>.
</div> </div>
</div> </div>

View File

@ -1,49 +1,8 @@
{{ template "base.html" . }} {{ template "base-2024.html" . }}
{{ define "extrahead" }} {{ define "content" }}
<script src="{{ static "js/carousel.js" }}"></script> <div class="flex justify-center">
{{ end }} <div class="mw-site flex flex-column g3">
{{ define "all_projects" }}
<div>
{{ with .OfficialProjects }}
<div class="carousel-container project-carousel mw-100 mv2 mv3-ns m--center dn db-ns">
<div class="carousel pa3 h5 overflow-hidden bg2 br2-ns">
{{ range $index, $project := . }}
<div class="carousel-item flex pv3 pl3 w-100 h-100 bg2 items-center {{ if eq $index 0 }}active{{ end }}">
<div class="flex-grow-1 pr3 relative flex flex-column h-100 justify-center">
<a href="{{ $project.Url }}">
<h3 class="f3">{{ $project.Name }}</h3>
</a>
<div class="carousel-description">
{{ $project.ParsedDescription }}
</div>
<div class="carousel-fade"></div>
</div>
<div class="flex-shrink-0 order-0 order-1-ns">
<a href="{{ $project.Url }}">
<div class="image bg-center cover w5 h5 br2" style="background-image:url({{ $project.Logo }})" ></div>
</a>
</div>
</div>
{{ end }}
</div>
<div class="carousel-buttons pv2"></div>
</div>
{{ end }}
<div class="flex flex-column g3">
{{ if .CurrentJamProjects }}
<div class="ph3 pt3 bg2 br2 flex flex-column">
<h2>{{ template "jam_name" .CurrentJamSlug }}</h2>
<p>These projects are submissions to the {{ template "jam_name" .CurrentJamSlug }}, which is happening <b>right now!</b> <a href="{{ .CurrentJamLink }}">Learn more »</a>
<div class="grid grid-1 grid-2-ns g3">
{{ range .CurrentJamProjects }}
{{ template "project_card.html" . }}
{{ end }}
</div>
<a href="{{ .CurrentJamProjectsLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
{{ if .OfficialProjects }} {{ if .OfficialProjects }}
<div class="ph3 pt3 bg2 br2 flex flex-column"> <div class="ph3 pt3 bg2 br2 flex flex-column">
<h2 class="f3 mb2">Official Projects</h2> <h2 class="f3 mb2">Official Projects</h2>
@ -52,97 +11,8 @@
{{ template "project_card.html" . }} {{ template "project_card.html" . }}
{{ end }} {{ end }}
</div> </div>
<a href="{{ .OfficialProjectsLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
{{ if .PersonalProjects }}
<div class="ph3 pt3 bg2 br2 flex flex-column">
<h2 class="f3 mb2">Personal Projects</h2>
<div>Many community members have projects of their own. Want to join them? <a href="{{ .CreateProjectLink }}">Create your own.</a></div>
<div class="mt3 grid grid-1 grid-2-ns g3">
{{ range .PersonalProjects }}
{{ template "project_card.html" . }}
{{ end }}
</div>
<a href="{{ .PersonalProjectsLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
{{ if .PreviousJamProjects }}
<div class="ph3 pt3 bg2 br2 flex flex-column">
<h2>{{ template "jam_name" .PreviousJamSlug }}</h2>
<p>The following projects were submissions to our most recent jam. <a href="{{ .PreviousJamLink }}">Learn more »</a></p>
<div class="grid grid-1 grid-2-ns g3">
{{ range .PreviousJamProjects }}
{{ template "project_card.html" . }}
{{ end }}
</div>
<a href="{{ .PreviousJamProjectsLink }}" class="pa3 tc">See more »</a>
</div> </div>
{{ end }} {{ end }}
</div> </div>
</div> </div>
<script>
initCarousel(document.querySelector('.project-carousel'), {
durationMS: 10000,
});
</script>
{{ end }}
{{ define "single_category" }}
{{ if eq .Category "official" }}
<h2>Official Projects</h2>
{{ else if eq .Category "personal" }}
<h2>Personal Projects</h2>
<p>Many community members have projects of their own. Want to join them? <a href="{{ .CreateProjectLink }}">Create your own.</a></p>
{{ else }}
{{/* Here we are assuming everything else is a jam. */}}
<h2>{{ template "jam_name" .Category }}</h2>
<p>The following projects were submissions to the {{ template "jam_name" .Category }}. <a href="{{ .PageJamLink }}">Learn more »</a></p>
{{ end }}
<div class="bg2-ns br2">
{{ if gt .Pagination.Total 1 }}
<div class="optionbar pv2 ph3">
<div class="options"></div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div>
{{ end }}
<div class="pa3 grid grid-1 grid-2-ns g3">
{{ range .PageProjects }}
{{ template "project_card.html" . }}
{{ end }}
</div>
{{ if gt .Pagination.Total 1 }}
<div class="optionbar bottom pv2 ph3">
<div class="options order-1"></div>
<div class="options order-0 order-last-ns">{{ template "pagination.html" .Pagination }}</div>
</div>
{{ end }}
</div>
{{ end }}
{{ define "jam_name" }}
{{- if eq . "2022" -}}
2022 Wheel Reinvention Jam
{{- else if eq . "2023" -}}
2023 Wheel Reinvention Jam
{{- else if eq . "visibility-2023" -}}
2023 Visibility Jam
{{- else if eq . "learning-2024" -}}
2024 Learning Jam
{{- else -}}
???
{{- end -}}
{{ end }}
{{ define "content" }}
{{ if .AllProjects }}
{{ template "all_projects" . }}
{{ else }}
{{ template "single_category" . }}
{{ end }}
{{ end }} {{ end }}

View File

@ -74,7 +74,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
LoginWithDiscordUrl: hmnurl.BuildLoginWithDiscord(c.FullUrl()), LoginWithDiscordUrl: hmnurl.BuildLoginWithDiscord(c.FullUrl()),
HMNHomepageUrl: hmnurl.BuildHomepage(), HMNHomepageUrl: hmnurl.BuildHomepage(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1, ""), ProjectIndexUrl: hmnurl.BuildProjectIndex(),
PodcastUrl: hmnurl.BuildPodcast(), PodcastUrl: hmnurl.BuildPodcast(),
FishbowlUrl: hmnurl.BuildFishbowlIndex(), FishbowlUrl: hmnurl.BuildFishbowlIndex(),
ForumsUrl: hmnurl.HMNProjectContext.BuildForum(nil, 1), ForumsUrl: hmnurl.HMNProjectContext.BuildForum(nil, 1),
@ -90,7 +90,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
AboutUrl: hmnurl.BuildAbout(), AboutUrl: hmnurl.BuildAbout(),
ManifestoUrl: hmnurl.BuildManifesto(), ManifestoUrl: hmnurl.BuildManifesto(),
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(), CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1, ""), ProjectIndexUrl: hmnurl.BuildProjectIndex(),
ContactUrl: hmnurl.BuildContactPage(), ContactUrl: hmnurl.BuildContactPage(),
SearchActionUrl: "https://duckduckgo.com", SearchActionUrl: "https://duckduckgo.com",
}, },

View File

@ -149,7 +149,7 @@ func AtomFeed(c *RequestContext) ResponseData {
feedData.FeedType = FeedTypeProjects feedData.FeedType = FeedTypeProjects
feedData.FeedID = FeedIDProjects feedData.FeedID = FeedIDProjects
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects() feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects()
feedData.FeedUrl = hmnurl.BuildProjectIndex(1, "") feedData.FeedUrl = hmnurl.BuildProjectIndex()
c.Perf.StartBlock("SQL", "Fetching projects") c.Perf.StartBlock("SQL", "Fetching projects")
_, hasAll := c.Req.URL.Query()["all"] _, hasAll := c.Req.URL.Query()["all"]

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"image" "image"
"io" "io"
"math/rand"
"net/http" "net/http"
"path" "path"
"sort" "sort"
@ -60,167 +59,25 @@ func ProjectCSS(c *RequestContext) ResponseData {
type ProjectTemplateData struct { type ProjectTemplateData struct {
templates.BaseData templates.BaseData
AllProjects bool
// Stuff for all projects
OfficialProjects []templates.Project OfficialProjects []templates.Project
OfficialProjectsLink string
PersonalProjects []templates.Project
PersonalProjectsLink string
CurrentJamProjects []templates.Project
CurrentJamProjectsLink string
CurrentJamLink string
CurrentJamSlug string
PreviousJamProjects []templates.Project
PreviousJamProjectsLink string
PreviousJamLink string
PreviousJamSlug string
// Stuff for pages of projects only
Category string
Pagination templates.Pagination
PageProjects []templates.Project
PageJamLink string
// Stuff for both
CreateProjectLink string
} }
func ProjectIndex(c *RequestContext) ResponseData { func ProjectIndex(c *RequestContext) ResponseData {
cat := c.PathParams["category"]
pageStr := c.PathParams["page"]
currentJam := hmndata.CurrentJam()
previousJam := hmndata.PreviousJam()
if cat == "" && pageStr == "" {
const projectsPerSection = 8
officialProjects, err := getShuffledOfficialProjects(c) officialProjects, err := getShuffledOfficialProjects(c)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} }
personalProjects, err := getPersonalProjects(c, "")
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
var currentJamProjects []templates.Project
if currentJam != nil {
var err error
currentJamProjects, err = getPersonalProjects(c, currentJam.Slug)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
}
previousJamProjects, err := getPersonalProjects(c, previousJam.Slug)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
baseData := getBaseDataAutocrumb(c, "Projects") baseData := getBaseDataAutocrumb(c, "Projects")
tmpl := ProjectTemplateData{ tmpl := ProjectTemplateData{
BaseData: baseData, BaseData: baseData,
AllProjects: true, OfficialProjects: officialProjects,
OfficialProjects: officialProjects[:utils.Min(len(officialProjects), projectsPerSection)],
OfficialProjectsLink: hmnurl.BuildProjectIndex(1, "official"),
PersonalProjects: personalProjects[:utils.Min(len(personalProjects), projectsPerSection)],
PersonalProjectsLink: hmnurl.BuildProjectIndex(1, "personal"),
// Current jam stuff set later
PreviousJamProjects: previousJamProjects[:utils.Min(len(previousJamProjects), projectsPerSection)],
PreviousJamProjectsLink: hmnurl.BuildProjectIndex(1, previousJam.UrlSlug),
PreviousJamLink: hmnurl.BuildJamIndexAny(previousJam.UrlSlug),
PreviousJamSlug: previousJam.UrlSlug,
CreateProjectLink: hmnurl.BuildProjectNew(),
}
if currentJam != nil {
tmpl.CurrentJamProjects = currentJamProjects[:utils.Min(len(currentJamProjects), projectsPerSection)]
tmpl.CurrentJamProjectsLink = hmnurl.BuildProjectIndex(1, currentJam.UrlSlug)
tmpl.CurrentJamLink = hmnurl.BuildJamIndexAny(currentJam.UrlSlug)
tmpl.CurrentJamSlug = currentJam.UrlSlug
} }
var res ResponseData var res ResponseData
res.MustWriteTemplate("project_index.html", tmpl, c.Perf) res.MustWriteTemplate("project_index.html", tmpl, c.Perf)
return res return res
} else {
const projectsPerPage = 20
var breadcrumb templates.Breadcrumb
breadcrumbUrl := hmnurl.BuildProjectIndex(1, cat)
var projects []templates.Project
var jamLink string
var err error
switch cat {
case hmndata.WRJ2022.UrlSlug:
projects, err = getPersonalProjects(c, hmndata.WRJ2022.Slug)
breadcrumb = templates.Breadcrumb{Name: "Wheel Reinvention Jam 2022"}
jamLink = hmnurl.BuildJamIndex2022()
case hmndata.VJ2023.UrlSlug:
projects, err = getPersonalProjects(c, hmndata.VJ2023.Slug)
breadcrumb = templates.Breadcrumb{"2023 Visibility Jam", breadcrumbUrl}
jamLink = hmnurl.BuildJamIndex2023_Visibility()
case hmndata.WRJ2023.UrlSlug:
projects, err = getPersonalProjects(c, hmndata.WRJ2023.Slug)
breadcrumb = templates.Breadcrumb{"Wheel Reinvention Jam 2023", breadcrumbUrl}
jamLink = hmnurl.BuildJamIndex2023()
case "personal":
projects, err = getPersonalProjects(c, "")
breadcrumb = templates.Breadcrumb{"Personal Projects", breadcrumbUrl}
case "official":
projects, err = getShuffledOfficialProjects(c)
breadcrumb = templates.Breadcrumb{"Official Projects", breadcrumbUrl}
default:
return c.Redirect(hmnurl.BuildProjectIndex(1, ""), http.StatusSeeOther)
}
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
page, numPages, ok := getPageInfo(pageStr, len(projects), projectsPerPage)
if !ok {
return c.Redirect(hmnurl.BuildProjectIndex(1, cat), http.StatusSeeOther)
}
pagination := templates.Pagination{
Current: page,
Total: numPages,
FirstUrl: hmnurl.BuildProjectIndex(1, cat),
LastUrl: hmnurl.BuildProjectIndex(numPages, cat),
NextUrl: hmnurl.BuildProjectIndex(utils.Clamp(1, page+1, numPages), cat),
PreviousUrl: hmnurl.BuildProjectIndex(utils.Clamp(1, page-1, numPages), cat),
}
firstProjectIndex := (page - 1) * projectsPerPage
endIndex := utils.Min(firstProjectIndex+projectsPerPage, len(projects))
pageProjects := projects[firstProjectIndex:endIndex]
baseData := getBaseData(c, "Projects", []templates.Breadcrumb{
{"Projects", hmnurl.BuildProjectIndex(1, "")},
breadcrumb,
})
var res ResponseData
res.MustWriteTemplate("project_index.html", ProjectTemplateData{
BaseData: baseData,
AllProjects: false,
Category: cat,
Pagination: pagination,
PageProjects: pageProjects,
CreateProjectLink: hmnurl.BuildProjectNew(),
PageJamLink: jamLink,
}, c.Perf)
return res
}
} }
func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error) { func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error) {
@ -232,47 +89,44 @@ func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error)
} }
c.Perf.StartBlock("PROJECTS", "Grouping and sorting") c.Perf.StartBlock("PROJECTS", "Grouping and sorting")
var handmadeHero *templates.Project var handmadeHero hmndata.ProjectAndStuff
var featuredProjects []templates.Project var featuredProjects []hmndata.ProjectAndStuff
var recentProjects []templates.Project var restProjects []hmndata.ProjectAndStuff
var restProjects []templates.Project
now := time.Now()
for _, p := range official { for _, p := range official {
templateProject := templates.ProjectAndStuffToTemplate(&p)
if p.Project.Slug == "hero" { if p.Project.Slug == "hero" {
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list. // NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
handmadeHero = &templateProject handmadeHero = p
continue continue
} }
if p.Project.Featured { if p.Project.Featured {
featuredProjects = append(featuredProjects, templateProject) featuredProjects = append(featuredProjects, p)
} else if now.Sub(p.Project.AllLastUpdated).Seconds() < models.RecentProjectUpdateTimespanSec {
recentProjects = append(recentProjects, templateProject)
} else { } else {
restProjects = append(restProjects, templateProject) restProjects = append(restProjects, p)
} }
} }
_, randSeed := now.ISOWeek() sort.Slice(featuredProjects, func(i, j int) bool {
random := rand.New(rand.NewSource(int64(randSeed))) return featuredProjects[i].Project.AllLastUpdated.After(featuredProjects[j].Project.AllLastUpdated)
random.Shuffle(len(featuredProjects), func(i, j int) { featuredProjects[i], featuredProjects[j] = featuredProjects[j], featuredProjects[i] }) })
random.Shuffle(len(recentProjects), func(i, j int) { recentProjects[i], recentProjects[j] = recentProjects[j], recentProjects[i] }) sort.Slice(restProjects, func(i, j int) bool {
random.Shuffle(len(restProjects), func(i, j int) { restProjects[i], restProjects[j] = restProjects[j], restProjects[i] }) return restProjects[i].Project.AllLastUpdated.After(restProjects[j].Project.AllLastUpdated)
})
if handmadeHero != nil { projects := make([]templates.Project, 0, 1+len(featuredProjects)+len(restProjects))
if handmadeHero.Project.ID != 0 {
// NOTE(asaf): As mentioned above, inserting HMH first. // NOTE(asaf): As mentioned above, inserting HMH first.
featuredProjects = append([]templates.Project{*handmadeHero}, featuredProjects...) projects = append(projects, templates.ProjectAndStuffToTemplate(&handmadeHero))
}
for _, p := range featuredProjects {
projects = append(projects, templates.ProjectAndStuffToTemplate(&p))
}
for _, p := range restProjects {
projects = append(projects, templates.ProjectAndStuffToTemplate(&p))
} }
officialProjects := make([]templates.Project, 0, len(featuredProjects)+len(recentProjects)+len(restProjects))
officialProjects = append(officialProjects, featuredProjects...)
officialProjects = append(officialProjects, recentProjects...)
officialProjects = append(officialProjects, restProjects...)
c.Perf.EndBlock() c.Perf.EndBlock()
return officialProjects, nil return projects, nil
} }
func getPersonalProjects(c *RequestContext, jamSlug string) ([]templates.Project, error) { func getPersonalProjects(c *RequestContext, jamSlug string) ([]templates.Project, error) {

View File

@ -382,6 +382,8 @@ func SnippetEditSubmit(c *RequestContext) ResponseData {
} }
} }
hmndata.UpdateSnippetLastPostedForAllProjects(c, tx)
err = tx.Commit(c) err = tx.Commit(c)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction"))