henwen/front/server.go

389 lines
10 KiB
Go

package front
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/rickb777/date"
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.A(a.Href(pathCreate))(
hm.Text("Create event"),
),
}
_ = h.writePage(w, "Welcome!", 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",
}
type voteState struct {
eventAlphaID string
name string
earliest, latest date.Date
}
func voteForm(disabled bool, st voteState) hm.Term {
dateSpan := st.latest.Sub(st.earliest)
var dates []date.Date
for offset := date.PeriodOfDays(0); offset <= dateSpan; offset++ {
dates = append(dates, st.earliest.Add(offset))
}
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 := range dates {
value := fmt.Sprintf("%8s-%02d", day.Format("20060102"), hour)
row = append(row, e.Td()(e.Input(
a.Name(fieldNameDateHour),
a.Value(value),
a.Type("checkbox"),
a.Disabled(disabled),
)))
}
rows = append(rows, e.Tr()(row))
}
return e.Form(a.Method(http.MethodPost), a.Action(pathDoVote))(
e.Input(a.Type("hidden"), a.Name(fieldNameEventID), a.Value(st.eventAlphaID)),
e.Label(a.For(fieldNameGuestName))(hm.Text("What's your name?")),
e.Br(),
e.Input(a.Name(fieldNameGuestName), a.Size(40), a.Value(st.name), a.Disabled(disabled)),
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) {
eventAlphaID := r.URL.Query().Get(fieldNameEventID)
event, err := h.store.GetEventMetadata(context.Background(), back.GetEventMetadataQuery{
AlphaID: eventAlphaID,
})
// TODO return 404 if event not found
if err != nil {
fmt.Fprint(w, err)
return
}
state := voteState{
eventAlphaID: eventAlphaID,
earliest: event.Earliest,
latest: event.Latest,
}
body := hm.Terms{
e.P()(hm.Text(event.Description)),
voteForm(false, state),
}
_ = h.writePage(w, event.Name, body)
}
func (h *handler) handleDoVote(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
fmt.Printf("received form: %+v\n", r.Form)
cmd := back.CreateEventResponseCommand{
EventAlphaID: r.Form.Get(fieldNameEventID),
GuestName: r.Form.Get(fieldNameGuestName),
DateHours: make(map[date.Date]map[int]struct{}),
}
for _, dateHourString := range r.Form[fieldNameDateHour] {
var dateString string
var hour int
_, err := fmt.Sscanf(dateHourString, "%8s-%02d", &dateString, &hour)
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
d, err := date.Parse("20060102", dateString)
if err != nil {
log.Println(err)
http.Error(w, "invalid form values", http.StatusBadRequest)
return
}
fmt.Println("parsed date/hour:", d, hour)
if cmd.DateHours[d] == nil {
cmd.DateHours[d] = make(map[int]struct{})
}
cmd.DateHours[d][hour] = struct{}{}
}
_, err = h.store.CreateEventResponse(r.Context(), cmd)
if err != nil {
log.Println(err)
// TODO handle not found
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// TODO Consider redirecting to vote edit page once that exists.
// TODO Use actual data.
state := voteState{
name: r.Form.Get(fieldNameGuestName),
earliest: date.New(2006, time.May, 3),
latest: date.New(2006, time.May, 8),
}
body := hm.Terms{
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, "Billy's birthday party", body)
}
func (h *handler) handleCreate(w http.ResponseWriter, r *http.Request) {
body := hm.Terms{
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, "Create an event", body)
}
func (h *handler) handleDoCreate(w http.ResponseWriter, r *http.Request) {
// TODO consider redirecting to admin
earliest, err := date.Parse(formDateLayout, r.FormValue(fieldNameEarliest))
if err != nil {
fmt.Fprint(w, "bad earliest date")
return
}
latest, err := date.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,
Earliest: earliest,
Latest: latest,
})
if err != nil {
fmt.Fprint(w, err)
return
}
var adminQuery = make(url.Values)
adminQuery.Add(fieldNameEventID, event.AlphaID)
adminQuery.Add(fieldNameAdminCode, event.AdminCode)
adminURL := h.baseURL + pathAdmin + "?" + adminQuery.Encode()
var voteQuery = make(url.Values)
voteQuery.Add(fieldNameEventID, event.AlphaID)
voteURL := h.baseURL + pathVote + "?" + voteQuery.Encode()
const dateDisplayFmt = "Monday, January 2, 2006"
body := hm.Terms{
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, "Created event", body)
}
func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
eventID := query.Get(fieldNameEventID)
// TODO authenticate with admin code
metadata, err := h.store.GetEventMetadata(context.Background(), back.GetEventMetadataQuery{
AlphaID: eventID,
})
// TODO return 404 if event not found
if err != nil {
fmt.Fprint(w, err)
return
}
responses, err := h.store.GetEventResponseSummary(context.Background(), back.GetEventResponseSummaryQuery{
AlphaID: eventID,
})
if err != nil {
fmt.Fprint(w, err)
return
}
// TODO show results (number of responses, grid)
body := hm.Terms{
e.Form()(
e.Label(a.For(fieldNameEventName))(hm.Text("Event name")),
e.Input(
a.Name(fieldNameEventName),
a.Value(metadata.Name),
),
e.Br(),
e.Label(a.For(fieldNameDescription))(hm.Text("Description")),
e.Textarea(a.Name(fieldNameDescription))(hm.Text(metadata.Description)),
e.Br(),
e.Label(a.For(fieldNameEarliest))(hm.Text("Earliest date")),
e.Input(
a.Name(fieldNameEarliest),
a.Type("date"),
a.Value(metadata.Earliest.Format(formDateLayout)),
),
e.Br(),
e.Label(a.For(fieldNameLatest))(hm.Text("Latest date")),
e.Input(
a.Name(fieldNameLatest),
a.Type("date"),
a.Value(metadata.Latest.Format(formDateLayout)),
),
e.Br(),
e.Input(a.Type("submit")),
),
e.H3()(hm.Text("Responses")),
e.P()(hm.Text(strconv.Itoa(responses.TotalResponses))),
}
_ = h.writePage(w, "Edit your event", body)
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
func (h *handler) writePage(w io.Writer, title string, contents hm.Term) error {
// TODO Need optional subtitles, and titles should be optional.
// Take a Page struct with title, subtitle, and contents.
page := e.Html()(
e.Head()(
e.Title()(hm.Text(h.title+" — "+title)),
),
e.Body()(
e.H1()(e.A(a.Href(h.baseURL+pathRoot))(hm.Text(h.title))),
e.Div()(
e.H2()(hm.Text(title)),
contents,
),
),
)
_, err := hm.WriteDocument(w, page)
return err
}
const (
fieldNameDateHour = "date_hour"
fieldNameEarliest = "earliest_date"
fieldNameLatest = "latest_date"
fieldNameEventName = "event_name"
fieldNameDescription = "event_description"
fieldNameGuestName = "guest_name"
fieldNameEventID = "event_id"
fieldNameAdminCode = "admin_code"
)
const formDateLayout = "2006-01-02"