add handler, html templates, Makefile and migrations

This commit is contained in:
Philipp 2021-08-29 16:37:20 +02:00
commit a06e8db2ff
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 276B613AF9DBE9C3
22 changed files with 1001 additions and 0 deletions

18
Makefile Normal file
View file

@ -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

19
cmd/goddit/main.go Normal file
View file

@ -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)
}

10
go.mod Normal file
View file

@ -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
)

26
go.sum Normal file
View file

@ -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=

57
goddit.go Normal file
View file

@ -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
}

View file

@ -0,0 +1,3 @@
DROP TABLE comments;
DROP TABLE posts;
DROP TABLE threads;

View file

@ -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
);

58
postgres/comment_store.go Normal file
View file

@ -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
}

87
postgres/post_store.go Normal file
View file

@ -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
}

30
postgres/store.go Normal file
View file

@ -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
}

56
postgres/thread_store.go Normal file
View file

@ -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
}

44
templates/home.html Normal file
View file

@ -0,0 +1,44 @@
{{define "header"}}
<h1 class="mb-0">Welcome to goddit</h1>
{{end}}
{{define "content"}}
{{range .Posts}}
<div class="card mb-4">
<div class="d-flex">
<div class="py-4 pl-4 text-center flex-shrink-0" style="width: 3rem">
<a href="/threads/{{.ThreadID}}/{{.ID}}/vote?dir=up" class="d-block text-body text-decoration-none">
<svg viewBox="0 0 10 16" width="10" height="16">
<path fill-rule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5 5 5z"></path>
</svg>
</a>
<div class="mt-1">{{.Votes}}</div>
<a href="/threads/{{.ThreadID}}/{{.ID}}/vote?dir=down" class="d-block text-body text-decoration-none">
<svg viewBox="0 0 10 16" width="10" height="16">
<path fill-rule="evenodd" d="M5 11L0 6l1.5-1.5L5 8.25 8.5 4.5 10 6l-5 5z"></path>
</svg>
</a>
</div>
<div class="card-body">
<a href="/threads/{{.ThreadID}}" class="small text-secondary">{{.ThreadTitle}}</a>
<a href="/threads/{{.ThreadID}}/{{.ID}}" class="d-block card-title text-body mt-1 h5">
{{.Title}}
</a>
<p class="card-text">{{.Content}}</p>
<a href="/threads/{{.ThreadID}}/{{.ID}}">{{.CommentsCount}} Comments</a>
</div>
</div>
</div>
{{end}}
{{end}}
{{define "sidebar"}}
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Explore interesting threads</h5>
<p class="card-text">Browse through hundreds of interesting threads with great communities.</p>
<a href="/threads" class="btn btn-primary btn-block">Browse Threads</a>
</div>
</div>
{{end}}

31
templates/layout.html Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-light container">
<a class="navbar-brand text-primary" href="/">goddit</a>
</nav>
<div class="header bg-light border-bottom border-top py-5">
<div class="container">
{{block "header" .}}{{end}}
</div>
</div>
<div class="container py-5">
<div class="row">
<div class="col-xl-8 order-1 order-xl-0">
{{block "content" .}}{{end}}
</div>
<div class="col-xl-4 order-0 order-xl-1">
{{block "sidebar" .}}{{end}}
</div>
</div>
</div>
</body>
</html>

46
templates/post.html Normal file
View file

@ -0,0 +1,46 @@
{{define "header"}}
<div class="row">
<div class="col-xl-8">
<a href="/threads/{{.Thread.ID}}" class="text-secondary mb-2 mt-2 d-flex align-items-center">
<svg viewBox="0 0 8 16" width="8" height="16" fill="currentColor">
<path fill-rule="evenodd" d="M5.5 3L7 4.5 3.25 8 7 11.5 5.5 13l-5-5 5-5z"></path>
</svg>
<span class="ml-2">Back</span>
</a>
<h1>{{.Post.Title}}</h1>
<p class="m-0">
{{.Post.Content}}
</p>
</div>
</div>
{{end}}
{{define "content"}}
<div class="card mb-4">
<div class="text-right">
<form action="/threads/{{.Thread.ID}}/{{.Post.ID}}" method="POST">
<textarea name="content" class="form-control border-0 border-bottom-1 p-3"
placeholder="What are your thoughts?" rows="4"></textarea>
<div class="border-top p-1">
<button class="btn btn-primary btn-sm">Comment</button>
</div>
</form>
</div>
</div>
<div class="card mb-4 px-4">
{{range .Comments}}
<div class="d-flex my-4">
<div class="text-center flex-shrink-0" style="width: 1.5rem">
<a href="/comments/{{.ID}}/vote?dir=up" class="d-block text-body text-decoration-none">&#x25B2</a>
<div>{{.Votes}}</div>
<a href="/comments/{{.ID}}/vote?dir=down" class="d-block text-body text-decoration-none">&#x25BC</a>
</div>
<div class="pl-4">
<p class="card-text" style="white-space: pre-line;">{{.Content}}</p>
</div>
</div>
{{end}}
</div>
{{end}}

