From a06e8db2ffb7656c9c98c446325cb89502c917ff Mon Sep 17 00:00:00 2001 From: Spaenny Date: Sun, 29 Aug 2021 16:37:20 +0200 Subject: [PATCH] add handler, html templates, Makefile and migrations --- Makefile | 18 ++++ cmd/goddit/main.go | 19 ++++ go.mod | 10 ++ go.sum | 26 +++++ goddit.go | 57 +++++++++++ migrations/1_create_tables.down.sql | 3 + migrations/1_create_tables.up.sql | 20 ++++ postgres/comment_store.go | 58 +++++++++++ postgres/post_store.go | 87 ++++++++++++++++ postgres/store.go | 30 ++++++ postgres/thread_store.go | 56 +++++++++++ templates/home.html | 44 ++++++++ templates/layout.html | 31 ++++++ templates/post.html | 46 +++++++++ templates/post_create.html | 20 ++++ templates/thread.html | 45 +++++++++ templates/thread_create.html | 18 ++++ templates/threads.html | 28 ++++++ web/comment_handler.go | 69 +++++++++++++ web/handler.go | 63 ++++++++++++ web/post_handler.go | 150 ++++++++++++++++++++++++++++ web/thread_handler.go | 103 +++++++++++++++++++ 22 files changed, 1001 insertions(+) create mode 100644 Makefile create mode 100644 cmd/goddit/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 goddit.go create mode 100644 migrations/1_create_tables.down.sql create mode 100644 migrations/1_create_tables.up.sql create mode 100644 postgres/comment_store.go create mode 100644 postgres/post_store.go create mode 100644 postgres/store.go create mode 100644 postgres/thread_store.go create mode 100644 templates/home.html create mode 100644 templates/layout.html create mode 100644 templates/post.html create mode 100644 templates/post_create.html create mode 100644 templates/thread.html create mode 100644 templates/thread_create.html create mode 100644 templates/threads.html create mode 100644 web/comment_handler.go create mode 100644 web/handler.go create mode 100644 web/post_handler.go create mode 100644 web/thread_handler.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..322ccb2 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: postgres adminer migrate + +postgres: + docker run --rm -ti --network host -e POSTGRES_PASSWORD=secret postgres + +adminer: + docker run --rm -ti --network host adminer + +reflex: + reflex -s go run cmd/goddit/main.go + +migrate: + migrate -source file://migrations \ + -database postgres://postgres:secret@localhost/postgres?sslmode=disable up + +migrate-down: + migrate -source file://migrations \ + -database postgres://postgres:secret@localhost/postgres?sslmode=disable down \ No newline at end of file diff --git a/cmd/goddit/main.go b/cmd/goddit/main.go new file mode 100644 index 0000000..97bb1b1 --- /dev/null +++ b/cmd/goddit/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + "net/http" + + "git.snrd.de/Spaenny/goddit/postgres" + "git.snrd.de/Spaenny/goddit/web" +) + +func main() { + store, err := postgres.NewStore("postgres://postgres:secret@localhost/postgres?sslmode=disable") + if err != nil { + log.Fatal(err) + } + + h := web.NewHandler(store) + http.ListenAndServe(":3000", h) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f6937c --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.snrd.de/Spaenny/goddit + +go 1.16 + +require ( + github.com/go-chi/chi v1.5.4 // indirect + github.com/google/uuid v1.3.0 // direct + github.com/jmoiron/sqlx v1.3.4 // indirect + github.com/lib/pq v1.10.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..48daa10 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk= +github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= +github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/goddit.go b/goddit.go new file mode 100644 index 0000000..edb8ff5 --- /dev/null +++ b/goddit.go @@ -0,0 +1,57 @@ +package goddit + +import "github.com/google/uuid" + +type Thread struct { + ID uuid.UUID `db:"id"` + Title string `db:"title"` + Description string `db:"description"` +} + +type Post struct { + ID uuid.UUID `db:"id"` + ThreadID uuid.UUID `db:"thread_id"` + Title string `db:"title"` + Content string `db:"content"` + Votes int `db:"votes"` + CommentsCount int `db:"comments_count"` + ThreadTitle string `db:"thread_title"` +} + +type Comment struct { + ID uuid.UUID `db:"id"` + PostID uuid.UUID `db:"post_id"` + Content string `db:"content"` + Votes int `db:"votes"` +} + +type ThreadStore interface { + Thread(id uuid.UUID) (Thread, error) + Threads() ([]Thread, error) + CreateThread(t *Thread) error + UpdateThread(t *Thread) error + DeleteThread(id uuid.UUID) error +} + +type PostStore interface { + Post(id uuid.UUID) (Post, error) + Posts() ([]Post, error) + PostsByThread(threadID uuid.UUID) ([]Post, error) + CreatePost(p *Post) error + UpdatePost(p *Post) error + DeletePost(id uuid.UUID) error +} + +type CommentStore interface { + Comment(id uuid.UUID) (Comment, error) + CommentsByPost(postID uuid.UUID) ([]Comment, error) + CreateComment(c *Comment) error + UpdateComment(c *Comment) error + DeleteComment(id uuid.UUID) error +} + +type Store interface { + ThreadStore + PostStore + CommentStore +} diff --git a/migrations/1_create_tables.down.sql b/migrations/1_create_tables.down.sql new file mode 100644 index 0000000..9593236 --- /dev/null +++ b/migrations/1_create_tables.down.sql @@ -0,0 +1,3 @@ +DROP TABLE comments; +DROP TABLE posts; +DROP TABLE threads; \ No newline at end of file diff --git a/migrations/1_create_tables.up.sql b/migrations/1_create_tables.up.sql new file mode 100644 index 0000000..9d33638 --- /dev/null +++ b/migrations/1_create_tables.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE threads ( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL +); + +CREATE TABLE posts ( + id UUID PRIMARY KEY, + thread_id UUID NOT NULL REFERENCES threads(id) ON DELETE CASCADE, + title TEXT NOT NULL, + content TEXT NOT NULL, + votes INT NOT NULL +); + +CREATE TABLE comments ( + id UUID PRIMARY KEY, + post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + content TEXT NOT NULL, + votes INT NOT NULL +); \ No newline at end of file diff --git a/postgres/comment_store.go b/postgres/comment_store.go new file mode 100644 index 0000000..bee146b --- /dev/null +++ b/postgres/comment_store.go @@ -0,0 +1,58 @@ +package postgres + +import ( + "fmt" + + "git.snrd.de/Spaenny/goddit" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +type CommentStore struct { + *sqlx.DB +} + +func (s *CommentStore) Comment(id uuid.UUID) (goddit.Comment, error) { + var c goddit.Comment + if err := s.Get(&c, `SELECT * FROM comments WHERE id = $1`, id); err != nil { + return goddit.Comment{}, fmt.Errorf("error getting comment: %w", err) + } + return c, nil +} + +func (s *CommentStore) CommentsByPost(postID uuid.UUID) ([]goddit.Comment, error) { + var cc []goddit.Comment + if err := s.Select(&cc, `SELECT * FROM comments WHERE post_id = $1 ORDER BY votes DESC`, postID); err != nil { + return []goddit.Comment{}, fmt.Errorf("error getting comments: %w", err) + } + return cc, nil +} + +func (s *CommentStore) CreateComment(c *goddit.Comment) error { + if err := s.Get(c, `INSERT INTO comments VALUES($1, $2, $3, $4) RETURNING *`, + c.ID, + c.PostID, + c.Content, + c.Votes); err != nil { + return fmt.Errorf("error creating comment: %w", err) + } + return nil +} + +func (s *CommentStore) UpdateComment(c *goddit.Comment) error { + if err := s.Get(c, `UPDATE comments SET post_id = $1, content = $2, votes = $3 WHERE id = $4 RETURNING *`, + c.PostID, + c.Content, + c.Votes, + c.ID); err != nil { + return fmt.Errorf("error updating comments: %w", err) + } + return nil +} + +func (s *CommentStore) DeleteComment(id uuid.UUID) error { + if _, err := s.Exec(`DELETE FROM comments WHERE id = $1`, id); err != nil { + return fmt.Errorf("error deleting comment: %w", err) + } + return nil +} diff --git a/postgres/post_store.go b/postgres/post_store.go new file mode 100644 index 0000000..d54ad55 --- /dev/null +++ b/postgres/post_store.go @@ -0,0 +1,87 @@ +package postgres + +import ( + "fmt" + + "git.snrd.de/Spaenny/goddit" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +type PostStore struct { + *sqlx.DB +} + +func (s *PostStore) Post(id uuid.UUID) (goddit.Post, error) { + var p goddit.Post + if err := s.Get(&p, `SELECT * FROM posts WHERE id = $1`, id); err != nil { + return goddit.Post{}, fmt.Errorf("error getting post: %w", err) + } + return p, nil +} + +func (s *PostStore) PostsByThread(threadID uuid.UUID) ([]goddit.Post, error) { + var pp []goddit.Post + var query = ` + SELECT + posts.*, + COUNT(comments.*) AS comments_count + FROM posts + LEFT JOIN comments ON comments.post_id = posts.id + WHERE thread_ID = $1 + GROUP BY posts.id + ORDER BY votes DESC` + if err := s.Select(&pp, query, threadID); err != nil { + return []goddit.Post{}, fmt.Errorf("error gettings posts: %w", err) + } + return pp, nil +} + +func (s *PostStore) Posts() ([]goddit.Post, error) { + var pp []goddit.Post + var query = ` + SELECT + posts.*, + COUNT(comments.*) AS comments_count, + threads.title AS thread_title + FROM posts + LEFT JOIN comments ON comments.post_id = posts.id + JOIN threads ON threads.id = posts.thread_id + GROUP BY posts.id, threads.title + ORDER BY votes DESC` + if err := s.Select(&pp, query); err != nil { + return []goddit.Post{}, fmt.Errorf("error gettings posts: %w", err) + } + return pp, nil +} + +func (s *PostStore) CreatePost(p *goddit.Post) error { + if err := s.Get(p, `INSERT INTO posts VALUES($1, $2, $3, $4, $5) RETURNING *`, + p.ID, + p.ThreadID, + p.Title, + p.Content, + p.Votes); err != nil { + return fmt.Errorf("error creating post: %w", err) + } + return nil +} + +func (s *PostStore) UpdatePost(p *goddit.Post) error { + if err := s.Get(p, `UPDATE posts set thread_id = $1, title = $2, content = $3, votes = $4 WHERE id = $5 RETURNING *`, + p.ThreadID, + p.Title, + p.Content, + p.Votes, + p.ID); err != nil { + return fmt.Errorf("error updating post: %w", err) + } + return nil +} + +func (s *PostStore) DeletePost(id uuid.UUID) error { + if _, err := s.Exec(`DELETE FROM posts WHERE id = $1`, id); err != nil { + return fmt.Errorf("error deleting post: %w", err) + } + return nil +} diff --git a/postgres/store.go b/postgres/store.go new file mode 100644 index 0000000..980e87a --- /dev/null +++ b/postgres/store.go @@ -0,0 +1,30 @@ +package postgres + +import ( + "fmt" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +func NewStore(dataSourceName string) (*Store, error) { + db, err := sqlx.Open("postgres", dataSourceName) + if err != nil { + return nil, fmt.Errorf("error opening database: %w", err) + } + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("error connecting to database: %w", err) + } + + return &Store{ + ThreadStore: &ThreadStore{DB: db}, + PostStore: &PostStore{DB: db}, + CommentStore: &CommentStore{DB: db}, + }, nil +} + +type Store struct { + *ThreadStore + *PostStore + *CommentStore +} diff --git a/postgres/thread_store.go b/postgres/thread_store.go new file mode 100644 index 0000000..a942fc2 --- /dev/null +++ b/postgres/thread_store.go @@ -0,0 +1,56 @@ +package postgres + +import ( + "fmt" + + "git.snrd.de/Spaenny/goddit" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +type ThreadStore struct { + *sqlx.DB +} + +func (s *ThreadStore) Thread(id uuid.UUID) (goddit.Thread, error) { + var t goddit.Thread + if err := s.Get(&t, `SELECT * FROM threads WHERE id = $1`, id); err != nil { + return goddit.Thread{}, fmt.Errorf("error getting thread: %w", err) + } + return t, nil +} + +func (s *ThreadStore) Threads() ([]goddit.Thread, error) { + var tt []goddit.Thread + if err := s.Select(&tt, `SELECT * FROM threads`); err != nil { + return []goddit.Thread{}, fmt.Errorf("error getting threads: %w", err) + } + return tt, nil +} + +func (s *ThreadStore) CreateThread(t *goddit.Thread) error { + if err := s.Get(t, `INSERT INTO threads VALUES($1, $2, $3) RETURNING *`, + t.ID, + t.Title, + t.Description); err != nil { + return fmt.Errorf("error creating thread: %w", err) + } + return nil +} + +func (s *ThreadStore) UpdateThread(t *goddit.Thread) error { + if err := s.Get(t, `UPDATE INTO threads SET title = $1,description = $2 WHERE id = $3) RETURNING *`, + t.Title, + t.Description, + t.ID); err != nil { + return fmt.Errorf("error updating thread: %w", err) + } + return nil +} + +func (s *ThreadStore) DeleteThread(id uuid.UUID) error { + if _, err := s.Exec(`DELETE FROM threads WHERE id = $1`, id); err != nil { + return fmt.Errorf("error deleteing thread: %w", err) + } + return nil +} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..2c2fb58 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,44 @@ + + {{define "header"}} +

