Add Technical Kiwi website with Go, templ, and HTMX.

Single-page site with gallery by album and event, contact form over SMTP,
Docker dev/prod setup, and on-server image derivatives. Gallery photos stay
local (app/images/ is gitignored).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 23:57:59 +12:00
parent c21be097b0
commit 509e7ccb43
33 changed files with 2635 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
package contact
import (
"net/mail"
"strings"
"unicode/utf8"
)
const (
maxNameLen = 120
maxEmailLen = 254
maxMessageLen = 8000
minMessageLen = 10
)
// Submission is validated contact form input.
type Submission struct {
Name string
Email string
Message string
}
// Errors maps field names to user-facing messages.
type Errors map[string]string
func (e Errors) Any() bool {
return len(e) > 0
}
// Parse reads and validates a contact form POST.
func Parse(name, email, message string) (Submission, Errors) {
errs := make(Errors)
name = strings.TrimSpace(name)
email = strings.TrimSpace(email)
message = strings.TrimSpace(message)
if name == "" {
errs["name"] = "Name is required."
} else if utf8.RuneCountInString(name) > maxNameLen {
errs["name"] = "Name is too long."
}
if email == "" {
errs["email"] = "Email is required."
} else if utf8.RuneCountInString(email) > maxEmailLen {
errs["email"] = "Email is too long."
} else if _, err := mail.ParseAddress(email); err != nil {
errs["email"] = "Enter a valid email address."
}
if message == "" {
errs["message"] = "Message is required."
} else if utf8.RuneCountInString(message) < minMessageLen {
errs["message"] = "Message must be at least 10 characters."
} else if utf8.RuneCountInString(message) > maxMessageLen {
errs["message"] = "Message is too long."
}
if errs.Any() {
return Submission{}, errs
}
return Submission{Name: name, Email: email, Message: message}, nil
}