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-warn-color: #aa7d30;
--notice-failure-color: #b42222;
--site-width: 54rem;
--site-width-narrow: 40rem;
--site-width: 58rem;
--site-width-narrow: 44rem;
--avatar-size-small: 1.5rem;
--avatar-size-normal: 2.5rem;
--theme-color: #b1b1b1;

View File

@ -20,6 +20,7 @@ import (
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"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/logging"
"git.handmade.network/hmn/hmn/src/models"
@ -514,6 +515,8 @@ func init() {
}
}
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, conn)
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)

View File

@ -526,3 +526,20 @@ func SetProjectTag(
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) {
AssertRegexMatch(t, BuildProjectIndex(1, ""), 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, "") })
AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
}
func TestProjectNew(t *testing.T) {

View File

@ -453,22 +453,11 @@ func BuildAtomFeedForShowcase() string {
* 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()
if page < 1 {
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)
}
return Url("/projects", nil)
}
var RegexProjectNew = regexp.MustCompile("^/p/new$")

View File

@ -365,90 +365,87 @@ func ResetDB() {
ctx := context.Background()
// 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(
[]pgCredentials{
{config.Config.Postgres.User, config.Config.Postgres.Password, false}, // Existing HMN user
{getSystemUsername(), "", true}, // Postgres.app on Mac
},
guessCredentials()...,
)
var workingCred pgCredentials
var createUserConn *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
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))
}
result := superuserConn.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()
pgErr, isPgError := err.(*pgconn.PgError)
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))
}
}
}
// 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
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
{
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))
}
}
defer conn.Close(ctx)
// 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
}
const RecentProjectUpdateTimespanSec = 60 * 60 * 24 * 28 // NOTE(asaf): Four weeks
type Project struct {
ID int `db:"id"`
@ -81,6 +79,7 @@ type Project struct {
ForumLastUpdated time.Time `db:"forum_last_updated"`
BlogLastUpdated time.Time `db:"blog_last_updated"`
AnnotationLastUpdated time.Time `db:"annotation_last_updated"`
SnippetLastPosted time.Time `db:"snippet_last_posted"`
ForumEnabled bool `db:"forum_enabled"`
BlogEnabled bool `db:"blog_enabled"`

View File

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

View File

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

View File

@ -1,148 +1,18 @@
{{ template "base.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 }}
{{ template "base-2024.html" . }}
{{ define "content" }}
{{ if .AllProjects }}
{{ template "all_projects" . }}
{{ else }}
{{ template "single_category" . }}
{{ end }}
<div class="flex justify-center">
<div class="mw-site flex flex-column g3">
{{ 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>
</div>
{{ end }}
</div>
</div>
{{ end }}

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import (
"fmt"
"image"
"io"
"math/rand"
"net/http"
"path"
"sort"
@ -60,167 +59,25 @@ func ProjectCSS(c *RequestContext) ResponseData {
type ProjectTemplateData struct {
templates.BaseData
AllProjects bool
// 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
OfficialProjects []templates.Project
}
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)
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
officialProjects, err := getShuffledOfficialProjects(c)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
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) {
@ -232,47 +89,44 @@ func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error)
}
c.Perf.StartBlock("PROJECTS", "Grouping and sorting")
var handmadeHero *templates.Project
var featuredProjects []templates.Project
var recentProjects []templates.Project
var restProjects []templates.Project
now := time.Now()
var handmadeHero hmndata.ProjectAndStuff
var featuredProjects []hmndata.ProjectAndStuff
var restProjects []hmndata.ProjectAndStuff
for _, p := range official {
templateProject := templates.ProjectAndStuffToTemplate(&p)
if p.Project.Slug == "hero" {
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
handmadeHero = &templateProject
handmadeHero = p
continue
}
if p.Project.Featured {
featuredProjects = append(featuredProjects, templateProject)
} else if now.Sub(p.Project.AllLastUpdated).Seconds() < models.RecentProjectUpdateTimespanSec {
recentProjects = append(recentProjects, templateProject)
featuredProjects = append(featuredProjects, p)
} else {
restProjects = append(restProjects, templateProject)
restProjects = append(restProjects, p)
}
}
_, randSeed := now.ISOWeek()
random := rand.New(rand.NewSource(int64(randSeed)))
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] })
random.Shuffle(len(restProjects), func(i, j int) { restProjects[i], restProjects[j] = restProjects[j], restProjects[i] })
sort.Slice(featuredProjects, func(i, j int) bool {
return featuredProjects[i].Project.AllLastUpdated.After(featuredProjects[j].Project.AllLastUpdated)
})
sort.Slice(restProjects, func(i, j int) bool {
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.
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()
return officialProjects, nil
return projects, nil
}
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)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction"))