View file

@ -0,0 +1,20 @@
{{define "header"}}
<h5>Create a new post in</h5>
<h1 class="mb-0">{{.Thread.Title}}</h1>
{{end}}
{{define "content"}}
<form action=/threads/{{.Thread.ID}} method="POST">
<div class="form-group">
<label>Title</label>
<input name="title" type="text" class="form-control" placeholder="Give your post a great title">
</div>
<div class="form-group">
<label>Text</label>
<textarea name="content" class="form-control" rows="3"
placeholder="Tell people about your thoughts"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit Post</button>
</form>
{{end}}

45
templates/thread.html Normal file
View file

@ -0,0 +1,45 @@
{{define "header"}}
<h1 class="mb-0">{{.Thread.Title}}</h1>
{{end}}
{{define "content"}}
{{range .Posts}}
<div class="card mb-4">
<div class="d-flex">
<div class="py-4 pl-4 text-center flex-shrink-0" style="width: 3rem">
<a href="/threads/{{$.Thread.ID}}/{{.ID}}/vote?dir=up" class="d-block text-body text-decoration-none">
<svg viewBox="0 0 10 16" width="10" height="16">
<path fill-rule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5 5 5z"></path>
</svg>
</a>
<div class="mt-1">{{.Votes}}</div>
<a href="/threads/{{$.Thread.ID}}/{{.ID}}/vote?dir=down" class="d-block text-body text-decoration-none">
<svg viewBox="0 0 10 16" width="10" height="16">
<path fill-rule="evenodd" d="M5 11L0 6l1.5-1.5L5 8.25 8.5 4.5 10 6l-5 5z"></path>
</svg>
</a>
</div>
<div class="card-body">
<h5 class="card-title">{{.Title}}</h5>
<p class="card-text">{{.Content}}</p>
<a href="/threads/{{$.Thread.ID}}/{{.ID}}">{{.CommentsCount}} Comments</a>
</div>
</div>
</div>
{{end}}
{{end}}
{{define "sidebar"}}
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">About Community</h5>
<p class="card-text">{{.Thread.Description}}</p>
<a href="/threads/{{.Thread.ID}}/new" class="btn btn-primary btn-block">Create Post</a>
</div>
</div>
<div class="text-center">
<form action=/threads/{{.Thread.ID}}/delete method="POST">
<button type="submit" class="text-danger btn-sm btn btn-link">Delete this thread</button>
</form>
</div>
{{end}}

View file

@ -0,0 +1,18 @@
{{define "header"}}
<h1 class="mb-0">Create a new thread</h1>
{{end}}
{{define "content"}}
<form action="/threads" method="POST">
<div class="form-group">
<label>Title</label>
<input name="title" type="text" class="form-control" placeholder="Give your thread a great title">
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" class="form-control" rows="3"
placeholder="Tell people what your thread is about"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Thread</button>
</form>
{{end}}

28
templates/threads.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
{{define "header"}}
<h1 class="mb-0">Explore threads</h1>
{{end}}
{{define "content"}}
{{range .Threads}}
<div class="card mb-4">
<div class="card-body">
<a href="#" class="d-block card-title text-body mt-1 h5">
{{.Title}}
</a>
<p class="card-text">{{.Description}}</p>
<a href="/threads/{{.ID}}" class="btn btn-primary">Browse Thread</a>
</div>
</div>
{{end}}
{{end}}
{{define "sidebar"}}
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Create a new thread</h5>
<p class="card-text">Start a new community on goddit and share experiences.</p>
<a href="/threads/new" class="btn btn-primary btn-block">Create Thread</a>
</div>
</div>
{{end}}

69
web/comment_handler.go Normal file
View file

@ -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)
}
}

63
web/handler.go Normal file
View file

@ -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})
}
}

150
web/post_handler.go Normal file
View file

@ -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)
}
}

103
web/thread_handler.go Normal file
View file

@ -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)
}
}