Welcome to goddit

+ {{end}} + + {{define "content"}} + {{range .Posts}} +
+ +
+ {{end}} + {{end}} + + {{define "sidebar"}} +
+
+
Explore interesting threads
+

Browse through hundreds of interesting threads with great communities.

+ Browse Threads +
+
+ {{end}} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..252a681 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,31 @@ + + + + + + + + + + + +
+
+ {{block "header" .}}{{end}} +
+
+
+
+
+ {{block "content" .}}{{end}} +
+
+ {{block "sidebar" .}}{{end}} +
+
+
+ + + diff --git a/templates/post.html b/templates/post.html new file mode 100644 index 0000000..6eea0fa --- /dev/null +++ b/templates/post.html @@ -0,0 +1,46 @@ + {{define "header"}} +
+
+ + + + + Back + +

{{.Post.Title}}

+

+ {{.Post.Content}} +

+
+
+ {{end}} + + {{define "content"}} +
+
+
+ +
+ +
+
+
+
+ +
+ {{range .Comments}} +
+
+ +
{{.Votes}}
+ +
+
+

{{.Content}}

+
+
+ {{end}} +
+ {{end}} + diff --git a/templates/post_create.html b/templates/post_create.html new file mode 100644 index 0000000..abc3d59 --- /dev/null +++ b/templates/post_create.html @@ -0,0 +1,20 @@ + + {{define "header"}} +
Create a new post in
+

