diff --git a/go.mod b/go.mod index 7f13e96..1828cfd 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.18 require ( crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c + github.com/russross/blackfriday/v2 v2.1.0 gitlab.codemonkeysoftware.net/b/hatmill v0.0.6 ) diff --git a/go.sum b/go.sum index 99668d5..b5d6324 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,10 @@ +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw= crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= -crawshaw.io/sqlite v0.3.2 h1:N6IzTjkiw9FItHAa0jp+ZKC6tuLzXqAYIv+ccIWos1I= -crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU= github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= gitlab.codemonkeysoftware.net/b/hatmill v0.0.6 h1:5Vs30ORHoujCYRvtbIwrusGBQgPzbKv011xKrTFa5ng= gitlab.codemonkeysoftware.net/b/hatmill v0.0.6/go.mod h1:T19ms3BmsEzy5YTS4icMO1oLC53xWhOP9MFeBv4NcFQ= diff --git a/handler.go b/handler.go index 4847fed..fa511e1 100644 --- a/handler.go +++ b/handler.go @@ -1,19 +1,22 @@ package main import ( - "log" + "fmt" "net/http" "strconv" h "gitlab.codemonkeysoftware.net/b/hatmill" ha "gitlab.codemonkeysoftware.net/b/hatmill/attribute" he "gitlab.codemonkeysoftware.net/b/hatmill/element" + + "github.com/russross/blackfriday/v2" ) const ( pathRoot = "/" pathCreatePost = "/create-post" pathDoCreatePost = "/create-post/do" + pathPreviewPost = "/preview-post" ) type handler struct { @@ -32,6 +35,7 @@ func NewHandler(store *Store, baseURL string) http.Handler { h.mux.HandleFunc(pathRoot, h.getPosts) h.mux.HandleFunc(pathCreatePost, h.createPost) h.mux.HandleFunc(pathDoCreatePost, h.doCreatePost) + h.mux.HandleFunc(pathPreviewPost, h.previewPost) return h } @@ -49,13 +53,16 @@ func (hnd *handler) getPosts(w http.ResponseWriter, r *http.Request) { var rows h.Terms for _, post := range result.Posts { + previewURL := fmt.Sprintf("%s?%s=%d", pathPreviewPost, fieldNamePostID, post.Id) rows = append(rows, he.Tr()( he.Td()(h.Text(strconv.FormatInt(post.Id, 10))), he.Td()(h.Text(post.CreatedAt)), he.Td()(h.Text(post.UpdatedAt)), he.Td()(h.Text(post.Author)), he.Td()(h.Text(post.Title)), - // he.Td()(h.Text(post.Body)), + he.Td()( + he.A(ha.Href(previewURL))(h.Text("Preview")), + ), )) } @@ -71,6 +78,7 @@ func (hnd *handler) getPosts(w http.ResponseWriter, r *http.Request) { he.Th()(h.Text("Updated")), he.Th()(h.Text("Author")), he.Th()(h.Text("Title")), + he.Th()(), ), ), he.Tbody()( @@ -88,6 +96,40 @@ func (hnd *handler) getPosts(w http.ResponseWriter, r *http.Request) { } } +const fieldNamePostID = "post-id" + +func (hnd *handler) previewPost(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Query().Get(fieldNamePostID) + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "invalid post ID: "+err.Error(), http.StatusBadRequest) + } + result, err := hnd.store.GetPost(r.Context(), GetPostQuery{PostID: id}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + renderedBody := string(blackfriday.Run([]byte(result.Body))) + + html := he.Html()( + he.Head()( + he.Title()(h.Text(result.Title)), + ), + he.Body()( + he.H1()(h.Text(result.Title)), + he.H2()(h.Text(result.Author)), + he.H3()(h.Text(fmt.Sprintf("%s (Updated %s)", result.CreatedAt, result.UpdatedAt))), + he.Main()(h.RawText(renderedBody)), + ), + ) + _, err = h.WriteDocument(w, html) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + const ( fieldNamePostTitle = "post-title" fieldNamePostBody = "post-body" @@ -99,7 +141,11 @@ func (hnd *handler) createPost(w http.ResponseWriter, r *http.Request) { he.Head()(), he.Body()( he.H1()(h.Text("Create post")), - he.Form(ha.Action(hnd.baseURL+pathDoCreatePost), ha.Method("POST"))( + he.Form( + ha.Action(hnd.baseURL+pathDoCreatePost), + ha.Method("POST"), + ha.Enctype("multipart/form-data"), + )( he.Label(ha.For(fieldNamePostTitle))(h.Text("Title")), he.Input(ha.Name(fieldNamePostTitle)), he.Br(), @@ -107,7 +153,7 @@ func (hnd *handler) createPost(w http.ResponseWriter, r *http.Request) { he.Input(ha.Name(fieldNamePostAuthor)), he.Br(), he.Label(ha.For(fieldNamePostBody))(h.Text("Body")), - he.Textarea(ha.For(fieldNamePostBody))(), + he.Textarea(ha.Name(fieldNamePostBody))(), he.Br(), he.Input(ha.Type("submit")), ), @@ -121,7 +167,7 @@ func (hnd *handler) createPost(w http.ResponseWriter, r *http.Request) { } func (hnd *handler) doCreatePost(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() + err := r.ParseMultipartForm(0) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -132,11 +178,10 @@ func (hnd *handler) doCreatePost(w http.ResponseWriter, r *http.Request) { Author: r.PostForm.Get(fieldNamePostAuthor), Body: r.PostForm.Get(fieldNamePostBody), } - result, err := hnd.store.CreatePost(r.Context(), cmd) + _, err = hnd.store.CreatePost(r.Context(), cmd) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - log.Println("created post", result.PostId) http.Redirect(w, r, pathRoot, http.StatusFound) } diff --git a/store.go b/store.go index 1457c5f..59bdabb 100644 --- a/store.go +++ b/store.go @@ -116,12 +116,12 @@ func (s *Store) GetPosts(ctx context.Context) (GetPostsResult, error) { err := sqlitex.Exec(conn, dbQuery, func(stmt *sqlite.Stmt) error { result.Posts = append(result.Posts, Post{ - Id: stmt.ColumnInt64(0), - Author: stmt.ColumnText(1), - CreatedAt: stmt.ColumnText(2), - UpdatedAt: stmt.ColumnText(3), - Title: stmt.ColumnText(4), - Body: stmt.ColumnText(5), + Id: stmt.GetInt64("id"), + Author: stmt.GetText("author"), + CreatedAt: stmt.GetText("created_at"), + UpdatedAt: stmt.GetText("updated_at"), + Title: stmt.GetText("title"), + Body: stmt.GetText("body"), }) return nil }) @@ -161,3 +161,38 @@ func (s *Store) CreatePost(ctx context.Context, cmd CreatePostCommand) (CreatePo }, cmd.Title, cmd.Author, cmd.Body) return result, err } + +type GetPostQuery struct { + PostID int64 +} + +type GetPostResult struct { + Post +} + +func (s *Store) GetPost(ctx context.Context, query GetPostQuery) (GetPostResult, error) { + conn := s.pool.Get(ctx) + if conn == nil { + return GetPostResult{}, ErrNoConn + } + defer s.pool.Put(conn) + + const dbQuery = ` + SELECT id, created_at, updated_at, author, title, body + FROM post + WHERE id = ?;` + var result GetPostResult + err := sqlitex.Exec(conn, dbQuery, + func(stmt *sqlite.Stmt) error { + result.Post = Post{ + Id: stmt.GetInt64("id"), + CreatedAt: stmt.GetText("created_at"), + UpdatedAt: stmt.GetText("updated_at"), + Author: stmt.GetText("author"), + Title: stmt.GetText("title"), + Body: stmt.GetText("body"), + } + return nil + }, query.PostID) + return result, err +}