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,90 +365,87 @@ func ResetDB() {
ctx := context.Background() ctx := context.Background()
// Create the HMN database user // Create the HMN database user
credentials := append(
[]pgCredentials{
{getSystemUsername(), "", true}, // Postgres.app on Mac
},
guessCredentials()...,
)
var superuserConn *pgconn.PgConn
var connErrors []error
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.
var err error
superuserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
if err == nil {
if cred.SafeToPrint {
fmt.Printf("Connected by guessing username \"%s\" and password \"%s\".\n", cred.User, cred.Password)
}
break
} else {
connErrors = append(connErrors, err)
}
}
if superuserConn == nil {
fmt.Println("Failed to connect to the db to reset it.")
fmt.Println("The following errors occurred for each attempted set of credentials:")
for _, err := range connErrors {
fmt.Printf("- %v\n", err)
}
fmt.Println()
fmt.Println("If this is a local development environment, please let us know what platform you")
fmt.Println("are using and how you installed Postgres. We want to try and streamline the setup")
fmt.Println("process for you.")
fmt.Println()
fmt.Println("If on the other hand this is a real deployment, please go into psql and manually")
fmt.Println("create the user:")
fmt.Println()
fmt.Println(" CREATE USER <username> WITH")
fmt.Println(" ENCRYPTED PASSWORD '<password>'")
fmt.Println(" CREATEDB;")
fmt.Println()
fmt.Println("and add the username and password to your config.")
os.Exit(1)
}
defer superuserConn.Close(ctx)
// Create the HMN user
{ {
credentials := append( result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
[]pgCredentials{ CREATE USER %s WITH
{config.Config.Postgres.User, config.Config.Postgres.Password, false}, // Existing HMN user ENCRYPTED PASSWORD '%s'
{getSystemUsername(), "", true}, // Postgres.app on Mac CREATEDB
}, `, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil)
guessCredentials()..., _, err := result.Close()
) pgErr, isPgError := err.(*pgconn.PgError)
if err != nil {
var workingCred pgCredentials if !(isPgError && pgErr.SQLState() == "42710") { // NOTE(ben): 42710 means "duplicate object", i.e. already exists
var createUserConn *pgconn.PgConn panic(fmt.Errorf("failed to create HMN user: %w", err))
var connErrors []error
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.
var err error
createUserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
if err == nil {
workingCred = cred
if cred.SafeToPrint {
fmt.Printf("Connected by guessing username \"%s\" and password \"%s\".\n", cred.User, cred.Password)
}
break
} else {
connErrors = append(connErrors, err)
}
}
if createUserConn == nil {
fmt.Println("Failed to connect to the db to reset it.")
fmt.Println("The following errors occurred for each attempted set of credentials:")
for _, err := range connErrors {
fmt.Printf("- %v\n", err)
}
fmt.Println()
fmt.Println("If this is a local development environment, please let us know what platform you")
fmt.Println("are using and how you installed Postgres. We want to try and streamline the setup")
fmt.Println("process for you.")
fmt.Println()
fmt.Println("If on the other hand this is a real deployment, please go into psql and manually")
fmt.Println("create the user:")
fmt.Println()
fmt.Println(" CREATE USER <username> WITH")
fmt.Println(" ENCRYPTED PASSWORD '<password>'")
fmt.Println(" CREATEDB;")
fmt.Println()
fmt.Println("and add the username and password to your config.")
os.Exit(1)
}
defer createUserConn.Close(ctx)
// Create the HMN user
{
userExists := workingCred.User == config.Config.Postgres.User && workingCred.Password == config.Config.Postgres.Password
if !userExists {
result := createUserConn.ExecParams(ctx, fmt.Sprintf(`
CREATE USER %s WITH
ENCRYPTED PASSWORD '%s'
CREATEDB
`, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil)
_, err := result.Close()
if err != nil {
panic(fmt.Errorf("failed to create HMN user: %w", err))
}
} }
} }
} }
// Disconnect all other users
{
result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid()
`, config.Config.Postgres.DbName), nil, nil, nil, nil)
_, err := result.Close()
if err != nil {
panic(fmt.Errorf("failed to disconnect other users: %w", err))
}
}
superuserConn.Close(ctx)
// Connect as the HMN user // Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password) conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err)) panic(fmt.Errorf("failed to connect to db: %w", err))
} }
defer conn.Close(ctx)
// Disconnect all other users
{
result := conn.ExecParams(ctx, fmt.Sprintf(`
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid()
`, config.Config.Postgres.DbName), nil, nil, nil, nil)
_, err := result.Close()
if err != nil {
panic(fmt.Errorf("failed to disconnect other users: %w", err))
}
}
// 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,148 +1,18 @@
{{ template "base.html" . }} {{ template "base-2024.html" . }}
{{ define "extrahead" }}
<script src="{{ static "js/carousel.js" }}"></script>
{{ end }}
{{ 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 }}
<div class="ph3 pt3 bg2 br2 flex flex-column">
<h2 class="f3 mb2">Official Projects</h2>
<div class="grid grid-1 grid-2-ns g3">
{{ range .OfficialProjects }}
{{ template "project_card.html" . }}
{{ end }}
</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>
{{ end }}
</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" }} {{ define "content" }}
{{ if .AllProjects }} <div class="flex justify-center">
{{ template "all_projects" . }} <div class="mw-site flex flex-column g3">
{{ else }} {{ if .OfficialProjects }}
{{ template "single_category" . }} <div class="ph3 pt3 bg2 br2 flex flex-column">
{{ end }} <h2 class="f3 mb2">Official Projects</h2>
<div class="grid grid-1 grid-2-ns g3">
{{ range .OfficialProjects }}
{{ template "project_card.html" . }}
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
{{ 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 OfficialProjects []templates.Project
// Stuff for all projects
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"] officialProjects, err := getShuffledOfficialProjects(c)
pageStr := c.PathParams["page"] if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
currentJam := hmndata.CurrentJam()
previousJam := hmndata.PreviousJam()
if cat == "" && pageStr == "" {
const projectsPerSection = 8
officialProjects, err := getShuffledOfficialProjects(c)
if err != nil {
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")
tmpl := ProjectTemplateData{
BaseData: baseData,
AllProjects: true,
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
res.MustWriteTemplate("project_index.html", tmpl, c.Perf)
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
} }
baseData := getBaseDataAutocrumb(c, "Projects")
tmpl := ProjectTemplateData{
BaseData: baseData,
OfficialProjects: officialProjects,
}
var res ResponseData
res.MustWriteTemplate("project_index.html", tmpl, 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"))