Start back end and skeleton front end
This commit is contained in:
		
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| module gitlab.codemonkeysoftware.net/b/henwen | ||||
|  | ||||
| go 1.14 | ||||
|  | ||||
| require ( | ||||
| 	crawshaw.io/sqlite v0.2.5 | ||||
| 	gitlab.codemonkeysoftware.net/b/hatmill v0.0.5 | ||||
| ) | ||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= | ||||
| crawshaw.io/sqlite v0.2.5 h1:lUjWtEQJZApoL4V83cHsNeaGZkY5Ofmh8cGGBdZNxPU= | ||||
| crawshaw.io/sqlite v0.2.5/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= | ||||
| github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= | ||||
| gitlab.codemonkeysoftware.net/b/hatmill v0.0.5 h1:7L70G1Qmdm7bSwH58qWY5psXOwoqcvokLhc4BPSIjWo= | ||||
| gitlab.codemonkeysoftware.net/b/hatmill v0.0.5/go.mod h1:3jggrD9qkK8kXt0d3+Kwg3zti2PfPcociz7r3oxtjWU= | ||||
							
								
								
									
										121
									
								
								http/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								http/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	h "gitlab.codemonkeysoftware.net/b/hatmill" | ||||
| 	a "gitlab.codemonkeysoftware.net/b/hatmill/attribute" | ||||
| 	e "gitlab.codemonkeysoftware.net/b/hatmill/element" | ||||
| 	_ "gitlab.codemonkeysoftware.net/b/henwen" | ||||
| ) | ||||
|  | ||||
| const Addr = ":8080" | ||||
| const Title = "Henwen" | ||||
|  | ||||
| const ( | ||||
| 	PathRoot     = "/" | ||||
| 	PathCreate   = "/create" | ||||
| 	PathDoCreate = "/create/do" | ||||
| ) | ||||
|  | ||||
| func writePage(w io.Writer, contents h.Term) error { | ||||
| 	page := e.Html()( | ||||
| 		e.Head()( | ||||
| 			e.Title()(h.Text(Title)), | ||||
| 		), | ||||
| 		e.Body()( | ||||
| 			e.H1()(h.Text(Title)), | ||||
| 			e.Div()(contents), | ||||
| 		), | ||||
| 	) | ||||
| 	_, err := h.WriteDocument(w, page) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func pageRoot() h.Term { | ||||
| 	return h.Terms{ | ||||
| 		e.H2()(h.Text("Welcome!")), | ||||
| 		e.A(a.Href(PathCreate))( | ||||
| 			h.Text("Create event"), | ||||
| 		), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	fieldNameEarliest  = "earliestDate" | ||||
| 	fieldNameLatest    = "latestDate" | ||||
| 	fieldNameEventName = "eventName" | ||||
| ) | ||||
|  | ||||
| func pageCreate() h.Term { | ||||
| 	return h.Terms{ | ||||
| 		e.H2()(h.Text("Create an event")), | ||||
| 		e.Form(a.Action(PathDoCreate), a.Method("POST"))( | ||||
| 			e.Label(a.For(fieldNameEventName))(h.Text("Event name")), | ||||
| 			e.Input(a.Name(fieldNameEventName)), | ||||
|  | ||||
| 			e.Label(a.For(fieldNameEarliest))(h.Text("Earliest date")), | ||||
| 			e.Input(a.Name(fieldNameEarliest), a.Type("date")), | ||||
|  | ||||
| 			e.Label(a.For(fieldNameLatest))(h.Text("Latest date")), | ||||
| 			e.Input(a.Name(fieldNameLatest), a.Type("date")), | ||||
|  | ||||
| 			e.Input(a.Type("submit")), | ||||
| 		), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pageDoCreate(name string, earliest, latest time.Time) h.Term { | ||||
| 	return h.Terms{ | ||||
| 		e.H2()(h.Text("Created event!")), | ||||
|  | ||||
| 		e.H3()(h.Text("Name")), | ||||
| 		h.Text(name), | ||||
|  | ||||
| 		e.H3()(h.Text("Earliest date")), | ||||
| 		h.Text(earliest.Format(time.ANSIC)), | ||||
|  | ||||
| 		e.H3()(h.Text("Latest date")), | ||||
| 		h.Text(latest.Format(time.ANSIC)), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.HandleFunc(PathRoot, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		_ = writePage(w, pageRoot()) | ||||
| 	}) | ||||
| 	mux.HandleFunc(PathCreate, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		_ = writePage(w, pageCreate()) | ||||
| 	}) | ||||
| 	mux.HandleFunc(PathDoCreate, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		const dateFmt = "2006-01-02" | ||||
| 		earliest, err := time.Parse(dateFmt, r.FormValue(fieldNameEarliest)) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprint(w, "bad earliest date") | ||||
| 			return | ||||
| 		} | ||||
| 		latest, err := time.Parse(dateFmt, 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 | ||||
| 		} | ||||
|  | ||||
| 		_ = writePage(w, pageDoCreate(eventName, earliest, latest)) | ||||
| 	}) | ||||
|  | ||||
| 	srv := http.Server{ | ||||
| 		Addr:    Addr, | ||||
| 		Handler: mux, | ||||
| 	} | ||||
| 	log.Println(srv.ListenAndServe()) | ||||
| } | ||||
							
								
								
									
										131
									
								
								store.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								store.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| package henwen | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"crawshaw.io/sqlite/sqlitex" | ||||
| ) | ||||
|  | ||||
| type GenString func() (string, error) | ||||
|  | ||||
| type Store struct { | ||||
| 	pool      *sqlitex.Pool | ||||
| 	genString GenString | ||||
| } | ||||
|  | ||||
| const InMemory = ":memory:" | ||||
|  | ||||
| func NewStore(filename string, genString GenString) (*Store, error) { | ||||
| 	var needCreate bool | ||||
| 	if filename == InMemory { | ||||
| 		filename = "file::memory:?mode=memory" | ||||
| 		needCreate = true | ||||
| 	} else if _, err := os.Stat(filename); os.IsNotExist(err) { | ||||
| 		needCreate = true | ||||
| 	} | ||||
| 	// If the file exists, then assume it was created properly. | ||||
|  | ||||
| 	pool, err := sqlitex.Open(filename, 0, 10) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	store := &Store{ | ||||
| 		pool:      pool, | ||||
| 		genString: genString, | ||||
| 	} | ||||
| 	if needCreate { | ||||
| 		err = store.createSchema() | ||||
| 		if err != nil { | ||||
| 			defer pool.Close() | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return store, nil | ||||
| } | ||||
|  | ||||
| func (s *Store) Close() error { | ||||
| 	return s.pool.Close() | ||||
| } | ||||
|  | ||||
| const schema = ` | ||||
| 	CREATE TABLE event ( | ||||
| 		id INTEGER PRIMARY KEY, | ||||
| 		alpha_id TEXT NOT NULL, | ||||
| 		admin_alpha_id TEXT NOT NULL, | ||||
| 		name TEXT NOT NULL, | ||||
| 		earliest_date DATE NOT NULL, | ||||
| 		latest_date DATE NOT NULL, | ||||
| 		duration INTEGER NOT NULL, | ||||
|  | ||||
| 		UNIQUE (alpha_id) | ||||
| 	); | ||||
|  | ||||
| 	CREATE TABLE response ( | ||||
| 		id INTEGER PRIMARY KEY, | ||||
| 		alpha_id TEXT NOT NULL, | ||||
| 		event_id INTEGER NOT NULL, | ||||
|  | ||||
| 		UNIQUE (alpha_id), | ||||
| 		FOREIGN KEY (event_id) REFERENCES event(id) | ||||
| 	); | ||||
|  | ||||
| 	CREATE TABLE response_time ( | ||||
| 		response_id INTEGER PRIMARY KEY, | ||||
| 		time INTEGER NOT NULL, | ||||
|  | ||||
| 		CHECK (0 <= time < 24) | ||||
| 	);` | ||||
|  | ||||
| func (s *Store) createSchema() error { | ||||
| 	conn := s.pool.Get(context.Background()) | ||||
| 	defer s.pool.Put(conn) | ||||
| 	return sqlitex.ExecScript(conn, schema) | ||||
| } | ||||
|  | ||||
| type CreateEventCommand struct { | ||||
| 	Name                     string | ||||
| 	EarliestDate, LatestDate time.Time | ||||
| 	Duration                 int | ||||
| } | ||||
|  | ||||
| type CreateEventResult struct { | ||||
| 	AlphaID, AdminAlphaID string | ||||
| } | ||||
|  | ||||
| func (s *Store) CreateEvent(ctx context.Context, cmd CreateEventCommand) (result CreateEventResult, err error) { | ||||
| 	conn := s.pool.Get(ctx) | ||||
| 	defer s.pool.Put(conn) | ||||
|  | ||||
| 	alphaID, err := s.genString() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	adminAlphaID, err := s.genString() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	const dateFmt = "2006-01-02" | ||||
| 	const query = ` | ||||
| 		INSERT INTO event(alpha_id, admin_alpha_id, name, earliest_date, latest_date, duration) | ||||
| 		VALUES (?, ?, ?, ?, ?, ?);` | ||||
| 	err = sqlitex.Exec(conn, query, nil, | ||||
| 		alphaID, | ||||
| 		adminAlphaID, | ||||
| 		cmd.Name, | ||||
| 		cmd.EarliestDate.Format(dateFmt), | ||||
| 		cmd.LatestDate.Format(dateFmt), | ||||
| 		cmd.Duration, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	result.AdminAlphaID = adminAlphaID | ||||
| 	result.AlphaID = alphaID | ||||
| 	return | ||||
| } | ||||
		Reference in New Issue
	
	Block a user