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:
65
app/internal/contact/validate.go
Normal file
65
app/internal/contact/validate.go
Normal 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
|
||||
}
|
||||
27
app/internal/contact/validate_test.go
Normal file
27
app/internal/contact/validate_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package contact
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParse_valid(t *testing.T) {
|
||||
sub, errs := Parse("Jimmy", "jim@example.com", "Hello there, this is a test message.")
|
||||
if errs.Any() {
|
||||
t.Fatalf("unexpected errors: %v", errs)
|
||||
}
|
||||
if sub.Name != "Jimmy" || sub.Email != "jim@example.com" {
|
||||
t.Fatalf("got %+v", sub)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_shortMessage(t *testing.T) {
|
||||
_, errs := Parse("Jimmy", "jim@example.com", "short")
|
||||
if !errs.Any() {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_invalidEmail(t *testing.T) {
|
||||
_, errs := Parse("Jimmy", "not-an-email", "This message is long enough to pass.")
|
||||
if errs["email"] == "" {
|
||||
t.Fatal("expected email error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user