dodo
diff --git a/apps/dodo/main.go b/apps/dodo/main.go
new file mode 100644
index 0000000..3445b59
--- /dev/null
+++ b/apps/dodo/main.go
@@ -0,0 +1,202 @@
+package main
+
+import (
+ "database/sql"
+ _ "embed"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ _ "github.com/glebarez/go-sqlite"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+var dbPath = flag.String("db-path", "entries.db", "Path to the sqlite file")
+
+//go:embed index.html
+var indexHtml []byte
+
+//go:embed ok.html
+var okHtml []byte
+
+type entry struct {
+ Email string
+ InstallationType string
+ NumMembers int
+ Apps []string
+ PayPerMonth float64
+ PrepayFullYear bool
+ Thoughts string
+}
+
+func getFormValues(f url.Values, name string) ([]string, error) {
+ return f[name], nil
+}
+
+func getFormValue(f url.Values, name string) (string, error) {
+ if ret, ok := f[name]; ok {
+ switch len(ret) {
+ case 0:
+ return "", fmt.Errorf("%s is required", name)
+ case 1:
+ return ret[0], nil
+ default:
+ return "", fmt.Errorf("%s too many values", name)
+ }
+ }
+ return "", fmt.Errorf("%s is required", name)
+}
+
+func getFormValueInt(f url.Values, name string) (int, error) {
+ v, err := getFormValue(f, name)
+ if err != nil {
+ return 0, err
+ }
+ return strconv.Atoi(v)
+}
+
+func getFormValueBool(f url.Values, name string) (bool, error) {
+ v, err := getFormValue(f, name)
+ if err != nil {
+ return false, err
+ }
+ return strconv.ParseBool(v)
+}
+
+func getFormValueFloat64(f url.Values, name string) (float64, error) {
+ v, err := getFormValue(f, name)
+ if err != nil {
+ return 0, err
+ }
+ return strconv.ParseFloat(v, 64)
+}
+
+func NewEntry(f url.Values) (*entry, error) {
+ var e entry
+ var err error = nil
+ e.Email, err = getFormValue(f, "email")
+ if err != nil {
+ return nil, err
+ }
+ e.InstallationType, err = getFormValue(f, "installation_type")
+ if err != nil {
+ return nil, err
+ }
+ e.NumMembers, err = getFormValueInt(f, "num_members")
+ if err != nil {
+ return nil, err
+ }
+ e.Apps, err = getFormValues(f, "apps")
+ if err != nil {
+ return nil, err
+ }
+ e.PayPerMonth, err = getFormValueFloat64(f, "pay_per_month")
+ if err != nil {
+ return nil, err
+ }
+ e.PrepayFullYear, err = getFormValueBool(f, "pay_full_year")
+ if err != nil {
+ return nil, err
+ }
+ e.Thoughts, err = getFormValue(f, "thoughts")
+ if err != nil {
+ return nil, err
+ }
+ return &e, nil
+}
+
+type AddToWaitlistFn func(e *entry) error
+
+type Server struct {
+ port int
+ indexHtml []byte
+ okHtml []byte
+ addToWaitlist func(e *entry) error
+}
+
+func NewServer(port int, indexHtml, okHtml []byte, addToWaitlist AddToWaitlistFn) *Server {
+ return &Server{
+ port,
+ indexHtml,
+ okHtml,
+ addToWaitlist,
+ }
+}
+
+func (s *Server) Start() {
+ http.HandleFunc("/waitlist", s.waitlist)
+ http.HandleFunc("/", s.index)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
+
+func (s *Server) index(w http.ResponseWriter, r *http.Request) {
+ if _, err := w.Write(s.indexHtml); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *Server) waitlist(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request", http.StatusBadRequest)
+ return
+ }
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Invalid request", http.StatusBadRequest)
+ return
+ }
+ e, err := NewEntry(r.PostForm)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Invalid request: %s", err), http.StatusBadRequest)
+ return
+ }
+ if err := s.addToWaitlist(e); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte(s.okHtml))
+}
+
+func NewAddToWaitlist(db *sql.DB) AddToWaitlistFn {
+ return func(e *entry) error {
+ tx, err := db.Begin()
+ if err != nil {
+ return err
+ }
+ stm, err := tx.Prepare(`
+INSERT INTO waitlist (
+ email,
+ installation_type,
+ num_members,
+ apps,
+ pay_per_month,
+ prepay_full_year,
+ thoughts
+) VALUES (
+ ?, ?, ?, ?, ?, ?, ?
+);`)
+ if err != nil {
+ return err
+ }
+ defer stm.Close()
+ if _, err := stm.Exec(e.Email, e.InstallationType, e.NumMembers, strings.Join(e.Apps, ","), e.PayPerMonth, e.PrepayFullYear, e.Thoughts); err != nil {
+ return err
+ }
+ return tx.Commit()
+ }
+}
+
+func main() {
+ flag.Parse()
+ db, err := sql.Open("sqlite", *dbPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer db.Close()
+ s := NewServer(*port, indexHtml, okHtml, NewAddToWaitlist(db))
+ s.Start()
+}