Add upcoming jam page
This commit is contained in:
parent
7fd57f692b
commit
22265c9081
|
@ -16,6 +16,7 @@ local/backups
|
||||||
/tmp
|
/tmp
|
||||||
*.exe
|
*.exe
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__debug_bin*
|
||||||
|
|
||||||
# vim session saves
|
# vim session saves
|
||||||
Session.vim
|
Session.vim
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
|
@ -7346,12 +7346,6 @@ article code {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 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-fair {
|
||||||
flex-basis: 1px;
|
flex-basis: 1px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -7597,7 +7591,19 @@ article code {
|
||||||
grid-template-columns: 1fr 1fr; }
|
grid-template-columns: 1fr 1fr; }
|
||||||
.bg--dim-ns {
|
.bg--dim-ns {
|
||||||
background-color: #f0f0f0;
|
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) {
|
@media screen and (min-width: 35em) and (max-width: 60em) {
|
||||||
.bi-avoid-m {
|
.bi-avoid-m {
|
||||||
|
@ -7626,7 +7632,19 @@ article code {
|
||||||
grid-template-columns: 1fr 1fr; }
|
grid-template-columns: 1fr 1fr; }
|
||||||
.bg--dim-m {
|
.bg--dim-m {
|
||||||
background-color: #f0f0f0;
|
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) {
|
@media screen and (min-width: 60em) {
|
||||||
.bi-avoid-l {
|
.bi-avoid-l {
|
||||||
|
@ -7655,7 +7673,19 @@ article code {
|
||||||
grid-template-columns: 1fr 1fr; }
|
grid-template-columns: 1fr 1fr; }
|
||||||
.bg--dim-l {
|
.bg--dim-l {
|
||||||
background-color: #f0f0f0;
|
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 {
|
.not-first:first-child {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
|
@ -463,8 +463,8 @@ func TestTimeMachineFormDone(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewsletterSignup(t *testing.T) {
|
func TestNewsletterSignup(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildNewsletterSignup(), RegexNewsletterSignup, nil)
|
AssertRegexMatch(t, BuildAPINewsletterSignup(), RegexNewsletterSignup, nil)
|
||||||
AssertSubdomain(t, BuildNewsletterSignup(), "")
|
AssertSubdomain(t, BuildAPINewsletterSignup(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProjectNewJam(t *testing.T) {
|
func TestProjectNewJam(t *testing.T) {
|
||||||
|
|
|
@ -49,13 +49,6 @@ func BuildWhenIsIt() string {
|
||||||
return Url("/whenisit", nil)
|
return Url("/whenisit", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexNewsletterSignup = regexp.MustCompile("^/newsletter$")
|
|
||||||
|
|
||||||
func BuildNewsletterSignup() string {
|
|
||||||
defer CatchPanic()
|
|
||||||
return Url("/newsletter", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var RegexJamsIndex = regexp.MustCompile("^/jams$")
|
var RegexJamsIndex = regexp.MustCompile("^/jams$")
|
||||||
|
|
||||||
func BuildJamsIndex() string {
|
func BuildJamsIndex() string {
|
||||||
|
@ -147,6 +140,13 @@ func BuildJamGuidelines2024_Learning() string {
|
||||||
return Url("/jam/learning-2024/guidelines", nil)
|
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 {
|
func BuildJamIndexAny(slug string) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
return Url(fmt.Sprintf("/jam/%s", slug), nil)
|
return Url(fmt.Sprintf("/jam/%s", slug), nil)
|
||||||
|
@ -945,6 +945,12 @@ func BuildAPICheckUsername() string {
|
||||||
return Url("/api/check_username", nil)
|
return Url("/api/check_username", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var RegexAPINewsletterSignup = regexp.MustCompile("^/api/newsletter_signup$")
|
||||||
|
|
||||||
|
func BuildAPINewsletterSignup() string {
|
||||||
|
return Url("/api/newsletter_signup", nil)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Twitch stuff
|
* Twitch stuff
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -167,14 +167,6 @@ article code {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-shrink-0 {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-grow-1 {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-fair {
|
.flex-fair {
|
||||||
flex-basis: 1px;
|
flex-basis: 1px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -410,6 +402,13 @@ article code {
|
||||||
.bg--dim-ns {
|
.bg--dim-ns {
|
||||||
@include usevar(background-color, dim-background);
|
@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} {
|
@media #{$breakpoint-medium} {
|
||||||
|
@ -429,6 +428,13 @@ article code {
|
||||||
.bg--dim-m {
|
.bg--dim-m {
|
||||||
@include usevar(background-color, dim-background);
|
@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} {
|
@media #{$breakpoint-large} {
|
||||||
|
@ -448,6 +454,13 @@ article code {
|
||||||
.bg--dim-l {
|
.bg--dim-l {
|
||||||
@include usevar(background-color, dim-background);
|
@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 {
|
.not-first:first-child {
|
||||||
|
|
|
@ -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.
|
||||||
|
*/}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US" {{ if .OpenGraphItems }} prefix="og: http://ogp.me/ns#"{{ end }}>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ static "favicon-16x16.png" }}">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ static "favicon-32x32.png" }}">
|
||||||
|
|
||||||
|
{{ if .CanonicalLink }}<link rel="canonical" href="{{ .CanonicalLink }}">{{ end }}
|
||||||
|
{{ range .OpenGraphItems }}
|
||||||
|
{{ if .Property }}
|
||||||
|
<meta property="{{ .Property }}" content="{{ .Value }}" />
|
||||||
|
{{ else }}
|
||||||
|
<meta name="{{ .Name }}" content="{{ .Value }}" />
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Title }}
|
||||||
|
<title>{{ .Title }} | Handmade Network</title>
|
||||||
|
{{ else }}
|
||||||
|
<title>Handmade Network</title>
|
||||||
|
{{ end }}
|
||||||
|
<meta name="theme-color" content="#232426">
|
||||||
|
|
||||||
|
<!-- <script src="{{ static "js/templates.js" }}"></script> -->
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ static "fonts/mohave/stylesheet.css" }}">
|
||||||
|
<link href='https://fonts.googleapis.com/css?family=Fira+Sans:300,400,500,600' rel='stylesheet' type='text/css'>
|
||||||
|
<link href='https://fonts.googleapis.com/css?family=Fira+Mono:300,400,500,700' rel='stylesheet' type='text/css'>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--theme-gradient-dark: linear-gradient(to bottom right, #232426, #BFB6BE);
|
||||||
|
--theme-gradient-light: linear-gradient(to bottom right, #8BD5FF, #5899FF);
|
||||||
|
--white: #fff;
|
||||||
|
--bg-button: rgba(255, 255, 255, 0);
|
||||||
|
--bg-button-hover: rgba(255, 255, 255, 0.1);
|
||||||
|
--charcoal: #2F2F2F;
|
||||||
|
--gray: #CBCBCB;
|
||||||
|
--rich-gray: #494949;
|
||||||
|
|
||||||
|
--spacing-0: 0;
|
||||||
|
--spacing-1: .25rem;
|
||||||
|
--spacing-2: .5rem;
|
||||||
|
--spacing-3: 1rem;
|
||||||
|
--spacing-4: 2rem;
|
||||||
|
--spacing-5: 4rem;
|
||||||
|
--spacing-6: 8rem;
|
||||||
|
--spacing-7: 16rem;
|
||||||
|
--border-radius-2: 0.25rem;
|
||||||
|
|
||||||
|
--link-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Inter", "Fira Sans", sans-serif;
|
||||||
|
font-size: 1rem; /* remove this override when base stylesheet is less dumb */
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w2-5 {
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link--white {
|
||||||
|
--link-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content p {
|
||||||
|
/* stupid override, should be done by .post-content instead of .content */
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg--theme-gradient-dark {
|
||||||
|
background: var(--theme-gradient-dark);
|
||||||
|
/* background: linear-gradient(to bottom right, #333, #BBB); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg--theme-gradient-light {
|
||||||
|
background: var(--theme-gradient-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg--charcoal {
|
||||||
|
background: var(--charcoal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg--rich-gray {
|
||||||
|
background: var(--rich-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg--gray {
|
||||||
|
background: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b--charcoal {
|
||||||
|
border-color: var(--charcoal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b--rich-gray {
|
||||||
|
border-color: var(--rich-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c--theme-gradient-dark {
|
||||||
|
background: var(--theme-gradient-dark);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c--theme-gradient-light {
|
||||||
|
background: var(--theme-gradient-light);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c--theme-gradient-light {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c--gray {
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--jam {
|
||||||
|
border: 1px solid var(--white);
|
||||||
|
border-radius: var(--border-radius-2) !important;
|
||||||
|
padding: var(--spacing-2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--jam {
|
||||||
|
border: 1px solid var(--white);
|
||||||
|
border-radius: var(--border-radius-2);
|
||||||
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
|
|
||||||
|
background-color: var(--bg-button);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--jam.small {
|
||||||
|
padding: var(--spacing-1) var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--jam:hover {
|
||||||
|
background-color: var(--bg-button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-simple {
|
||||||
|
background: var(--bg-button);
|
||||||
|
border: 1px solid var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-simple:hover {
|
||||||
|
background: var(--bg-button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-white {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-mask {
|
||||||
|
mask: var(--mask-url);
|
||||||
|
-webkit-mask: var(--mask-url);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-current {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide-screen {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-fill iframe {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hmn-logo {
|
||||||
|
font-family: 'MohaveHMN', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
padding: 0.2rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
padding-right: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar.header {
|
||||||
|
width: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jam-logo {
|
||||||
|
max-width: 18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jam-logo-small {
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jam-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
/* align the bounding box with the visible text */
|
||||||
|
margin-left: -0.06em;
|
||||||
|
margin-right: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-headline {
|
||||||
|
width: 20.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* not small */
|
||||||
|
@media screen and (min-width: 35em) {
|
||||||
|
.w-headline {
|
||||||
|
width: 44.67rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jam-title {
|
||||||
|
font-size: 5.53rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg--theme-gradient-dark c-white flex flex-column">
|
||||||
|
<div class="bg-black-20 flex flex-row items-center link--white">
|
||||||
|
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo flex-shrink-0">
|
||||||
|
Handmade
|
||||||
|
</a>
|
||||||
|
<div class="flex-grow-1 flex-shrink-1"></div>
|
||||||
|
<div class="header-nav flex flex-row items-center g3 lh-solid f6">
|
||||||
|
<a href="{{ .Header.ProjectIndexUrl }}">Projects</a>
|
||||||
|
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
||||||
|
<a class="db" href="{{ or .Header.UserProfileUrl .LoginPageUrl }}">
|
||||||
|
<img class="user-avatar header" src="{{ .UserAvatarUrl }}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tc flex-grow-1 flex justify-center items-center-ns g5 pv5">
|
||||||
|
<div class="w-headline flex flex-column">
|
||||||
|
<div class="jam-title mb4 lh-solid">Handmade Jams</div>
|
||||||
|
<div class="flex flex-column flex-row-ns justify-between-ns g4 g0-ns lh-solid">
|
||||||
|
<div>
|
||||||
|
<div class="fw7 f3 f2-ns">Visibility Jam</div>
|
||||||
|
<div class="fw3 f4 mt1">July 19-21, 2024</div>
|
||||||
|
<div class="fw6 f5 pt3">
|
||||||
|
<a href="{{ .Visibility2023Url }}" class="db">
|
||||||
|
Last year's jam<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw7 f3 f2-ns">Wheel Reinvention Jam</div>
|
||||||
|
<div class="fw3 f4 mt1">September 23-29, 2024</div>
|
||||||
|
<div class="fw6 f5 pt3">
|
||||||
|
<a href="{{ .WRJ2023Url }}" class="db">
|
||||||
|
Last year's jam<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tl tj-ns mt4 post-content">
|
||||||
|
<p>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). <a class="fw6" href="{{ .JamsUrl }}">See all our prior jams here.</a></p>
|
||||||
|
<p>To participate in the next one, <a class="fw6" href="https://discord.gg/hmn">join our Discord</a>, or sign up for our mailing list:</p>
|
||||||
|
</div>
|
||||||
|
<form id="newsletter-form" class="mt4 flex flex-column g1">
|
||||||
|
<div class="flex g2 justify-center">
|
||||||
|
<input id="newsletter-email" type="email" class="input--jam flex-grow-1 flex-grow-0-ns" placeholder="me@example.com">
|
||||||
|
<button id="newsletter-button" class="btn--jam">Sign up</button>
|
||||||
|
</div>
|
||||||
|
<div id="newsletter-message" class="f7">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector("#newsletter-form");
|
||||||
|
const emailField = document.querySelector("#newsletter-email");
|
||||||
|
const button = document.querySelector("#newsletter-button");
|
||||||
|
const message = document.querySelector("#newsletter-message");
|
||||||
|
|
||||||
|
form.addEventListener("submit", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("{{ .NewsletterSignupUrl }}", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"email": emailField.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (res.status >= 400) {
|
||||||
|
throw new Error("bad request");
|
||||||
|
}
|
||||||
|
|
||||||
|
message.innerText = "Signed up successfully!";
|
||||||
|
} catch (err) {
|
||||||
|
message.innerText = "There was an error signing up.";
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,13 +1,16 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func APICheckUsername(c *RequestContext) ResponseData {
|
func APICheckUsername(c *RequestContext) ResponseData {
|
||||||
|
@ -44,12 +47,59 @@ func APICheckUsername(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.Header().Set("Content-Type", "application/json")
|
|
||||||
addCORSHeaders(c, &res)
|
addCORSHeaders(c, &res)
|
||||||
if found {
|
if found {
|
||||||
res.Write([]byte(fmt.Sprintf(`{ "found": true, "canonical": "%s" }`, canonicalUsername)))
|
res.WriteJson(map[string]any{
|
||||||
|
"found": true,
|
||||||
|
"canonical": canonicalUsername,
|
||||||
|
}, nil)
|
||||||
} else {
|
} else {
|
||||||
res.Write([]byte(`{ "found": false }`))
|
res.WriteJson(map[string]any{
|
||||||
|
"found": false,
|
||||||
|
}, nil)
|
||||||
}
|
}
|
||||||
return res
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -43,6 +43,39 @@ func JamsIndex(c *RequestContext) ResponseData {
|
||||||
return res
|
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 {
|
func JamIndex2024_Learning(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package website
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"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) {
|
func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -55,13 +55,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
||||||
hmnOnly.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
hmnOnly.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
||||||
hmnOnly.GET(hmnurl.RegexConferences, Conferences)
|
hmnOnly.GET(hmnurl.RegexConferences, Conferences)
|
||||||
hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt)
|
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.RegexJamsIndex, JamsIndex)
|
||||||
hmnOnly.GET(hmnurl.RegexJamIndex, func(c *RequestContext) ResponseData {
|
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.RegexJamIndex2021, JamIndex2021)
|
||||||
hmnOnly.GET(hmnurl.RegexJamIndex2022, JamIndex2022)
|
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.RegexJamIndex2024_Learning, JamIndex2024_Learning)
|
||||||
hmnOnly.GET(hmnurl.RegexJamFeed2024_Learning, JamFeed2024_Learning)
|
hmnOnly.GET(hmnurl.RegexJamFeed2024_Learning, JamFeed2024_Learning)
|
||||||
hmnOnly.GET(hmnurl.RegexJamGuidelines2024_Learning, JamGuidelines2024_Learning)
|
hmnOnly.GET(hmnurl.RegexJamGuidelines2024_Learning, JamGuidelines2024_Learning)
|
||||||
|
hmnOnly.GET(hmnurl.RegexJamSaveTheDate, JamSaveTheDate)
|
||||||
|
|
||||||
hmnOnly.GET(hmnurl.RegexTimeMachine, TimeMachine)
|
hmnOnly.GET(hmnurl.RegexTimeMachine, TimeMachine)
|
||||||
hmnOnly.GET(hmnurl.RegexTimeMachineSubmissions, TimeMachineSubmissions)
|
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.RegexEducationArticleDelete, educationAuthorsOnly(csrfMiddleware(EducationArticleDeleteSubmit)))
|
||||||
|
|
||||||
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
||||||
|
hmnOnly.POST(hmnurl.RegexAPINewsletterSignup, APINewsletterSignup)
|
||||||
|
|
||||||
hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
|
hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
|
||||||
return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)
|
return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)
|
||||||
|
|
Loading…
Reference in New Issue