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-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;
|
||||||
|
|
|
@ -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")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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$")
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
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"`
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
Loading…
Reference in New Issue