Update project index a bit, track snippet updates for project recency
This commit is contained in:
parent
06b270514c
commit
9093f38b47
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
},
|
||||
}
|
||||
|
|
|
@ -995,6 +995,8 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, tx)
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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$")
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"))
|
||||
|
|
Reference in New Issue