{{.Thread.Title}}

+ {{end}} + + {{define "content"}} +
+
+ + +
+
+ + +
+ +
+ {{end}} diff --git a/templates/thread.html b/templates/thread.html new file mode 100644 index 0000000..82c5ef6 --- /dev/null +++ b/templates/thread.html @@ -0,0 +1,45 @@ +{{define "header"}} +

{{.Thread.Title}}

+{{end}} + + {{define "content"}} + {{range .Posts}} +
+
+
+ + + + + +
{{.Votes}}
+ + + + + +
+
+
{{.Title}}
+

{{.Content}}

+ {{.CommentsCount}} Comments +
+
+
+ {{end}} + {{end}} + + {{define "sidebar"}} +
+
+
About Community
+

{{.Thread.Description}}

+ Create Post +
+
+
+
+ +
+
+ {{end}} diff --git a/templates/thread_create.html b/templates/thread_create.html new file mode 100644 index 0000000..517c8fc --- /dev/null +++ b/templates/thread_create.html @@ -0,0 +1,18 @@ + {{define "header"}} +

Create a new thread

+ {{end}} + + {{define "content"}} +
+
+ + +
+
+ + +
+ +
+ {{end}} diff --git a/templates/threads.html b/templates/threads.html new file mode 100644 index 0000000..07d4291 --- /dev/null +++ b/templates/threads.html @@ -0,0 +1,28 @@ + + {{define "header"}} +

