henwen/front/server.go

334 lines
8.1 KiB
Go

package front
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
hm "gitlab.codemonkeysoftware.net/b/hatmill"
a "gitlab.codemonkeysoftware.net/b/hatmill/attribute"
e "gitlab.codemonkeysoftware.net/b/hatmill/element"
"gitlab.codemonkeysoftware.net/b/henwen/back"
)
const (
pathRoot = "/"
pathCreate = "/create"
pathDoCreate = "/create/do"
pathAdmin = "/admin"
pathVote = "/vote"
pathDoVote = "/vote/do"
)
type handler struct {
mux *http.ServeMux
store *back.Store
title string
baseURL string
}
type HandlerParams struct {
Title string
Store *back.Store
BaseURL string
}
func NewHandler(params HandlerParams) http.Handler {
h := &handler{
store: params.Store,
title: params.Title,
baseURL: params.BaseURL,
mux: http.NewServeMux(),
}
h.mux.HandleFunc(pathRoot, h.handleRoot)
h.mux.HandleFunc(pathCreate, h.handleCreate)
h.mux.HandleFunc(pathDoCreate, h.handleDoCreate)
h.mux.HandleFunc(pathAdmin, h.handleAdmin)
h.mux.HandleFunc(pathVote, h.handleVote)
h.mux.HandleFunc(pathDoVote, h.handleDoVote)
return h
}
func (h *handler) handleRoot(w http.ResponseWriter, r *http.Request) {
body := hm.Terms{
e.H2()(hm.Text("Welcome!")),
e.A(a.Href(pathCreate))(
hm.Text("Create event"),
),
}
_ = h.writePage(w, body)
}
var timeLabels = []string{
"12 AM", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",
"12 PM", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",
}
func disableIf(cond bool, input hm.Term) hm.Term {
if !cond {
return input
}
switch e := input.(type) {
case hm.ParentElement:
e.Attribs = append(e.Attribs, a.Disabled())
return e
case hm.VoidElement:
e.Attribs = append(e.Attribs, a.Disabled())
return e
default:
return e
}
}
type voteState struct {
name string
earliestDate, latestDate time.Time
}
func voteForm(disabled bool, st voteState) hm.Term {
var dates []time.Time
for curr := st.earliestDate; curr.Before(st.latestDate.Add(time.Hour)); curr = curr.AddDate(0, 0, 1) {
dates = append(dates, curr)
}
var ths = hm.Terms{e.Th()()}
for _, date := range dates {
ths = append(ths, e.Th()(hm.Text(date.Format("Jan 2"))))
}
var rows = hm.Terms{e.Thead()(ths)}
for hour := 0; hour < 24; hour++ {
var row = hm.Terms{
e.Td()(hm.Text(timeLabels[hour])),
}
for day := 0; day < len(dates); day++ {
row = append(row, e.Td()(disableIf(disabled, e.Input(a.Type("checkbox")))))
}
rows = append(rows, e.Tr()(row))
}
return e.Form(a.Method(http.MethodPost), a.Action(pathDoVote))(
e.Label()(hm.Text("What's your name?")),
e.Br(),
disableIf(disabled, e.Input(a.Size(40), a.Value(st.name))),
e.Fieldset()(
e.Legend()(hm.Text("When are you available?")),
e.Table()(rows),
),
e.Input(a.Type("submit"), a.Value("Submit")),
)
}
func (h *handler) handleVote(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
event, err := h.store.GetEvent(context.Background(), back.GetEventQuery{
AlphaID: query.Get(keyEventID),
})
// TODO return 404 if event not found
if err != nil {
fmt.Fprint(w, err)
return
}
state := voteState{
earliestDate: event.EarliestDate,
latestDate: event.LatestDate,
}
body := hm.Terms{
e.H2()(hm.Text(event.Name)),
e.P()(hm.Text(event.Description)),
voteForm(false, state),
}
_ = h.writePage(w, body)
}
func (h *handler) handleDoVote(w http.ResponseWriter, r *http.Request) {
// TODO use actual data
state := voteState{
name: "Suzie Q",
earliestDate: time.Date(2006, time.May, 3, 0, 0, 0, 0, time.UTC),
latestDate: time.Date(2006, time.May, 8, 0, 0, 0, 0, time.UTC),
}
body := hm.Terms{
e.H2()(hm.Text("Billy's birthday party")),
e.P()(hm.Text("At Billy's house. Bring presents. Eat cake.")),
e.H3()(hm.Text("Thanks for voting!")),
e.P()(
hm.Text("You can edit your response anytime at "),
e.A(a.Href("#"))(hm.Text("this link")),
hm.Text("."),
),
voteForm(true, state),
}
_ = h.writePage(w, body)
}
func (h *handler) handleCreate(w http.ResponseWriter, r *http.Request) {
body := hm.Terms{
e.H2()(hm.Text("Create an event")),
e.Form(a.Action(pathDoCreate), a.Method(http.MethodPost))(
e.Label(a.For(fieldNameEventName))(hm.Text("Event name")),
e.Input(a.Name(fieldNameEventName)),
e.Label(a.For(fieldNameDescription))(hm.Text("Description")),
e.Textarea(a.Name(fieldNameDescription), a.Placeholder("What's going on?"))(),
e.Label(a.For(fieldNameEarliest))(hm.Text("Earliest date")),
e.Input(a.Name(fieldNameEarliest), a.Type("date")),
e.Label(a.For(fieldNameLatest))(hm.Text("Latest date")),
e.Input(a.Name(fieldNameLatest), a.Type("date")),
e.Input(a.Type("submit")),
),
}
_ = h.writePage(w, body)
}
func (h *handler) handleDoCreate(w http.ResponseWriter, r *http.Request) {
earliest, err := time.Parse(formDateLayout, r.FormValue(fieldNameEarliest))
if err != nil {
fmt.Fprint(w, "bad earliest date")
return
}
latest, err := time.Parse(formDateLayout, r.FormValue(fieldNameLatest))
if err != nil {
fmt.Fprint(w, "bad latest date")
return
}
eventName := r.FormValue(fieldNameEventName)
if eventName == "" {
fmt.Fprint(w, "event name is required")
return
}
description := r.FormValue(fieldNameDescription)
event, err := h.store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: eventName,
Description: description,
EarliestDate: earliest,
LatestDate: latest,
})
if err != nil {
fmt.Fprint(w, err)
return
}
var adminQuery = make(url.Values)
adminQuery.Add(keyEventID, event.AlphaID)
adminQuery.Add(keyAdminCode, event.AdminCode)
adminURL := h.baseURL + pathAdmin + "?" + adminQuery.Encode()
var voteQuery = make(url.Values)
voteQuery.Add(keyEventID, event.AlphaID)
voteURL := h.baseURL + pathVote + "?" + voteQuery.Encode()
const dateDisplayFmt = "Monday, January 2, 2006"
body := hm.Terms{
e.H2()(hm.Text("Created event!")),
e.P()(
hm.Text("You can find it again at "),
e.A(a.Href(adminURL))(hm.Text(adminURL)),
hm.Text("."),
),
e.P()(
hm.Text("Your guests can vote on times at "),
e.A(a.Href(voteURL))(hm.Text(voteURL)),
hm.Text("."),
),
e.H3()(hm.Text("Name")),
hm.Text(eventName),
e.H3()(hm.Text("Description")),
hm.Text(description),
e.H3()(hm.Text("Earliest date")),
hm.Text(earliest.Format(dateDisplayFmt)),
e.H3()(hm.Text("Latest date")),
hm.Text(latest.Format(dateDisplayFmt)),
}
_ = h.writePage(w, body)
}
func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
// TODO authenticate with admin code
event, err := h.store.GetEvent(context.Background(), back.GetEventQuery{
AlphaID: query.Get(keyEventID),
})
// TODO return 404 if event not found
if err != nil {
fmt.Fprint(w, err)
return
}
body := hm.Terms{
e.H2()(hm.Text("Edit your event")),
e.Form()(
e.Label(a.For(fieldNameEventName))(hm.Text("Event name")),
e.Input(
a.Name(fieldNameEventName),
a.Value(event.Name),
),
e.Br(),
e.Label(a.For(fieldNameDescription))(hm.Text("Description")),
e.Textarea(a.Name(fieldNameDescription))(hm.Text(event.Description)),
e.Br(),
e.Label(a.For(fieldNameEarliest))(hm.Text("Earliest date")),
e.Input(
a.Name(fieldNameEarliest),
a.Type("date"),
a.Value(event.EarliestDate.Format(formDateLayout)),
),
e.Br(),
e.Label(a.For(fieldNameLatest))(hm.Text("Latest date")),
e.Input(
a.Name(fieldNameLatest),
a.Type("date"),
a.Value(event.LatestDate.Format(formDateLayout)),
),
e.Br(),
e.Input(a.Type("submit")),
),
}
_ = h.writePage(w, body)
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
func (h *handler) writePage(w io.Writer, contents hm.Term) error {
page := e.Html()(
e.Head()(
e.Title()(hm.Text(h.title)),
),
e.Body()(
e.H1()(hm.Text(h.title)),
e.Div()(contents),
),
)
_, err := hm.WriteDocument(w, page)
return err
}
const (
fieldNameEarliest = "earliestDate"
fieldNameLatest = "latestDate"
fieldNameEventName = "eventName"
fieldNameDescription = "eventDescription"
)
const keyEventID = "event_id"
const keyAdminCode = "admin_code"
const formDateLayout = "2006-01-02"