diff --git a/.gitignore b/.gitignore
index 7258ec0d..831d1cea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ local/backups
/tmp
*.exe
.DS_Store
+__debug_bin*
# vim session saves
Session.vim
diff --git a/public/HMNLogo_SaveTheDate.png b/public/HMNLogo_SaveTheDate.png
new file mode 100644
index 00000000..e304d23c
Binary files /dev/null and b/public/HMNLogo_SaveTheDate.png differ
diff --git a/public/style.css b/public/style.css
index 82073832..1faf5210 100644
--- a/public/style.css
+++ b/public/style.css
@@ -7346,12 +7346,6 @@ article code {
margin-left: auto;
margin-right: auto; }
-.flex-shrink-0, .edit-form .edit-form-row > :first-child {
- flex-shrink: 0; }
-
-.flex-grow-1, .edit-form .edit-form-row > :nth-child(2) {
- flex-grow: 1; }
-
.flex-fair {
flex-basis: 1px;
flex-grow: 1;
@@ -7597,7 +7591,19 @@ article code {
grid-template-columns: 1fr 1fr; }
.bg--dim-ns {
background-color: #f0f0f0;
- background-color: var(--dim-background); } }
+ background-color: var(--dim-background); }
+ .g0-ns {
+ gap: 0; }
+ .g1-ns {
+ gap: 0.25rem; }
+ .g2-ns {
+ gap: 0.5rem; }
+ .g3-ns {
+ gap: 1rem; }
+ .g4-ns {
+ gap: 2rem; }
+ .g5-ns {
+ gap: 4rem; } }
@media screen and (min-width: 35em) and (max-width: 60em) {
.bi-avoid-m {
@@ -7626,7 +7632,19 @@ article code {
grid-template-columns: 1fr 1fr; }
.bg--dim-m {
background-color: #f0f0f0;
- background-color: var(--dim-background); } }
+ background-color: var(--dim-background); }
+ .g0-m {
+ gap: 0; }
+ .g1-m {
+ gap: 0.25rem; }
+ .g2-m {
+ gap: 0.5rem; }
+ .g3-m {
+ gap: 1rem; }
+ .g4-m {
+ gap: 2rem; }
+ .g5-m {
+ gap: 4rem; } }
@media screen and (min-width: 60em) {
.bi-avoid-l {
@@ -7655,7 +7673,19 @@ article code {
grid-template-columns: 1fr 1fr; }
.bg--dim-l {
background-color: #f0f0f0;
- background-color: var(--dim-background); } }
+ background-color: var(--dim-background); }
+ .g0-l {
+ gap: 0; }
+ .g1-l {
+ gap: 0.25rem; }
+ .g2-l {
+ gap: 0.5rem; }
+ .g3-l {
+ gap: 1rem; }
+ .g4-l {
+ gap: 2rem; }
+ .g5-l {
+ gap: 4rem; } }
.not-first:first-child {
display: none; }
diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go
index b4e18379..047a53ce 100644
--- a/src/hmnurl/hmnurl_test.go
+++ b/src/hmnurl/hmnurl_test.go
@@ -463,8 +463,8 @@ func TestTimeMachineFormDone(t *testing.T) {
}
func TestNewsletterSignup(t *testing.T) {
- AssertRegexMatch(t, BuildNewsletterSignup(), RegexNewsletterSignup, nil)
- AssertSubdomain(t, BuildNewsletterSignup(), "")
+ AssertRegexMatch(t, BuildAPINewsletterSignup(), RegexNewsletterSignup, nil)
+ AssertSubdomain(t, BuildAPINewsletterSignup(), "")
}
func TestProjectNewJam(t *testing.T) {
diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go
index d894ea17..24ce8623 100644
--- a/src/hmnurl/urls.go
+++ b/src/hmnurl/urls.go
@@ -49,13 +49,6 @@ func BuildWhenIsIt() string {
return Url("/whenisit", nil)
}
-var RegexNewsletterSignup = regexp.MustCompile("^/newsletter$")
-
-func BuildNewsletterSignup() string {
- defer CatchPanic()
- return Url("/newsletter", nil)
-}
-
var RegexJamsIndex = regexp.MustCompile("^/jams$")
func BuildJamsIndex() string {
@@ -147,6 +140,13 @@ func BuildJamGuidelines2024_Learning() string {
return Url("/jam/learning-2024/guidelines", nil)
}
+var RegexJamSaveTheDate = regexp.MustCompile("^/jam/upcoming$")
+
+func BuildJamSaveTheDate() string {
+ defer CatchPanic()
+ return Url("/jam/upcoming", nil)
+}
+
func BuildJamIndexAny(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/jam/%s", slug), nil)
@@ -945,6 +945,12 @@ func BuildAPICheckUsername() string {
return Url("/api/check_username", nil)
}
+var RegexAPINewsletterSignup = regexp.MustCompile("^/api/newsletter_signup$")
+
+func BuildAPINewsletterSignup() string {
+ return Url("/api/newsletter_signup", nil)
+}
+
/*
* Twitch stuff
*/
diff --git a/src/migration/migrations/2024-05-07T013432Z_newsletter.go b/src/migration/migrations/2024-05-07T013432Z_newsletter.go
new file mode 100644
index 00000000..bc12289e
--- /dev/null
+++ b/src/migration/migrations/2024-05-07T013432Z_newsletter.go
@@ -0,0 +1,47 @@
+package migrations
+
+import (
+ "context"
+ "time"
+
+ "git.handmade.network/hmn/hmn/src/migration/types"
+ "github.com/jackc/pgx/v5"
+)
+
+func init() {
+ registerMigration(newsletter{})
+}
+
+type newsletter struct{}
+
+func (m newsletter) Version() types.MigrationVersion {
+ return types.MigrationVersion(time.Date(2024, 5, 7, 1, 34, 32, 0, time.UTC))
+}
+
+func (m newsletter) Name() string {
+ return "newsletter"
+}
+
+func (m newsletter) Description() string {
+ return "Adds the newsletter signup"
+}
+
+func (m newsletter) Up(ctx context.Context, tx pgx.Tx) error {
+ _, err := tx.Exec(ctx,
+ `
+ CREATE TABLE newsletter_emails (
+ email VARCHAR(255) NOT NULL PRIMARY KEY
+ );
+ `,
+ )
+ return err
+}
+
+func (m newsletter) Down(ctx context.Context, tx pgx.Tx) error {
+ _, err := tx.Exec(ctx,
+ `
+ DROP TABLE newsletter_emails;
+ `,
+ )
+ return err
+}
diff --git a/src/rawdata/scss/_core.scss b/src/rawdata/scss/_core.scss
index 067cce12..29a20705 100644
--- a/src/rawdata/scss/_core.scss
+++ b/src/rawdata/scss/_core.scss
@@ -167,14 +167,6 @@ article code {
margin-right: auto;
}
-.flex-shrink-0 {
- flex-shrink: 0;
-}
-
-.flex-grow-1 {
- flex-grow: 1;
-}
-
.flex-fair {
flex-basis: 1px;
flex-grow: 1;
@@ -410,6 +402,13 @@ article code {
.bg--dim-ns {
@include usevar(background-color, dim-background);
}
+
+ .g0-ns { gap: $spacing-none; }
+ .g1-ns { gap: $spacing-extra-small; }
+ .g2-ns { gap: $spacing-small; }
+ .g3-ns { gap: $spacing-medium; }
+ .g4-ns { gap: $spacing-large; }
+ .g5-ns { gap: $spacing-extra-large; }
}
@media #{$breakpoint-medium} {
@@ -429,6 +428,13 @@ article code {
.bg--dim-m {
@include usevar(background-color, dim-background);
}
+
+ .g0-m { gap: $spacing-none; }
+ .g1-m { gap: $spacing-extra-small; }
+ .g2-m { gap: $spacing-small; }
+ .g3-m { gap: $spacing-medium; }
+ .g4-m { gap: $spacing-large; }
+ .g5-m { gap: $spacing-extra-large; }
}
@media #{$breakpoint-large} {
@@ -448,6 +454,13 @@ article code {
.bg--dim-l {
@include usevar(background-color, dim-background);
}
+
+ .g0-l { gap: $spacing-none; }
+ .g1-l { gap: $spacing-extra-small; }
+ .g2-l { gap: $spacing-small; }
+ .g3-l { gap: $spacing-medium; }
+ .g4-l { gap: $spacing-large; }
+ .g5-l { gap: $spacing-extra-large; }
}
.not-first:first-child {
diff --git a/src/templates/src/jam_save_the_date.html b/src/templates/src/jam_save_the_date.html
new file mode 100644
index 00000000..581cfcf8
--- /dev/null
+++ b/src/templates/src/jam_save_the_date.html
@@ -0,0 +1,333 @@
+{{/*
+This is a copy-paste from base.html because we want to preserve the unique
+style of this page no matter what future changes we make to the base.
+*/}}
+
+
+
+
+
+
+
+
+
+ {{ if .CanonicalLink }} {{ end }}
+ {{ range .OpenGraphItems }}
+ {{ if .Property }}
+
+ {{ else }}
+
+ {{ end }}
+ {{ end }}
+ {{ if .Title }}
+ {{ .Title }} | Handmade Network
+ {{ else }}
+ Handmade Network
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Handmade Jams
+
+
+
Visibility Jam
+
July 19-21, 2024
+
+
+
+
Wheel Reinvention Jam
+
September 23-29, 2024
+
+
+
+
+
The Handmade community runs multiple programming jams every year. If you've participated in a game jam before, the idea is similar—an event where participants build something from scratch in a short period of time. Unlike a game jam, though, we don't declare winners (although we do celebrate our favorite results). See all our prior jams here.
+
To participate in the next one, join our Discord , or sign up for our mailing list:
+
+
+
+
+
+
+
+
+
diff --git a/src/website/api.go b/src/website/api.go
index 1bbc29e9..80645159 100644
--- a/src/website/api.go
+++ b/src/website/api.go
@@ -1,13 +1,16 @@
package website
import (
+ "encoding/json"
"errors"
- "fmt"
+ "io"
"net/http"
+ "strings"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
+ "git.handmade.network/hmn/hmn/src/utils"
)
func APICheckUsername(c *RequestContext) ResponseData {
@@ -44,12 +47,59 @@ func APICheckUsername(c *RequestContext) ResponseData {
}
var res ResponseData
- res.Header().Set("Content-Type", "application/json")
addCORSHeaders(c, &res)
if found {
- res.Write([]byte(fmt.Sprintf(`{ "found": true, "canonical": "%s" }`, canonicalUsername)))
+ res.WriteJson(map[string]any{
+ "found": true,
+ "canonical": canonicalUsername,
+ }, nil)
} else {
- res.Write([]byte(`{ "found": false }`))
+ res.WriteJson(map[string]any{
+ "found": false,
+ }, nil)
}
return res
}
+
+func APINewsletterSignup(c *RequestContext) ResponseData {
+ bodyBytes := utils.Must1(io.ReadAll(c.Req.Body))
+ type Input struct {
+ Email string `json:"email"`
+ }
+ var input Input
+ err := json.Unmarshal(bodyBytes, &input)
+ if err != nil {
+ return c.ErrorResponse(http.StatusBadRequest, err)
+ }
+
+ var res ResponseData
+
+ sanitized := input.Email
+ sanitized = strings.TrimSpace(sanitized)
+ sanitized = strings.ToLower(sanitized)
+ if len(sanitized) > 200 {
+ res.StatusCode = http.StatusBadRequest
+ return res
+ }
+ if !strings.Contains(sanitized, "@") {
+ res.StatusCode = http.StatusBadRequest
+ res.WriteJson(map[string]any{
+ "error": "bad email",
+ }, nil)
+ return res
+ }
+
+ _, err = c.Conn.Exec(c,
+ `
+ INSERT INTO newsletter_emails (email) VALUES ($1)
+ ON CONFLICT DO NOTHING
+ `,
+ sanitized,
+ )
+ if err != nil {
+ return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save email into database"))
+ }
+
+ res.WriteHeader(http.StatusNoContent)
+ return res
+}
diff --git a/src/website/jam.go b/src/website/jam.go
index 9061c60c..dfb42b23 100644
--- a/src/website/jam.go
+++ b/src/website/jam.go
@@ -43,6 +43,39 @@ func JamsIndex(c *RequestContext) ResponseData {
return res
}
+func JamSaveTheDate(c *RequestContext) ResponseData {
+ var res ResponseData
+
+ type TemplateData struct {
+ templates.BaseData
+ UserAvatarUrl string
+ JamsUrl string
+ Visibility2023Url string
+ WRJ2023Url string
+ NewsletterSignupUrl string
+ }
+
+ tmpl := TemplateData{
+ BaseData: getBaseDataAutocrumb(c, "Upcoming Jams"),
+ UserAvatarUrl: templates.UserAvatarUrl(c.CurrentUser, "dark"),
+ JamsUrl: hmnurl.BuildJamsIndex(),
+ Visibility2023Url: hmnurl.BuildJamIndex2023_Visibility(),
+ WRJ2023Url: hmnurl.BuildJamIndex2023(),
+ NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
+ }
+ tmpl.OpenGraphItems = []templates.OpenGraphItem{
+ {Property: "og:title", Value: "Upcoming Jams"},
+ {Property: "og:site_name", Value: "Handmade Network"},
+ {Property: "og:type", Value: "website"},
+ {Property: "og:image", Value: hmnurl.BuildPublic("HMNLogo_SaveThedate.png", true)},
+ {Property: "og:description", Value: "Upcoming programming jams from the Handmade community."},
+ {Property: "og:url", Value: hmnurl.BuildJamSaveTheDate()},
+ }
+
+ res.MustWriteTemplate("jam_save_the_date.html", tmpl, c.Perf)
+ return res
+}
+
func JamIndex2024_Learning(c *RequestContext) ResponseData {
var res ResponseData
diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go
index 8274f2fe..d0b6680c 100644
--- a/src/website/requesthandling.go
+++ b/src/website/requesthandling.go
@@ -3,6 +3,7 @@ package website
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"html"
"html/template"
@@ -454,6 +455,15 @@ func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *per
}
}
+func (rd *ResponseData) WriteJson(data any, rp *perf.RequestPerf) {
+ dataJson, err := json.Marshal(data)
+ if err != nil {
+ panic(err)
+ }
+ rd.Header().Set("Content-Type", "application/json")
+ rd.Write(dataJson)
+}
+
func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) {
defer func() {
/*
diff --git a/src/website/routes.go b/src/website/routes.go
index 1be977df..354a8691 100644
--- a/src/website/routes.go
+++ b/src/website/routes.go
@@ -55,13 +55,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
hmnOnly.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
hmnOnly.GET(hmnurl.RegexConferences, Conferences)
hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt)
- hmnOnly.GET(hmnurl.RegexNewsletterSignup, func(c *RequestContext) ResponseData {
- return c.Redirect("https://cdn.forms-content.sg-form.com/9c83182a-f04a-11ed-a42d-f6f307313b7c", http.StatusFound)
- })
hmnOnly.GET(hmnurl.RegexJamsIndex, JamsIndex)
hmnOnly.GET(hmnurl.RegexJamIndex, func(c *RequestContext) ResponseData {
- return c.Redirect(hmnurl.BuildJamIndex2024_Learning(), http.StatusFound)
+ return c.Redirect(hmnurl.BuildJamSaveTheDate(), http.StatusFound)
})
hmnOnly.GET(hmnurl.RegexJamIndex2021, JamIndex2021)
hmnOnly.GET(hmnurl.RegexJamIndex2022, JamIndex2022)
@@ -74,6 +71,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
hmnOnly.GET(hmnurl.RegexJamIndex2024_Learning, JamIndex2024_Learning)
hmnOnly.GET(hmnurl.RegexJamFeed2024_Learning, JamFeed2024_Learning)
hmnOnly.GET(hmnurl.RegexJamGuidelines2024_Learning, JamGuidelines2024_Learning)
+ hmnOnly.GET(hmnurl.RegexJamSaveTheDate, JamSaveTheDate)
hmnOnly.GET(hmnurl.RegexTimeMachine, TimeMachine)
hmnOnly.GET(hmnurl.RegexTimeMachineSubmissions, TimeMachineSubmissions)
@@ -160,6 +158,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
hmnOnly.POST(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(csrfMiddleware(EducationArticleDeleteSubmit)))
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
+ hmnOnly.POST(hmnurl.RegexAPINewsletterSignup, APINewsletterSignup)
hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)