Explore threads

+ {{end}} + + {{define "content"}} + {{range .Threads}} +
+
+ + {{.Title}} + +

{{.Description}}

+ Browse Thread +
+
+ {{end}} + {{end}} + + {{define "sidebar"}} +
+
+
Create a new thread
+

Start a new community on goddit and share experiences.

+ Create Thread +
+
+ {{end}} \ No newline at end of file diff --git a/web/comment_handler.go b/web/comment_handler.go new file mode 100644 index 0000000..1c495df --- /dev/null +++ b/web/comment_handler.go @@ -0,0 +1,69 @@ +package web + +import ( + "net/http" + + "git.snrd.de/Spaenny/goddit" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +type CommentHandler struct { + store goddit.Store +} + +func (h *CommentHandler) Store() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + content := r.FormValue("content") + idStr := chi.URLParam(r, "postID") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := h.store.CreateComment(&goddit.Comment{ + ID: uuid.New(), + PostID: id, + Content: content, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, r.Referer(), http.StatusFound) + } +} + +func (h *CommentHandler) Vote() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + c, err := h.store.Comment(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + dir := r.URL.Query().Get("dir") + if dir == "up" { + c.Votes++ + } else if dir == "down" { + c.Votes-- + } + + if err := h.store.UpdateComment(&c); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, r.Referer(), http.StatusFound) + } +} diff --git a/web/handler.go b/web/handler.go new file mode 100644 index 0000000..ad62cc9 --- /dev/null +++ b/web/handler.go @@ -0,0 +1,63 @@ +package web + +import ( + "html/template" + "net/http" + + "git.snrd.de/Spaenny/goddit" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func NewHandler(store goddit.Store) *Handler { + h := &Handler{ + Mux: chi.NewMux(), + store: store, + } + + threads := ThreadHandler{store: store} + posts := PostHandler{store: store} + comments := CommentHandler{store: store} + + h.Use(middleware.Logger) + + h.Get("/", h.Home()) + h.Route("/threads", func(r chi.Router) { + r.Get("/", threads.List()) + r.Get("/new", threads.Create()) + r.Post("/", threads.Store()) + r.Get("/{id}", threads.Show()) + r.Post("/{id}/delete", threads.Delete()) + r.Get("/{id}/new", posts.Create()) + r.Post("/{id}", posts.Store()) + r.Get("/{threadID}/{postID}", posts.Show()) + r.Get("/{threadID}/{postID}/vote", posts.Vote()) + r.Post("/{threadID}/{postID}", comments.Store()) + }) + h.Get("/comments/{id}/vote", comments.Vote()) + + return h +} + +type Handler struct { + *chi.Mux + + store goddit.Store +} + +func (h *Handler) Home() http.HandlerFunc { + type data struct { + Posts []goddit.Post + } + + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/home.html")) + return func(w http.ResponseWriter, r *http.Request) { + pp, err := h.store.Posts() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl.Execute(w, data{Posts: pp}) + } +} diff --git a/web/post_handler.go b/web/post_handler.go new file mode 100644 index 0000000..4fa193f --- /dev/null +++ b/web/post_handler.go @@ -0,0 +1,150 @@ +package web + +import ( + "html/template" + "net/http" + + "git.snrd.de/Spaenny/goddit" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +type PostHandler struct { + store goddit.Store +} + +func (h *PostHandler) Create() http.HandlerFunc { + type data struct { + Thread goddit.Thread + } + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/post_create.html")) + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + t, err := h.store.Thread(id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tmpl.Execute(w, data{Thread: t}) + } +} + +func (h *PostHandler) Show() http.HandlerFunc { + type data struct { + Thread goddit.Thread + Post goddit.Post + Comments []goddit.Comment + } + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/post.html")) + return func(w http.ResponseWriter, r *http.Request) { + postIDStr := chi.URLParam(r, "postID") + threadIDStr := chi.URLParam(r, "threadID") + + postID, err := uuid.Parse(postIDStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + threadID, err := uuid.Parse(threadIDStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + p, err := h.store.Post(postID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cc, err := h.store.CommentsByPost(p.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + t, err := h.store.Thread(threadID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl.Execute(w, data{Thread: t, Post: p, Comments: cc}) + } +} + +func (h *PostHandler) Store() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + title := r.FormValue("title") + content := r.FormValue("content") + + idStr := chi.URLParam(r, "id") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + t, err := h.store.Thread(id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + p := &goddit.Post{ + ID: uuid.New(), + ThreadID: t.ID, + Title: title, + Content: content, + } + + if err := h.store.CreatePost(p); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/threads/"+t.ID.String()+"/"+p.ID.String(), http.StatusFound) + } +} + +func (h *PostHandler) Vote() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "postID") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + p, err := h.store.Post(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + dir := r.URL.Query().Get("dir") + if dir == "up" { + p.Votes++ + } else if dir == "down" { + p.Votes-- + } + + if err := h.store.UpdatePost(&p); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, r.Referer(), http.StatusFound) + } +} diff --git a/web/thread_handler.go b/web/thread_handler.go new file mode 100644 index 0000000..5f78077 --- /dev/null +++ b/web/thread_handler.go @@ -0,0 +1,103 @@ +package web + +import ( + "html/template" + "net/http" + + "git.snrd.de/Spaenny/goddit" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +type ThreadHandler struct { + store goddit.Store +} + +func (h *ThreadHandler) List() http.HandlerFunc { + type data struct { + Threads []goddit.Thread + } + + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/threads.html")) + return func(w http.ResponseWriter, r *http.Request) { + tt, err := h.store.Threads() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl.Execute(w, data{Threads: tt}) + } +} + +func (h *ThreadHandler) Create() http.HandlerFunc { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/thread_create.html")) + return func(w http.ResponseWriter, r *http.Request) { + tmpl.Execute(w, nil) + } +} + +func (h *ThreadHandler) Show() http.HandlerFunc { + type data struct { + Thread goddit.Thread + Posts []goddit.Post + } + + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/thread.html")) + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := uuid.Parse(idStr) + if err != nil { + http.NotFound(w, r) + return + } + t, err := h.store.Thread(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pp, err := h.store.PostsByThread(t.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl.Execute(w, data{Thread: t, Posts: pp}) + } +} + +func (h *ThreadHandler) Store() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + title := r.FormValue("title") + description := r.FormValue("description") + + if err := h.store.CreateThread(&goddit.Thread{ + ID: uuid.New(), + Title: title, + Description: description, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/threads", http.StatusFound) + } +} + +func (h *ThreadHandler) Delete() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + + id, err := uuid.Parse(idStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := h.store.DeleteThread(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/threads", http.StatusFound) + } +}